@xdxer/dingtalk-agent 0.1.1 → 0.1.4-beta.0

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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -76
  3. package/dist/bin/dingtalk-agent.js +763 -0
  4. package/dist/bin/dingtalk-agent.js.map +1 -0
  5. package/dist/src/actions.js +562 -0
  6. package/dist/src/actions.js.map +1 -0
  7. package/dist/src/boot.js +70 -0
  8. package/dist/src/boot.js.map +1 -0
  9. package/dist/src/bootstrap.js +144 -0
  10. package/dist/src/bootstrap.js.map +1 -0
  11. package/dist/src/config.js +86 -0
  12. package/dist/src/config.js.map +1 -0
  13. package/dist/src/doctor.js +166 -0
  14. package/dist/src/doctor.js.map +1 -0
  15. package/dist/src/driver.js +45 -0
  16. package/dist/src/driver.js.map +1 -0
  17. package/{src → dist/src}/duty.js +42 -44
  18. package/dist/src/duty.js.map +1 -0
  19. package/dist/src/dws.js +270 -0
  20. package/dist/src/dws.js.map +1 -0
  21. package/dist/src/events.js +233 -0
  22. package/dist/src/events.js.map +1 -0
  23. package/dist/src/fields.js +132 -0
  24. package/dist/src/fields.js.map +1 -0
  25. package/dist/src/init.js +41 -0
  26. package/dist/src/init.js.map +1 -0
  27. package/dist/src/kb.js +240 -0
  28. package/dist/src/kb.js.map +1 -0
  29. package/dist/src/package-root.js +17 -0
  30. package/dist/src/package-root.js.map +1 -0
  31. package/dist/src/runs.js +79 -0
  32. package/dist/src/runs.js.map +1 -0
  33. package/dist/src/sessions.js +668 -0
  34. package/dist/src/sessions.js.map +1 -0
  35. package/dist/src/setup.js +101 -0
  36. package/dist/src/setup.js.map +1 -0
  37. package/dist/src/skill-manager.js +288 -0
  38. package/dist/src/skill-manager.js.map +1 -0
  39. package/dist/src/skills.js +200 -0
  40. package/dist/src/skills.js.map +1 -0
  41. package/dist/src/types.js +2 -0
  42. package/dist/src/types.js.map +1 -0
  43. package/dist/src/waits.js +945 -0
  44. package/dist/src/waits.js.map +1 -0
  45. package/dist/src/workspace.js +173 -0
  46. package/dist/src/workspace.js.map +1 -0
  47. package/docs/ARCHITECTURE.md +217 -0
  48. package/docs/INSTALLATION.md +135 -0
  49. package/docs/MINIMAL-WORKSPACE-V1.md +172 -0
  50. package/docs/OPEN-SOURCE-REFERENCES.md +107 -0
  51. package/docs/SELF-TEST.md +252 -0
  52. package/docs/architecture/dingtalk-agent-blueprint.png +0 -0
  53. package/docs/architecture/dingtalk-agent-blueprint.svg +144 -0
  54. package/docs/architecture/durable-async-agent-runtime.png +0 -0
  55. package/docs/architecture/durable-async-agent-runtime.svg +234 -0
  56. package/docs//345/206/205/347/275/221/345/256/236/347/233/270.md +77 -0
  57. package/evals/baselines/2026-07-14/behavior-summary.json +28 -0
  58. package/evals/baselines/2026-07-14/contract-summary.json +18 -0
  59. package/evals/baselines/2026-07-14/live-canary-summary.json +25 -0
  60. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/SKILL.md +72 -0
  61. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/action-contract.md +31 -0
  62. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/event-to-behavior.md +22 -0
  63. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/memory-and-evolution.md +25 -0
  64. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/runtime-modes.md +34 -0
  65. package/evals/baselines/2026-07-15/task-lifecycle-summary.json +50 -0
  66. package/evals/evals.json +316 -0
  67. package/evals/fixtures/dm-ambiguous-send.json +4 -0
  68. package/evals/fixtures/dm-blocked.json +4 -0
  69. package/evals/fixtures/dm-clear.json +4 -0
  70. package/evals/fixtures/dm-discussion.json +4 -0
  71. package/evals/fixtures/dm-doc-write-no-tool.json +4 -0
  72. package/evals/fixtures/dm-long-task-ack.json +4 -0
  73. package/evals/fixtures/dm-nonblocking-gap.json +4 -0
  74. package/evals/fixtures/dm-structured-task.json +4 -0
  75. package/evals/fixtures/group.json +10 -0
  76. package/evals/fixtures/mentioned.json +3 -0
  77. package/evals/run-contract-evals.mjs +1106 -0
  78. package/evals/run-shadow-evals.mjs +267 -0
  79. package/evals/runners/README.md +66 -0
  80. package/evals/runners/claude-shadow.mjs +533 -0
  81. package/evals/schemas/action-request.schema.json +77 -0
  82. package/evals/shadow-evals.json +133 -0
  83. package/package.json +28 -6
  84. package/skills/AGENTS.md +21 -3
  85. package/skills/dingtalk-basic-behavior/SKILL.md +86 -0
  86. package/skills/dingtalk-basic-behavior/assets/task-checkpoint.md +37 -0
  87. package/skills/dingtalk-basic-behavior/references/action-contract.md +31 -0
  88. package/skills/dingtalk-basic-behavior/references/event-to-behavior.md +24 -0
  89. package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +27 -0
  90. package/skills/dingtalk-basic-behavior/references/runtime-modes.md +34 -0
  91. package/skills/dingtalk-basic-behavior/references/task-lifecycle.md +108 -0
  92. package/skills//345/237/272/347/241/200/350/241/214/344/270/272.md +44 -0
  93. package/skills//345/277/203/350/267/263.md +11 -0
  94. package/skills//346/266/210/346/201/257.md +14 -14
  95. package/skills//350/257/204/346/265/213.md +14 -1
  96. package/skills//351/222/211/351/222/211.md +3 -2
  97. package/templates/behaviors/basic.json +68 -0
  98. package/templates/fields/default/field.json +25 -0
  99. package/bin/dingtalk-agent.js +0 -289
  100. package/src/boot.js +0 -65
  101. package/src/config.js +0 -42
  102. package/src/dws.js +0 -192
  103. package/src/init.js +0 -84
  104. package/src/kb.js +0 -221
  105. package/src/runs.js +0 -77
@@ -0,0 +1,763 @@
1
+ #!/usr/bin/env node
2
+ // dingtalk-agent —— 钉钉数字员工的标准范式(TypeScript source)。
3
+ //
4
+ // CLI = 拦 标准动作:可程序化、可强制、可验证。撞上去没得商量。
5
+ // Skill = 劝 判断:只有 LLM 能做的(该不该答、话题归属、怎么写点评)。
6
+ // 本体 = 记 它是谁 + 它知道什么。git clone 就带走。
7
+ //
8
+ // **分界线:凡是能程序化的,收进 CLI;凡是需要判断的,留在 Skill。**
9
+ //
10
+ // 因为:文档会被漏读、误读、自我说服;**撞到闸门上就没得商量。**
11
+ import { parseArgs } from 'node:util';
12
+ import { join, resolve } from 'node:path';
13
+ import { readFileSync } from 'node:fs';
14
+ import { spawn, spawnSync } from 'node:child_process';
15
+ import * as cfgmod from '../src/config.js';
16
+ import * as kbmod from '../src/kb.js';
17
+ import * as runsmod from '../src/runs.js';
18
+ import * as dutymod from '../src/duty.js';
19
+ import { boot } from '../src/boot.js';
20
+ import { init } from '../src/init.js';
21
+ import { ackDispatch, listPendingDispatches, prepareSession } from '../src/sessions.js';
22
+ import { readEventFile, consumeNdjson, heartbeatEvent } from '../src/driver.js';
23
+ import { findRun, previewAction, executeAction } from '../src/actions.js';
24
+ import { resolvePackageRoot } from '../src/package-root.js';
25
+ import { bootstrap } from '../src/bootstrap.js';
26
+ import { doctor } from '../src/doctor.js';
27
+ import { setup } from '../src/setup.js';
28
+ import { installGlobalSkill, skillStatus, uninstallGlobalSkill, upgradeGlobalSkill, } from '../src/skill-manager.js';
29
+ const PACKAGE_ROOT = resolvePackageRoot(import.meta.url);
30
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
31
+ // ROOT = **你的工作区**(当前目录),不是包所在的目录。
32
+ // 全局装 / npx 跑的时候,包住在 node_modules 里 —— 那不是 agent 的家。
33
+ const ROOT = process.env.DTA_ROOT
34
+ ? resolve(process.env.DTA_ROOT)
35
+ : (cfgmod.findRoot(process.cwd()) || process.cwd());
36
+ const HELP = `
37
+ dingtalk-agent —— Skill-first 的钉钉数字员工行为容器
38
+
39
+ 首次使用(无全局命令时: npx --yes @xdxer/dingtalk-agent setup)
40
+ setup 用户级安装 CLI、修复 PATH、安装 Skill、检查 DWS
41
+ doctor 检查 PATH / Node / DWS / Skill / Agent 客户端
42
+
43
+ Skill(不要求 Workspace)
44
+ skill install 安装全局 Basic Behavior(Claude Code + Codex + OpenCode)
45
+ skill status 检查版本、漂移和客户端可发现性
46
+ skill upgrade 升级本工具管理的全局 Skill
47
+ skill uninstall 安全卸载本工具管理的全局 Skill
48
+ 通用: [--dry-run] [--json] [--discard-local-changes]
49
+
50
+ 每个 Agent Session(不要求 init)
51
+ bootstrap 发现当前本地 Workspace;不修改项目文件
52
+ [--storage <uri>] local-dir:<path> | dingtalk-doc:<node-or-url>
53
+ [--state-dir <path>] 远端文档只读快照目录
54
+ [--profile <dws-profile>] [--json]
55
+
56
+ 可信事件模式(可选;需要一次 init)
57
+ init [--context-id X] 仅为 Prepared Run 建最小 Workspace
58
+ prepare --event-file <file|-> 事件 → Session → Run
59
+ act ack 对原消息贴「收到」;不代表接单
60
+ act reply --text "…" 引用回复原消息
61
+ act ask --text "…" 只问一个真正阻塞的问题
62
+ act silence [--reason "…"] 不外发,但留下行为回执
63
+ 通用: [--run <path>] [--dry-run] [--json]
64
+
65
+ smoke 极简本地验收(不写钉钉)
66
+ eval contract / behavior 完整合同或模型行为评测
67
+
68
+ help runtime 事件宿主与 controller 命令
69
+ help adapters 可选 DWS listen 适配器
70
+ help legacy 查看兼容的本体、KB、值班与进化命令
71
+ `;
72
+ const RUNTIME_HELP = `
73
+ dingtalk-agent 可信事件运行时(可选宿主能力)
74
+
75
+ init [--context-id X] [--memory <uri>] [--knowledge <uri>]
76
+ prepare --event-file <file|->
77
+ run --stdin | --event-file <file> | --heartbeat
78
+ dispatch pending
79
+ dispatch ack --event-id X --run-id X
80
+ act ack|reply|ask|silence
81
+
82
+ 这些命令负责目标冻结、Session/Run、Wait、幂等和回执;普通 Agent Session 不必使用。
83
+ `;
84
+ const ADAPTER_HELP = `
85
+ dingtalk-agent 可选 Driver / 开发联调适配器
86
+
87
+ listen mention --once
88
+ listen dm --user <userId> --once
89
+ listen group --group <openConversationId> --once
90
+
91
+ listen 不是 Agent 主入口。Claude Code、DWS 云端或其他宿主都可以直接提供事件。
92
+ `;
93
+ const LEGACY_HELP = `
94
+ dingtalk-agent 兼容命令(不属于 P0 四对象主链)
95
+
96
+ boot
97
+ kb mount|list|sync|search
98
+ duty --check|--run|--list
99
+ todo
100
+ log / feedback / runs / evolve
101
+
102
+ 这些能力继续可用,但新增 Agent 应先安装全局 Skill;只有可信事件宿主才需要 runtime。
103
+ `;
104
+ function fail(msg, code = 1) {
105
+ console.error(msg);
106
+ process.exit(code);
107
+ }
108
+ const argv = process.argv.slice(2);
109
+ const cmd = argv[0];
110
+ const sub = argv[1];
111
+ try {
112
+ await main();
113
+ }
114
+ catch (e) {
115
+ if (e.exitCode)
116
+ process.exit(e.exitCode);
117
+ fail(String(e.message || e), 2);
118
+ }
119
+ async function main() {
120
+ if (cmd === 'help' && sub === 'legacy') {
121
+ console.log(LEGACY_HELP);
122
+ return;
123
+ }
124
+ if (cmd === 'help' && sub === 'runtime') {
125
+ console.log(RUNTIME_HELP);
126
+ return;
127
+ }
128
+ if (cmd === 'help' && sub === 'adapters') {
129
+ console.log(ADAPTER_HELP);
130
+ return;
131
+ }
132
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
133
+ console.log(HELP);
134
+ return;
135
+ }
136
+ if (cmd === '--version' || cmd === '-V' || cmd === 'version') {
137
+ console.log(PACKAGE_JSON.version);
138
+ return;
139
+ }
140
+ // ── setup / doctor:npx 永远可启动的首次使用入口。──
141
+ if (cmd === 'setup') {
142
+ const { values: o } = parseArgs({
143
+ args: argv.slice(1),
144
+ options: {
145
+ prefix: { type: 'string' }, 'dry-run': { type: 'boolean' },
146
+ 'no-shell-write': { type: 'boolean' }, 'skip-cli-install': { type: 'boolean' },
147
+ json: { type: 'boolean' },
148
+ },
149
+ });
150
+ const out = setup(PACKAGE_ROOT, {
151
+ prefix: o.prefix,
152
+ dryRun: o['dry-run'],
153
+ writeShell: !o['no-shell-write'],
154
+ skipCliInstall: o['skip-cli-install'],
155
+ });
156
+ if (o.json)
157
+ console.log(JSON.stringify(out, null, 2));
158
+ else
159
+ printSetup(out);
160
+ if (!out.doctor.ready)
161
+ process.exitCode = 2;
162
+ return;
163
+ }
164
+ if (cmd === 'doctor') {
165
+ const { values: o } = parseArgs({
166
+ args: argv.slice(1), options: { json: { type: 'boolean' } },
167
+ });
168
+ const out = doctor(PACKAGE_ROOT);
169
+ if (o.json)
170
+ console.log(JSON.stringify(out, null, 2));
171
+ else
172
+ printDoctor(out);
173
+ if (!out.ready)
174
+ process.exitCode = 2;
175
+ return;
176
+ }
177
+ // ── skill:唯一一次性的主入口;完全不依赖 Workspace。──
178
+ if (cmd === 'skill') {
179
+ const { values: o } = parseArgs({
180
+ args: argv.slice(2),
181
+ options: {
182
+ 'dry-run': { type: 'boolean' }, json: { type: 'boolean' },
183
+ 'discard-local-changes': { type: 'boolean' },
184
+ },
185
+ });
186
+ const options = {
187
+ dryRun: o['dry-run'],
188
+ discardLocalChanges: o['discard-local-changes'],
189
+ };
190
+ let out;
191
+ if (sub === 'install')
192
+ out = installGlobalSkill(PACKAGE_ROOT, options);
193
+ else if (sub === 'status')
194
+ out = skillStatus(PACKAGE_ROOT, options);
195
+ else if (sub === 'upgrade')
196
+ out = upgradeGlobalSkill(PACKAGE_ROOT, options);
197
+ else if (sub === 'uninstall')
198
+ out = uninstallGlobalSkill(PACKAGE_ROOT, options);
199
+ else
200
+ fail('skill 的子命令: install / status / upgrade / uninstall');
201
+ if (o.json)
202
+ console.log(JSON.stringify(out, null, 2));
203
+ else
204
+ printSkillStatus(out, sub, Boolean(o['dry-run']));
205
+ return;
206
+ }
207
+ // ── bootstrap:当前 Agent Session 按需发现/拉取语义上下文。──
208
+ if (cmd === 'bootstrap') {
209
+ const { values: o } = parseArgs({
210
+ args: argv.slice(1),
211
+ options: {
212
+ storage: { type: 'string' }, 'state-dir': { type: 'string' },
213
+ profile: { type: 'string' }, json: { type: 'boolean' },
214
+ },
215
+ });
216
+ const out = bootstrap(ROOT, {
217
+ storage: o.storage, stateDir: o['state-dir'], profile: o.profile,
218
+ });
219
+ if (o.json)
220
+ console.log(JSON.stringify(out, null, 2));
221
+ else {
222
+ console.log(`✅ ${out.mode === 'workspace' ? '已装载 Workspace' : '直接会话模式'}`);
223
+ console.log(` scope=${out.scopeId} initialized=${out.initialized}`);
224
+ if (!out.mounts.length)
225
+ console.log(' 没有发现持久上下文;这不是错误,也不需要自动 init。');
226
+ for (const mount of out.mounts)
227
+ console.log(` ${mount.slot}: ${mount.path}`);
228
+ console.log(` ${out.note}`);
229
+ }
230
+ return;
231
+ }
232
+ // ── init ──
233
+ if (cmd === 'init') {
234
+ const { values: o } = parseArgs({
235
+ args: argv.slice(1),
236
+ options: {
237
+ 'context-id': { type: 'string' }, id: { type: 'string' },
238
+ memory: { type: 'string' }, knowledge: { type: 'string' },
239
+ },
240
+ });
241
+ // init 的语义是“初始化当前目录”;不能因为父目录恰好有配置就改到父工作区。
242
+ init(process.env.DTA_ROOT ? ROOT : process.cwd(), {
243
+ contextId: o['context-id'], id: o.id,
244
+ memory: o.memory, knowledge: o.knowledge,
245
+ });
246
+ return;
247
+ }
248
+ // ── prepare:一个事件物化成一个可交给沙箱的 Run ──
249
+ if (cmd === 'prepare') {
250
+ const cfg = cfgmod.must(ROOT);
251
+ const { values: o, positionals } = parseArgs({
252
+ args: argv.slice(1), allowPositionals: true,
253
+ options: {
254
+ 'event-file': { type: 'string' }, field: { type: 'string' },
255
+ 'session-key': { type: 'string' }, 'context-id': { type: 'string' },
256
+ json: { type: 'boolean' },
257
+ },
258
+ });
259
+ const file = o['event-file'] || positionals[0];
260
+ if (!file)
261
+ fail('prepare 需要 --event-file <file|->');
262
+ const out = prepareSession(ROOT, cfg, readEventFile(file), {
263
+ fieldId: o.field, sessionKey: o['session-key'], contextId: o['context-id'],
264
+ });
265
+ printDispatch(out, o.json);
266
+ return;
267
+ }
268
+ // ── run:不内置模型,只把事件流转换为 sandbox dispatch ──
269
+ if (cmd === 'run') {
270
+ const cfg = cfgmod.must(ROOT);
271
+ const { values: o, positionals } = parseArgs({
272
+ args: argv.slice(1), allowPositionals: true,
273
+ options: {
274
+ stdin: { type: 'boolean' }, 'event-file': { type: 'string' },
275
+ field: { type: 'string' }, 'session-key': { type: 'string' },
276
+ 'context-id': { type: 'string' },
277
+ heartbeat: { type: 'boolean' }, 'heartbeat-key': { type: 'string' },
278
+ 'heartbeat-event-id': { type: 'string' },
279
+ },
280
+ });
281
+ const options = {
282
+ fieldId: o.field, sessionKey: o['session-key'], contextId: o['context-id'],
283
+ };
284
+ const modes = [Boolean(o.stdin), Boolean(o.heartbeat), Boolean(o['event-file'] || positionals[0])];
285
+ if (modes.filter(Boolean).length !== 1) {
286
+ fail('run 的 --stdin / --event-file <file> / --heartbeat 必须且只能选一个');
287
+ }
288
+ if (o.stdin) {
289
+ const result = await consumeNdjson(ROOT, cfg, process.stdin, options);
290
+ if (result.rejected)
291
+ process.exitCode = 2;
292
+ return;
293
+ }
294
+ let event;
295
+ if (o.heartbeat)
296
+ event = heartbeatEvent(o['heartbeat-key'], o['heartbeat-event-id']);
297
+ else {
298
+ const file = o['event-file'] || positionals[0];
299
+ if (!file)
300
+ fail('run 需要 --stdin / --event-file <file> / --heartbeat 三选一');
301
+ event = readEventFile(file);
302
+ }
303
+ const out = prepareSession(ROOT, cfg, event, options);
304
+ process.stdout.write(JSON.stringify(out) + '\n');
305
+ return;
306
+ }
307
+ // ── listen:本地 DWS Driver;云端 Driver 仍走 run --stdin/--event-file。──
308
+ if (cmd === 'listen') {
309
+ const cfg = cfgmod.must(ROOT);
310
+ const modes = {
311
+ mention: { event: 'user_im_message_receive_at' },
312
+ dm: { event: 'user_im_message_receive_o2o', required: 'user' },
313
+ group: { event: 'user_im_message_receive_group', required: 'group' },
314
+ };
315
+ const mode = modes[sub];
316
+ if (!mode)
317
+ fail('listen 的类型只能是 mention / dm / group');
318
+ const { values: o } = parseArgs({
319
+ args: argv.slice(2),
320
+ options: {
321
+ user: { type: 'string' }, group: { type: 'string' },
322
+ once: { type: 'boolean' }, duration: { type: 'string' },
323
+ 'dry-run': { type: 'boolean' }, 'context-id': { type: 'string' },
324
+ field: { type: 'string' }, 'session-key': { type: 'string' },
325
+ },
326
+ });
327
+ if (mode.required === 'user' && !o.user)
328
+ fail('listen dm 需要 --user <userId>');
329
+ if (mode.required === 'group' && !o.group)
330
+ fail('listen group 需要 --group <openConversationId>');
331
+ const dwsArgs = ['event', 'consume', mode.event, '-f', 'ndjson'];
332
+ if (o.user)
333
+ dwsArgs.push('--user', o.user);
334
+ if (o.group)
335
+ dwsArgs.push('--group', o.group);
336
+ if (o.once)
337
+ dwsArgs.push('--max-events', '1', '--ephemeral');
338
+ if (o.duration)
339
+ dwsArgs.push('--duration', o.duration);
340
+ if (o['dry-run']) {
341
+ const child = spawnSync('dws', [...dwsArgs, '--dry-run'], { stdio: 'inherit' });
342
+ if (child.error)
343
+ throw child.error;
344
+ process.exitCode = child.status ?? 2;
345
+ return;
346
+ }
347
+ const child = spawn('dws', dwsArgs, { stdio: ['ignore', 'pipe', 'inherit'] });
348
+ const status = new Promise((resolveStatus, rejectStatus) => {
349
+ child.once('error', rejectStatus);
350
+ child.once('close', (code, signal) => {
351
+ if (signal)
352
+ rejectStatus(new Error(`DWS listener 被 ${signal} 终止`));
353
+ else
354
+ resolveStatus(code ?? 2);
355
+ });
356
+ });
357
+ const result = await consumeNdjson(ROOT, cfg, child.stdout, {
358
+ fieldId: o.field,
359
+ sessionKey: o['session-key'],
360
+ contextId: o['context-id'],
361
+ });
362
+ const exitCode = await status;
363
+ if (exitCode !== 0)
364
+ fail(`DWS listener 退出码 ${exitCode}`, exitCode);
365
+ if (result.rejected)
366
+ process.exitCode = 2;
367
+ return;
368
+ }
369
+ // ── dispatch:durable outbox。ack 表示 controller 已幂等入队,不表示 Run 已完成。──
370
+ if (cmd === 'dispatch') {
371
+ const cfg = cfgmod.must(ROOT);
372
+ const { values: o } = parseArgs({
373
+ args: argv.slice(2),
374
+ options: {
375
+ 'event-id': { type: 'string' }, 'run-id': { type: 'string' },
376
+ consumer: { type: 'string' }, json: { type: 'boolean' },
377
+ },
378
+ });
379
+ if (sub === 'pending') {
380
+ for (const out of listPendingDispatches(ROOT, cfg)) {
381
+ process.stdout.write(JSON.stringify(out) + '\n');
382
+ }
383
+ return;
384
+ }
385
+ if (sub === 'ack') {
386
+ if (!o['event-id'] || !o['run-id'])
387
+ fail('dispatch ack 需要 --event-id 和 --run-id');
388
+ const out = ackDispatch(ROOT, cfg, o['event-id'], o['run-id'], o.consumer || '');
389
+ process.stdout.write(JSON.stringify(out) + '\n');
390
+ return;
391
+ }
392
+ fail('dispatch 的子命令: pending / ack');
393
+ }
394
+ // ── act:元语化动作;目标/身份/预算/状态转移都由 Run 固定 ──
395
+ if (cmd === 'act') {
396
+ const kind = sub;
397
+ const { values: o } = parseArgs({
398
+ args: argv.slice(2),
399
+ options: {
400
+ run: { type: 'string' }, text: { type: 'string' },
401
+ 'text-file': { type: 'string' }, reason: { type: 'string' },
402
+ 'dry-run': { type: 'boolean' }, json: { type: 'boolean' },
403
+ },
404
+ });
405
+ if (o.text && o['text-file'])
406
+ fail('--text 和 --text-file 只能选一个');
407
+ const text = o['text-file']
408
+ ? readFileSync(resolve(process.cwd(), o['text-file']), 'utf8')
409
+ : o.text;
410
+ const runPath = findRun(process.cwd(), o.run);
411
+ const out = o['dry-run']
412
+ ? previewAction(runPath, kind, { text, reason: o.reason })
413
+ : executeAction(runPath, kind, { text, reason: o.reason });
414
+ if (o.json || o['dry-run'])
415
+ console.log(JSON.stringify(out, null, 2));
416
+ else {
417
+ console.log(`${out.verified ? '✅' : '⚠️'} ${out.kind} · ${out.outcome} · ${out.toState}`);
418
+ console.log(` action=${out.actionId} run=${out.runId}`);
419
+ if (!out.verified)
420
+ console.log(` ${out.verification}`);
421
+ }
422
+ if (out.outcome === 'uncertain')
423
+ process.exitCode = 2;
424
+ return;
425
+ }
426
+ // ── eval:宿主评测入口;shadow 永不调用 act / DWS。──
427
+ if (cmd === 'eval') {
428
+ const script = sub === 'contract'
429
+ ? join(PACKAGE_ROOT, 'evals', 'run-contract-evals.mjs')
430
+ : sub === 'shadow'
431
+ ? join(PACKAGE_ROOT, 'evals', 'runners', 'claude-shadow.mjs')
432
+ : sub === 'behavior'
433
+ ? join(PACKAGE_ROOT, 'evals', 'run-shadow-evals.mjs')
434
+ : '';
435
+ if (!script)
436
+ fail('eval 的子命令: contract / shadow / behavior');
437
+ const childEnv = { ...process.env };
438
+ // 合同评测会创建隔离工作区;继承宿主 DTA_ROOT 会让 Action Gate 错读外层 control store。
439
+ delete childEnv.DTA_ROOT;
440
+ const child = spawnSync(process.execPath, [script, ...argv.slice(2)], {
441
+ cwd: ROOT,
442
+ env: childEnv,
443
+ stdio: 'inherit',
444
+ });
445
+ if (child.error)
446
+ throw child.error;
447
+ if (child.signal)
448
+ throw new Error(`评测进程被 ${child.signal} 终止`);
449
+ process.exitCode = child.status ?? 2;
450
+ return;
451
+ }
452
+ if (cmd === 'smoke') {
453
+ const script = join(PACKAGE_ROOT, 'evals', 'run-contract-evals.mjs');
454
+ const childEnv = { ...process.env };
455
+ delete childEnv.DTA_ROOT;
456
+ const child = spawnSync(process.execPath, [script, '--smoke'], {
457
+ cwd: ROOT, env: childEnv, stdio: 'inherit',
458
+ });
459
+ if (child.error)
460
+ throw child.error;
461
+ if (child.signal)
462
+ throw new Error(`Smoke 被 ${child.signal} 终止`);
463
+ process.exitCode = child.status ?? 2;
464
+ return;
465
+ }
466
+ // ── kb ──
467
+ if (cmd === 'kb') {
468
+ const cfg = cfgmod.must(ROOT);
469
+ const { values: o } = parseArgs({
470
+ args: argv.slice(2), allowPositionals: true,
471
+ options: {
472
+ name: { type: 'string' }, from: { type: 'string' }, own: { type: 'string' },
473
+ sync: { type: 'string' }, local: { type: 'string' }, force: { type: 'boolean' },
474
+ },
475
+ });
476
+ if (sub === 'mount') {
477
+ if (!o.name || !o.from)
478
+ fail('要 --name 和 --from。源的写法见 --help');
479
+ kbmod.parseSource(o.from); // 写错了当场炸
480
+ const own = o.own || kbmod.EXTERNAL;
481
+ const sync = o.sync || (own === kbmod.OWNED ? kbmod.PUSH : kbmod.PULL);
482
+ if (own === kbmod.OWNED && sync === kbmod.PULL) {
483
+ fail('owned 的真值在 Git,不该 pull —— 那会把远端的东西覆盖到本地。\n' +
484
+ ' **所有权决定真值方向。** owned → push;external → pull。');
485
+ }
486
+ const local = o.local || join('.cache/kb', o.name);
487
+ cfg.kb = (cfg.kb || []).filter((k) => k.name !== o.name);
488
+ cfg.kb.push({ name: o.name, from: o.from, own, sync, local });
489
+ cfgmod.save(ROOT, cfg);
490
+ console.log(`✅ 挂上「${o.name}」 ${o.from}`);
491
+ console.log(` 所有权=${own} · 同步=${sync} · 落在 ${local}`);
492
+ console.log(own === kbmod.OWNED
493
+ ? ' (真值在 Git,钉钉是暴露层。人在钉钉里改了 → sync 时会【停下来问】,不盲覆盖。)'
494
+ : ' (真值在钉钉,本地只是【可丢弃的缓存】。过期了 kb search 会警告。)');
495
+ return;
496
+ }
497
+ if (sub === 'list') {
498
+ if (!cfg.kb?.length)
499
+ return console.log('一个都没挂。 kb mount --help');
500
+ for (const kb of cfg.kb) {
501
+ const s = kbmod.stale(kb);
502
+ console.log(` ${s ? '⚠️' : '·'} ${kb.name.padEnd(14)} ${kb.own.padEnd(9)} ${kb.sync.padEnd(5)} ${kb.from}`);
503
+ console.log(` 落在 ${kb.local} 上次同步 ${kb._syncedAt || '从未'}${s ? ' ← **过期了,不许拿来回答**' : ''}`);
504
+ }
505
+ return;
506
+ }
507
+ if (sub === 'sync') {
508
+ const targets = (cfg.kb || []).filter((k) => !o.name || k.name === o.name);
509
+ if (!targets.length)
510
+ fail('没有这个知识库。 kb list');
511
+ for (const kb of targets) {
512
+ if (kb.sync === kbmod.NONE) {
513
+ console.log(` skip ${kb.name}(sync=none)`);
514
+ continue;
515
+ }
516
+ if (kb.sync === kbmod.PULL)
517
+ kbmod.pull(kb, ROOT, o.force);
518
+ else
519
+ kbmod.push(kb, ROOT, o.force);
520
+ }
521
+ cfgmod.save(ROOT, cfg);
522
+ return;
523
+ }
524
+ if (sub === 'search') {
525
+ const kw = argv[2];
526
+ if (!kw)
527
+ fail('要给关键词。 kb search "<关键词>"');
528
+ kbmod.search(cfg, ROOT, kw);
529
+ return;
530
+ }
531
+ fail('kb 的子命令: mount / list / sync / search');
532
+ }
533
+ // ── boot ──
534
+ if (cmd === 'boot') {
535
+ boot(cfgmod.must(ROOT), ROOT);
536
+ return;
537
+ }
538
+ // ── duty ──
539
+ if (cmd === 'duty') {
540
+ const cfg = cfgmod.must(ROOT);
541
+ const { values: o } = parseArgs({
542
+ args: argv.slice(1), allowPositionals: true,
543
+ options: { check: { type: 'boolean' }, run: { type: 'string' },
544
+ list: { type: 'boolean' }, force: { type: 'boolean' } },
545
+ });
546
+ const duties = dutymod.loadDuties(cfg, ROOT);
547
+ if (o.list) {
548
+ const ks = Object.keys(duties);
549
+ console.log(ks.length ? ks.join('\n')
550
+ : '(值班表是空的 —— 母体出厂如此,这是【正确】的。在 .dingtalk-agent/config.json 的 duties 里加。)');
551
+ return;
552
+ }
553
+ if (o.run) {
554
+ const fn = duties[o.run];
555
+ if (!fn)
556
+ fail(`没有这个值班: ${o.run}(有: ${Object.keys(duties).join('、') || '无'})`);
557
+ const s = fn();
558
+ if (s.kind === '交付' && !dutymod.inDeliver() && !o.force) {
559
+ return console.log(`⏸ 「${o.run}」是外发动作,静默时段顺延,**不打扰第三方**。`);
560
+ }
561
+ if (s.done)
562
+ return console.log(`✅ 「${o.run}」已完成,本拍无需执行。${s.detail}`);
563
+ console.log(`【${o.run}】该做了。${s.gap}`);
564
+ console.log(` 剧本: ${s.playbook || '(没写剧本 —— 这个值班不合格)'}`);
565
+ console.log(` ⚠️ 送达【回读确认】之后才准记 pass。没送达就写 pass = 自己骗自己。`);
566
+ return;
567
+ }
568
+ // --check(默认)
569
+ console.log('值班表(**完成状态在数据里,不在记忆里**;幂等,漏拍下一拍自愈)');
570
+ console.log(` 现在 ${runsmod.now().slice(11, 16)} · ${dutymod.inNight() ? '夜间窗口(重活可跑)' : '白天(重活挂起)'} · 外发窗口${dutymod.inDeliver() ? '开' : '关'}`);
571
+ const ids = Object.keys(duties);
572
+ if (!ids.length) {
573
+ console.log('\n (值班表是空的 —— 母体出厂如此。)');
574
+ return;
575
+ }
576
+ const todo = [];
577
+ for (const id of ids) {
578
+ const s = duties[id]();
579
+ const mark = s.done ? '✅ 已完成' : (s.due ? '⬜ 待补齐' : '⏸ 不在窗口');
580
+ console.log(` ${s.id.padEnd(8)} [${s.kind}] ${mark} ${s.detail}${s.gap ? ' → ' + s.gap : ''}`);
581
+ if (!s.done && s.due)
582
+ todo.push(s.id);
583
+ }
584
+ if (todo.length) {
585
+ console.log(`\n本拍要做: ${todo.map((t) => 'duty --run ' + t).join('、')}`);
586
+ process.exit(1);
587
+ }
588
+ console.log('\n全部补齐,静默即可(**不要为此打扰主人**)');
589
+ return;
590
+ }
591
+ // ── 留痕与进化 ──
592
+ if (cmd === 'log' || cmd === 'logq') {
593
+ const { values: o } = parseArgs({
594
+ args: argv.slice(1),
595
+ options: { did: { type: 'string' }, asked: { type: 'string' }, kind: { type: 'string' },
596
+ secs: { type: 'string' }, r: { type: 'string' }, ev: { type: 'string' },
597
+ ch: { type: 'string' }, issue: { type: 'string' },
598
+ conv: { type: 'string' }, msg: { type: 'string' } },
599
+ });
600
+ if (!o.did)
601
+ fail('要 --did(我怎么答的)');
602
+ const t = runsmod.logq(ROOT, {
603
+ ...o, did: o.did, secs: +(o.secs || 0), r: o.r || 'pass',
604
+ });
605
+ console.log(`运行记录已落: runs/${runsmod.today()}.md @${t}`);
606
+ if (!o.conv)
607
+ console.log(' ⚠️ 没带 --conv —— 事后【拉不出对话流】,这一条基本没有训练价值。');
608
+ return;
609
+ }
610
+ if (cmd === 'feedback') {
611
+ const { values: o } = parseArgs({
612
+ args: argv.slice(1),
613
+ options: { kind: { type: 'string' }, text: { type: 'string' },
614
+ conv: { type: 'string' }, at: { type: 'string' } },
615
+ });
616
+ if (!o.kind || ![runsmod.FB_OK, runsmod.FB_MORE, runsmod.FB_FIX].includes(o.kind)) {
617
+ fail('--kind 只能是 认可 / 追问 / 纠正');
618
+ }
619
+ const day = runsmod.today();
620
+ const rows = runsmod.read(ROOT, day);
621
+ let cand = rows.filter((r) => r.asked);
622
+ if (o.conv)
623
+ cand = cand.filter((r) => r.conv === o.conv);
624
+ if (!cand.length)
625
+ fail(`今天没有可回填的问答行 —— **是不是答完忘了 logq?**`);
626
+ const tgt = o.at ? cand.find((r) => r.t === o.at) : cand[cand.length - 1];
627
+ if (!tgt)
628
+ fail(`找不到 t=${o.at} 的行`);
629
+ tgt.fb = o.kind;
630
+ if (o.text)
631
+ tgt.fbt = o.text.slice(0, 600);
632
+ runsmod.write(ROOT, day, rows);
633
+ console.log(`已回填: ${day} @${tgt.t} → 「${o.kind}」(原问: ${(tgt.asked || '').slice(0, 40)})`);
634
+ return;
635
+ }
636
+ if (cmd === 'runs') {
637
+ const { values: o } = parseArgs({
638
+ args: argv.slice(1),
639
+ options: { days: { type: 'string' }, conv: { type: 'string' },
640
+ issue: { type: 'string' }, top: { type: 'string' } },
641
+ });
642
+ let rows = runsmod.scan(ROOT, +(o.days || 1));
643
+ if (o.conv) {
644
+ rows = rows.filter((r) => r.conv === o.conv);
645
+ console.log(`【对话流】会话 ${o.conv} · ${rows.length} 条 —— **这就是训练素材**`);
646
+ for (const r of rows) {
647
+ console.log(` ── ${r._day} ${r.t} issue=${r.issue || '-'} msg=${r.msg || '-'}`);
648
+ if (r.asked)
649
+ console.log(` 主人: ${r.asked}`);
650
+ console.log(` 我 : ${r.did} [${r.r}]`);
651
+ if (r.fb)
652
+ console.log(` 反馈: ${r.fb}${r.fbt ? ` 「${r.fbt}」` : ''}`);
653
+ }
654
+ if (!rows.length)
655
+ console.log(' (空 —— 这个会话没留痕。**没留痕 = 这些交互等于没发生过。**)');
656
+ return;
657
+ }
658
+ if (o.issue) {
659
+ const issue = o.issue;
660
+ rows = rows.filter((r) => (r.issue || '').startsWith(issue));
661
+ }
662
+ console.log(`运行记录 runs/ · ${rows.length} 条`);
663
+ for (const r of rows.slice(-(+(o.top || 30)))) {
664
+ console.log(` ${r._day} ${r.t} [${(r.r || '').padEnd(11)}] ${(r.ch || '').padEnd(6)} ${(r.did || '').slice(0, 30)} ${r.fb ? '← ' + r.fb : ''}`);
665
+ }
666
+ return;
667
+ }
668
+ if (cmd === 'evolve') {
669
+ const { values: o } = parseArgs({ args: argv.slice(1), options: { days: { type: 'string' } } });
670
+ const rows = runsmod.scan(ROOT, +(o.days || 7));
671
+ const ans = rows.filter((r) => r.asked);
672
+ const bad = ans.filter((r) => [runsmod.FB_FIX, runsmod.FB_MORE].includes(r.fb || ''));
673
+ const unk = ans.filter((r) => !r.fb);
674
+ const failed = rows.filter((r) => ['fail', 'unsupported', 'degraded'].includes(r.r));
675
+ console.log(`【进化清单】近 ${o.days || 7} 天`);
676
+ console.log(` 面向主人的回答 ${ans.length} 条 · 被【打回】${bad.length} 条 · 引擎层没答出来 ${failed.length} 条`);
677
+ if (unk.length)
678
+ console.log(` ⚠️ ${unk.length} 条【没收到反馈就结案了】—— 答完没回填,这次交互等于白跑。`);
679
+ if (bad.length) {
680
+ console.log('\n── 主人打回的(**直接就是这周该改什么**)──');
681
+ for (const r of bad) {
682
+ console.log(` ${r._day} ${r.t} [${r.fb}] 会话=${r.conv || '-'}`);
683
+ console.log(` 主人问: ${(r.asked || '').slice(0, 60)}`);
684
+ console.log(` 我答 : ${(r.did || '').slice(0, 60)}`);
685
+ console.log(` 他说 : ${r.fbt || '(没记下来)'}`);
686
+ console.log(` 复盘 : dingtalk-agent runs --conv ${r.conv || '<会话id>'}`);
687
+ }
688
+ }
689
+ if (!bad.length && !failed.length) {
690
+ console.log('\n (没有打回,也没有失败 —— 要么真的都对,要么【没人回填反馈】。看上面那行。)');
691
+ }
692
+ return;
693
+ }
694
+ // ── todo:把待办拉全(心跳收件用)──
695
+ if (cmd === 'todo') {
696
+ const dws = await import('../src/dws.js');
697
+ const all = dws.todosAll('false');
698
+ console.log(`未完成待办 ${all.length} 条(**已翻页拉全** —— 只看第一页会漏掉循环件)`);
699
+ for (const t of all)
700
+ console.log(` ${t.taskId} ${(t.subject || '').slice(0, 60)}`);
701
+ return;
702
+ }
703
+ fail(`不认识的命令: ${cmd}\n${HELP}`);
704
+ }
705
+ function printDispatch(out, json = false) {
706
+ if (json)
707
+ return console.log(JSON.stringify(out, null, 2));
708
+ const mark = out.duplicate ? (out.launch ? '🔁 REDELIVER' : '↩️ DUPLICATE') : '✅ READY';
709
+ console.log(`${mark} ${out.runId}`);
710
+ console.log(` Field=${out.fieldId} Session=${out.sessionId} Event=${out.eventKind}`);
711
+ console.log(` cwd=${out.cwd}`);
712
+ console.log(` entrypoint=${out.entrypoint} queue=${out.queue.key} (max=${out.queue.maxConcurrency})`);
713
+ console.log(` launch=${out.launch} delivery=${out.delivery?.status || 'unknown'}`);
714
+ console.log(` allowed=${out.allowedActions.join(', ')} default=${out.defaultAction}`);
715
+ console.log(' Local 模式可直接用 cwd/env;生产模式由宿主 Broker 保留 DWS 与 control 权限。');
716
+ }
717
+ function printSkillStatus(out, action, dryRun) {
718
+ const prefix = dryRun ? 'DRY-RUN' : '✅';
719
+ const verb = action === 'status' ? '全局 Skill 状态' : `skill ${action}`;
720
+ console.log(`${prefix} ${verb}: ${out.name}`);
721
+ console.log(` bundled=${out.bundled.skillVersion || '-'} hash=${out.bundled.hash.slice(0, 12)}`);
722
+ console.log(` canonical=${out.canonical.path}`);
723
+ console.log(` installed=${out.canonical.exists} managed=${out.canonical.managed} ` +
724
+ `modified=${out.canonical.modified} current=${out.canonical.current}`);
725
+ for (const client of out.clients) {
726
+ console.log(` ${client.label}=${client.state} (${client.discovery}: ${client.path})`);
727
+ }
728
+ if (out.legacyCodex.conflict) {
729
+ console.log(` ⚠️ legacy Codex 同名路径存在: ${out.legacyCodex.path}`);
730
+ }
731
+ if (action === 'install' && !dryRun) {
732
+ console.log(' 新 Agent 会话将自动发现;当前会话未发现时请重启 Claude Code/Codex。');
733
+ }
734
+ }
735
+ function printDoctor(out) {
736
+ console.log(`dingtalk-agent doctor · ${out.ready ? 'READY' : 'NOT READY'}`);
737
+ for (const check of out.checks) {
738
+ const mark = check.level === 'pass' ? '✅' : check.level === 'warn' ? '⚠️' : '❌';
739
+ console.log(`${mark} ${check.summary}`);
740
+ if (check.detail)
741
+ console.log(` ${check.detail}`);
742
+ if (check.fix)
743
+ console.log(` 修复: ${check.fix}`);
744
+ }
745
+ if (out.nextSteps.length) {
746
+ console.log('\n下一步:');
747
+ for (const step of out.nextSteps)
748
+ console.log(` ${step}`);
749
+ }
750
+ }
751
+ function printSetup(out) {
752
+ console.log(`${out.dryRun ? 'DRY-RUN' : '✅'} dingtalk-agent setup`);
753
+ console.log(` CLI: ${out.cli.executable}`);
754
+ console.log(` Skill: ${out.skill.canonical.path}`);
755
+ if (!out.shell.pathReady) {
756
+ console.log(` 当前 shell 尚未加载 PATH;本次终端执行: ${out.shell.activateCommand}`);
757
+ if (out.shell.rcFile)
758
+ console.log(` 新终端会从 ${out.shell.rcFile} 自动加载`);
759
+ }
760
+ printDoctor(out.doctor);
761
+ console.log(`\n开始使用: ${out.nextCommand}`);
762
+ }
763
+ //# sourceMappingURL=dingtalk-agent.js.map