@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
package/dist/src/kb.js ADDED
@@ -0,0 +1,240 @@
1
+ // 知识库挂载与同步(TypeScript source)。
2
+ //
3
+ // ════════════════════════════════════════════════════════════════════
4
+ // 【核心判断】所有权决定真值方向。
5
+ // ════════════════════════════════════════════════════════════════════
6
+ //
7
+ // owned(我的本体) external(别人维护的)
8
+ // 真值在 **Git**(本地) **钉钉**(远端)
9
+ // 同步方向 push 本地 → 钉钉 pull 钉钉 → 本地
10
+ // 为什么 给人看(人机同源) 给我 grep(快、能看上下文)
11
+ // 冲突了 远端被人改过 → **停** 远端赢,本地缓存可丢弃
12
+ // 落在 ontology/(入 Git) .cache/kb/<name>/(gitignore)
13
+ //
14
+ // 这就是「Git 内核 × 钉钉暴露层」:**Git 是常量层,钉钉是可替换的暴露层。**
15
+ //
16
+ // ════════════════════════════════════════════════════════════════════
17
+ // 【三个硬约束】—— 全是踩出来的
18
+ // ════════════════════════════════════════════════════════════════════
19
+ //
20
+ // 1. **绝不双向自动 merge。** 一个方向是真值,另一个是投影。
21
+ // 自动合并两个都在变的东西 = 灾难,而且是【静默】灾难。
22
+ //
23
+ // 2. **重排版不是漂移。** 判据是「剥掉 markdown 符号后的文字流」,不是行级 diff。
24
+ // 用行级 diff → 每次 sync 都误报 → **你就会开始无脑 --force,真漂移也一起覆盖掉。**
25
+ //
26
+ // 3. **缓存不新鲜就不许用(fail-closed)。** external KB 是快照。
27
+ // **拿着过期快照回答,比说「我不知道」危险得多。**
28
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, statSync } from 'node:fs';
29
+ import { readdirSync } from 'node:fs';
30
+ import { join, dirname, relative, basename } from 'node:path';
31
+ import { spawnSync } from 'node:child_process';
32
+ import * as dws from './dws.js';
33
+ export const OWNED = 'owned', EXTERNAL = 'external';
34
+ export const PULL = 'pull', PUSH = 'push', NONE = 'none';
35
+ /** external 缓存超过这么久就算过期。过期 → kb search 会警告并要求先 sync。 */
36
+ export const STALE_HOURS = 24;
37
+ const now = () => new Date(Date.now() + 8 * 3600e3).toISOString().replace('Z', '+08:00').slice(0, 19);
38
+ class DriftError extends Error {
39
+ exitCode = 1;
40
+ }
41
+ /** dingtalk:doc:<folderId> · dingtalk:wiki:<spaceId> · local:<path> */
42
+ export function parseSource(s) {
43
+ let m = /^dingtalk:(doc|wiki):(.+)$/.exec(s);
44
+ if (m)
45
+ return { kind: m[1], id: m[2] };
46
+ m = /^local:(.+)$/.exec(s);
47
+ if (m)
48
+ return { kind: 'local', id: m[1].replace(/^~/, process.env.HOME || '~') };
49
+ throw new Error(`挂载源写错了: ${s}\n` +
50
+ ` 支持三种:\n` +
51
+ ` dingtalk:doc:<folderId> 文档文件夹(最常见 —— 很多组织根本没建"知识库")\n` +
52
+ ` dingtalk:wiki:<spaceId> 钉钉知识库空间\n` +
53
+ ` local:<路径> 本地目录(只读挂载,比如另一个仓库的 wiki/)`);
54
+ }
55
+ // ════════════════ pull:钉钉 → 本地缓存 ════════════════
56
+ function walkDoc(folder, prefix = '') {
57
+ const out = [];
58
+ for (const n of dws.nodes(dws.run(['doc', 'list', '--folder', folder, '--page-size', '50']))) {
59
+ const name = n.name || '', nid = n.nodeId;
60
+ if (n.nodeType === 'folder') {
61
+ out.push(...walkDoc(nid, join(prefix, name)));
62
+ continue;
63
+ }
64
+ if (n.contentType && n.contentType !== 'ALIDOC')
65
+ continue; // 二进制附件不是知识
66
+ out.push({ rel: join(prefix, name.endsWith('.md') ? name : `${name}.md`),
67
+ nid, mtime: n.updateTime || 0 });
68
+ }
69
+ return out;
70
+ }
71
+ function walkWiki(space, folder = '', prefix = '') {
72
+ const args = ['wiki', 'node', 'list', '--workspace', space, '--limit', '50'];
73
+ if (folder)
74
+ args.push('--folder', folder);
75
+ const out = [];
76
+ for (const n of dws.nodes(dws.run(args))) {
77
+ const name = n.name || '', nid = n.nodeId;
78
+ if (n.nodeType === 'folder' || n.hasChildren)
79
+ out.push(...walkWiki(space, nid, join(prefix, name)));
80
+ out.push({ rel: join(prefix, name.endsWith('.md') ? name : `${name}.md`),
81
+ nid, mtime: n.updateTime || 0 });
82
+ }
83
+ return out;
84
+ }
85
+ /** 钉钉 → 本地缓存。**远端赢**(external 的真值在远端)。增量靠 updateTime。 */
86
+ export function pull(kb, root, force = false) {
87
+ const { kind, id } = parseSource(kb.from);
88
+ if (kind === 'local') {
89
+ console.log(` local ${kb.name} 就在本地,不用同步`);
90
+ return;
91
+ }
92
+ const dest = join(root, kb.local);
93
+ const items = kind === 'doc' ? walkDoc(id) : walkWiki(id);
94
+ if (!items.length) {
95
+ throw new Error(`「${kb.name}」远端一个节点都没有 —— **不许当成【这个知识库是空的】。**\n` +
96
+ ` 可能是: ID 写错 / 没权限 / 它真的空。先用 dws 手工确认再说。`);
97
+ }
98
+ kb._state ??= {};
99
+ let pulled = 0, skipped = 0;
100
+ for (const { rel, nid, mtime } of items) {
101
+ const p = join(dest, rel);
102
+ if (!force && kb._state[nid]?.mtime === mtime && existsSync(p)) {
103
+ skipped++;
104
+ continue;
105
+ }
106
+ const md = dws.readDoc(nid);
107
+ mkdirSync(dirname(p), { recursive: true });
108
+ // 头三行钉住来源 —— **没有指针的知识,用的时候不敢信。**
109
+ writeFileSync(p, `<!-- 来自钉钉 · 只读缓存 · 不要在这里改,改了下次 sync 会被覆盖 -->\n` +
110
+ `<!-- nodeId: ${nid} -->\n<!-- 拉取: ${now()} -->\n\n${md}`);
111
+ kb._state[nid] = { rel, mtime };
112
+ pulled++;
113
+ }
114
+ kb._syncedAt = now();
115
+ console.log(` pull ${kb.name.padEnd(14)} 拉了 ${pulled} 篇 · 没变 ${skipped} 篇`);
116
+ }
117
+ // ════════════════ push:本地 → 钉钉(人机同源)════════════════
118
+ function walkLocal(dir, base = dir) {
119
+ const out = [];
120
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
121
+ const p = join(dir, e.name);
122
+ if (e.isDirectory()) {
123
+ out.push(...walkLocal(p, base));
124
+ continue;
125
+ }
126
+ if (e.name.endsWith('.md'))
127
+ out.push({ path: p, rel: relative(base, p) });
128
+ }
129
+ return out;
130
+ }
131
+ /**
132
+ * 本地 → 钉钉。**Git 赢**(owned 的真值在 Git)。
133
+ *
134
+ * ⚠️ **推之前先查漂移** —— 远端被人改过就【停下来问】,绝不盲覆盖。
135
+ * 人在钉钉里改了东西,说明他有话要说。**盲覆盖 = 把人的意见删了。**
136
+ */
137
+ export function push(kb, root, force = false) {
138
+ const { kind, id } = parseSource(kb.from);
139
+ if (kind !== 'doc') {
140
+ throw new Error('push 目前只支持 dingtalk:doc:<folderId>(文档文件夹)。\n' +
141
+ ' 知识库空间的写入面还没验过 —— **不许假装支持。**');
142
+ }
143
+ const src = join(root, kb.local);
144
+ if (!existsSync(src))
145
+ throw new Error(`本地目录不存在: ${src}`);
146
+ kb._state ??= {};
147
+ const drift = [];
148
+ let pushed = 0, unchanged = 0;
149
+ for (const { path, rel } of walkLocal(src)) {
150
+ const body = readFileSync(path, 'utf8');
151
+ let nid = Object.keys(kb._state).find((k) => kb._state[k].rel === rel);
152
+ if (!nid) {
153
+ nid = String(dws.createDoc(id, basename(rel), path) || '');
154
+ if (!nid)
155
+ throw new Error(`创建 ${rel} 后没有返回 nodeId`);
156
+ kb._state[nid] = { rel, pushedWords: dws.words(body) };
157
+ pushed++;
158
+ console.log(` + 新建 ${rel}`);
159
+ continue;
160
+ }
161
+ const remote = dws.readDoc(nid);
162
+ if (dws.same(remote, body)) {
163
+ unchanged++;
164
+ continue;
165
+ }
166
+ // 远端 ≠ 我上次推上去的 → **有人在钉钉里改过**
167
+ const last = kb._state[nid].pushedWords;
168
+ if (last && dws.words(remote) !== last && !force) {
169
+ drift.push({ rel, nid });
170
+ continue;
171
+ }
172
+ dws.updateDoc(nid, path);
173
+ if (!dws.same(dws.readDoc(nid), body)) {
174
+ throw new Error(`推了 ${rel} 但回读不一致 —— **写操作报错≠失败,回读定成败。**`);
175
+ }
176
+ kb._state[nid].pushedWords = dws.words(body);
177
+ pushed++;
178
+ console.log(` ↑ 更新 ${rel}`);
179
+ }
180
+ if (drift.length) {
181
+ console.log(`\n ⚠️ **远端被人改过**,默认拒绝覆盖(${drift.length} 篇):`);
182
+ for (const { rel, nid } of drift)
183
+ console.log(` ${rel} nodeId=${nid}`);
184
+ console.log(` 人在钉钉里改了东西,说明他有话要说。**先去看一眼**,别盲覆盖。`);
185
+ console.log(` 确认要覆盖: dingtalk-agent kb sync --name ${kb.name} --force`);
186
+ throw new DriftError('有漂移,拒绝覆盖');
187
+ }
188
+ kb._syncedAt = now();
189
+ console.log(` push ${kb.name.padEnd(14)} 推了 ${pushed} 篇 · 一致 ${unchanged} 篇`);
190
+ }
191
+ // ════════════════ search:跨所有挂载点 ════════════════
192
+ export function stale(kb) {
193
+ if (kb.own !== EXTERNAL || kb.sync === NONE)
194
+ return false;
195
+ if (!kb._syncedAt)
196
+ return true;
197
+ const age = (Date.now() - Date.parse(kb._syncedAt)) / 3600e3;
198
+ return age > STALE_HOURS;
199
+ }
200
+ /**
201
+ * 跨所有挂载的 KB 搜。**本地 grep 为主** —— 快、确定、能看到上下文。
202
+ *
203
+ * fail-closed:不新鲜的缓存会【警告】。
204
+ * **拿着过期快照回答,比说「我不知道」危险得多。**
205
+ */
206
+ export function search(cfg, root, kw, top = 20) {
207
+ const hits = [];
208
+ const warned = [];
209
+ for (const kb of cfg.kb || []) {
210
+ const base = kb.local.startsWith('/') ? kb.local : join(root, kb.local);
211
+ if (!existsSync(base))
212
+ continue;
213
+ if (stale(kb))
214
+ warned.push(kb.name);
215
+ const r = spawnSync('grep', ['-ril', '--include=*.md', kw, base], { encoding: 'utf8' });
216
+ for (const p of (r.stdout || '').split('\n').filter(Boolean)) {
217
+ const ctx = spawnSync('grep', ['-i', '-n', '-m', '2', kw, p], { encoding: 'utf8' }).stdout || '';
218
+ hits.push({ kb: kb.name, file: relative(root, p), ctx: ctx.trim() });
219
+ }
220
+ }
221
+ if (warned.length) {
222
+ console.log(`⚠️ 这些知识库的缓存【超过 ${STALE_HOURS} 小时没同步】了: ${warned.join('、')}`);
223
+ console.log(` **拿着过期快照回答,比说「我不知道」危险得多。**`);
224
+ console.log(` 先跑: dingtalk-agent kb sync\n`);
225
+ }
226
+ if (!hits.length) {
227
+ console.log(`没搜到「${kw}」。`);
228
+ console.log(`**搜不到就说搜不到 —— 不要编。** 挂了哪些库: dingtalk-agent kb list`);
229
+ return [];
230
+ }
231
+ console.log(`命中 ${hits.length} 处:`);
232
+ for (const h of hits.slice(0, top)) {
233
+ console.log(`\n── [${h.kb}] ${h.file}`);
234
+ for (const l of h.ctx.split('\n').slice(0, 2))
235
+ console.log(` ${l.slice(0, 110)}`);
236
+ }
237
+ console.log(`\n**别只看这几行就作答** —— 打开命中的那一页读完再说。`);
238
+ return hits;
239
+ }
240
+ //# sourceMappingURL=kb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kb.js","sourceRoot":"","sources":["../../src/kb.ts"],"names":[],"mappings":"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","sourcesContent":["// 知识库挂载与同步(TypeScript source)。\n//\n// ════════════════════════════════════════════════════════════════════\n// 【核心判断】所有权决定真值方向。\n// ════════════════════════════════════════════════════════════════════\n//\n// owned(我的本体) external(别人维护的)\n// 真值在 **Git**(本地) **钉钉**(远端)\n// 同步方向 push 本地 → 钉钉 pull 钉钉 → 本地\n// 为什么 给人看(人机同源) 给我 grep(快、能看上下文)\n// 冲突了 远端被人改过 → **停** 远端赢,本地缓存可丢弃\n// 落在 ontology/(入 Git) .cache/kb/<name>/(gitignore)\n//\n// 这就是「Git 内核 × 钉钉暴露层」:**Git 是常量层,钉钉是可替换的暴露层。**\n//\n// ════════════════════════════════════════════════════════════════════\n// 【三个硬约束】—— 全是踩出来的\n// ════════════════════════════════════════════════════════════════════\n//\n// 1. **绝不双向自动 merge。** 一个方向是真值,另一个是投影。\n// 自动合并两个都在变的东西 = 灾难,而且是【静默】灾难。\n//\n// 2. **重排版不是漂移。** 判据是「剥掉 markdown 符号后的文字流」,不是行级 diff。\n// 用行级 diff → 每次 sync 都误报 → **你就会开始无脑 --force,真漂移也一起覆盖掉。**\n//\n// 3. **缓存不新鲜就不许用(fail-closed)。** external KB 是快照。\n// **拿着过期快照回答,比说「我不知道」危险得多。**\n\nimport { mkdirSync, writeFileSync, readFileSync, existsSync, statSync } from 'node:fs'\nimport { readdirSync } from 'node:fs'\nimport { join, dirname, relative, basename } from 'node:path'\nimport { spawnSync } from 'node:child_process'\nimport * as dws from './dws.js'\n\nexport const OWNED = 'owned', EXTERNAL = 'external'\nexport const PULL = 'pull', PUSH = 'push', NONE = 'none'\n\n/** external 缓存超过这么久就算过期。过期 → kb search 会警告并要求先 sync。 */\nexport const STALE_HOURS = 24\n\nconst now = () => new Date(Date.now() + 8 * 3600e3).toISOString().replace('Z', '+08:00').slice(0, 19)\n\ninterface RemoteItem { rel: string; nid: string; mtime: string | number }\ninterface LocalItem { path: string; rel: string }\ninterface SearchHit { kb: string; file: string; ctx: string }\n\nclass DriftError extends Error {\n exitCode = 1\n}\n\n/** dingtalk:doc:<folderId> · dingtalk:wiki:<spaceId> · local:<path> */\nexport function parseSource(s): { kind: 'doc' | 'wiki' | 'local'; id: string } {\n let m = /^dingtalk:(doc|wiki):(.+)$/.exec(s)\n if (m) return { kind: m[1] as 'doc' | 'wiki', id: m[2] }\n m = /^local:(.+)$/.exec(s)\n if (m) return { kind: 'local', id: m[1].replace(/^~/, process.env.HOME || '~') }\n throw new Error(\n `挂载源写错了: ${s}\\n` +\n ` 支持三种:\\n` +\n ` dingtalk:doc:<folderId> 文档文件夹(最常见 —— 很多组织根本没建\"知识库\")\\n` +\n ` dingtalk:wiki:<spaceId> 钉钉知识库空间\\n` +\n ` local:<路径> 本地目录(只读挂载,比如另一个仓库的 wiki/)`)\n}\n\n// ════════════════ pull:钉钉 → 本地缓存 ════════════════\nfunction walkDoc(folder, prefix = ''): RemoteItem[] {\n const out: RemoteItem[] = []\n for (const n of dws.nodes(dws.run(['doc', 'list', '--folder', folder, '--page-size', '50']))) {\n const name = n.name || '', nid = n.nodeId\n if (n.nodeType === 'folder') { out.push(...walkDoc(nid, join(prefix, name))); continue }\n if (n.contentType && n.contentType !== 'ALIDOC') continue // 二进制附件不是知识\n out.push({ rel: join(prefix, name.endsWith('.md') ? name : `${name}.md`),\n nid, mtime: n.updateTime || 0 })\n }\n return out\n}\n\nfunction walkWiki(space, folder = '', prefix = ''): RemoteItem[] {\n const args = ['wiki', 'node', 'list', '--workspace', space, '--limit', '50']\n if (folder) args.push('--folder', folder)\n const out: RemoteItem[] = []\n for (const n of dws.nodes(dws.run(args))) {\n const name = n.name || '', nid = n.nodeId\n if (n.nodeType === 'folder' || n.hasChildren) out.push(...walkWiki(space, nid, join(prefix, name)))\n out.push({ rel: join(prefix, name.endsWith('.md') ? name : `${name}.md`),\n nid, mtime: n.updateTime || 0 })\n }\n return out\n}\n\n/** 钉钉 → 本地缓存。**远端赢**(external 的真值在远端)。增量靠 updateTime。 */\nexport function pull(kb, root, force = false) {\n const { kind, id } = parseSource(kb.from)\n if (kind === 'local') { console.log(` local ${kb.name} 就在本地,不用同步`); return }\n\n const dest = join(root, kb.local)\n const items = kind === 'doc' ? walkDoc(id) : walkWiki(id)\n if (!items.length) {\n throw new Error(\n `「${kb.name}」远端一个节点都没有 —— **不许当成【这个知识库是空的】。**\\n` +\n ` 可能是: ID 写错 / 没权限 / 它真的空。先用 dws 手工确认再说。`)\n }\n kb._state ??= {}\n let pulled = 0, skipped = 0\n for (const { rel, nid, mtime } of items) {\n const p = join(dest, rel)\n if (!force && kb._state[nid]?.mtime === mtime && existsSync(p)) { skipped++; continue }\n const md = dws.readDoc(nid)\n mkdirSync(dirname(p), { recursive: true })\n // 头三行钉住来源 —— **没有指针的知识,用的时候不敢信。**\n writeFileSync(p,\n `<!-- 来自钉钉 · 只读缓存 · 不要在这里改,改了下次 sync 会被覆盖 -->\\n` +\n `<!-- nodeId: ${nid} -->\\n<!-- 拉取: ${now()} -->\\n\\n${md}`)\n kb._state[nid] = { rel, mtime }\n pulled++\n }\n kb._syncedAt = now()\n console.log(` pull ${kb.name.padEnd(14)} 拉了 ${pulled} 篇 · 没变 ${skipped} 篇`)\n}\n\n// ════════════════ push:本地 → 钉钉(人机同源)════════════════\nfunction walkLocal(dir, base = dir): LocalItem[] {\n const out: LocalItem[] = []\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, e.name)\n if (e.isDirectory()) { out.push(...walkLocal(p, base)); continue }\n if (e.name.endsWith('.md')) out.push({ path: p, rel: relative(base, p) })\n }\n return out\n}\n\n/**\n * 本地 → 钉钉。**Git 赢**(owned 的真值在 Git)。\n *\n * ⚠️ **推之前先查漂移** —— 远端被人改过就【停下来问】,绝不盲覆盖。\n * 人在钉钉里改了东西,说明他有话要说。**盲覆盖 = 把人的意见删了。**\n */\nexport function push(kb, root, force = false) {\n const { kind, id } = parseSource(kb.from)\n if (kind !== 'doc') {\n throw new Error('push 目前只支持 dingtalk:doc:<folderId>(文档文件夹)。\\n' +\n ' 知识库空间的写入面还没验过 —— **不许假装支持。**')\n }\n const src = join(root, kb.local)\n if (!existsSync(src)) throw new Error(`本地目录不存在: ${src}`)\n\n kb._state ??= {}\n const drift: Array<{ rel: string; nid: string }> = []\n let pushed = 0, unchanged = 0\n\n for (const { path, rel } of walkLocal(src)) {\n const body = readFileSync(path, 'utf8')\n let nid = Object.keys(kb._state).find((k) => kb._state[k].rel === rel)\n if (!nid) {\n nid = String(dws.createDoc(id, basename(rel), path) || '')\n if (!nid) throw new Error(`创建 ${rel} 后没有返回 nodeId`)\n kb._state[nid] = { rel, pushedWords: dws.words(body) }\n pushed++; console.log(` + 新建 ${rel}`); continue\n }\n const remote = dws.readDoc(nid)\n if (dws.same(remote, body)) { unchanged++; continue }\n\n // 远端 ≠ 我上次推上去的 → **有人在钉钉里改过**\n const last = kb._state[nid].pushedWords\n if (last && dws.words(remote) !== last && !force) { drift.push({ rel, nid }); continue }\n\n dws.updateDoc(nid, path)\n if (!dws.same(dws.readDoc(nid), body)) {\n throw new Error(`推了 ${rel} 但回读不一致 —— **写操作报错≠失败,回读定成败。**`)\n }\n kb._state[nid].pushedWords = dws.words(body)\n pushed++; console.log(` ↑ 更新 ${rel}`)\n }\n\n if (drift.length) {\n console.log(`\\n ⚠️ **远端被人改过**,默认拒绝覆盖(${drift.length} 篇):`)\n for (const { rel, nid } of drift) console.log(` ${rel} nodeId=${nid}`)\n console.log(` 人在钉钉里改了东西,说明他有话要说。**先去看一眼**,别盲覆盖。`)\n console.log(` 确认要覆盖: dingtalk-agent kb sync --name ${kb.name} --force`)\n throw new DriftError('有漂移,拒绝覆盖')\n }\n kb._syncedAt = now()\n console.log(` push ${kb.name.padEnd(14)} 推了 ${pushed} 篇 · 一致 ${unchanged} 篇`)\n}\n\n// ════════════════ search:跨所有挂载点 ════════════════\nexport function stale(kb) {\n if (kb.own !== EXTERNAL || kb.sync === NONE) return false\n if (!kb._syncedAt) return true\n const age = (Date.now() - Date.parse(kb._syncedAt)) / 3600e3\n return age > STALE_HOURS\n}\n\n/**\n * 跨所有挂载的 KB 搜。**本地 grep 为主** —— 快、确定、能看到上下文。\n *\n * fail-closed:不新鲜的缓存会【警告】。\n * **拿着过期快照回答,比说「我不知道」危险得多。**\n */\nexport function search(cfg, root, kw, top = 20) {\n const hits: SearchHit[] = []\n const warned: string[] = []\n for (const kb of cfg.kb || []) {\n const base = kb.local.startsWith('/') ? kb.local : join(root, kb.local)\n if (!existsSync(base)) continue\n if (stale(kb)) warned.push(kb.name)\n const r = spawnSync('grep', ['-ril', '--include=*.md', kw, base], { encoding: 'utf8' })\n for (const p of (r.stdout || '').split('\\n').filter(Boolean)) {\n const ctx = spawnSync('grep', ['-i', '-n', '-m', '2', kw, p], { encoding: 'utf8' }).stdout || ''\n hits.push({ kb: kb.name, file: relative(root, p), ctx: ctx.trim() })\n }\n }\n\n if (warned.length) {\n console.log(`⚠️ 这些知识库的缓存【超过 ${STALE_HOURS} 小时没同步】了: ${warned.join('、')}`)\n console.log(` **拿着过期快照回答,比说「我不知道」危险得多。**`)\n console.log(` 先跑: dingtalk-agent kb sync\\n`)\n }\n if (!hits.length) {\n console.log(`没搜到「${kw}」。`)\n console.log(`**搜不到就说搜不到 —— 不要编。** 挂了哪些库: dingtalk-agent kb list`)\n return []\n }\n console.log(`命中 ${hits.length} 处:`)\n for (const h of hits.slice(0, top)) {\n console.log(`\\n── [${h.kb}] ${h.file}`)\n for (const l of h.ctx.split('\\n').slice(0, 2)) console.log(` ${l.slice(0, 110)}`)\n }\n console.log(`\\n**别只看这几行就作答** —— 打开命中的那一页读完再说。`)\n return hits\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * 源码位于 src|bin,编译产物位于 dist/src|dist/bin。
6
+ * 资源仍发布在包根,所以不能用固定的 dirname 层数猜包根。
7
+ */
8
+ export function resolvePackageRoot(metaUrl) {
9
+ let candidate = resolve(dirname(fileURLToPath(metaUrl)), '..');
10
+ for (let depth = 0; depth < 2; depth += 1) {
11
+ if (existsSync(resolve(candidate, 'package.json')))
12
+ return candidate;
13
+ candidate = resolve(candidate, '..');
14
+ }
15
+ throw new Error(`无法从 ${metaUrl} 定位 @xdxer/dingtalk-agent 包根`);
16
+ }
17
+ //# sourceMappingURL=package-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-root.js","sourceRoot":"","sources":["../../src/package-root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,IAAI,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAC9D,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YAAE,OAAO,SAAS,CAAA;QACpE,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IACtC,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,OAAO,OAAO,8BAA8B,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["import { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * 源码位于 src|bin,编译产物位于 dist/src|dist/bin。\n * 资源仍发布在包根,所以不能用固定的 dirname 层数猜包根。\n */\nexport function resolvePackageRoot(metaUrl: string): string {\n let candidate = resolve(dirname(fileURLToPath(metaUrl)), '..')\n for (let depth = 0; depth < 2; depth += 1) {\n if (existsSync(resolve(candidate, 'package.json'))) return candidate\n candidate = resolve(candidate, '..')\n }\n throw new Error(`无法从 ${metaUrl} 定位 @xdxer/dingtalk-agent 包根`)\n}\n"]}
@@ -0,0 +1,79 @@
1
+ // 留痕与集中进化 —— runs/YYYY-MM-DD.md,一天一档 jsonl(TypeScript source)。
2
+ //
3
+ // 每行钉住【三个硬 ID】—— 出问题才反查得到:
4
+ // issue → 平台派单 id,翻得出整个运行过程
5
+ // conv → 会话 id,**filter 得出整条对话流(这就是训练素材)**
6
+ // msg → 触发消息 id,定位到那一句
7
+ // **缺了这三个,记录就只是一堆"我做了什么",出了问题什么也追不到。**
8
+ //
9
+ // r = 【引擎层】成败(跑通没有)
10
+ // fb = 【用户层】成败(主人认不认)
11
+ // **两者必须分开** —— 引擎跑通 ≠ 答对 ≠ 主人满意。
12
+ // 只看 r,你会看到一片绿,然后什么也进化不了。
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ export const FB_OK = '认可', FB_MORE = '追问', FB_FIX = '纠正';
16
+ const MARKER = 'AGENT_RUN_LOG_V1';
17
+ const cst = (d = new Date()) => new Date(d.getTime() + 8 * 3600e3);
18
+ export const today = () => cst().toISOString().slice(0, 10);
19
+ export const now = () => cst().toISOString().replace('Z', '+08:00').slice(0, 19);
20
+ export const todayZero = () => today() + 'T00:00:00+08:00';
21
+ /** 【今晚跑过没有】= 数据水位 ≥ 今天 00:00。纯【数据派生】——
22
+ * 不记状态、不建标记文件、不看会话记忆。重启/换实例/漏拍都不受影响。 */
23
+ export const ranTonight = (wm) => Boolean(wm) && String(wm) >= todayZero();
24
+ const file = (root, day) => join(root, 'runs', `${day}.md`);
25
+ export function read(root, day) {
26
+ const p = file(root, day);
27
+ if (!existsSync(p))
28
+ return [];
29
+ const out = [];
30
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
31
+ const s = line.trim();
32
+ if (!s.startsWith('{'))
33
+ continue;
34
+ try {
35
+ out.push(JSON.parse(s));
36
+ }
37
+ catch {
38
+ console.error(` ⚠️ 运行记录有坏行,跳过但【不当成空】: ${s.slice(0, 60)}`);
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ export function write(root, day, rows) {
44
+ mkdirSync(join(root, 'runs'), { recursive: true });
45
+ const body = rows.map((r) => JSON.stringify(r)).join('\n');
46
+ writeFileSync(file(root, day), `${MARKER} · ${day}\n\n\`\`\`jsonl\n${body}\n\`\`\`\n`);
47
+ }
48
+ /** 追加一行 + 回读校验。**写了没落上 = 这次没发生过,要说出来。** */
49
+ export function append(root, row) {
50
+ const day = today();
51
+ const rows = read(root, day);
52
+ rows.push(row);
53
+ write(root, day, rows);
54
+ const ok = read(root, day).some((r) => r.t === row.t && r.did === row.did);
55
+ if (!ok)
56
+ console.error(' ⚠️ 运行记录写后回读不到 —— 动作照常,但【留痕失败】');
57
+ return ok ? row.t : '';
58
+ }
59
+ export function scan(root, days = 1) {
60
+ const out = [];
61
+ for (let i = 0; i < Math.max(1, days); i++) {
62
+ const d = new Date(Date.now() - i * 86400e3);
63
+ const day = cst(d).toISOString().slice(0, 10);
64
+ for (const r of read(root, day))
65
+ out.push({ ...r, _day: day });
66
+ }
67
+ return out.sort((a, b) => (a._day + a.t).localeCompare(b._day + b.t));
68
+ }
69
+ export function logq(root, { did, kind = '问答', secs = 0, r = 'pass', ev = '', ch = '钉钉单聊', asked = '', issue = '', conv = '', msg = '' }) {
70
+ const row = {
71
+ t: now().slice(11, 19), ch, kind, did: String(did).slice(0, 300), secs: +secs, r,
72
+ };
73
+ for (const [k, v] of [['asked', asked], ['issue', issue], ['conv', conv], ['msg', msg], ['ev', ev]]) {
74
+ if (v)
75
+ row[k] = String(v).slice(0, 600);
76
+ }
77
+ return append(root, row);
78
+ }
79
+ //# sourceMappingURL=runs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runs.js","sourceRoot":"","sources":["../../src/runs.ts"],"names":[],"mappings":"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","sourcesContent":["// 留痕与集中进化 —— runs/YYYY-MM-DD.md,一天一档 jsonl(TypeScript source)。\n//\n// 每行钉住【三个硬 ID】—— 出问题才反查得到:\n// issue → 平台派单 id,翻得出整个运行过程\n// conv → 会话 id,**filter 得出整条对话流(这就是训练素材)**\n// msg → 触发消息 id,定位到那一句\n// **缺了这三个,记录就只是一堆\"我做了什么\",出了问题什么也追不到。**\n//\n// r = 【引擎层】成败(跑通没有)\n// fb = 【用户层】成败(主人认不认)\n// **两者必须分开** —— 引擎跑通 ≠ 答对 ≠ 主人满意。\n// 只看 r,你会看到一片绿,然后什么也进化不了。\nimport { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport const FB_OK = '认可', FB_MORE = '追问', FB_FIX = '纠正'\nconst MARKER = 'AGENT_RUN_LOG_V1'\n\nexport interface RunLogRecord {\n t: string\n did: string\n r: string\n _day?: string\n asked?: string\n kind?: string\n secs?: number\n ev?: string\n ch?: string\n issue?: string\n conv?: string\n msg?: string\n fb?: string\n fbt?: string\n [key: string]: unknown\n}\n\nconst cst = (d = new Date()) => new Date(d.getTime() + 8 * 3600e3)\nexport const today = () => cst().toISOString().slice(0, 10)\nexport const now = () => cst().toISOString().replace('Z', '+08:00').slice(0, 19)\nexport const todayZero = () => today() + 'T00:00:00+08:00'\n\n/** 【今晚跑过没有】= 数据水位 ≥ 今天 00:00。纯【数据派生】——\n * 不记状态、不建标记文件、不看会话记忆。重启/换实例/漏拍都不受影响。 */\nexport const ranTonight = (wm) => Boolean(wm) && String(wm) >= todayZero()\n\nconst file = (root, day) => join(root, 'runs', `${day}.md`)\n\nexport function read(root, day): RunLogRecord[] {\n const p = file(root, day)\n if (!existsSync(p)) return []\n const out: RunLogRecord[] = []\n for (const line of readFileSync(p, 'utf8').split('\\n')) {\n const s = line.trim()\n if (!s.startsWith('{')) continue\n try { out.push(JSON.parse(s) as RunLogRecord) }\n catch { console.error(` ⚠️ 运行记录有坏行,跳过但【不当成空】: ${s.slice(0, 60)}`) }\n }\n return out\n}\n\nexport function write(root, day, rows: RunLogRecord[]) {\n mkdirSync(join(root, 'runs'), { recursive: true })\n const body = rows.map((r) => JSON.stringify(r)).join('\\n')\n writeFileSync(file(root, day), `${MARKER} · ${day}\\n\\n\\`\\`\\`jsonl\\n${body}\\n\\`\\`\\`\\n`)\n}\n\n/** 追加一行 + 回读校验。**写了没落上 = 这次没发生过,要说出来。** */\nexport function append(root, row: RunLogRecord) {\n const day = today()\n const rows = read(root, day)\n rows.push(row)\n write(root, day, rows)\n const ok = read(root, day).some((r) => r.t === row.t && r.did === row.did)\n if (!ok) console.error(' ⚠️ 运行记录写后回读不到 —— 动作照常,但【留痕失败】')\n return ok ? row.t : ''\n}\n\nexport function scan(root, days = 1): RunLogRecord[] {\n const out: RunLogRecord[] = []\n for (let i = 0; i < Math.max(1, days); i++) {\n const d = new Date(Date.now() - i * 86400e3)\n const day = cst(d).toISOString().slice(0, 10)\n for (const r of read(root, day)) out.push({ ...r, _day: day })\n }\n return out.sort((a, b) => (a._day + a.t).localeCompare(b._day + b.t))\n}\n\nexport function logq(root, { did, kind = '问答', secs = 0, r = 'pass', ev = '',\n ch = '钉钉单聊', asked = '', issue = '', conv = '', msg = '' }): string {\n const row: RunLogRecord = {\n t: now().slice(11, 19), ch, kind, did: String(did).slice(0, 300), secs: +secs, r,\n }\n for (const [k, v] of [['asked', asked], ['issue', issue], ['conv', conv], ['msg', msg], ['ev', ev]]) {\n if (v) row[k] = String(v).slice(0, 600)\n }\n return append(root, row)\n}\n"]}