Skip to content

创建翻译任务

通过 API 创建翻译任务,支持指定目标语言和翻译内容。该接口是异步的:调用成功后立即返回 task_idtrace_id,后续进度通过你配置的回调地址(callback_url)按工作流节点推送。

与前端手动创建的等价性

该接口等价于前端「任务 → 新建任务 + 上传文件 + 默认勾选扫描术语 + 自动应用」流程,只是把参数搬到 API 调用。源语言使用项目当前配置的源语言,无需(也不支持)通过 API 指定。

接口行为:快速返回 + 后台执行

接口立刻返回 task_idtrace_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-TimestampUnix 秒(5 分钟有效)
X-Loxily-SignMD5(timestamp + body + appSecret)

请求参数(Body)

请求体是纯业务 JSON,不含任何鉴权字段。

项目归属自动解析

无需传 project_id——系统根据 X-Loxily-AppKey 自动查出对应的项目(App Key 和项目一一对应)。

字段类型必填默认说明
client_task_idstring幂等键,用于防止网络重试导致重复建任务。推荐传 UUID。详见幂等性章节
namestring任务名称,1–200 字符
descriptionstring""任务描述,≤ 2000 字符
target_languagesstring[]目标语言代码数组,1–50 个;可用的代码见语言列表
xlsx_urlstring二选一可公开下载的 XLSX URL(https;≤ 50MB)。系统同步下载并解析
stringsobject[]二选一内联字符串数组,1–5000 条;每项 {string_id, content, remark?, kb_reference?, max_len?}
skip_tmbooleanfalsetrue 时不参考翻译记忆库
skip_tbbooleanfalsetrue 时不参考术语库
scan_termsbooleantrue是否对 content 做术语扫描(TB_SCAN 工作流)。false → 直接走 TRANSLATE,不会产生 term_scan.completed 回调
auto_apply_termsbooleantrue术语扫描完成后是否自动进入翻译。false → 停在 waiting_confirmation,等调用 /tasks/confirm-terms 显式确认后再开始 TRANSLATE(仅 scan_terms=true 时生效)
callback_urlstring回调地址,http / https 皆可。未提供则不回调
callback_eventsstring[]全部事件订阅的事件名子集(见回调机制

四种"扫描 × 自动应用"组合

scan_termsauto_apply_terms行为
true(默认)true(默认)扫描 → 自动进入翻译;term_scan.completed 回调后直接收到翻译节点事件
truefalse扫描 → 等待 API 确认;集成方需调用 /tasks/terms 审核、可选 /tasks/terms/update / /tasks/terms/delete 修改,最后 /tasks/confirm-terms 触发翻译
false不关心跳过术语扫描直接翻译;不会有 term_scan.completed 回调
falsetrue同上(auto_apply_terms 仅在 scan_terms=true 时生效)

scan_terms=true 时,term_scan.completed 回调的 detail 会带上 auto_apply_termsrequires_confirmation 两个字段;requires_confirmation=true 即表示需调 /tasks/confirm-terms

strings 条目格式

字段类型必填说明
string_idstring文案 ID
contentstring源语言文本
remarkstring备注 / 场景说明
kb_referencestringKB 参考信息
max_lenstring译文最大长度限制

幂等性

场景:集成方请求经常因为网络超时/连接中断而收不到响应——此时你不知道服务端实际是否已建任务,自动重试就可能造成重复建同一个任务(翻译两遍、扣两份额度)。

解决:在发起请求前生成一个 client_task_id(推荐 UUID),整个重试循环共用同一个值。服务端会:

  • 首次请求 (project_id, client_task_id) → 正常创建任务,返回 HTTP 200
  • 24 小时内再次看到同一个 (project_id, client_task_id)不重复创建,直接返回已有的 task_id,响应头附带 X-Loxily-Idempotent-Replay: true
js
// ✅ 正确:重试共用一个 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)

json
{
  "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

失败

json
{
  "success": false,
  "code": 401,
  "msg": "Invalid sign",
  "trace_id": "...",
  "data": null
}

错误码

code说明
400请求参数错误 / 缺少签名 Header / xlsx_url 非法
401签名无效 / 时间戳超出 5 分钟窗口
403项目未配置 App Secret
404App Key 不存在
413strings 条数或 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.completeddetail 详解:

字段类型说明
skippedbooleantrue = 未实际触发 TB_SCAN(所有内容已在术语库 / 无内容 / 触发异常)
reasonstringskipped=true 时的原因:all_exist_in_termbase / no_unique_content / tb_scan_error
auto_apply_termsboolean回显创建任务时的 auto_apply_terms
requires_confirmationbooleantrue 时集成方需调 /tasks/confirm-terms 才能继续翻译

TRANSLATE 工作流内部子步骤事件(7 个)

翻译工作流内部会按顺序跑 7 个子步骤,每个子步骤终态都会发一次事件:

event中文step_index
tmMatching翻译记忆库匹配1
tbMatching术语匹配(对已有术语库)2
kbExtraction知识库抽取3
translateBatch翻译4
lqaBatchLQA 质检5
translateEnhanceBatch翻译增强6
aiScoringBatchAI 评分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:

json
{
  "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

验签步骤

  1. 读 3 个 Header:X-Loxily-AppKey / X-Loxily-Timestamp / X-Loxily-Sign
  2. 读 HTTP 请求体的原始字节(不要先解析 JSON 再重新序列化,那会让字节序列变化)
  3. 根据 X-Loxily-AppKey 在本地查出对应的 appSecret
  4. 计算 MD5(timestamp + body + appSecret),与 X-Loxily-Sign 比对
  5. 可选:校验 X-Loxily-Timestamp 与当前时间差(建议 5 分钟窗口防重放)

重试策略

  • 成功条件:返回 HTTP 2xx 且 10 秒内完成响应
  • 最多 4 次尝试(首次立即 + 3 次重试),退避:16s → 32s → 64s
  • 总重试窗口约 112 秒,超过后标记为 dead,可在开放平台页面「调用日志」中查看
  • 每次尝试完都会实时写入 call_logsstatus: retrying → delivered/deadattempts 1 → 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 内联)

bash
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)

bash
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 签名 + 调用

javascript
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)

javascript
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 可以在开放平台页面的「调用日志」标签里反向检索,查看该次请求的处理情况以及所有回调事件的投递记录。