| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- #!/usr/bin/env node
- /**
- * progress.mjs — 任务进度四态上报(AgentTaskStatus 表)
- *
- * 四态: ack → running → done | failed,running 期间心跳保鲜。
- * 幂等: 同 agentId+taskId 覆盖更新(重复上报不产生脏数据)。
- *
- * 用法:
- * node progress.mjs ack --agent <agentId> --task <taskId> [--name <任务名>] [--note <备注>]
- * node progress.mjs running --agent <agentId> --task <taskId> [--note <备注>]
- * node progress.mjs done --agent <agentId> --task <taskId> [--note <结果备注>]
- * node progress.mjs failed --agent <agentId> --task <taskId> --note <失败根因>
- * node progress.mjs heartbeat --agent <agentId> --task <taskId>
- *
- * 纪律: running 状态 30s 无心跳 = 疑似死亡,调度层应主动查,不靠"以为还在跑"。
- */
- import {
- resolveCredentials, failNoCredentials, wrapDate, upsertObject, queryObjects,
- } from './parse-client.mjs';
- const TABLE = 'AgentTaskStatus';
- const VALID = ['ack', 'running', 'done', 'failed'];
- function parseArgs(argv) {
- const state = argv[0];
- const args = { state };
- for (let i = 1; i < argv.length; i += 2) {
- const key = (argv[i] || '').replace(/^--/, '');
- args[key] = argv[i + 1];
- }
- return args;
- }
- const args = parseArgs(process.argv.slice(2));
- const isHeartbeat = args.state === 'heartbeat';
- if (!VALID.includes(args.state) && !isHeartbeat) {
- console.error(`用法: node progress.mjs <ack|running|done|failed|heartbeat> --agent <agentId> --task <taskId> [--name <任务名>] [--note <备注>]`);
- process.exit(1);
- }
- if (!args.agent || !args.task) {
- console.error('[task-progress] --agent 与 --task 必填(幂等键 agentId+taskId)');
- process.exit(1);
- }
- const cred = resolveCredentials();
- if (!cred) failNoCredentials();
- const now = new Date();
- const where = { agentId: args.agent, taskId: args.task };
- // 心跳:只刷 heartbeatAt,不动状态字段
- if (isHeartbeat) {
- const existing = await queryObjects(TABLE, cred, { where, limit: 1 });
- if (existing.length === 0) {
- console.error(`[task-progress] 心跳目标不存在(先 ack): ${args.agent}/${args.task}`);
- process.exit(1);
- }
- const r = await upsertObject(TABLE, cred, where, { heartbeatAt: wrapDate(now) });
- console.log(JSON.stringify({ ok: true, op: 'heartbeat', objectId: r.objectId, at: now.toISOString() }));
- process.exit(0);
- }
- // 四态上报:字段组装
- const data = {
- agentId: args.agent,
- taskId: args.task,
- status: args.state,
- heartbeatAt: wrapDate(now),
- };
- if (args.name) data.taskName = args.name;
- if (args.note) data.resultNote = args.note;
- if (args.state === 'ack') data.startedAt = wrapDate(now);
- if (args.state === 'done' || args.state === 'failed') data.endedAt = wrapDate(now);
- const r = await upsertObject(TABLE, cred, where, data);
- console.log(JSON.stringify({
- ok: true,
- op: 'progress',
- state: args.state,
- agentId: args.agent,
- taskId: args.task,
- objectId: r.objectId,
- created: r.created,
- at: now.toISOString(),
- }));
|