botmux 2.33.0 → 2.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -1
- package/README.md +45 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +11 -0
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/cli/bots-list-output.d.ts +21 -0
- package/dist/cli/bots-list-output.d.ts.map +1 -0
- package/dist/cli/bots-list-output.js +23 -0
- package/dist/cli/bots-list-output.js.map +1 -0
- package/dist/cli/workflow.d.ts +13 -0
- package/dist/cli/workflow.d.ts.map +1 -0
- package/dist/cli/workflow.js +781 -0
- package/dist/cli/workflow.js.map +1 -0
- package/dist/cli.js +69 -14
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +211 -4
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/session-manager.d.ts +6 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +22 -12
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/worker-pool.d.ts +13 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +100 -6
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +884 -3
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +36 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +22 -0
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/web/app.js +20 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +356 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/workflow-catalog.d.ts +2 -0
- package/dist/dashboard/web/workflow-catalog.d.ts.map +1 -0
- package/dist/dashboard/web/workflow-catalog.js +323 -0
- package/dist/dashboard/web/workflow-catalog.js.map +1 -0
- package/dist/dashboard/web/workflows.d.ts +2 -0
- package/dist/dashboard/web/workflows.d.ts.map +1 -0
- package/dist/dashboard/web/workflows.js +1618 -0
- package/dist/dashboard/web/workflows.js.map +1 -0
- package/dist/dashboard/workflow-api.d.ts +23 -0
- package/dist/dashboard/workflow-api.d.ts.map +1 -0
- package/dist/dashboard/workflow-api.js +463 -0
- package/dist/dashboard/workflow-api.js.map +1 -0
- package/dist/dashboard-web/app.js +494 -199
- package/dist/dashboard-web/index.html +1 -0
- package/dist/dashboard-web/style.css +160 -6
- package/dist/dashboard-web/terminal-replay.html +227 -0
- package/dist/dashboard.js +29 -12
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +12 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +12 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +3 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +27 -1
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +19 -2
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +21 -2
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/workflow-card-handler.d.ts +50 -0
- package/dist/im/lark/workflow-card-handler.d.ts.map +1 -0
- package/dist/im/lark/workflow-card-handler.js +152 -0
- package/dist/im/lark/workflow-card-handler.js.map +1 -0
- package/dist/im/lark/workflow-cards.d.ts +46 -0
- package/dist/im/lark/workflow-cards.d.ts.map +1 -0
- package/dist/im/lark/workflow-cards.js +226 -0
- package/dist/im/lark/workflow-cards.js.map +1 -0
- package/dist/im/lark/workflow-progress-card.d.ts +76 -0
- package/dist/im/lark/workflow-progress-card.d.ts.map +1 -0
- package/dist/im/lark/workflow-progress-card.js +279 -0
- package/dist/im/lark/workflow-progress-card.js.map +1 -0
- package/dist/im/lark/workflow-slash-command.d.ts +92 -0
- package/dist/im/lark/workflow-slash-command.d.ts.map +1 -0
- package/dist/im/lark/workflow-slash-command.js +185 -0
- package/dist/im/lark/workflow-slash-command.js.map +1 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +17 -4
- package/dist/services/group-creator.js.map +1 -1
- package/dist/services/groups-store.d.ts +11 -0
- package/dist/services/groups-store.d.ts.map +1 -1
- package/dist/services/groups-store.js +26 -0
- package/dist/services/groups-store.js.map +1 -1
- package/dist/services/jsonl-cursor.d.ts +12 -0
- package/dist/services/jsonl-cursor.d.ts.map +1 -0
- package/dist/services/jsonl-cursor.js +45 -0
- package/dist/services/jsonl-cursor.js.map +1 -0
- package/dist/services/schedule-store.d.ts +35 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +108 -1
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +399 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.js +4 -0
- package/dist/utils/cli-usage-limit.js.map +1 -1
- package/dist/worker.js +118 -14
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts +114 -0
- package/dist/workflows/attempt-resume.d.ts.map +1 -0
- package/dist/workflows/attempt-resume.js +385 -0
- package/dist/workflows/attempt-resume.js.map +1 -0
- package/dist/workflows/attempt-terminal.d.ts +21 -0
- package/dist/workflows/attempt-terminal.d.ts.map +1 -0
- package/dist/workflows/attempt-terminal.js +7 -0
- package/dist/workflows/attempt-terminal.js.map +1 -0
- package/dist/workflows/blob.d.ts +27 -0
- package/dist/workflows/blob.d.ts.map +1 -0
- package/dist/workflows/blob.js +39 -0
- package/dist/workflows/blob.js.map +1 -0
- package/dist/workflows/cancel-run.d.ts +45 -0
- package/dist/workflows/cancel-run.d.ts.map +1 -0
- package/dist/workflows/cancel-run.js +99 -0
- package/dist/workflows/cancel-run.js.map +1 -0
- package/dist/workflows/cancel.d.ts +111 -0
- package/dist/workflows/cancel.d.ts.map +1 -0
- package/dist/workflows/cancel.js +120 -0
- package/dist/workflows/cancel.js.map +1 -0
- package/dist/workflows/catalog.d.ts +60 -0
- package/dist/workflows/catalog.d.ts.map +1 -0
- package/dist/workflows/catalog.js +119 -0
- package/dist/workflows/catalog.js.map +1 -0
- package/dist/workflows/cold-attach.d.ts +30 -0
- package/dist/workflows/cold-attach.d.ts.map +1 -0
- package/dist/workflows/cold-attach.js +40 -0
- package/dist/workflows/cold-attach.js.map +1 -0
- package/dist/workflows/cold-scan.d.ts +21 -0
- package/dist/workflows/cold-scan.d.ts.map +1 -0
- package/dist/workflows/cold-scan.js +70 -0
- package/dist/workflows/cold-scan.js.map +1 -0
- package/dist/workflows/daemon-spawn.d.ts +117 -0
- package/dist/workflows/daemon-spawn.d.ts.map +1 -0
- package/dist/workflows/daemon-spawn.js +551 -0
- package/dist/workflows/daemon-spawn.js.map +1 -0
- package/dist/workflows/definition.d.ts +1309 -0
- package/dist/workflows/definition.d.ts.map +1 -0
- package/dist/workflows/definition.js +334 -0
- package/dist/workflows/definition.js.map +1 -0
- package/dist/workflows/effect-input.d.ts +4 -0
- package/dist/workflows/effect-input.d.ts.map +1 -0
- package/dist/workflows/effect-input.js +18 -0
- package/dist/workflows/effect-input.js.map +1 -0
- package/dist/workflows/events/append.d.ts +77 -0
- package/dist/workflows/events/append.d.ts.map +1 -0
- package/dist/workflows/events/append.js +214 -0
- package/dist/workflows/events/append.js.map +1 -0
- package/dist/workflows/events/idempotency.d.ts +77 -0
- package/dist/workflows/events/idempotency.d.ts.map +1 -0
- package/dist/workflows/events/idempotency.js +116 -0
- package/dist/workflows/events/idempotency.js.map +1 -0
- package/dist/workflows/events/index.d.ts +7 -0
- package/dist/workflows/events/index.d.ts.map +1 -0
- package/dist/workflows/events/index.js +7 -0
- package/dist/workflows/events/index.js.map +1 -0
- package/dist/workflows/events/payloads.d.ts +917 -0
- package/dist/workflows/events/payloads.d.ts.map +1 -0
- package/dist/workflows/events/payloads.js +337 -0
- package/dist/workflows/events/payloads.js.map +1 -0
- package/dist/workflows/events/replay.d.ts +238 -0
- package/dist/workflows/events/replay.d.ts.map +1 -0
- package/dist/workflows/events/replay.js +608 -0
- package/dist/workflows/events/replay.js.map +1 -0
- package/dist/workflows/events/schema.d.ts +5242 -0
- package/dist/workflows/events/schema.d.ts.map +1 -0
- package/dist/workflows/events/schema.js +295 -0
- package/dist/workflows/events/schema.js.map +1 -0
- package/dist/workflows/events/types.d.ts +34 -0
- package/dist/workflows/events/types.d.ts.map +1 -0
- package/dist/workflows/events/types.js +2 -0
- package/dist/workflows/events/types.js.map +1 -0
- package/dist/workflows/fanout.d.ts +36 -0
- package/dist/workflows/fanout.d.ts.map +1 -0
- package/dist/workflows/fanout.js +114 -0
- package/dist/workflows/fanout.js.map +1 -0
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts +41 -0
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/botmux-schedule.js +121 -0
- package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -0
- package/dist/workflows/hostExecutors/feishu-im.d.ts +12 -0
- package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/feishu-im.js +49 -0
- package/dist/workflows/hostExecutors/feishu-im.js.map +1 -0
- package/dist/workflows/hostExecutors/feishu-reply.d.ts +24 -0
- package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/feishu-reply.js +88 -0
- package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -0
- package/dist/workflows/hostExecutors/feishu-send.d.ts +23 -0
- package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/feishu-send.js +124 -0
- package/dist/workflows/hostExecutors/feishu-send.js.map +1 -0
- package/dist/workflows/hostExecutors/index.d.ts +8 -0
- package/dist/workflows/hostExecutors/index.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/index.js +8 -0
- package/dist/workflows/hostExecutors/index.js.map +1 -0
- package/dist/workflows/hostExecutors/protocol.d.ts +42 -0
- package/dist/workflows/hostExecutors/protocol.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/protocol.js +181 -0
- package/dist/workflows/hostExecutors/protocol.js.map +1 -0
- package/dist/workflows/hostExecutors/registry.d.ts +10 -0
- package/dist/workflows/hostExecutors/registry.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/registry.js +36 -0
- package/dist/workflows/hostExecutors/registry.js.map +1 -0
- package/dist/workflows/hostExecutors/types.d.ts +78 -0
- package/dist/workflows/hostExecutors/types.d.ts.map +1 -0
- package/dist/workflows/hostExecutors/types.js +2 -0
- package/dist/workflows/hostExecutors/types.js.map +1 -0
- package/dist/workflows/loader.d.ts +16 -0
- package/dist/workflows/loader.d.ts.map +1 -0
- package/dist/workflows/loader.js +56 -0
- package/dist/workflows/loader.js.map +1 -0
- package/dist/workflows/loop.d.ts +50 -0
- package/dist/workflows/loop.d.ts.map +1 -0
- package/dist/workflows/loop.js +350 -0
- package/dist/workflows/loop.js.map +1 -0
- package/dist/workflows/ops-projection.d.ts +168 -0
- package/dist/workflows/ops-projection.d.ts.map +1 -0
- package/dist/workflows/ops-projection.js +707 -0
- package/dist/workflows/ops-projection.js.map +1 -0
- package/dist/workflows/orchestrator.d.ts +107 -0
- package/dist/workflows/orchestrator.d.ts.map +1 -0
- package/dist/workflows/orchestrator.js +197 -0
- package/dist/workflows/orchestrator.js.map +1 -0
- package/dist/workflows/output-binding.d.ts +70 -0
- package/dist/workflows/output-binding.d.ts.map +1 -0
- package/dist/workflows/output-binding.js +265 -0
- package/dist/workflows/output-binding.js.map +1 -0
- package/dist/workflows/params.d.ts +61 -0
- package/dist/workflows/params.d.ts.map +1 -0
- package/dist/workflows/params.js +195 -0
- package/dist/workflows/params.js.map +1 -0
- package/dist/workflows/resume.d.ts +263 -0
- package/dist/workflows/resume.d.ts.map +1 -0
- package/dist/workflows/resume.js +808 -0
- package/dist/workflows/resume.js.map +1 -0
- package/dist/workflows/run-id.d.ts +2 -0
- package/dist/workflows/run-id.d.ts.map +1 -0
- package/dist/workflows/run-id.js +7 -0
- package/dist/workflows/run-id.js.map +1 -0
- package/dist/workflows/run-init.d.ts +48 -0
- package/dist/workflows/run-init.d.ts.map +1 -0
- package/dist/workflows/run-init.js +99 -0
- package/dist/workflows/run-init.js.map +1 -0
- package/dist/workflows/runs-dir.d.ts +4 -0
- package/dist/workflows/runs-dir.d.ts.map +1 -0
- package/dist/workflows/runs-dir.js +15 -0
- package/dist/workflows/runs-dir.js.map +1 -0
- package/dist/workflows/runtime.d.ts +211 -0
- package/dist/workflows/runtime.d.ts.map +1 -0
- package/dist/workflows/runtime.js +594 -0
- package/dist/workflows/runtime.js.map +1 -0
- package/dist/workflows/spawn-bot.d.ts +165 -0
- package/dist/workflows/spawn-bot.d.ts.map +1 -0
- package/dist/workflows/spawn-bot.js +215 -0
- package/dist/workflows/spawn-bot.js.map +1 -0
- package/dist/workflows/system.d.ts +49 -0
- package/dist/workflows/system.d.ts.map +1 -0
- package/dist/workflows/system.js +48 -0
- package/dist/workflows/system.js.map +1 -0
- package/dist/workflows/trigger-run.d.ts +70 -0
- package/dist/workflows/trigger-run.d.ts.map +1 -0
- package/dist/workflows/trigger-run.js +88 -0
- package/dist/workflows/trigger-run.js.map +1 -0
- package/dist/workflows/wait.d.ts +120 -0
- package/dist/workflows/wait.d.ts.map +1 -0
- package/dist/workflows/wait.js +181 -0
- package/dist/workflows/wait.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `botmux workflow <sub>` CLI subcommand handlers.
|
|
3
|
+
*
|
|
4
|
+
* v0 offline-runner: load a workflow definition, drive `runLoop` against
|
|
5
|
+
* a stub spawn, and print events to stdout. No daemon / no IM
|
|
6
|
+
* integration — used for smoke-testing the orchestrator end-to-end.
|
|
7
|
+
*
|
|
8
|
+
* The on-daemon path (with lark fan-out, real worker spawn) lives in
|
|
9
|
+
* the `/workflow run` Skill (Slice E-2). This module deliberately
|
|
10
|
+
* keeps the CLI route the simplest possible smoke test.
|
|
11
|
+
*/
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { ZodError } from 'zod';
|
|
15
|
+
import { EventLog } from '../workflows/events/append.js';
|
|
16
|
+
import { replay } from '../workflows/events/replay.js';
|
|
17
|
+
import { parseWorkflowDefinition } from '../workflows/definition.js';
|
|
18
|
+
import { loadWorkflowDefinition } from '../workflows/loader.js';
|
|
19
|
+
import { coerceWorkflowParams, ParamCoerceFailure, } from '../workflows/params.js';
|
|
20
|
+
import { runLoop } from '../workflows/loop.js';
|
|
21
|
+
import { mintWorkflowRunId } from '../workflows/run-id.js';
|
|
22
|
+
import { createRun } from '../workflows/run-init.js';
|
|
23
|
+
import { getRunsDir, runDir } from '../workflows/runs-dir.js';
|
|
24
|
+
import { createDefaultHostExecutorRegistry, createDefaultProviderReconcilers, } from '../workflows/hostExecutors/registry.js';
|
|
25
|
+
import { loadEffectInputSidecar } from '../workflows/effect-input.js';
|
|
26
|
+
import { cancelWorkflowRun, isTerminalRunStatus, } from '../workflows/cancel-run.js';
|
|
27
|
+
import { createStubSpawnFn, } from '../workflows/spawn-bot.js';
|
|
28
|
+
import { eventSeqFromId, extractEventContext, listRuns, } from '../workflows/ops-projection.js';
|
|
29
|
+
// Local arg parsers — mirror cli.ts shape; deliberately not exported.
|
|
30
|
+
function argValue(args, ...flags) {
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const a = args[i];
|
|
33
|
+
for (const f of flags) {
|
|
34
|
+
if (a === f && i + 1 < args.length)
|
|
35
|
+
return args[i + 1];
|
|
36
|
+
if (a.startsWith(f + '='))
|
|
37
|
+
return a.slice(f.length + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function positionals(args) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const a = args[i];
|
|
46
|
+
if (a.startsWith('--')) {
|
|
47
|
+
if (!a.includes('=') && i + 1 < args.length)
|
|
48
|
+
i++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
out.push(a);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
export async function cmdWorkflow(sub, rest) {
|
|
56
|
+
switch (sub) {
|
|
57
|
+
case 'run':
|
|
58
|
+
await cmdWorkflowRun(rest);
|
|
59
|
+
return;
|
|
60
|
+
case 'resume':
|
|
61
|
+
await cmdWorkflowResume(rest);
|
|
62
|
+
return;
|
|
63
|
+
case 'cancel':
|
|
64
|
+
await cmdWorkflowCancel(rest);
|
|
65
|
+
return;
|
|
66
|
+
case 'ls':
|
|
67
|
+
case 'list':
|
|
68
|
+
await cmdWorkflowLs(rest);
|
|
69
|
+
return;
|
|
70
|
+
case 'tail':
|
|
71
|
+
await cmdWorkflowTail(rest);
|
|
72
|
+
return;
|
|
73
|
+
case 'validate':
|
|
74
|
+
await cmdWorkflowValidate(rest);
|
|
75
|
+
return;
|
|
76
|
+
case 'show':
|
|
77
|
+
await cmdWorkflowShow(rest);
|
|
78
|
+
return;
|
|
79
|
+
case 'help':
|
|
80
|
+
case '':
|
|
81
|
+
case undefined:
|
|
82
|
+
printHelp();
|
|
83
|
+
return;
|
|
84
|
+
default:
|
|
85
|
+
console.error(`未知子命令: workflow ${sub}`);
|
|
86
|
+
printHelp();
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function printHelp() {
|
|
91
|
+
console.log(`用法: botmux workflow <run|resume|cancel|ls|tail|validate|show> [...]
|
|
92
|
+
|
|
93
|
+
子命令:
|
|
94
|
+
run <id> [--param key=value ...] [--param-json key=<json> ...] [--run-id <id>] [--bot-resolver echo]
|
|
95
|
+
离线驱动 workflow(stub spawn)。事件 / 状态打到 stdout。
|
|
96
|
+
humanGate 节点跑到 'awaiting-wait' 即退出(CLI 离线场景下没有审批入口)。
|
|
97
|
+
--param 适合标量(string/number/boolean);--param-json 适合 object/array
|
|
98
|
+
或希望严格保留 JSON 类型的值,例如 --param-json users='["a","b"]'。
|
|
99
|
+
未声明的 param 名会被拒;type 不匹配 / 缺 required 会清晰报错。
|
|
100
|
+
|
|
101
|
+
resume <runId>
|
|
102
|
+
从磁盘 runDir 冷恢复一个已有 run。R0 recovery 先收 dangling effect,
|
|
103
|
+
之后 orchestrator 继续推进;遇到 humanGate 只输出 awaiting-wait,
|
|
104
|
+
不伪造审批;run 已 terminal 则直接打摘要,零事件写入。
|
|
105
|
+
CLI 不会 spawn 新 subagent —— 现有 in-flight subagent 会被标记
|
|
106
|
+
WorkerCrashed/manual 并由 orchestrator 终结 run。
|
|
107
|
+
|
|
108
|
+
cancel <runId> [--reason <text>]
|
|
109
|
+
写入 run-level cancelRequested 并驱动 cancel recovery。terminal run
|
|
110
|
+
直接 no-op;不会发 IM 通知或重发审批卡。
|
|
111
|
+
|
|
112
|
+
ls [--all] [--status running,failed,...] [--wide] [--json]
|
|
113
|
+
列出 runsDir 下所有 run。默认仅 non-terminal;--all 全列;--status
|
|
114
|
+
支持逗号多选;--wide 增加 failedNodeId/chatId/larkAppId;--json
|
|
115
|
+
输出完整 JSON 行。
|
|
116
|
+
|
|
117
|
+
tail <runId> [--from <seq>] [--follow] [--json]
|
|
118
|
+
打印 run 的事件简表(seq / type / node / activity / errorCode)。
|
|
119
|
+
默认 history-only;--follow 才轮询 events.ndjson 增量。--from 默认 1。
|
|
120
|
+
|
|
121
|
+
validate <path>
|
|
122
|
+
校验 workflow.json 文件。成功打印 workflowId / node 数;失败打印
|
|
123
|
+
JSON parse、Zod issue path + message,或 graph invariant 错误。
|
|
124
|
+
|
|
125
|
+
show <runId>
|
|
126
|
+
replay 当前 run 的事件,打印 Snapshot 摘要 JSON(含 nodes/dangling 等)。
|
|
127
|
+
|
|
128
|
+
环境变量:
|
|
129
|
+
BOTMUX_WORKFLOW_RUNS_DIR=<path> 覆盖 runs 根目录(默认 ~/.botmux/workflow-runs)
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
// ─── run ──────────────────────────────────────────────────────────────────
|
|
133
|
+
async function cmdWorkflowRun(rest) {
|
|
134
|
+
const id = positionals(rest)[0];
|
|
135
|
+
if (!id) {
|
|
136
|
+
console.error('用法: botmux workflow run <id> [--param key=value ...] [--param-json key=<json> ...]');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const runId = argValue(rest, '--run-id') ?? mintWorkflowRunId(id);
|
|
140
|
+
const rawParams = collectRawParams(rest);
|
|
141
|
+
const def = await loadWorkflowDefinition(id).catch((err) => {
|
|
142
|
+
console.error(err.message);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
145
|
+
// unreachable after process.exit, but TS doesn't know
|
|
146
|
+
if (!def)
|
|
147
|
+
return;
|
|
148
|
+
let params;
|
|
149
|
+
try {
|
|
150
|
+
params = coerceWorkflowParams(def, rawParams);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err instanceof ParamCoerceFailure) {
|
|
154
|
+
console.error('参数校验失败:');
|
|
155
|
+
for (const issue of err.issues) {
|
|
156
|
+
console.error(`- ${issue.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.error(`参数校验失败:${err instanceof Error ? err.message : String(err)}`);
|
|
161
|
+
}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
// Bootstrap the in-memory bot registry so hostExecutors like
|
|
165
|
+
// feishu-send can resolve `larkAppId` → Lark client. IM path inherits
|
|
166
|
+
// the daemon's already-registered bots; the standalone CLI doesn't.
|
|
167
|
+
try {
|
|
168
|
+
const { registerBot, loadBotConfigs } = await import('../bot-registry.js');
|
|
169
|
+
for (const cfg of loadBotConfigs())
|
|
170
|
+
registerBot(cfg);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Missing/invalid bots.json is fine — workflows that don't touch
|
|
174
|
+
// Feishu still run; the host executor will surface a clear
|
|
175
|
+
// "Bot not registered" error if one does.
|
|
176
|
+
}
|
|
177
|
+
const log = new EventLog(runId, getRunsDir());
|
|
178
|
+
const botResolver = () => ({});
|
|
179
|
+
const spawnSubagent = createStubSpawnFn(echoHandler);
|
|
180
|
+
console.log(`workflow=${id} runId=${runId} params=${JSON.stringify(params)}`);
|
|
181
|
+
console.log(`runsDir=${getRunsDir()}`);
|
|
182
|
+
await createRun(log, { def, params, initiator: 'cli', botResolver });
|
|
183
|
+
console.log('runCreated, runStarted');
|
|
184
|
+
const ctx = {
|
|
185
|
+
log,
|
|
186
|
+
def,
|
|
187
|
+
spawnSubagent,
|
|
188
|
+
hostExecutors: createDefaultHostExecutorRegistry(),
|
|
189
|
+
reconcilers: createDefaultProviderReconcilers(),
|
|
190
|
+
loadEffectInput: (activityId, attemptId) => loadEffectInputSidecar(log, activityId, attemptId),
|
|
191
|
+
};
|
|
192
|
+
const result = await runLoop(ctx, { maxTicks: 200 });
|
|
193
|
+
console.log(`\nloop stopped: ${result.reason} after ${result.ticks} tick(s)`);
|
|
194
|
+
console.log(`run.status=${result.lastSnapshot.run.status}`);
|
|
195
|
+
console.log(`events: ${result.lastSnapshot.lastSeq}`);
|
|
196
|
+
if (result.reason === 'awaiting-wait') {
|
|
197
|
+
console.log(`awaiting-wait on: ${result.lastSnapshot.danglingWaits.join(', ')}`);
|
|
198
|
+
console.log(`(CLI 离线模式没有审批入口;从 IM 用 /workflow run 跑能拿到审批卡)`);
|
|
199
|
+
}
|
|
200
|
+
if (result.reason === 'terminal' && result.lastSnapshot.run.output) {
|
|
201
|
+
console.log(`output: ${result.lastSnapshot.run.output.outputHash}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const echoHandler = (input) => ({
|
|
205
|
+
echo: input.prompt.slice(0, 200),
|
|
206
|
+
bot: input.botName,
|
|
207
|
+
activityId: input.activityId,
|
|
208
|
+
});
|
|
209
|
+
// ─── resume ───────────────────────────────────────────────────────────────
|
|
210
|
+
/**
|
|
211
|
+
* R1 cold resume — pick up an existing run from its on-disk runDir.
|
|
212
|
+
*
|
|
213
|
+
* Contract (codex-loopy review 2026-05-20):
|
|
214
|
+
* - Replay first. If the run is already terminal, print summary and
|
|
215
|
+
* write zero events.
|
|
216
|
+
* - Do NOT call `createRun` / write `runStarted` — those are mint-time
|
|
217
|
+
* events. Resume just attaches a fresh ctx to the existing log.
|
|
218
|
+
* - `spawnSubagent` is a no-throw failure stub: returns
|
|
219
|
+
* `WorkerCrashed/manual` so any subagent dispatch the orchestrator
|
|
220
|
+
* decides to do during resume lands as a recorded `activityFailed`
|
|
221
|
+
* (NOT a thrown JS error that would crash the CLI). manual class
|
|
222
|
+
* prevents R0 from auto-retrying.
|
|
223
|
+
* - hostExecutors / reconcilers / loadEffectInput are wired so the
|
|
224
|
+
* recovery phase can settle dangling side-effects via reconciler.
|
|
225
|
+
*
|
|
226
|
+
* Out of scope for R1: daemon-startup scan, watcher rebuild, real worker
|
|
227
|
+
* reattach, dashboard surface.
|
|
228
|
+
*/
|
|
229
|
+
async function cmdWorkflowResume(rest) {
|
|
230
|
+
const runId = positionals(rest)[0];
|
|
231
|
+
if (!runId) {
|
|
232
|
+
console.error('用法: botmux workflow resume <runId>');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const runsDir = getRunsDir();
|
|
236
|
+
const dir = runDir(runId, runsDir);
|
|
237
|
+
const workflowJsonPath = join(dir, 'workflow.json');
|
|
238
|
+
let defRaw;
|
|
239
|
+
try {
|
|
240
|
+
defRaw = await fs.readFile(workflowJsonPath, 'utf-8');
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (err.code === 'ENOENT') {
|
|
244
|
+
console.error(`找不到 runDir 的 workflow.json:${workflowJsonPath}`);
|
|
245
|
+
console.error(`(runsDir=${runsDir};用 BOTMUX_WORKFLOW_RUNS_DIR 覆盖)`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.error(`读取 ${workflowJsonPath} 失败:${err.message}`);
|
|
249
|
+
}
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
let def;
|
|
253
|
+
try {
|
|
254
|
+
def = parseWorkflowDefinition(JSON.parse(defRaw));
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error(`解析 ${workflowJsonPath} 失败:${err.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Same as run: load bots so feishu host executors can resolve larkAppId.
|
|
262
|
+
try {
|
|
263
|
+
const { registerBot, loadBotConfigs } = await import('../bot-registry.js');
|
|
264
|
+
for (const cfg of loadBotConfigs())
|
|
265
|
+
registerBot(cfg);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// bots.json missing/invalid is fine — workflows that don't touch IM
|
|
269
|
+
// still resume; IM-touching steps will surface a clear error.
|
|
270
|
+
}
|
|
271
|
+
const log = new EventLog(runId, runsDir);
|
|
272
|
+
const events = await log.readAll();
|
|
273
|
+
if (events.length === 0) {
|
|
274
|
+
console.error(`runId=${runId} 没找到任何事件 (runsDir=${runsDir})`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const { replay } = await import('../workflows/events/replay.js');
|
|
278
|
+
const initialSnap = replay(events);
|
|
279
|
+
console.log(`workflow=${def.workflowId} runId=${runId}`);
|
|
280
|
+
console.log(`runsDir=${runsDir}`);
|
|
281
|
+
// ── Terminal short-circuit ────────────────────────────────────────────
|
|
282
|
+
// Per codex review: replay first; if the run already finished, print
|
|
283
|
+
// summary and DON'T enter runLoop (no new events written).
|
|
284
|
+
if (initialSnap.run.status === 'succeeded' ||
|
|
285
|
+
initialSnap.run.status === 'failed' ||
|
|
286
|
+
initialSnap.run.status === 'cancelled') {
|
|
287
|
+
console.log(`\nrun.status=${initialSnap.run.status} (terminal — nothing to resume)`);
|
|
288
|
+
console.log(`events: ${initialSnap.lastSeq}`);
|
|
289
|
+
if (initialSnap.run.output) {
|
|
290
|
+
console.log(`output: ${initialSnap.run.output.outputHash}`);
|
|
291
|
+
}
|
|
292
|
+
if (initialSnap.run.status !== 'succeeded') {
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const spawnSubagent = async (input) => ({
|
|
298
|
+
kind: 'failure',
|
|
299
|
+
errorCode: 'WorkerCrashed',
|
|
300
|
+
errorClass: 'manual',
|
|
301
|
+
errorMessage: `subagent '${input.botName}' (node=${input.nodeId}, activity=${input.activityId}) ` +
|
|
302
|
+
`is not resumable via 'botmux workflow resume' — CLI does not spawn workers. ` +
|
|
303
|
+
`Use IM /workflow run for full execution, or restart the run.`,
|
|
304
|
+
});
|
|
305
|
+
const ctx = {
|
|
306
|
+
log,
|
|
307
|
+
def: def,
|
|
308
|
+
spawnSubagent,
|
|
309
|
+
hostExecutors: createDefaultHostExecutorRegistry(),
|
|
310
|
+
reconcilers: createDefaultProviderReconcilers(),
|
|
311
|
+
loadEffectInput: (activityId, attemptId) => loadEffectInputSidecar(log, activityId, attemptId),
|
|
312
|
+
};
|
|
313
|
+
const result = await runLoop(ctx, { maxTicks: 200 });
|
|
314
|
+
console.log(`\nloop stopped: ${result.reason} after ${result.ticks} tick(s)`);
|
|
315
|
+
console.log(`run.status=${result.lastSnapshot.run.status}`);
|
|
316
|
+
console.log(`events: ${result.lastSnapshot.lastSeq}`);
|
|
317
|
+
if (result.reason === 'awaiting-wait') {
|
|
318
|
+
console.log(`awaiting-wait on: ${result.lastSnapshot.danglingWaits.join(', ')}`);
|
|
319
|
+
console.log(`(CLI resume 不发卡;从 IM 用 /workflow run 进的话审批入口在那边)`);
|
|
320
|
+
}
|
|
321
|
+
if (result.reason === 'no-progress') {
|
|
322
|
+
if (result.lastSnapshot.danglingEffectAttempted.length > 0) {
|
|
323
|
+
console.log(`dangling effects: ${result.lastSnapshot.danglingEffectAttempted.join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
const danglingNonEffect = result.lastSnapshot.danglingActivities.filter((a) => !result.lastSnapshot.danglingEffectAttempted.includes(a));
|
|
326
|
+
if (danglingNonEffect.length > 0) {
|
|
327
|
+
console.log(`dangling activities (non-effect): ${danglingNonEffect.join(', ')}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (result.reason === 'terminal' && result.lastSnapshot.run.output) {
|
|
331
|
+
console.log(`output: ${result.lastSnapshot.run.output.outputHash}`);
|
|
332
|
+
}
|
|
333
|
+
// Non-zero exit when the run did not resolve to a clean terminal/awaiting.
|
|
334
|
+
if (result.reason !== 'terminal' &&
|
|
335
|
+
result.reason !== 'awaiting-wait') {
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
if (result.reason === 'terminal' && result.lastSnapshot.run.status !== 'succeeded') {
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// ─── cancel ───────────────────────────────────────────────────────────────
|
|
343
|
+
async function cmdWorkflowCancel(rest) {
|
|
344
|
+
const runId = positionals(rest)[0];
|
|
345
|
+
if (!runId) {
|
|
346
|
+
console.error('用法: botmux workflow cancel <runId> [--reason <text>]');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const reason = argValue(rest, '--reason') ?? 'cancelled via botmux workflow cancel';
|
|
350
|
+
const runsDir = getRunsDir();
|
|
351
|
+
const log = new EventLog(runId, runsDir);
|
|
352
|
+
const def = await loadRunWorkflowDefinition(runId, runsDir);
|
|
353
|
+
let snapshot = replay(await readExistingRunEvents(log, runsDir, runId));
|
|
354
|
+
console.log(`workflow=${def.workflowId} runId=${runId}`);
|
|
355
|
+
console.log(`runsDir=${runsDir}`);
|
|
356
|
+
if (isTerminalRunStatus(snapshot.run.status)) {
|
|
357
|
+
console.log(`\nrun.status=${snapshot.run.status} (terminal — nothing to cancel)`);
|
|
358
|
+
console.log(`events: ${snapshot.lastSeq}`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const ctx = workflowCliRuntimeContext(log, def, cliResumeSpawnSubagent);
|
|
362
|
+
const result = await cancelWorkflowRun({
|
|
363
|
+
ctx,
|
|
364
|
+
reason,
|
|
365
|
+
by: 'cli',
|
|
366
|
+
actor: 'human',
|
|
367
|
+
maxTicks: 200,
|
|
368
|
+
});
|
|
369
|
+
snapshot = result.snapshot;
|
|
370
|
+
if (result.cancelEventId) {
|
|
371
|
+
console.log(result.cancelAlreadyRequested
|
|
372
|
+
? `cancel already requested: ${result.cancelEventId}`
|
|
373
|
+
: `cancelRequested: ${result.cancelEventId}`);
|
|
374
|
+
}
|
|
375
|
+
console.log(`\nloop stopped: ${result.loopResult?.reason ?? 'terminal'} ` +
|
|
376
|
+
`after ${result.loopResult?.ticks ?? 0} tick(s)`);
|
|
377
|
+
console.log(`run.status=${snapshot.run.status}`);
|
|
378
|
+
console.log(`events: ${snapshot.lastSeq}`);
|
|
379
|
+
if (snapshot.danglingCancels.length > 0) {
|
|
380
|
+
console.log(`dangling cancels: ${snapshot.danglingCancels.join(', ')}`);
|
|
381
|
+
}
|
|
382
|
+
if (snapshot.danglingEffectAttempted.length > 0) {
|
|
383
|
+
console.log(`dangling effects: ${snapshot.danglingEffectAttempted.join(', ')}`);
|
|
384
|
+
}
|
|
385
|
+
if (snapshot.danglingWaits.length > 0) {
|
|
386
|
+
console.log(`dangling waits: ${snapshot.danglingWaits.join(', ')}`);
|
|
387
|
+
}
|
|
388
|
+
if (snapshot.run.status !== 'cancelled') {
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function workflowCliRuntimeContext(log, def, spawnSubagent) {
|
|
393
|
+
return {
|
|
394
|
+
log,
|
|
395
|
+
def,
|
|
396
|
+
spawnSubagent,
|
|
397
|
+
hostExecutors: createDefaultHostExecutorRegistry(),
|
|
398
|
+
reconcilers: createDefaultProviderReconcilers(),
|
|
399
|
+
loadEffectInput: (activityId, attemptId) => loadEffectInputSidecar(log, activityId, attemptId),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const cliResumeSpawnSubagent = async (input) => ({
|
|
403
|
+
kind: 'failure',
|
|
404
|
+
errorCode: 'WorkerCrashed',
|
|
405
|
+
errorClass: 'manual',
|
|
406
|
+
errorMessage: `subagent '${input.botName}' (node=${input.nodeId}, activity=${input.activityId}) ` +
|
|
407
|
+
`is not resumable via 'botmux workflow resume' — CLI does not spawn workers. ` +
|
|
408
|
+
`Use IM /workflow run for full execution, or restart the run.`,
|
|
409
|
+
});
|
|
410
|
+
async function loadRunWorkflowDefinition(runId, runsDir = getRunsDir()) {
|
|
411
|
+
const workflowJsonPath = join(runDir(runId, runsDir), 'workflow.json');
|
|
412
|
+
let defRaw;
|
|
413
|
+
try {
|
|
414
|
+
defRaw = await fs.readFile(workflowJsonPath, 'utf-8');
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
if (err.code === 'ENOENT') {
|
|
418
|
+
console.error(`找不到 runDir 的 workflow.json:${workflowJsonPath}`);
|
|
419
|
+
console.error(`(runsDir=${runsDir};用 BOTMUX_WORKFLOW_RUNS_DIR 覆盖)`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.error(`读取 ${workflowJsonPath} 失败:${err.message}`);
|
|
423
|
+
}
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
return parseWorkflowDefinition(JSON.parse(defRaw));
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
console.error(`解析 ${workflowJsonPath} 失败:${err.message}`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function readExistingRunEvents(log, runsDir, runId) {
|
|
435
|
+
const events = await log.readAll();
|
|
436
|
+
if (events.length === 0) {
|
|
437
|
+
console.error(`runId=${runId} 没找到任何事件 (runsDir=${runsDir})`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
return events;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Parse CLI args into raw param inputs. Each `--param key=value` carries a
|
|
444
|
+
* plain string (type coercion happens in `coerceWorkflowParams` against the
|
|
445
|
+
* workflow's `params` schema); each `--param-json key=<json>` carries a
|
|
446
|
+
* parsed JSON value, which is the only way to thread `object` / `array`
|
|
447
|
+
* params (or numbers / booleans you'd rather not stringify) into a run.
|
|
448
|
+
*
|
|
449
|
+
* Both flags accept the `--flag value` and `--flag=value` forms.
|
|
450
|
+
*/
|
|
451
|
+
function collectRawParams(rest) {
|
|
452
|
+
const out = {};
|
|
453
|
+
const ingestStringKV = (kv) => {
|
|
454
|
+
const eq = kv.indexOf('=');
|
|
455
|
+
if (eq <= 0) {
|
|
456
|
+
console.error(`--param 期望 key=value,收到 "${kv}"`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
out[kv.slice(0, eq)] = { kind: 'string', value: kv.slice(eq + 1) };
|
|
460
|
+
};
|
|
461
|
+
const ingestJsonKV = (kv) => {
|
|
462
|
+
const eq = kv.indexOf('=');
|
|
463
|
+
if (eq <= 0) {
|
|
464
|
+
console.error(`--param-json 期望 key=<json>,收到 "${kv}"`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
const key = kv.slice(0, eq);
|
|
468
|
+
const jsonText = kv.slice(eq + 1);
|
|
469
|
+
try {
|
|
470
|
+
out[key] = { kind: 'json', value: JSON.parse(jsonText) };
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
console.error(`--param-json ${key} 的 JSON 解析失败:` +
|
|
474
|
+
(err instanceof Error ? err.message : String(err)));
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
for (let i = 0; i < rest.length; i++) {
|
|
479
|
+
if (rest[i] === '--param' && i + 1 < rest.length) {
|
|
480
|
+
ingestStringKV(rest[i + 1]);
|
|
481
|
+
i++;
|
|
482
|
+
}
|
|
483
|
+
else if (rest[i]?.startsWith('--param=')) {
|
|
484
|
+
ingestStringKV(rest[i].slice('--param='.length));
|
|
485
|
+
}
|
|
486
|
+
else if (rest[i] === '--param-json' && i + 1 < rest.length) {
|
|
487
|
+
ingestJsonKV(rest[i + 1]);
|
|
488
|
+
i++;
|
|
489
|
+
}
|
|
490
|
+
else if (rest[i]?.startsWith('--param-json=')) {
|
|
491
|
+
ingestJsonKV(rest[i].slice('--param-json='.length));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
// ─── validate ─────────────────────────────────────────────────────────────
|
|
497
|
+
async function cmdWorkflowValidate(rest) {
|
|
498
|
+
const path = positionals(rest)[0];
|
|
499
|
+
if (!path) {
|
|
500
|
+
console.error('用法: botmux workflow validate <path>');
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
let rawText;
|
|
504
|
+
try {
|
|
505
|
+
rawText = await fs.readFile(path, 'utf-8');
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
console.error(`读取 ${path} 失败:${err.message}`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
let raw;
|
|
512
|
+
try {
|
|
513
|
+
raw = JSON.parse(rawText);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
console.error(`解析 JSON 失败:${err.message}`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const def = parseWorkflowDefinition(raw);
|
|
521
|
+
console.log(`workflow valid: ${def.workflowId} ` +
|
|
522
|
+
`(version=${def.version}, nodes=${Object.keys(def.nodes).length})`);
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
console.error(`workflow invalid: ${path}`);
|
|
526
|
+
if (err instanceof ZodError) {
|
|
527
|
+
for (const issue of err.issues) {
|
|
528
|
+
const p = issue.path.length ? issue.path.join('.') : '<root>';
|
|
529
|
+
console.error(`- ${p}: ${issue.message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
console.error(`- ${err instanceof Error ? err.message : String(err)}`);
|
|
534
|
+
}
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// ─── ls ───────────────────────────────────────────────────────────────────
|
|
539
|
+
/**
|
|
540
|
+
* `botmux workflow ls` — operator surface for "what's running on disk?"
|
|
541
|
+
*
|
|
542
|
+
* Read-only: walks runsDir/<runId>/events.ndjson, replays each, projects a
|
|
543
|
+
* row. By default lists only non-terminal runs (the typical operator
|
|
544
|
+
* question: "what's still hot?"). Terminal runs are useful for triage
|
|
545
|
+
* and stay one `--all` flag away.
|
|
546
|
+
*
|
|
547
|
+
* Output:
|
|
548
|
+
* - default: aligned table on stdout. Column set tuned to fit ~120
|
|
549
|
+
* cols: runId | workflowId | status | lastSeq | dEf/dAct/dWait | updatedAt
|
|
550
|
+
* - `--wide`: appends failedNodeId / chatId / larkAppId.
|
|
551
|
+
* - `--json`: one JSON object per line (machine-parseable).
|
|
552
|
+
*
|
|
553
|
+
* Filters:
|
|
554
|
+
* - `--all`: include terminal (succeeded/failed/cancelled).
|
|
555
|
+
* - `--status running,failed`: comma-separated set; overrides `--all`.
|
|
556
|
+
*/
|
|
557
|
+
async function cmdWorkflowLs(rest) {
|
|
558
|
+
const all = rest.includes('--all');
|
|
559
|
+
const wide = rest.includes('--wide');
|
|
560
|
+
const json = rest.includes('--json');
|
|
561
|
+
const statusFilter = argValue(rest, '--status');
|
|
562
|
+
const wantStatuses = statusFilter
|
|
563
|
+
? new Set(statusFilter.split(',').map((s) => s.trim()).filter(Boolean))
|
|
564
|
+
: undefined;
|
|
565
|
+
const runsDir = getRunsDir();
|
|
566
|
+
let rows;
|
|
567
|
+
try {
|
|
568
|
+
rows = await listRuns(runsDir, {
|
|
569
|
+
all,
|
|
570
|
+
statuses: wantStatuses,
|
|
571
|
+
// chat-binding columns are only printed in --wide or --json; skip the
|
|
572
|
+
// extra fs op otherwise.
|
|
573
|
+
includeBinding: wide || json,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
console.error(`读取 ${runsDir} 失败:${err.message}`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
if (json) {
|
|
581
|
+
for (const r of rows)
|
|
582
|
+
console.log(JSON.stringify(r));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (rows.length === 0) {
|
|
586
|
+
console.log('(no runs match)');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const headers = wide
|
|
590
|
+
? ['RUN_ID', 'WORKFLOW', 'STATUS', 'LAST_SEQ', 'dEf/dAct/dWait', 'UPDATED', 'FAILED_NODE', 'CHAT_ID', 'LARK_APP']
|
|
591
|
+
: ['RUN_ID', 'WORKFLOW', 'STATUS', 'LAST_SEQ', 'dEf/dAct/dWait', 'UPDATED'];
|
|
592
|
+
const rowCells = rows.map((r) => {
|
|
593
|
+
const dangling = `${r.dEf}/${r.dAct}/${r.dWait}`;
|
|
594
|
+
const updated = new Date(r.updatedAt).toISOString().slice(0, 19).replace('T', ' ');
|
|
595
|
+
const base = [r.runId, r.workflowId, r.status, String(r.lastSeq), dangling, updated];
|
|
596
|
+
if (!wide)
|
|
597
|
+
return base;
|
|
598
|
+
return [...base, r.failedNodeId ?? '-', r.chatId ?? '-', r.larkAppId ?? '-'];
|
|
599
|
+
});
|
|
600
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rowCells.map((row) => row[i].length)));
|
|
601
|
+
const pad = (s, w) => s + ' '.repeat(w - s.length);
|
|
602
|
+
console.log(headers.map((h, i) => pad(h, widths[i])).join(' '));
|
|
603
|
+
for (const cells of rowCells) {
|
|
604
|
+
console.log(cells.map((c, i) => pad(c, widths[i])).join(' '));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// ─── tail ─────────────────────────────────────────────────────────────────
|
|
608
|
+
/**
|
|
609
|
+
* `botmux workflow tail <runId>` — operator surface for "show me the
|
|
610
|
+
* event stream of this run".
|
|
611
|
+
*
|
|
612
|
+
* Default mode is history-only (codex review 2026-05-20): print every
|
|
613
|
+
* event from `--from` (default 1) and exit. CLI defaults that hang are
|
|
614
|
+
* a footgun for scripts and tests — `--follow` is the opt-in that turns
|
|
615
|
+
* on the watch loop.
|
|
616
|
+
*
|
|
617
|
+
* Follow strategy: poll `fs.stat` on the events.ndjson file at 200ms
|
|
618
|
+
* cadence and incrementally read new bytes from the recorded offset.
|
|
619
|
+
* NDJSON makes the boundary handling trivial — we only emit on `\n`.
|
|
620
|
+
* Truncation / rotation isn't supported here (events.ndjson is
|
|
621
|
+
* append-only by design); if the file shrinks we surface a warning.
|
|
622
|
+
*/
|
|
623
|
+
async function cmdWorkflowTail(rest) {
|
|
624
|
+
const runId = positionals(rest)[0];
|
|
625
|
+
if (!runId) {
|
|
626
|
+
console.error('用法: botmux workflow tail <runId> [--from <seq>] [--follow] [--json]');
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
const fromArg = argValue(rest, '--from');
|
|
630
|
+
const fromSeq = fromArg ? Number(fromArg) : 1;
|
|
631
|
+
if (!Number.isFinite(fromSeq) || fromSeq < 1) {
|
|
632
|
+
console.error(`--from 必须是 >=1 的整数,收到 "${fromArg}"`);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const follow = rest.includes('--follow') || rest.includes('-f');
|
|
636
|
+
const json = rest.includes('--json');
|
|
637
|
+
const runsDir = getRunsDir();
|
|
638
|
+
const eventsPath = join(runsDir, runId, 'events.ndjson');
|
|
639
|
+
const log = new EventLog(runId, runsDir);
|
|
640
|
+
// Capture the watch starting offset BEFORE readAll so that any event
|
|
641
|
+
// appended between readAll and the first stat is still picked up by
|
|
642
|
+
// the watch loop (lastSeq dedups any overlap). Codex review (O1
|
|
643
|
+
// medium #1): if we stat AFTER readAll, a race-window event lands
|
|
644
|
+
// past readAll's view but inside the offset, and follow silently
|
|
645
|
+
// skips it forever.
|
|
646
|
+
let followOffset = 0;
|
|
647
|
+
if (follow) {
|
|
648
|
+
try {
|
|
649
|
+
followOffset = (await fs.stat(eventsPath)).size;
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// events.ndjson must exist if readAll below succeeds; defensive
|
|
653
|
+
// fallback keeps offset 0 so the watch re-reads the whole file
|
|
654
|
+
// and lastSeq still dedups.
|
|
655
|
+
followOffset = 0;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let initial;
|
|
659
|
+
try {
|
|
660
|
+
initial = await log.readAll();
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
console.error(`读取 ${eventsPath} 失败:${err.message}`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
if (initial.length === 0) {
|
|
667
|
+
console.error(`runId=${runId} 没找到任何事件 (runsDir=${runsDir})`);
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
for (const ev of initial) {
|
|
671
|
+
const seq = eventSeqFromId(ev.eventId);
|
|
672
|
+
if (seq < fromSeq)
|
|
673
|
+
continue;
|
|
674
|
+
printEventLine(ev, json);
|
|
675
|
+
}
|
|
676
|
+
if (!follow)
|
|
677
|
+
return;
|
|
678
|
+
// Watch loop. Resume from `followOffset` (captured pre-readAll); parse
|
|
679
|
+
// incrementally by line. Stop on Ctrl-C; until then we never resolve.
|
|
680
|
+
let offset = followOffset;
|
|
681
|
+
let lastSeq = eventSeqFromId(initial[initial.length - 1].eventId);
|
|
682
|
+
let buffer = '';
|
|
683
|
+
process.on('SIGINT', () => process.exit(0));
|
|
684
|
+
while (true) {
|
|
685
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
686
|
+
const stat = await fs.stat(eventsPath).catch(() => null);
|
|
687
|
+
if (!stat)
|
|
688
|
+
continue;
|
|
689
|
+
if (stat.size < offset) {
|
|
690
|
+
console.error(`(events.ndjson 大小回退 ${offset} → ${stat.size},停止 tail)`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (stat.size === offset)
|
|
694
|
+
continue;
|
|
695
|
+
const fd = await fs.open(eventsPath, 'r');
|
|
696
|
+
try {
|
|
697
|
+
const chunk = Buffer.alloc(stat.size - offset);
|
|
698
|
+
await fd.read(chunk, 0, chunk.length, offset);
|
|
699
|
+
offset = stat.size;
|
|
700
|
+
buffer += chunk.toString('utf-8');
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
await fd.close();
|
|
704
|
+
}
|
|
705
|
+
let nl;
|
|
706
|
+
while ((nl = buffer.indexOf('\n')) >= 0) {
|
|
707
|
+
const line = buffer.slice(0, nl);
|
|
708
|
+
buffer = buffer.slice(nl + 1);
|
|
709
|
+
if (!line.trim())
|
|
710
|
+
continue;
|
|
711
|
+
let ev;
|
|
712
|
+
try {
|
|
713
|
+
ev = JSON.parse(line);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (typeof ev?.eventId !== 'string')
|
|
719
|
+
continue;
|
|
720
|
+
const seq = eventSeqFromId(ev.eventId);
|
|
721
|
+
if (seq <= lastSeq)
|
|
722
|
+
continue;
|
|
723
|
+
lastSeq = seq;
|
|
724
|
+
if (seq < fromSeq)
|
|
725
|
+
continue;
|
|
726
|
+
printEventLine(ev, json);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function printEventLine(ev, json) {
|
|
731
|
+
if (json) {
|
|
732
|
+
console.log(JSON.stringify(ev));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const e = ev;
|
|
736
|
+
const seq = String(eventSeqFromId(e.eventId)).padStart(4);
|
|
737
|
+
const type = e.type.padEnd(22);
|
|
738
|
+
const ctx = extractEventContext(e.payload);
|
|
739
|
+
const parts = [];
|
|
740
|
+
if (ctx.nodeId)
|
|
741
|
+
parts.push('node=' + ctx.nodeId);
|
|
742
|
+
if (ctx.activityId)
|
|
743
|
+
parts.push('act=' + ctx.activityId);
|
|
744
|
+
const where = parts.join(' ');
|
|
745
|
+
const err = ctx.errorCode ? ' err=' + ctx.errorCode : '';
|
|
746
|
+
console.log(seq + ' ' + type + ' ' + where + err);
|
|
747
|
+
}
|
|
748
|
+
// ─── show ─────────────────────────────────────────────────────────────────
|
|
749
|
+
async function cmdWorkflowShow(rest) {
|
|
750
|
+
const runId = positionals(rest)[0];
|
|
751
|
+
if (!runId) {
|
|
752
|
+
console.error('用法: botmux workflow show <runId>');
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
const { replay } = await import('../workflows/events/replay.js');
|
|
756
|
+
const log = new EventLog(runId, getRunsDir());
|
|
757
|
+
const events = await log.readAll();
|
|
758
|
+
if (events.length === 0) {
|
|
759
|
+
console.error(`runId=${runId} 没找到任何事件 (runsDir=${getRunsDir()})`);
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
const snap = replay(events);
|
|
763
|
+
console.log(JSON.stringify({
|
|
764
|
+
runId,
|
|
765
|
+
workflowId: snap.run.workflowId,
|
|
766
|
+
revisionId: snap.run.revisionId,
|
|
767
|
+
status: snap.run.status,
|
|
768
|
+
lastSeq: snap.lastSeq,
|
|
769
|
+
nodes: [...snap.nodes.entries()].map(([id, n]) => ({
|
|
770
|
+
id,
|
|
771
|
+
status: n.status,
|
|
772
|
+
retryCount: n.retryCount,
|
|
773
|
+
})),
|
|
774
|
+
danglingActivities: snap.danglingActivities,
|
|
775
|
+
danglingWaits: snap.danglingWaits,
|
|
776
|
+
}, null, 2));
|
|
777
|
+
// `parseWorkflowDefinition` re-exported here only so the bundler keeps it
|
|
778
|
+
// alongside loader (some smoke tests dlopen the helpers directly).
|
|
779
|
+
void parseWorkflowDefinition;
|
|
780
|
+
}
|
|
781
|
+
//# sourceMappingURL=workflow.js.map
|