@xdxer/dingtalk-agent 0.1.0 → 0.1.2

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 (99) hide show
  1. package/README.md +81 -80
  2. package/dist/bin/dingtalk-agent.js +685 -0
  3. package/dist/bin/dingtalk-agent.js.map +1 -0
  4. package/dist/src/actions.js +562 -0
  5. package/dist/src/actions.js.map +1 -0
  6. package/dist/src/boot.js +70 -0
  7. package/dist/src/boot.js.map +1 -0
  8. package/dist/src/bootstrap.js +144 -0
  9. package/dist/src/bootstrap.js.map +1 -0
  10. package/dist/src/config.js +86 -0
  11. package/dist/src/config.js.map +1 -0
  12. package/dist/src/driver.js +45 -0
  13. package/dist/src/driver.js.map +1 -0
  14. package/{src → dist/src}/duty.js +42 -44
  15. package/dist/src/duty.js.map +1 -0
  16. package/dist/src/dws.js +270 -0
  17. package/dist/src/dws.js.map +1 -0
  18. package/dist/src/events.js +233 -0
  19. package/dist/src/events.js.map +1 -0
  20. package/dist/src/fields.js +132 -0
  21. package/dist/src/fields.js.map +1 -0
  22. package/dist/src/init.js +41 -0
  23. package/dist/src/init.js.map +1 -0
  24. package/dist/src/kb.js +240 -0
  25. package/dist/src/kb.js.map +1 -0
  26. package/dist/src/package-root.js +17 -0
  27. package/dist/src/package-root.js.map +1 -0
  28. package/dist/src/runs.js +79 -0
  29. package/dist/src/runs.js.map +1 -0
  30. package/dist/src/sessions.js +668 -0
  31. package/dist/src/sessions.js.map +1 -0
  32. package/dist/src/skill-manager.js +273 -0
  33. package/dist/src/skill-manager.js.map +1 -0
  34. package/dist/src/skills.js +200 -0
  35. package/dist/src/skills.js.map +1 -0
  36. package/dist/src/types.js +2 -0
  37. package/dist/src/types.js.map +1 -0
  38. package/dist/src/waits.js +945 -0
  39. package/dist/src/waits.js.map +1 -0
  40. package/dist/src/workspace.js +173 -0
  41. package/dist/src/workspace.js.map +1 -0
  42. package/docs/ARCHITECTURE.md +207 -0
  43. package/docs/MINIMAL-WORKSPACE-V1.md +172 -0
  44. package/docs/OPEN-SOURCE-REFERENCES.md +107 -0
  45. package/docs/SELF-TEST.md +252 -0
  46. package/docs/architecture/dingtalk-agent-blueprint.png +0 -0
  47. package/docs/architecture/dingtalk-agent-blueprint.svg +144 -0
  48. package/docs/architecture/durable-async-agent-runtime.png +0 -0
  49. package/docs/architecture/durable-async-agent-runtime.svg +234 -0
  50. package/docs//345/206/205/347/275/221/345/256/236/347/233/270.md +77 -0
  51. package/evals/baselines/2026-07-14/behavior-summary.json +28 -0
  52. package/evals/baselines/2026-07-14/contract-summary.json +18 -0
  53. package/evals/baselines/2026-07-14/live-canary-summary.json +25 -0
  54. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/SKILL.md +72 -0
  55. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/action-contract.md +31 -0
  56. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/event-to-behavior.md +22 -0
  57. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/memory-and-evolution.md +25 -0
  58. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/runtime-modes.md +34 -0
  59. package/evals/baselines/2026-07-15/task-lifecycle-summary.json +50 -0
  60. package/evals/evals.json +293 -0
  61. package/evals/fixtures/dm-ambiguous-send.json +4 -0
  62. package/evals/fixtures/dm-blocked.json +4 -0
  63. package/evals/fixtures/dm-clear.json +4 -0
  64. package/evals/fixtures/dm-discussion.json +4 -0
  65. package/evals/fixtures/dm-doc-write-no-tool.json +4 -0
  66. package/evals/fixtures/dm-long-task-ack.json +4 -0
  67. package/evals/fixtures/dm-nonblocking-gap.json +4 -0
  68. package/evals/fixtures/dm-structured-task.json +4 -0
  69. package/evals/fixtures/group.json +10 -0
  70. package/evals/fixtures/mentioned.json +3 -0
  71. package/evals/run-contract-evals.mjs +1031 -0
  72. package/evals/run-shadow-evals.mjs +267 -0
  73. package/evals/runners/README.md +66 -0
  74. package/evals/runners/claude-shadow.mjs +533 -0
  75. package/evals/schemas/action-request.schema.json +77 -0
  76. package/evals/shadow-evals.json +133 -0
  77. package/package.json +28 -6
  78. package/skills/AGENTS.md +21 -3
  79. package/skills/dingtalk-basic-behavior/SKILL.md +86 -0
  80. package/skills/dingtalk-basic-behavior/assets/task-checkpoint.md +37 -0
  81. package/skills/dingtalk-basic-behavior/references/action-contract.md +31 -0
  82. package/skills/dingtalk-basic-behavior/references/event-to-behavior.md +24 -0
  83. package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +27 -0
  84. package/skills/dingtalk-basic-behavior/references/runtime-modes.md +34 -0
  85. package/skills/dingtalk-basic-behavior/references/task-lifecycle.md +108 -0
  86. package/skills//345/237/272/347/241/200/350/241/214/344/270/272.md +44 -0
  87. package/skills//345/277/203/350/267/263.md +11 -0
  88. package/skills//346/266/210/346/201/257.md +14 -14
  89. package/skills//350/257/204/346/265/213.md +14 -1
  90. package/skills//351/222/211/351/222/211.md +3 -2
  91. package/templates/behaviors/basic.json +68 -0
  92. package/templates/fields/default/field.json +25 -0
  93. package/bin/dingtalk-agent.js +0 -287
  94. package/src/boot.js +0 -65
  95. package/src/config.js +0 -42
  96. package/src/dws.js +0 -192
  97. package/src/init.js +0 -84
  98. package/src/kb.js +0 -221
  99. package/src/runs.js +0 -77
@@ -1,4 +1,4 @@
1
- // 内化心跳值班 —— **待办是给人的,心跳是 Agent 的生理。**
1
+ // 内化心跳值班 —— **待办是给人的,心跳是 Agent 的生理。**(TypeScript source)
2
2
  //
3
3
  // 把「永远做不完的义务」塞进「有终态的待办」,是错配:
4
4
  // 接力链一断 → 列表里没有下一期 → 心跳无事可做、静默收工、**且不自知**。
@@ -9,24 +9,19 @@
9
9
  // 交付 08:00–22:00(外发,**静默时段顺延,不打扰第三方**)
10
10
  //
11
11
  // **完成判据必须【数据派生】** —— 不记状态、不建标记文件、不看会话记忆。
12
- import { scan, ranTonight, now, today } from './runs.js'
13
-
14
- export const NIGHT = [0, 6]
15
- export const DELIVER = [8, 22]
16
-
17
- const hour = () => new Date(Date.now() + 8 * 3600e3).getUTCHours()
18
- const minute = () => new Date(Date.now() + 8 * 3600e3).getUTCMinutes()
19
- const weekday = () => new Date(Date.now() + 8 * 3600e3).getUTCDay() // 0=周日
20
-
21
- export const inNight = () => hour() >= NIGHT[0] && hour() < NIGHT[1]
22
- export const inDeliver = () => hour() >= DELIVER[0] && hour() < DELIVER[1]
23
-
12
+ import { scan, ranTonight, now, today } from './runs.js';
13
+ export const NIGHT = [0, 6];
14
+ export const DELIVER = [8, 22];
15
+ const hour = () => new Date(Date.now() + 8 * 3600e3).getUTCHours();
16
+ const minute = () => new Date(Date.now() + 8 * 3600e3).getUTCMinutes();
17
+ const weekday = () => new Date(Date.now() + 8 * 3600e3).getUTCDay(); // 0=周日
18
+ export const inNight = () => hour() >= NIGHT[0] && hour() < NIGHT[1];
19
+ export const inDeliver = () => hour() >= DELIVER[0] && hour() < DELIVER[1];
24
20
  /** 近 days 天的运行记录里,有没有一条 what 且 pass 的。 */
25
21
  export function ranOk(root, what, days) {
26
- const hit = scan(root, days).find((r) => r.did === what && r.r === 'pass')
27
- return hit ? `${hit._day} ${hit.t}` : ''
22
+ const hit = scan(root, days).find((r) => r.did === what && r.r === 'pass');
23
+ return hit ? `${hit._day} ${hit.t}` : '';
28
24
  }
29
-
30
25
  /**
31
26
  * 交付类值班的判据。**外发没有派生数据可查 —— 唯一的证据就是留痕。**
32
27
  *
@@ -42,35 +37,38 @@ export function ranOk(root, what, days) {
42
37
  * **在 1 或 2 落地之前,只有单实例是安全的。**
43
38
  */
44
39
  export function deliverDuty(root, { id, hh, mm = 0, playbook, weekly = false }) {
45
- const what = '值班·' + id
46
- let days, span, due
47
- if (weekly) {
48
- days = weekday() === 0 ? 7 : weekday()
49
- span = '本周'
50
- // ⚠️ 不能写成「只有周一 due」—— 周一漏一拍 → 这一期【永不补、也永不报警】。
51
- // 正确语义:**本期内还没送达,就一直该做。**
52
- due = weekday() >= 1 && (weekday() > 1 || hour() >= hh)
53
- } else {
54
- days = 1; span = '今天'
55
- due = hour() > hh || (hour() === hh && minute() >= mm)
56
- }
57
- const at = ranOk(root, what, days)
58
- return {
59
- id, kind: '交付', playbook,
60
- due: due && inDeliver(),
61
- done: Boolean(at),
62
- detail: at ? `${span}已送达 ${at}`
63
- : `${span}还没送达 · 该做的时刻 ${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`,
64
- gap: at ? '' : '按剧本执行 → **回读确认送达** → runlog 记 pass',
65
- }
40
+ const what = '值班·' + id;
41
+ let days, span, due;
42
+ if (weekly) {
43
+ days = weekday() === 0 ? 7 : weekday();
44
+ span = '本周';
45
+ // ⚠️ 不能写成「只有周一 due」—— 周一漏一拍 → 这一期【永不补、也永不报警】。
46
+ // 正确语义:**本期内还没送达,就一直该做。**
47
+ due = weekday() >= 1 && (weekday() > 1 || hour() >= hh);
48
+ }
49
+ else {
50
+ days = 1;
51
+ span = '今天';
52
+ due = hour() > hh || (hour() === hh && minute() >= mm);
53
+ }
54
+ const at = ranOk(root, what, days);
55
+ return {
56
+ id, kind: '交付', playbook,
57
+ due: due && inDeliver(),
58
+ done: Boolean(at),
59
+ detail: at ? `${span}已送达 ${at}`
60
+ : `${span}还没送达 · 该做的时刻 ${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`,
61
+ gap: at ? '' : '按剧本执行 → **回读确认送达** → runlog 记 pass',
62
+ };
66
63
  }
67
-
68
64
  /** 值班表 —— **母体出厂是空的,这是【正确】的。** 具体岗位在 config.duties 里加。 */
69
65
  export function loadDuties(cfg, root) {
70
- const out = {}
71
- for (const d of cfg.duties || []) {
72
- if (d.kind === '交付') out[d.id] = () => deliverDuty(root, d)
73
- // 轻/重 类的 done 判据是【数据派生】的,随岗位而异 → fork 后在这里注册自己的探针。
74
- }
75
- return out
66
+ const out = {};
67
+ for (const d of cfg.duties || []) {
68
+ if (d.kind === '交付')
69
+ out[d.id] = () => deliverDuty(root, d);
70
+ // 轻/重 类的 done 判据是【数据派生】的,随岗位而异 → fork 后在这里注册自己的探针。
71
+ }
72
+ return out;
76
73
  }
74
+ //# sourceMappingURL=duty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duty.js","sourceRoot":"","sources":["../../src/duty.ts"],"names":[],"mappings":"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","sourcesContent":["// 内化心跳值班 —— **待办是给人的,心跳是 Agent 的生理。**(TypeScript source)\n//\n// 把「永远做不完的义务」塞进「有终态的待办」,是错配:\n// 接力链一断 → 列表里没有下一期 → 心跳无事可做、静默收工、**且不自知**。\n//\n// 三类 + 窗口闸门:\n// 轻 白天任意一拍(秒级)\n// 重 00:00–06:00(白天每 15 分钟全量重算一遍是纯浪费)\n// 交付 08:00–22:00(外发,**静默时段顺延,不打扰第三方**)\n//\n// **完成判据必须【数据派生】** —— 不记状态、不建标记文件、不看会话记忆。\nimport { scan, ranTonight, now, today } from './runs.js'\n\nexport const NIGHT = [0, 6]\nexport const DELIVER = [8, 22]\n\nconst hour = () => new Date(Date.now() + 8 * 3600e3).getUTCHours()\nconst minute = () => new Date(Date.now() + 8 * 3600e3).getUTCMinutes()\nconst weekday = () => new Date(Date.now() + 8 * 3600e3).getUTCDay() // 0=周日\n\nexport const inNight = () => hour() >= NIGHT[0] && hour() < NIGHT[1]\nexport const inDeliver = () => hour() >= DELIVER[0] && hour() < DELIVER[1]\n\n/** 近 days 天的运行记录里,有没有一条 what 且 pass 的。 */\nexport function ranOk(root, what, days) {\n const hit = scan(root, days).find((r) => r.did === what && r.r === 'pass')\n return hit ? `${hit._day} ${hit.t}` : ''\n}\n\n/**\n * 交付类值班的判据。**外发没有派生数据可查 —— 唯一的证据就是留痕。**\n *\n * ⚠️ 这条 pass 只能在【回读确认送达】之后写。\n * 没送达就写 pass = 自己骗自己,**比链断掉还糟**(断链至少看得出来\"今天没跑\")。\n *\n * ⚠️⚠️ **已知缺口**:判据靠本地 runs/,而 runs/ 是 gitignored 的。\n * 换实例 / 换容器 / 清了本地档 → **今天的群消息会再发一遍。群里刷屏不可撤销。**\n * 对策(按可靠性排序):\n * 1. 发之前**先回读目标本身**(群里今天已经有这条了吗)—— 证据在【外面的世界】\n * 2. runs/ 落共享存储,让多实例看到同一份\n * 3. 至少:发之前先 duty --check,人眼确认\n * **在 1 或 2 落地之前,只有单实例是安全的。**\n */\nexport function deliverDuty(root, { id, hh, mm = 0, playbook, weekly = false }) {\n const what = '值班·' + id\n let days, span, due\n if (weekly) {\n days = weekday() === 0 ? 7 : weekday()\n span = '本周'\n // ⚠️ 不能写成「只有周一 due」—— 周一漏一拍 → 这一期【永不补、也永不报警】。\n // 正确语义:**本期内还没送达,就一直该做。**\n due = weekday() >= 1 && (weekday() > 1 || hour() >= hh)\n } else {\n days = 1; span = '今天'\n due = hour() > hh || (hour() === hh && minute() >= mm)\n }\n const at = ranOk(root, what, days)\n return {\n id, kind: '交付', playbook,\n due: due && inDeliver(),\n done: Boolean(at),\n detail: at ? `${span}已送达 ${at}`\n : `${span}还没送达 · 该做的时刻 ${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`,\n gap: at ? '' : '按剧本执行 → **回读确认送达** → runlog 记 pass',\n }\n}\n\n/** 值班表 —— **母体出厂是空的,这是【正确】的。** 具体岗位在 config.duties 里加。 */\nexport function loadDuties(cfg, root) {\n const out: Record<string, () => ReturnType<typeof deliverDuty>> = {}\n for (const d of cfg.duties || []) {\n if (d.kind === '交付') out[d.id] = () => deliverDuty(root, d)\n // 轻/重 类的 done 判据是【数据派生】的,随岗位而异 → fork 后在这里注册自己的探针。\n }\n return out\n}\n"]}
@@ -0,0 +1,270 @@
1
+ // dws 硬化包装 —— 这一层是「拦」(TypeScript source)。
2
+ //
3
+ // **上面的每个模块都必须走它,不许自己 spawn dws。**
4
+ //
5
+ // ════════════════════════════════════════════════════════════════
6
+ // 【六条工程纪律】—— 每一条都是踩出来的,违反必出错,而且是【静默】出错
7
+ // ════════════════════════════════════════════════════════════════
8
+ //
9
+ // 1. **拿不到就响亮地死,绝不当成"是空的"。**
10
+ // 不同接口把数据藏在不同层级。读错层级 → 拿到 undefined → 当成"表是空的"
11
+ // → **一整天的数据可以这么无声丢掉,而且不报错。**
12
+ // 「查到 0 条」和「读错地方了」**必须分得开**。分不开就 die。
13
+ //
14
+ // 2. **分页会静默截断。** 默认页大小有上限,超了就悄悄少给你,
15
+ // **而且不一定有信号告诉你被截断了。**
16
+ // **拿不到分页信号 ≠ 没有下一页,那叫「没有信号」。**
17
+ // **半张表算出来的结果是错的,不是"差不多"。**
18
+ //
19
+ // 3. **创建类:报错了也可能其实建成功了。**
20
+ // **绝不看返回信封** —— 按名回查 + 内容回读定成败。
21
+ // 推论:**创建类永不盲重试**(会造重复件),失败先按幂等键回查。
22
+ //
23
+ // 4. **写操作报错 ≠ 失败。** 回读实态才定成败。返回 success 也不算验收。
24
+ //
25
+ // 5. **字段读写不对称。** 写进去的形状 ≠ 读回来的形状。**写完必回读。**
26
+ //
27
+ // 6. **markdown 会被重排版。重排版不是漂移。**
28
+ // 判漂移必须剥掉 markdown 符号只看文字流 —— 用行级 diff 会每次都误报,
29
+ // **然后你就会开始无脑 --force,真漂移也一起覆盖掉。**
30
+ //
31
+ // 一句话:**所有这些坑的形状都一样 —— 它不报错,它给你一个看起来很正常的答案。**
32
+ // 这一层做的就是:**把「看起来正常」和「真的正常」分开。**
33
+ import { spawnSync } from 'node:child_process';
34
+ export class Blocked extends Error {
35
+ }
36
+ export function die(msg) {
37
+ console.error('[FAIL] ' + msg);
38
+ throw new Blocked(msg);
39
+ }
40
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
41
+ /** 跑一条 dws。限流退避;超时不当成"空";坏响应响亮地死。 */
42
+ export function run(args, { allowFail = false, timeout = 180_000, retries = 2 } = {}) {
43
+ let last = '';
44
+ for (let attempt = 0; attempt <= retries; attempt++) {
45
+ const p = spawnSync('dws', [...args, '--format', 'json'], { encoding: 'utf8', timeout, maxBuffer: 64 * 1024 * 1024 });
46
+ if (p.error) {
47
+ if (p.error.code === 'ENOENT') {
48
+ if (allowFail)
49
+ return null;
50
+ die('dws 跑不起来 —— 先 export PATH="$HOME/.local/bin:$PATH"');
51
+ }
52
+ last = String(p.error);
53
+ sleep(2000 * (attempt + 1));
54
+ continue;
55
+ }
56
+ const out = p.stdout || p.stderr || '';
57
+ last = out.slice(0, 300);
58
+ const i = out.indexOf('{');
59
+ if (i < 0) {
60
+ sleep(2000 * (attempt + 1));
61
+ continue;
62
+ }
63
+ let d;
64
+ try {
65
+ d = JSON.parse(out.slice(i, lastBrace(out) + 1));
66
+ }
67
+ catch {
68
+ sleep(2000 * (attempt + 1));
69
+ continue;
70
+ }
71
+ const err = d.error || {};
72
+ if (err && (err.code || err.message)) {
73
+ const s = JSON.stringify(err).toLowerCase();
74
+ if (s.includes('limit') || s.includes('frequen') || s.includes('flow')) {
75
+ sleep(3000 * (attempt + 1));
76
+ continue; // 限流 → 退避
77
+ }
78
+ if (allowFail)
79
+ return null;
80
+ die(`dws 失败 ${args.slice(0, 4).join(' ')}\n ${JSON.stringify(err).slice(0, 300)}`);
81
+ }
82
+ if (p.status !== 0) {
83
+ if (allowFail)
84
+ return null;
85
+ die(`dws 以退出码 ${p.status} 结束,但没有结构化 error: ${args.slice(0, 4).join(' ')}\n ${last}`);
86
+ }
87
+ return d;
88
+ }
89
+ if (allowFail)
90
+ return null;
91
+ die(`dws 无响应(重试 ${retries} 次) ${args.slice(0, 4).join(' ')}\n ${last}`);
92
+ }
93
+ function lastBrace(s) {
94
+ // dws 有时在 JSON 后面粘一行 WARN / INFO —— 从最后一个 } 截断
95
+ for (let i = s.length - 1; i >= 0; i--)
96
+ if (s[i] === '}')
97
+ return i;
98
+ return s.length - 1;
99
+ }
100
+ // ──────────────── 三个层级,各读各的 ────────────────
101
+ const inner = (d) => (d && typeof d.data === 'object' && d.data !== null ? d.data : d);
102
+ /** 列表类查询的记录。拿不到就 die —— 绝不当成"表是空的"。 */
103
+ export function records(d) {
104
+ const x = inner(d);
105
+ if (!('records' in x)) {
106
+ die(`返回里没有 records(顶层键=${Object.keys(d)}) —— 接口形状可能变了。` +
107
+ `**绝不把它当成【表是空的】。**`);
108
+ }
109
+ if (x.hasMore || x.partial) {
110
+ die('分页未收敛 —— **部分结果不许当完整结果用。** 半张表算出来的是错的。');
111
+ }
112
+ return x.records || [];
113
+ }
114
+ /** 文档正文。**层级和列表类查询不一样** —— 两种形状都兜住,拿不到就 die。 */
115
+ export function markdown(d) {
116
+ const md = inner(d).markdown ?? d.markdown;
117
+ if (md === undefined || md === null) {
118
+ die(`拿不到文档正文(顶层键=${Object.keys(d)}) —— **绝不当成【这篇是空的】。**`);
119
+ }
120
+ return md;
121
+ }
122
+ /** 目录节点。 */
123
+ export const nodes = (d) => inner(d).nodes || [];
124
+ /**
125
+ * 待办翻页 —— **这是唯一有分页信号的读法。**
126
+ *
127
+ * ⚠️ **不要把页开大**。超过这个大小,分页信号会消失,
128
+ * 你拿到 undefined、当成 false,然后**静默漏掉剩下的**。
129
+ * 心跳的收件靠它 —— 漏了循环件就是「静默收工且不自知」。
130
+ */
131
+ export function todosPage(status = 'false', page = 1) {
132
+ const d = run(['todo', 'task', 'list', '--status', status, '--size', '20', '--page', String(page)]);
133
+ const r = d.result || {};
134
+ if (!('todoCards' in r)) {
135
+ die(`待办返回里没有 todoCards(result 键=${Object.keys(r)}) —— 绝不当成【没有待办】。`);
136
+ }
137
+ return { cards: r.todoCards || [], more: Boolean(r.hasMore) };
138
+ }
139
+ /** 把待办【拉全】。只看第一页会漏掉循环件。 */
140
+ export function todosAll(status = 'false', maxPages = 20) {
141
+ const out = [];
142
+ for (let page = 1; page <= maxPages; page++) {
143
+ const { cards, more } = todosPage(status, page);
144
+ out.push(...cards);
145
+ if (!more)
146
+ return out;
147
+ }
148
+ die(`待办翻页超过 ${maxPages} 页还没收敛 —— 不许把部分结果当完整。`);
149
+ }
150
+ // ──────────────── 创建类:报错也可能成功 → 按名回查 ────────────────
151
+ /** 目录列表可能把扩展名单列,比对时要拼回去。 */
152
+ export function findChild(folder, name) {
153
+ for (const n of nodes(run(['doc', 'list', '--folder', folder, '--page-size', '50']))) {
154
+ const nm = n.name || '', ext = n.extension || '';
155
+ if (nm === name || (ext && `${nm}.${ext}` === name))
156
+ return n.nodeId;
157
+ }
158
+ return null;
159
+ }
160
+ /** 建文档。**不看返回信封** —— 一律按名回查定成败。 */
161
+ export function createDoc(folder, name, file) {
162
+ const exist = findChild(folder, name);
163
+ if (exist)
164
+ return exist;
165
+ run(['doc', 'create', '--name', name, '--folder', folder,
166
+ '--content-file', file, '--content-format', 'markdown', '--yes'], { allowFail: true }); // ← **报错也可能其实成功了**
167
+ sleep(1200);
168
+ const nid = findChild(folder, name);
169
+ if (!nid)
170
+ die(`建不出文档 ${name} —— 别把它当成【建成功了】。`);
171
+ return nid;
172
+ }
173
+ export const updateDoc = (node, file) => run(['doc', 'update', '--node', node, '--content-file', file,
174
+ '--content-format', 'markdown', '--mode', 'overwrite', '--yes']);
175
+ export const readDoc = (node, profile = '') => markdown(run([
176
+ 'doc', 'read', '--node', node, ...profileArgs(profile),
177
+ ]));
178
+ export const docInfo = (node, profile = '') => inner(run([
179
+ 'doc', 'info', '--node', node, ...profileArgs(profile),
180
+ ]));
181
+ // ──────────────── 漂移判定:重排版【不是】漂移 ────────────────
182
+ /**
183
+ * 剥掉 markdown 语法和转义装饰,只留【文字本身】。
184
+ *
185
+ * 钉钉会重排版你推上去的 markdown。**重排版不是漂移。**
186
+ * 用行级 diff 判漂移 → 每次 sync 都误报 →
187
+ * **然后你就会开始无脑 --force,真正的漂移也一起覆盖掉了。**
188
+ */
189
+ export function words(s) {
190
+ let t = String(s ?? '');
191
+ for (let i = 0; i < 3; i++)
192
+ t = unescapeHtml(t); // 可能被多次转义
193
+ t = t.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n));
194
+ t = t.replace(/\[打开\]\([^)]*\)/g, ''); // 钉钉自动加的链接装饰
195
+ return t.replace(/[`*_>#|\\~\-\s]+/g, '');
196
+ }
197
+ function unescapeHtml(s) {
198
+ return s.replace(/&(amp|lt|gt|quot|#39);/g, (m, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" })[e]);
199
+ }
200
+ export const same = (a, b) => words(a) === words(b);
201
+ // ──────────────── 数字员工的消息原子动作(typed,目标不可自由拼装) ────────────────
202
+ function profileArgs(profile) {
203
+ return profile ? ['--profile', profile] : [];
204
+ }
205
+ function assertReplyTarget(target) {
206
+ const missing = [];
207
+ if (!target?.conversationId)
208
+ missing.push('conversationId');
209
+ if (!target?.messageId)
210
+ missing.push('messageId');
211
+ if (!target?.senderOpenDingTalkId)
212
+ missing.push('senderOpenDingTalkId');
213
+ if (missing.length)
214
+ die(`固定回复目标缺少 ${missing.join(' / ')}`);
215
+ }
216
+ /** ack 没有服务端 UUID,因此绝不自动重试。 */
217
+ export function ackMessage(target, { emoji = '收到', profile = '' } = {}) {
218
+ assertReplyTarget(target);
219
+ return run([
220
+ 'chat', 'message', 'add-emoji',
221
+ '--conversation-id', target.conversationId,
222
+ '--msg-id', target.messageId,
223
+ '--emoji', emoji,
224
+ ...profileArgs(profile),
225
+ ], { retries: 0 });
226
+ }
227
+ /** reply/ask 共用同一传输;uuid 在调用前已持久化,重试也必须复用。 */
228
+ export function replyMessage(target, text, { uuid = '', profile = '' } = {}) {
229
+ assertReplyTarget(target);
230
+ if (!String(text || '').trim())
231
+ die('回复正文不能为空');
232
+ if (!uuid)
233
+ die('回复前必须先持久化 uuid 幂等键');
234
+ return run([
235
+ 'chat', 'message', 'reply',
236
+ '--conversation-id', target.conversationId,
237
+ '--ref-msg-id', target.messageId,
238
+ '--ref-sender', target.senderOpenDingTalkId,
239
+ '--text', text,
240
+ '--uuid', uuid,
241
+ '--ai-tag=true',
242
+ ...profileArgs(profile),
243
+ '--yes',
244
+ ], { retries: 1 });
245
+ }
246
+ export function readMessageEmotions(messageId, profile = '') {
247
+ return run([
248
+ 'chat', 'message', 'list-emotion-replies', '--msg-ids', messageId,
249
+ ...profileArgs(profile),
250
+ ], { allowFail: true, retries: 0 });
251
+ }
252
+ export function querySendStatus(openTaskId, profile = '') {
253
+ return run([
254
+ 'chat', 'message', 'query-send-status', '--open-task-id', openTaskId,
255
+ ...profileArgs(profile),
256
+ ], { allowFail: true, retries: 1 });
257
+ }
258
+ export function readMessagesByIds(messageId, profile = '') {
259
+ return run([
260
+ 'chat', 'message', 'list-by-ids', '--msg-ids', messageId,
261
+ ...profileArgs(profile),
262
+ ], { allowFail: true, retries: 1 });
263
+ }
264
+ /** 外发前核对 profile 对应的真实执行身份。 */
265
+ export function getSelf(profile = '') {
266
+ return run([
267
+ 'contact', 'user', 'get-self', ...profileArgs(profile),
268
+ ], { retries: 1 });
269
+ }
270
+ //# sourceMappingURL=dws.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dws.js","sourceRoot":"","sources":["../../src/dws.ts"],"names":[],"mappings":"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","sourcesContent":["// dws 硬化包装 —— 这一层是「拦」(TypeScript source)。\n//\n// **上面的每个模块都必须走它,不许自己 spawn dws。**\n//\n// ════════════════════════════════════════════════════════════════\n// 【六条工程纪律】—— 每一条都是踩出来的,违反必出错,而且是【静默】出错\n// ════════════════════════════════════════════════════════════════\n//\n// 1. **拿不到就响亮地死,绝不当成\"是空的\"。**\n// 不同接口把数据藏在不同层级。读错层级 → 拿到 undefined → 当成\"表是空的\"\n// → **一整天的数据可以这么无声丢掉,而且不报错。**\n// 「查到 0 条」和「读错地方了」**必须分得开**。分不开就 die。\n//\n// 2. **分页会静默截断。** 默认页大小有上限,超了就悄悄少给你,\n// **而且不一定有信号告诉你被截断了。**\n// **拿不到分页信号 ≠ 没有下一页,那叫「没有信号」。**\n// **半张表算出来的结果是错的,不是\"差不多\"。**\n//\n// 3. **创建类:报错了也可能其实建成功了。**\n// **绝不看返回信封** —— 按名回查 + 内容回读定成败。\n// 推论:**创建类永不盲重试**(会造重复件),失败先按幂等键回查。\n//\n// 4. **写操作报错 ≠ 失败。** 回读实态才定成败。返回 success 也不算验收。\n//\n// 5. **字段读写不对称。** 写进去的形状 ≠ 读回来的形状。**写完必回读。**\n//\n// 6. **markdown 会被重排版。重排版不是漂移。**\n// 判漂移必须剥掉 markdown 符号只看文字流 —— 用行级 diff 会每次都误报,\n// **然后你就会开始无脑 --force,真漂移也一起覆盖掉。**\n//\n// 一句话:**所有这些坑的形状都一样 —— 它不报错,它给你一个看起来很正常的答案。**\n// 这一层做的就是:**把「看起来正常」和「真的正常」分开。**\n\nimport { spawnSync } from 'node:child_process'\n\nexport class Blocked extends Error {}\n\nexport function die(msg): never {\n console.error('[FAIL] ' + msg)\n throw new Blocked(msg)\n}\n\nconst sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)\n\n/** 跑一条 dws。限流退避;超时不当成\"空\";坏响应响亮地死。 */\nexport function run(args, { allowFail = false, timeout = 180_000, retries = 2 } = {}) {\n let last = ''\n for (let attempt = 0; attempt <= retries; attempt++) {\n const p = spawnSync('dws', [...args, '--format', 'json'],\n { encoding: 'utf8', timeout, maxBuffer: 64 * 1024 * 1024 })\n if (p.error) {\n if ((p.error as NodeJS.ErrnoException).code === 'ENOENT') {\n if (allowFail) return null\n die('dws 跑不起来 —— 先 export PATH=\"$HOME/.local/bin:$PATH\"')\n }\n last = String(p.error); sleep(2000 * (attempt + 1)); continue\n }\n const out = p.stdout || p.stderr || ''\n last = out.slice(0, 300)\n const i = out.indexOf('{')\n if (i < 0) { sleep(2000 * (attempt + 1)); continue }\n let d\n try { d = JSON.parse(out.slice(i, lastBrace(out) + 1)) }\n catch { sleep(2000 * (attempt + 1)); continue }\n const err = d.error || {}\n if (err && (err.code || err.message)) {\n const s = JSON.stringify(err).toLowerCase()\n if (s.includes('limit') || s.includes('frequen') || s.includes('flow')) {\n sleep(3000 * (attempt + 1)); continue // 限流 → 退避\n }\n if (allowFail) return null\n die(`dws 失败 ${args.slice(0, 4).join(' ')}\\n ${JSON.stringify(err).slice(0, 300)}`)\n }\n if (p.status !== 0) {\n if (allowFail) return null\n die(`dws 以退出码 ${p.status} 结束,但没有结构化 error: ${args.slice(0, 4).join(' ')}\\n ${last}`)\n }\n return d\n }\n if (allowFail) return null\n die(`dws 无响应(重试 ${retries} 次) ${args.slice(0, 4).join(' ')}\\n ${last}`)\n}\n\nfunction lastBrace(s) {\n // dws 有时在 JSON 后面粘一行 WARN / INFO —— 从最后一个 } 截断\n for (let i = s.length - 1; i >= 0; i--) if (s[i] === '}') return i\n return s.length - 1\n}\n\n// ──────────────── 三个层级,各读各的 ────────────────\nconst inner = (d) => (d && typeof d.data === 'object' && d.data !== null ? d.data : d)\n\n/** 列表类查询的记录。拿不到就 die —— 绝不当成\"表是空的\"。 */\nexport function records(d) {\n const x = inner(d)\n if (!('records' in x)) {\n die(`返回里没有 records(顶层键=${Object.keys(d)}) —— 接口形状可能变了。` +\n `**绝不把它当成【表是空的】。**`)\n }\n if (x.hasMore || x.partial) {\n die('分页未收敛 —— **部分结果不许当完整结果用。** 半张表算出来的是错的。')\n }\n return x.records || []\n}\n\n/** 文档正文。**层级和列表类查询不一样** —— 两种形状都兜住,拿不到就 die。 */\nexport function markdown(d) {\n const md = inner(d).markdown ?? d.markdown\n if (md === undefined || md === null) {\n die(`拿不到文档正文(顶层键=${Object.keys(d)}) —— **绝不当成【这篇是空的】。**`)\n }\n return md\n}\n\n/** 目录节点。 */\nexport const nodes = (d) => inner(d).nodes || []\n\n/**\n * 待办翻页 —— **这是唯一有分页信号的读法。**\n *\n * ⚠️ **不要把页开大**。超过这个大小,分页信号会消失,\n * 你拿到 undefined、当成 false,然后**静默漏掉剩下的**。\n * 心跳的收件靠它 —— 漏了循环件就是「静默收工且不自知」。\n */\nexport function todosPage(status = 'false', page = 1) {\n const d = run(['todo', 'task', 'list', '--status', status, '--size', '20', '--page', String(page)])\n const r = d.result || {}\n if (!('todoCards' in r)) {\n die(`待办返回里没有 todoCards(result 键=${Object.keys(r)}) —— 绝不当成【没有待办】。`)\n }\n return { cards: r.todoCards || [], more: Boolean(r.hasMore) }\n}\n\n/** 把待办【拉全】。只看第一页会漏掉循环件。 */\nexport function todosAll(status = 'false', maxPages = 20) {\n const out: any[] = []\n for (let page = 1; page <= maxPages; page++) {\n const { cards, more } = todosPage(status, page)\n out.push(...cards)\n if (!more) return out\n }\n die(`待办翻页超过 ${maxPages} 页还没收敛 —— 不许把部分结果当完整。`)\n}\n\n// ──────────────── 创建类:报错也可能成功 → 按名回查 ────────────────\n/** 目录列表可能把扩展名单列,比对时要拼回去。 */\nexport function findChild(folder, name) {\n for (const n of nodes(run(['doc', 'list', '--folder', folder, '--page-size', '50']))) {\n const nm = n.name || '', ext = n.extension || ''\n if (nm === name || (ext && `${nm}.${ext}` === name)) return n.nodeId\n }\n return null\n}\n\n/** 建文档。**不看返回信封** —— 一律按名回查定成败。 */\nexport function createDoc(folder, name, file) {\n const exist = findChild(folder, name)\n if (exist) return exist\n run(['doc', 'create', '--name', name, '--folder', folder,\n '--content-file', file, '--content-format', 'markdown', '--yes'],\n { allowFail: true }) // ← **报错也可能其实成功了**\n sleep(1200)\n const nid = findChild(folder, name)\n if (!nid) die(`建不出文档 ${name} —— 别把它当成【建成功了】。`)\n return nid\n}\n\nexport const updateDoc = (node, file) =>\n run(['doc', 'update', '--node', node, '--content-file', file,\n '--content-format', 'markdown', '--mode', 'overwrite', '--yes'])\n\nexport const readDoc = (node, profile = '') => markdown(run([\n 'doc', 'read', '--node', node, ...profileArgs(profile),\n]))\nexport const docInfo = (node, profile = '') => inner(run([\n 'doc', 'info', '--node', node, ...profileArgs(profile),\n]))\n\n// ──────────────── 漂移判定:重排版【不是】漂移 ────────────────\n/**\n * 剥掉 markdown 语法和转义装饰,只留【文字本身】。\n *\n * 钉钉会重排版你推上去的 markdown。**重排版不是漂移。**\n * 用行级 diff 判漂移 → 每次 sync 都误报 →\n * **然后你就会开始无脑 --force,真正的漂移也一起覆盖掉了。**\n */\nexport function words(s) {\n let t = String(s ?? '')\n for (let i = 0; i < 3; i++) t = unescapeHtml(t) // 可能被多次转义\n t = t.replace(/&#(\\d+);/g, (_, n) => String.fromCharCode(+n))\n t = t.replace(/\\[打开\\]\\([^)]*\\)/g, '') // 钉钉自动加的链接装饰\n return t.replace(/[`*_>#|\\\\~\\-\\s]+/g, '')\n}\n\nfunction unescapeHtml(s) {\n return s.replace(/&(amp|lt|gt|quot|#39);/g, (m, e) =>\n ({ amp: '&', lt: '<', gt: '>', quot: '\"', '#39': \"'\" })[e])\n}\n\nexport const same = (a, b) => words(a) === words(b)\n\n// ──────────────── 数字员工的消息原子动作(typed,目标不可自由拼装) ────────────────\nfunction profileArgs(profile) {\n return profile ? ['--profile', profile] : []\n}\n\nfunction assertReplyTarget(target) {\n const missing: string[] = []\n if (!target?.conversationId) missing.push('conversationId')\n if (!target?.messageId) missing.push('messageId')\n if (!target?.senderOpenDingTalkId) missing.push('senderOpenDingTalkId')\n if (missing.length) die(`固定回复目标缺少 ${missing.join(' / ')}`)\n}\n\n/** ack 没有服务端 UUID,因此绝不自动重试。 */\nexport function ackMessage(target, { emoji = '收到', profile = '' } = {}) {\n assertReplyTarget(target)\n return run([\n 'chat', 'message', 'add-emoji',\n '--conversation-id', target.conversationId,\n '--msg-id', target.messageId,\n '--emoji', emoji,\n ...profileArgs(profile),\n ], { retries: 0 })\n}\n\n/** reply/ask 共用同一传输;uuid 在调用前已持久化,重试也必须复用。 */\nexport function replyMessage(\n target,\n text,\n { uuid = '', profile = '' }: { uuid?: string; profile?: string } = {},\n) {\n assertReplyTarget(target)\n if (!String(text || '').trim()) die('回复正文不能为空')\n if (!uuid) die('回复前必须先持久化 uuid 幂等键')\n return run([\n 'chat', 'message', 'reply',\n '--conversation-id', target.conversationId,\n '--ref-msg-id', target.messageId,\n '--ref-sender', target.senderOpenDingTalkId,\n '--text', text,\n '--uuid', uuid,\n '--ai-tag=true',\n ...profileArgs(profile),\n '--yes',\n ], { retries: 1 })\n}\n\nexport function readMessageEmotions(messageId, profile = '') {\n return run([\n 'chat', 'message', 'list-emotion-replies', '--msg-ids', messageId,\n ...profileArgs(profile),\n ], { allowFail: true, retries: 0 })\n}\n\nexport function querySendStatus(openTaskId, profile = '') {\n return run([\n 'chat', 'message', 'query-send-status', '--open-task-id', openTaskId,\n ...profileArgs(profile),\n ], { allowFail: true, retries: 1 })\n}\n\nexport function readMessagesByIds(messageId, profile = '') {\n return run([\n 'chat', 'message', 'list-by-ids', '--msg-ids', messageId,\n ...profileArgs(profile),\n ], { allowFail: true, retries: 1 })\n}\n\n/** 外发前核对 profile 对应的真实执行身份。 */\nexport function getSelf(profile = '') {\n return run([\n 'contact', 'user', 'get-self', ...profileArgs(profile),\n ], { retries: 1 })\n}\n"]}