任务
概述
任务让 ChatGPT、Claude 等 AI 在你自己的服务器(Worker)上执行工作。说一句“在我们的 ERP 里给 A-1042 下 30 件的订单”,AI 就会把工作发给 Worker,并把结果显示在对话中。
- 3Min API 只负责投递任务并记录状态,实际工作由 Worker 完成
- Worker 是你自己运行的 HTTP 服务器
何时使用
- 让内部系统做事 — ERP 下单、查询库存、搜索内部数据库
- 耗时的工作 — 生成报表、批量转换(最长 24 小时)
- 只能在自己电脑上运行的脚本 — 用隧道加上公网 URL 即可(见下文)
如果目的是保存和收集数据,请使用端点。
不擅长写 Worker 代码?可以直接对你正在用的 AI 说:“帮我做一个 3minapi 任务 Worker”。AI 会通过
help工具读取与本页相同的约定并编写代码。
如何进入
- 注册 Worker:仪表盘 → 任务 → Worker 标签 →
注册 Worker - 查看运行:仪表盘 → 任务 → 运行记录 标签
工作原理
① 在网页上注册 Worker (只需一次)
② AI → task_create 创建任务,状态 working
③ 3Min API → POST 到 Worker 带签名的请求(包含 callback_url)
④ Worker → 立即返回 2xx 仅表示“已收到”,不是结果
⑤ Worker → callback_url 进度消息(可选)→ completed 或 failed
⑥ AI → task_get 读取结果
| 在哪里 | 做什么 |
|---|---|
| 网页仪表盘 | 注册、编辑、删除 Worker;查看和更换密钥;查看运行记录 |
| AI 对话 | 查看 Worker 列表、发送任务、查看状态和结果、取消 |
| Worker 服务器 | 接收请求、处理工作、报告结果 |
AI 看不到 Worker 的 URL 和密钥,两者都只能在网页上管理。
注册 Worker (网页)
| 字段 | 说明 | 限制 |
|---|---|---|
| 名称 | 必填。AI 使用的名称。不区分大小写且唯一,注册后不可修改 | 50 个字符 |
| 描述 | 必填。AI 根据描述选择 Worker | 500 个字符 |
| Worker URL | 必填。以 POST 接收任务的地址 | 2,048 字节 |
| 认证头 / 认证值 | 可选。用于 Worker 自身的认证。只填值时,头为 Authorization |
128 / 8,192 字节 |
- 描述要写具体。 AI 只看名称和描述,看不到 URL。与其写
erp-1,不如写“在我们的 ERP 中登记订单并返回订单号”,写清接收什么、返回什么,AI 才能选中它
两个密钥方向相反
注册后会自动生成两个密钥。在 Worker 详情页(仪表盘 → 任务 → Worker 标签 → 选择 Worker)通过 显示密钥、复制密钥 查看。
| 密钥 | 格式 | 方向 | 用途 |
|---|---|---|---|
| 签名密钥 | whsec_... |
3Min API → Worker | 验证请求确实来自 3Min API |
| 回调密钥 | tm_task_... |
Worker → 3Min API | 报告时以 Authorization: Bearer 发送 |
- 两者是不同的值,不能互换。 端点的 API 密钥(
tm_live_、tm_test_)也不能用作回调密钥
| 操作 | 效果 | Worker 需要做的 |
|---|---|---|
轮换签名密钥 |
48 小时内,新任务同时带有新旧两个签名。轮换之前创建的任务的重试(最长约 5 小时)只带旧签名 | 旧密钥至少保留 6 小时,再在 48 小时内切换 |
重新生成回调密钥 |
旧密钥立即失效(包括正在运行的任务) | 立即切换 |
48 小时内再次轮换,最初的密钥会立即失效。仪表盘只显示当前密钥,如需保留旧密钥,请在轮换之前复制。
实现 Worker
Worker 必须遵守的五条规则
- 在 15 秒内返回 2xx。 工作在响应之后再处理
- 即使工作失败,也要以 2xx 接收请求。 失败通过报告告知(5xx 会重试,其他 4xx 会立即失败 — 见响应码)
- 按
task_id去重。 同一任务可能到达两次(at-least-once) - 签名验证可选,但建议开启。 不验证的话,任何知道 Worker URL 的人都能发送伪造任务(签名验证)
- 把结果报告到
callback_url。 发送一次completed或failed。没有报告的任务会在 24 小时后变为failed(状态报告 API)
投递请求
3Min API 发送到 Worker URL 的请求。
POST https://worker.example.com/tasks
Content-Type: application/json
webhook-id: 0199a1b2-...
webhook-timestamp: 1757650867
webhook-signature: v1,K3XkZ1n0...
X-3minapi-Task-Id: 0199a1b2-...
X-3minapi-Delivery-Attempt: 1
Authorization: Bearer xxx
| 请求头 | 内容 |
|---|---|
webhook-id |
任务 ID(与 task_id 相同) |
webhook-timestamp |
发送时间(Unix 秒),每次尝试都会更新 |
webhook-signature |
签名。轮换期间为空格分隔的两个值(签名验证) |
X-3minapi-Task-Id |
任务 ID(与 webhook-id 相同) |
X-3minapi-Delivery-Attempt |
第几次投递尝试(从 1 开始) |
| 注册的认证头 | 仅在注册时填写了认证头和认证值时发送 |
请求体:
{
"type": "task.created",
"timestamp": "2026-09-12T04:21:07.123Z",
"data": {
"task_id": "0199a1b2-...",
"input": { "sku": "A-1042", "qty": 30 },
"callback_url": "https://api.3minapi.com/api/v1/tasks/0199a1b2-.../result"
}
}
| 字段 | 内容 |
|---|---|
task_id |
任务 ID,用于去重 |
input |
AI 发送的 JSON 对象(值相同,但键的顺序可能不同) |
callback_url |
该任务的报告地址。 状态和结果都发送到这里 |
Worker 的响应码
只看状态码,不读取响应体。
| 响应 | 结果 |
|---|---|
| 2xx | 已接收。在收到报告前保持 working |
| 5xx(501、505 除外)、429、15 秒超时、连接错误 | 在 30 秒、2 分钟、10 分钟、1 小时、4 小时后重试,共 6 次(约 5 小时)。429 的 Retry-After 仅在更长时生效 |
| 其他 4xx(401、404、409 等)、501、505 | 不重试,立即 failed |
- 签名验证失败时返回 401。如果密钥填错,第一个任务就会立即
failed,能很快发现配置问题
签名验证 (可选)
webhook-signature 请求头就是签名。3Min API 用 Worker 的签名密钥计算后附上,Worker 用同样的计算确认是否一致。
签名内容 = "{webhook-id}.{webhook-timestamp}.{原始请求体}"
密钥 = 去掉 whsec_ 前缀后 base64 解码得到的字节
签名 = "v1," + base64( HMAC-SHA256(密钥, 签名内容) )
- 使用官方 Standard Webhooks 库几行代码即可完成(示例代码)。各语言的包和手动实现方法见验证 Webhook 签名(同一规范)
- 用原始请求体字节验证。 先解析 JSON 再序列化一定会失败
webhook-timestamp偏差超过 5 分钟时,库会拒绝(防止重放)- 收到两个签名时,任意一个匹配即可通过(轮换规则)
- 如果不验证,至少要确认
callback_url以https://api.3minapi.com/开头 — 回调密钥会发送到这个地址
状态报告 API
把状态发送到投递请求中的 callback_url。进度消息可以发送多次,完成或失败只发送一次。
POST https://api.3minapi.com/api/v1/tasks/{task_id}/result
Authorization: Bearer tm_task_...
Content-Type: application/json
- 地址:
callback_url以此格式到达,任务 ID 已填好。不要自己拼接,直接使用 - 回调密钥:Worker 详情页的 回调密钥 →
复制密钥。保存在 Worker 服务器的环境变量等处
进行中 — 任务保持 working,AI 通过 task_get 查看消息
{ "status": "working", "status_message": "1/2 正在检查库存" }
完成
{
"status": "completed",
"result": { "order_id": "PO-20260912-001" },
"status_message": "订单 PO-20260912-001 已登记"
}
失败
{
"status": "failed",
"error": { "code": 1001, "message": "库存不足", "data": { "available": 12 } }
}
| 字段 | 规则 |
|---|---|
status |
working、completed、failed 之一 |
status_message |
working 时必填,其他可选。超过 256 个字符的部分会被截断。failed 未填时使用 error.message |
result |
completed 时必填。除 null 外的任意 JSON |
error |
failed 时必填。code(整数)+ message(字符串),data 可选 |
- 请求体整体须在 100 KB 以内。
result和error不会被截断,超出时整个报告会被 413 拒绝 - 每次被接收的报告计为 1 次 API 调用。进度消息只在有意义的阶段发送
状态报告 API 响应码
| 状态码 | 含义 | Worker 的处理 |
|---|---|---|
| 202 | 已接收,通常几秒内生效 | — |
| 400 | 请求体格式错误、任务 ID 无效、字符串中含 NUL 字符 | 修改代码 |
| 401 | 缺少回调密钥或密钥错误(包括端点 API 密钥) | 检查密钥 |
| 404 | 不是该 Worker 的任务 | 检查 callback_url |
| 409 | 任务已结束(完成、失败、取消) | 停止报告 |
| 413 | 请求体超过 100 KB,未记录任何内容 | 若任务仍为 working,缩小后重新发送 |
| 415 | Content-Type 不是 JSON | 修改请求头 |
| 503 | 临时故障 | 稍后重新发送 |
示例代码 (Node.js)
需要 Node.js 18 或更高版本,并在 package.json 中设置 "type": "module"。
// npm i express standardwebhooks
import express from 'express';
import { Webhook } from 'standardwebhooks';
// 签名密钥(whsec_...)和回调密钥(tm_task_...)是不同的值
const wh = new Webhook(process.env.TASK_SIGNING_SECRET.replace('whsec_', ''));
const CALLBACK_KEY = process.env.TASK_CALLBACK_KEY;
// 已接收的 task_id — 生产环境请保存到数据库或 Redis
const seen = new Set();
const app = express();
// 签名基于原始字节计算。先解析 JSON 会导致验证失败。
app.post('/tasks', express.raw({ type: 'application/json' }), (req, res) => {
let envelope;
try {
envelope = wh.verify(req.body, {
'webhook-id': req.header('webhook-id'),
'webhook-timestamp': req.header('webhook-timestamp'),
'webhook-signature': req.header('webhook-signature')
});
} catch {
return res.sendStatus(401);
}
const { task_id, input, callback_url } = envelope.data;
// 规则 3 — 已接收的任务不再处理
if (seen.has(task_id)) return res.sendStatus(202);
seen.add(task_id);
// 规则 1 — 先响应,再工作
res.sendStatus(202);
run(input, callback_url);
});
async function run(input, callbackUrl) {
try {
// 进度消息 — 任务保持 working,AI 通过 task_get 查看
await report(callbackUrl, { status: 'working', status_message: '1/2 正在检查库存' });
await checkStock(input); // ← 你的实际工作
await report(callbackUrl, { status: 'working', status_message: '2/2 正在登记订单' });
const orderId = await createOrder(input);
// 完成 — 结果和摘要消息
await report(callbackUrl, {
status: 'completed',
result: { order_id: orderId },
status_message: `订单 ${orderId} 已登记`
});
} catch (err) {
// 规则 2 — 工作或报告失败时,报告 failed
await report(callbackUrl, {
status: 'failed',
error: { code: 1001, message: String(err?.message ?? err) },
status_message: '订单登记失败'
}).catch(console.error);
}
}
// callbackUrl — 直接使用投递请求中的 callback_url
async function report(callbackUrl, body) {
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(callbackUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${CALLBACK_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}).catch(() => null);
// 202 已接收,409 任务已结束 — 无需再发送
if (res?.status === 202 || res?.status === 409) return;
// 400、401、404、413 重发结果也一样
if (res && res.status !== 503) throw new Error(`report rejected: HTTP ${res.status}`);
// 只有 503 和网络错误才稍后重试
if (attempt < 3) await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
throw new Error('report failed: service unavailable');
}
app.listen(8080);
在自己的电脑上运行 Worker
Worker 需要能从互联网访问的公网 URL。如果电脑在路由器后面,请用隧道给本地端口加上公网 HTTPS 地址。
cloudflared tunnel --url http://localhost:8080
ngrok http 8080
- 安装和使用:Cloudflare Quick Tunnels、ngrok 入门
- 在输出的地址后加上路径(例如
https://xxxx.trycloudflare.com/tasks),注册为 Worker URL - 不登录账号的临时地址每次运行都会变化。 每次都要更新 Worker URL,或使用各服务的固定地址功能
- 电脑关机或隧道断开时,任务通常在重试(约 5 小时)后变为
failed。如果服务对离线地址返回 404,任务会立即failed
发送任务 (AI)
注册 Worker 后即可在 AI 对话中直接使用,无需知道工具名称。
- “在我们的 ERP 里给 A-1042 下 30 件的订单”
- “刚才发的订单怎么样了?”
- “取消刚才发送的任务”
| 工具 | 作用 |
|---|---|
worker_list |
已注册 Worker 的名称和描述(看不到 URL 和密钥) |
task_create |
用 Worker 名称和 input(JSON 对象,100 KB 以内)创建任务 |
task_get |
单个任务的状态、消息、结果或错误 |
task_list |
最近的任务列表(不含结果正文) |
task_cancel |
取消运行中的任务(最多 100 个) |
- 向同一 Worker 在 2 分钟内(视时机最长约 4 分钟)发送相同的
input,会返回已有任务。已完成和已取消的任务也会原样返回,只有失败的任务会重新创建 - 取消不会通知 Worker。 只会停止剩余的投递尝试,已收到任务的 Worker 之后的报告会得到 409
查看状态
| 状态 | 含义 | 是否会变化 |
|---|---|---|
working |
投递中,或 Worker 正在处理 | 会 |
completed |
Worker 报告完成 | 最终 |
failed |
Worker 报告失败、投递失败或超过 24 小时 | 最终 |
cancelled |
已取消 | 最终 |
failed 的原因记录在 status_message 中。
| 原因 | status_message 示例 |
|---|---|
| Worker 报告失败 | Worker 发送的消息(没有则为 error.message) |
| 重试用尽 | The task could not be delivered to the worker after 6 attempts (no response within 15s). |
| 立即失败的响应 | The task could not be delivered to the worker after 1 attempt (HTTP 401). |
| 超过 24 小时 | Worker did not respond within 24 hours. |
查看位置:
- AI —
task_get、task_list。Worker 的result不经摘要原样传递 - 网页 — 运行记录 标签。按时间段(7、30、60 天,Free 为 7 天)和状态筛选,点击行可查看输入、结果、错误和投递。投递最终失败时,投递 中会显示尝试次数、最后响应和原因
限制
| 项目 | 值 |
|---|---|
| Worker 数量 | Free 1 个 / 付费方案无限制 |
| 任务数量·并发 | 无限制(在每月 API 调用额度内) |
| 期限 | 创建后 24 小时 — 超过则 failed(每小时检查一次,最长约 25 小时) |
| 历史保留 | 付费方案至少 60 天 / Free 在 Worker 注册 7 天后删除 Worker 及其全部运行记录 |
input 大小 |
100 KB |
| 报告请求体 | 100 KB(status_message 超过 256 个字符会被截断) |
| 投递超时·重试 | 15 秒、共 6 次,约 5 小时 |
| 签名密钥轮换宽限期 | 48 小时 |
| 重新生成回调密钥 | 旧密钥立即失效 |
| API 调用计数 | 成功的任务工具调用 1 次、被接收的报告 1 次各计 1 次。投递和重试计 0 次。超出额度时报告仍会被接收 |
| 投递保证 | at-least-once(同一任务可能到达两次) |
隐私说明
- Worker 的
result和error会不经脱敏原样进入 AI 对话 - 3Min API 不会识别自由格式 JSON 中的个人信息,请勿在结果中放入超出需要的个人信息
常见问题
- 所有任务都立即
failed:查看运行详情中的 投递 → 最后响应。如果是 401,可能是把回调密钥填到了签名密钥的位置,或者轮换后立即删除了旧密钥 - 任务一直是
working:Worker 只返回了 2xx,却没有发送报告。请在 Worker 日志中查看状态报告 API 的响应码 - 报告返回 401:确认使用的是以
tm_task_开头的回调密钥,而不是重新生成之前的密钥 - 报告返回 409:任务已结束或已取消,无需再发送
- 报告返回 413:请求体超过 100 KB。请缩小
result后重新发送 - 发送相同请求却没有创建新任务:2 分钟内相同的
input会返回已有任务。可以在input中加入日期等信息加以区分