progress.mjs 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. #!/usr/bin/env node
  2. /**
  3. * progress.mjs — 任务进度四态上报(AgentTaskStatus 表)
  4. *
  5. * 四态: ack → running → done | failed,running 期间心跳保鲜。
  6. * 幂等: 同 agentId+taskId 覆盖更新(重复上报不产生脏数据)。
  7. *
  8. * 用法:
  9. * node progress.mjs ack --agent <agentId> --task <taskId> [--name <任务名>] [--note <备注>]
  10. * node progress.mjs running --agent <agentId> --task <taskId> [--note <备注>]
  11. * node progress.mjs done --agent <agentId> --task <taskId> [--note <结果备注>]
  12. * node progress.mjs failed --agent <agentId> --task <taskId> --note <失败根因>
  13. * node progress.mjs heartbeat --agent <agentId> --task <taskId>
  14. *
  15. * 纪律: running 状态 30s 无心跳 = 疑似死亡,调度层应主动查,不靠"以为还在跑"。
  16. */
  17. import {
  18. resolveCredentials, failNoCredentials, wrapDate, upsertObject, queryObjects,
  19. } from './parse-client.mjs';
  20. const TABLE = 'AgentTaskStatus';
  21. const VALID = ['ack', 'running', 'done', 'failed'];
  22. function parseArgs(argv) {
  23. const state = argv[0];
  24. const args = { state };
  25. for (let i = 1; i < argv.length; i += 2) {
  26. const key = (argv[i] || '').replace(/^--/, '');
  27. args[key] = argv[i + 1];
  28. }
  29. return args;
  30. }
  31. const args = parseArgs(process.argv.slice(2));
  32. const isHeartbeat = args.state === 'heartbeat';
  33. if (!VALID.includes(args.state) && !isHeartbeat) {
  34. console.error(`用法: node progress.mjs <ack|running|done|failed|heartbeat> --agent <agentId> --task <taskId> [--name <任务名>] [--note <备注>]`);
  35. process.exit(1);
  36. }
  37. if (!args.agent || !args.task) {
  38. console.error('[task-progress] --agent 与 --task 必填(幂等键 agentId+taskId)');
  39. process.exit(1);
  40. }
  41. const cred = resolveCredentials();
  42. if (!cred) failNoCredentials();
  43. const now = new Date();
  44. const where = { agentId: args.agent, taskId: args.task };
  45. // 心跳:只刷 heartbeatAt,不动状态字段
  46. if (isHeartbeat) {
  47. const existing = await queryObjects(TABLE, cred, { where, limit: 1 });
  48. if (existing.length === 0) {
  49. console.error(`[task-progress] 心跳目标不存在(先 ack): ${args.agent}/${args.task}`);
  50. process.exit(1);
  51. }
  52. const r = await upsertObject(TABLE, cred, where, { heartbeatAt: wrapDate(now) });
  53. console.log(JSON.stringify({ ok: true, op: 'heartbeat', objectId: r.objectId, at: now.toISOString() }));
  54. process.exit(0);
  55. }
  56. // 四态上报:字段组装
  57. const data = {
  58. agentId: args.agent,
  59. taskId: args.task,
  60. status: args.state,
  61. heartbeatAt: wrapDate(now),
  62. };
  63. if (args.name) data.taskName = args.name;
  64. if (args.note) data.resultNote = args.note;
  65. if (args.state === 'ack') data.startedAt = wrapDate(now);
  66. if (args.state === 'done' || args.state === 'failed') data.endedAt = wrapDate(now);
  67. const r = await upsertObject(TABLE, cred, where, data);
  68. console.log(JSON.stringify({
  69. ok: true,
  70. op: 'progress',
  71. state: args.state,
  72. agentId: args.agent,
  73. taskId: args.task,
  74. objectId: r.objectId,
  75. created: r.created,
  76. at: now.toISOString(),
  77. }));