创建翻译任务
通过 API 创建翻译任务,支持指定目标语言和翻译内容。该接口是异步的:调用成功后立即返回 task_id 与 trace_id,后续进度通过你配置的回调地址(callback_url)按工作流节点推送。
与前端手动创建的等价性
该接口等价于前端「任务 → 新建任务 + 上传文件 + 默认勾选扫描术语 + 自动应用」流程,只是把参数搬到 API 调用。源语言使用项目当前配置的源语言,无需(也不支持)通过 API 指定。
接口行为:快速返回 + 后台执行
接口立刻返回 task_id 和 trace_id(~100ms),之后在后台依序执行:解析参数 → 构造 CSV → 上传 GCS → 触发工作流。任一步失败都会通过 task.failed 回调通知集成方(前提是请求里给了 callback_url)。
为什么这样设计
上传 GCS 和触发 GCP Workflow 都可能慢(尤其 xlsx_url 要下载 50MB 文件)。把这些耗时步骤移到后台,保证接口响应稳定在 100ms 级别。
任务链路(3 个独立工作流自动接力)
后台流程驱动 最多 3 个工作流按顺序串联:
同步返回 (task_id, accepted_at)
│
▼ (后台)
① 术语扫描 (TB_SCAN) ← 仅当 scan_terms=true(默认) 且检测到新术语时真正执行
│ term_scan.completed (跳过场景也会发此回调,detail.skipped=true)
▼
② 翻译 (TRANSLATE) ← 执行 TM 匹配 / 术语匹配 / KB 抽取 / 翻译 / LQA / 增强 / 评分
│ task.completed
▼
③ 智能优化 (TRANSLATE_QA) ← 仅当有低分行需要二次重翻时真正执行
│ smart_optimize.completed (跳过场景也会发此回调,detail.skipped=true)term_scan.completed回调何时发出scan_terms=true+ 有新术语:TB_SCAN 跑完后发,detail正常scan_terms=true+ 没有新术语:立即发,detail.skipped=true, reason='all_exist_in_termbase'scan_terms=true+ 准备阶段异常:立即发,detail.skipped=true, reason='tb_scan_error'scan_terms=false:不发(用户明确关闭了扫描)
smart_optimize.completed回调何时发出- 有低分行:TRANSLATE_QA 跑完后发
- 没有低分行:
task.completed后立即发,detail.skipped=true, reason='no_low_score_rows'
- 后台任一步失败 →
task.failed回调 + 开放平台「调用日志」里能看到具体错误 - 术语扫描完成后,可通过 查询任务术语 接口检索本次扫描发现的新术语
请求地址
POST {BASE_URL}/api/open/v1/tasks/create
Content-Type: application/json鉴权 Header
参见签名算法,简要如下:
| Header | 值 |
|---|---|
X-Loxily-AppKey | 项目 App Key |
X-Loxily-Timestamp | Unix 秒(5 分钟有效) |
X-Loxily-Sign | MD5(timestamp + body + appSecret) |
请求参数(Body)
请求体是纯业务 JSON,不含任何鉴权字段。
项目归属自动解析
无需传 project_id——系统根据 X-Loxily-AppKey 自动查出对应的项目(App Key 和项目一一对应)。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
client_task_id | string | 否 | — | 幂等键,用于防止网络重试导致重复建任务。推荐传 UUID。详见幂等性章节 |
name | string | 是 | — | 任务名称,1–200 字符 |
description | string | 否 | "" | 任务描述,≤ 2000 字符 |
target_languages | string[] | 是 | — | 目标语言代码数组,1–50 个;可用的代码见语言列表 |
xlsx_url | string | 二选一 | — | 可公开下载的 XLSX URL(https;≤ 50MB)。系统同步下载并解析 |
strings | object[] | 二选一 | — | 内联字符串数组,1–5000 条;每项 {string_id, content, remark?, kb_reference?, max_len?} |
skip_tm | boolean | 否 | false | true 时不参考翻译记忆库 |
skip_tb | boolean | 否 | false | true 时不参考术语库 |
scan_terms | boolean | 否 | true | 是否对 content 做术语扫描(TB_SCAN 工作流)。false → 直接走 TRANSLATE,不会产生 term_scan.completed 回调 |
auto_apply_terms | boolean | 否 | true | 术语扫描完成后是否自动进入翻译。false → 停在 waiting_confirmation,等调用 /tasks/confirm-terms 显式确认后再开始 TRANSLATE(仅 scan_terms=true 时生效) |
callback_url | string | 否 | — | 回调地址,http / https 皆可。未提供则不回调 |
callback_events | string[] | 否 | 全部事件 | 订阅的事件名子集(见回调机制) |
四种"扫描 × 自动应用"组合
scan_terms | auto_apply_terms | 行为 |
|---|---|---|
true(默认) | true(默认) | 扫描 → 自动进入翻译;term_scan.completed 回调后直接收到翻译节点事件 |
true | false | 扫描 → 等待 API 确认;集成方需调用 /tasks/terms 审核、可选 /tasks/terms/update / /tasks/terms/delete 修改,最后 /tasks/confirm-terms 触发翻译 |
false | 不关心 | 跳过术语扫描直接翻译;不会有 term_scan.completed 回调 |
false | true | 同上(auto_apply_terms 仅在 scan_terms=true 时生效) |
当 scan_terms=true 时,term_scan.completed 回调的 detail 会带上 auto_apply_terms 与 requires_confirmation 两个字段;requires_confirmation=true 即表示需调 /tasks/confirm-terms。
strings 条目格式
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
string_id | string | 是 | 文案 ID |
content | string | 是 | 源语言文本 |
remark | string | 否 | 备注 / 场景说明 |
kb_reference | string | 否 | KB 参考信息 |
max_len | string | 否 | 译文最大长度限制 |
幂等性
场景:集成方请求经常因为网络超时/连接中断而收不到响应——此时你不知道服务端实际是否已建任务,自动重试就可能造成重复建同一个任务(翻译两遍、扣两份额度)。
解决:在发起请求前生成一个 client_task_id(推荐 UUID),整个重试循环共用同一个值。服务端会:
- 首次请求
(project_id, client_task_id)→ 正常创建任务,返回 HTTP 200 - 24 小时内再次看到同一个
(project_id, client_task_id)→ 不重复创建,直接返回已有的task_id,响应头附带X-Loxily-Idempotent-Replay: true
// ✅ 正确:重试共用一个 client_task_id
const clientTaskId = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
try {
return await fetch(url, { body: JSON.stringify({ ..., client_task_id: clientTaskId }) });
} catch { continue; }
}
// ❌ 错误:每次重试都重新生成,等于没做幂等
for (let i = 0; i < 3; i++) {
const clientTaskId = crypto.randomUUID(); // 每轮不同 → 会创建多份任务
...
}不传 client_task_id 也可以——接口正常工作,只是服务端不做去重,每次调用都会创建新任务。适合一次性脚本、或者你自己已经在外层保证了不重复调用的场景。生产集成强烈建议传。
响应
成功(HTTP 200)
{
"success": true,
"code": 0,
"msg": "ok",
"trace_id": "8a3f12ab-4d2a-9f51-7e2c6d8a9101",
"data": {
"task_id": "68a12cb7e1f9a88b4ea23c77",
"accepted_at": "2026-04-22T10:22:31.123Z"
}
}返回字段说明:
| 字段 | 说明 |
|---|---|
trace_id | 链路 ID(顶层字段)。建议记录到本地日志,出问题时可在开放平台页面「调用日志」按此查询 |
data.task_id | 任务 ID。用于后续查询任务术语、查询状态、接收回调时匹配 |
data.accepted_at | 服务端受理时间 |
为什么只有这两个字段?
我们把所有耗时操作(CSV 生成、GCS 上传、工作流调用)都移到后台。接口调用返回时,后台可能还没决定是走术语扫描还是直接翻译,因此也不返回 pipeline_stage。集成方通过回调事件或查询接口掌握任务进度即可。
trace_id 是响应顶层公共字段,用于日志排查与调用日志查询。详见公共参数 · 响应公共字段。
幂等重放时同样返回 HTTP 200,额外附加响应头:
X-Loxily-Idempotent-Replay: true失败
{
"success": false,
"code": 401,
"msg": "Invalid sign",
"trace_id": "...",
"data": null
}错误码
| code | 说明 |
|---|---|
| 400 | 请求参数错误 / 缺少签名 Header / xlsx_url 非法 |
| 401 | 签名无效 / 时间戳超出 5 分钟窗口 |
| 403 | 项目未配置 App Secret |
| 404 | App Key 不存在 |
| 413 | strings 条数或 xlsx_url 文件过大 |
| 422 | 目标语言不在项目配置 / 项目无可用源语言 |
| 500 | 系统错误 |
| 502 | 工作流触发失败 |
回调机制
若请求提供了 callback_url,任务在每个工作流节点完成时会向该地址发起 HTTP(S) POST(http / https 皆可)。
事件列表
回调事件分两类:工作流级(标示链路阶段性完成)与 TRANSLATE 子步骤级(翻译工作流内部的细粒度进度)。
工作流级事件(4 个)
| event | 触发时机 | 关键 detail 字段 |
|---|---|---|
term_scan.completed | 术语扫描阶段结束(真实完成 或 被跳过都会发;scan_terms=false 时不发) | skipped / reason / auto_apply_terms / requires_confirmation |
task.completed | 翻译工作流成功结束——判断任务整体完成的主事件 | 透传原始事件 detail |
smart_optimize.completed | 智能优化阶段结束(真实完成 或 无低分跳过都会发) | 真实完成:透传;跳过:skipped / reason: 'no_low_score_rows' / threshold |
task.failed | 任一工作流以 FAILED 终态结束 | 透传错误信息 |
term_scan.completed 的 detail 详解:
| 字段 | 类型 | 说明 |
|---|---|---|
skipped | boolean | true = 未实际触发 TB_SCAN(所有内容已在术语库 / 无内容 / 触发异常) |
reason | string | skipped=true 时的原因:all_exist_in_termbase / no_unique_content / tb_scan_error |
auto_apply_terms | boolean | 回显创建任务时的 auto_apply_terms |
requires_confirmation | boolean | true 时集成方需调 /tasks/confirm-terms 才能继续翻译 |
TRANSLATE 工作流内部子步骤事件(7 个)
翻译工作流内部会按顺序跑 7 个子步骤,每个子步骤终态都会发一次事件:
| event | 中文 | step_index |
|---|---|---|
tmMatching | 翻译记忆库匹配 | 1 |
tbMatching | 术语匹配(对已有术语库) | 2 |
kbExtraction | 知识库抽取 | 3 |
translateBatch | 翻译 | 4 |
lqaBatch | LQA 质检 | 5 |
translateEnhanceBatch | 翻译增强 | 6 |
aiScoringBatch | AI 评分 | 7 |
全量 11 个事件。未设置 callback_events 时全部订阅;设置则按白名单过滤。
订阅建议
- 简单集成:只订阅 4 个工作流级事件
term_scan.completed/task.completed/smart_optimize.completed/task.failed即可覆盖链路全貌 - 需要展示实时进度条:再加上 7 个 TRANSLATE 子步骤事件
回调签名(与入站 API 完全一致)
回调采用和入站 API 相同的签名方案:全部鉴权信息放 Header,body 是纯业务 JSON。签名复用项目 appSecret(不需要单独的 Webhook Secret)。
回调请求
POST {callback_url}
Content-Type: application/json
X-Loxily-AppKey: <你的 App Key>
X-Loxily-Timestamp: <unix seconds>
X-Loxily-Sign: MD5(timestamp + body + appSecret)
X-Loxily-Event: translateBatch # 元信息,不参与签名
X-Loxily-Trace-Id: <原始 create 调用的 trace_id> # 元信息
X-Loxily-Delivery-Id: <每次投递唯一 UUID> # 元信息Body:
{
"event": "translateBatch",
"task_id": "68a12cb7e1f9a88b4ea23c77",
"project_id": "proj-7f3e12ab",
"trace_id": "8a3f12ab-4d2a-9f51-7e2c6d8a9101",
"delivery_id": "a1b2c3...",
"timestamp": "2026-04-22T10:25:17.455Z",
"status": "SUCCEEDED",
"step_index": 4,
"total_steps": 7,
"detail": { "rows_total": 512, "rows_done": 512 },
"error": null
}status 可能值:RUNNING / SUCCEEDED / COMPLETED / FAILED。
验签步骤
- 读 3 个 Header:
X-Loxily-AppKey/X-Loxily-Timestamp/X-Loxily-Sign - 读 HTTP 请求体的原始字节(不要先解析 JSON 再重新序列化,那会让字节序列变化)
- 根据
X-Loxily-AppKey在本地查出对应的appSecret - 计算
MD5(timestamp + body + appSecret),与X-Loxily-Sign比对 - 可选:校验
X-Loxily-Timestamp与当前时间差(建议 5 分钟窗口防重放)
重试策略
- 成功条件:返回 HTTP 2xx 且 10 秒内完成响应
- 最多 4 次尝试(首次立即 + 3 次重试),退避:
16s → 32s → 64s - 总重试窗口约 112 秒,超过后标记为
dead,可在开放平台页面「调用日志」中查看 - 每次尝试完都会实时写入
call_logs:status: retrying → delivered/dead,attempts1 → 2 → 3 → 4 增加,集成方在开放平台页面可实时看到 - 出站请求走 SSRF 防护;
callback_url支持 http / https,支持 RFC1918 内网 IP(便于客户放在 VPN 内),但始终拦截云元数据地址(169.254.0.0/16)和 loopback(127.0.0.0/8)
112 秒窗口
当前版本采用内联重试,总尝试窗口约 112 秒。请保证回调接收服务的单次故障恢复时间 < 2 分钟,否则该次回调会永久丢失(但 call_logs 会有 dead 事件记录可查)。
示例
cURL(strings 内联)
APP_KEY="5685414646a54423c891d87194d87f3f"
APP_SECRET="<你的 App Secret>"
BASE_URL="https://api.loxily.com"
TIMESTAMP=$(date +%s)
BODY='{"client_task_id":"c2a9f5e4-4bb2-4bf3-9fd7-ce30b3a9321c","name":"Q2 新手引导文案","target_languages":["en","ja","ko"],"strings":[{"string_id":"home.title","content":"欢迎来到 Loxily","remark":"首页主标题","max_len":"32"},{"string_id":"home.cta","content":"立即开始","kb_reference":"brand.voice"}],"callback_url":"https://your-service.example.com/loxily/webhook"}'
SIGN=$(printf '%s' "${TIMESTAMP}${BODY}${APP_SECRET}" | md5)
curl -X POST "${BASE_URL}/api/open/v1/tasks/create" \
-H "Content-Type: application/json" \
-H "X-Loxily-AppKey: ${APP_KEY}" \
-H "X-Loxily-Timestamp: ${TIMESTAMP}" \
-H "X-Loxily-Sign: ${SIGN}" \
-d "${BODY}"cURL(xlsx_url)
BODY='{"client_task_id":"b9d6e5c2-1f8e-4c24-a3c2-2a5b8f12b98a","name":"10 月运营活动","target_languages":["en-us","ja"],"xlsx_url":"https://cdn.example.com/loxily-uploads/2026-04/activity-q4.xlsx","callback_url":"https://your-service.example.com/loxily/webhook"}'
# 同样 TIMESTAMP + SIGN 头一起发送Node.js 签名 + 调用
import crypto from 'crypto';
const body = JSON.stringify({
client_task_id: crypto.randomUUID(),
name: 'Demo task',
target_languages: ['en', 'ja'],
strings: [{ string_id: 'home.title', content: '欢迎来到 Loxily' }],
callback_url: 'https://your-service.example.com/loxily/webhook',
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const sign = crypto.createHash('md5')
.update(`${timestamp}${body}${process.env.LOXILY_APP_SECRET}`, 'utf8')
.digest('hex');
const resp = await fetch(`${process.env.LOXILY_BASE_URL}/api/open/v1/tasks/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Loxily-AppKey': process.env.LOXILY_APP_KEY,
'X-Loxily-Timestamp': timestamp,
'X-Loxily-Sign': sign,
},
body, // 注意:必须用同一份 body 字符串,不要重新 JSON.stringify
});
const data = await resp.json();
console.log(data.trace_id, data.data?.task_id);Webhook 验签示例(Node.js / Express)
import crypto from 'crypto';
import express from 'express';
// express.raw 保留原始字节,确保验签用的 body 和签名端完全一致
const app = express();
app.post(
'/loxily/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const timestamp = req.header('X-Loxily-Timestamp') || '';
const sign = req.header('X-Loxily-Sign') || '';
const appKey = req.header('X-Loxily-AppKey') || '';
// 5 分钟窗口
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).json({ ok: false });
}
// 根据 appKey 查到 appSecret(与入站 API 共享)
const appSecret = process.env.LOXILY_APP_SECRET;
const rawBody = req.body.toString('utf8');
const expected = crypto.createHash('md5')
.update(`${timestamp}${rawBody}${appSecret}`, 'utf8')
.digest('hex');
if (expected !== sign) return res.status(401).json({ ok: false });
const payload = JSON.parse(rawBody);
// TODO: 业务处理(按 event + status 更新本地状态)
console.log(payload.event, payload.status, payload.task_id);
res.json({ ok: true });
},
);为什么用 express.raw?
签名是对原始请求字节做 MD5。如果用 express.json() 会先解析,再让你访问 req.body(对象),此时拿不到原始字节——即便你再 JSON.stringify(req.body),结果也会和发送方字节序列有差异(空格、数字精度、字段顺序都可能不同),导致验签失败。
链路 ID 查询
响应中的 trace_id 可以在开放平台页面的「调用日志」标签里反向检索,查看该次请求的处理情况以及所有回调事件的投递记录。