@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/src/dws.js DELETED
@@ -1,192 +0,0 @@
1
- // dws 硬化包装 —— 这一层是「拦」。
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
-
34
- import { spawnSync } from 'node:child_process'
35
-
36
- export class Blocked extends Error {}
37
-
38
- export function die(msg) {
39
- console.error('[FAIL] ' + msg)
40
- throw new Blocked(msg)
41
- }
42
-
43
- const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
44
-
45
- /** 跑一条 dws。限流退避;超时不当成"空";坏响应响亮地死。 */
46
- export function run(args, { allowFail = false, timeout = 180_000, retries = 2 } = {}) {
47
- let last = ''
48
- for (let attempt = 0; attempt <= retries; attempt++) {
49
- const p = spawnSync('dws', [...args, '--format', 'json'],
50
- { encoding: 'utf8', timeout, maxBuffer: 64 * 1024 * 1024 })
51
- if (p.error) {
52
- if (p.error.code === 'ENOENT') {
53
- if (allowFail) return null
54
- die('dws 跑不起来 —— 先 export PATH="$HOME/.local/bin:$PATH"')
55
- }
56
- last = String(p.error); sleep(2000 * (attempt + 1)); continue
57
- }
58
- const out = p.stdout || p.stderr || ''
59
- last = out.slice(0, 300)
60
- const i = out.indexOf('{')
61
- if (i < 0) { sleep(2000 * (attempt + 1)); continue }
62
- let d
63
- try { d = JSON.parse(out.slice(i, lastBrace(out) + 1)) }
64
- catch { sleep(2000 * (attempt + 1)); continue }
65
- const err = d.error || {}
66
- if (err && (err.code || err.message)) {
67
- const s = JSON.stringify(err).toLowerCase()
68
- if (s.includes('limit') || s.includes('frequen') || s.includes('flow')) {
69
- sleep(3000 * (attempt + 1)); continue // 限流 → 退避
70
- }
71
- if (allowFail) return null
72
- die(`dws 失败 ${args.slice(0, 4).join(' ')}\n ${JSON.stringify(err).slice(0, 300)}`)
73
- }
74
- return d
75
- }
76
- if (allowFail) return null
77
- die(`dws 无响应(重试 ${retries} 次) ${args.slice(0, 4).join(' ')}\n ${last}`)
78
- }
79
-
80
- function lastBrace(s) {
81
- // dws 有时在 JSON 后面粘一行 WARN / INFO —— 从最后一个 } 截断
82
- for (let i = s.length - 1; i >= 0; i--) if (s[i] === '}') return i
83
- return s.length - 1
84
- }
85
-
86
- // ──────────────── 三个层级,各读各的 ────────────────
87
- const inner = (d) => (d && typeof d.data === 'object' && d.data !== null ? d.data : d)
88
-
89
- /** 列表类查询的记录。拿不到就 die —— 绝不当成"表是空的"。 */
90
- export function records(d) {
91
- const x = inner(d)
92
- if (!('records' in x)) {
93
- die(`返回里没有 records(顶层键=${Object.keys(d)}) —— 接口形状可能变了。` +
94
- `**绝不把它当成【表是空的】。**`)
95
- }
96
- if (x.hasMore || x.partial) {
97
- die('分页未收敛 —— **部分结果不许当完整结果用。** 半张表算出来的是错的。')
98
- }
99
- return x.records || []
100
- }
101
-
102
- /** 文档正文。**层级和列表类查询不一样** —— 两种形状都兜住,拿不到就 die。 */
103
- export function markdown(d) {
104
- const md = inner(d).markdown ?? d.markdown
105
- if (md === undefined || md === null) {
106
- die(`拿不到文档正文(顶层键=${Object.keys(d)}) —— **绝不当成【这篇是空的】。**`)
107
- }
108
- return md
109
- }
110
-
111
- /** 目录节点。 */
112
- export const nodes = (d) => inner(d).nodes || []
113
-
114
- /**
115
- * 待办翻页 —— **这是唯一有分页信号的读法。**
116
- *
117
- * ⚠️ **不要把页开大**。超过这个大小,分页信号会消失,
118
- * 你拿到 undefined、当成 false,然后**静默漏掉剩下的**。
119
- * 心跳的收件靠它 —— 漏了循环件就是「静默收工且不自知」。
120
- */
121
- export function todosPage(status = 'false', page = 1) {
122
- const d = run(['todo', 'task', 'list', '--status', status, '--size', '20', '--page', String(page)])
123
- const r = d.result || {}
124
- if (!('todoCards' in r)) {
125
- die(`待办返回里没有 todoCards(result 键=${Object.keys(r)}) —— 绝不当成【没有待办】。`)
126
- }
127
- return { cards: r.todoCards || [], more: Boolean(r.hasMore) }
128
- }
129
-
130
- /** 把待办【拉全】。只看第一页会漏掉循环件。 */
131
- export function todosAll(status = 'false', maxPages = 20) {
132
- const out = []
133
- for (let page = 1; page <= maxPages; page++) {
134
- const { cards, more } = todosPage(status, page)
135
- out.push(...cards)
136
- if (!more) return out
137
- }
138
- die(`待办翻页超过 ${maxPages} 页还没收敛 —— 不许把部分结果当完整。`)
139
- }
140
-
141
- // ──────────────── 创建类:报错也可能成功 → 按名回查 ────────────────
142
- /** 目录列表可能把扩展名单列,比对时要拼回去。 */
143
- export function findChild(folder, name) {
144
- for (const n of nodes(run(['doc', 'list', '--folder', folder, '--page-size', '50']))) {
145
- const nm = n.name || '', ext = n.extension || ''
146
- if (nm === name || (ext && `${nm}.${ext}` === name)) return n.nodeId
147
- }
148
- return null
149
- }
150
-
151
- /** 建文档。**不看返回信封** —— 一律按名回查定成败。 */
152
- export function createDoc(folder, name, file) {
153
- const exist = findChild(folder, name)
154
- if (exist) return exist
155
- run(['doc', 'create', '--name', name, '--folder', folder,
156
- '--content-file', file, '--content-format', 'markdown', '--yes'],
157
- { allowFail: true }) // ← **报错也可能其实成功了**
158
- sleep(1200)
159
- const nid = findChild(folder, name)
160
- if (!nid) die(`建不出文档 ${name} —— 别把它当成【建成功了】。`)
161
- return nid
162
- }
163
-
164
- export const updateDoc = (node, file) =>
165
- run(['doc', 'update', '--node', node, '--content-file', file,
166
- '--content-format', 'markdown', '--mode', 'overwrite', '--yes'])
167
-
168
- export const readDoc = (node) => markdown(run(['doc', 'read', '--node', node]))
169
- export const docInfo = (node) => inner(run(['doc', 'info', '--node', node]))
170
-
171
- // ──────────────── 漂移判定:重排版【不是】漂移 ────────────────
172
- /**
173
- * 剥掉 markdown 语法和转义装饰,只留【文字本身】。
174
- *
175
- * 钉钉会重排版你推上去的 markdown。**重排版不是漂移。**
176
- * 用行级 diff 判漂移 → 每次 sync 都误报 →
177
- * **然后你就会开始无脑 --force,真正的漂移也一起覆盖掉了。**
178
- */
179
- export function words(s) {
180
- let t = String(s ?? '')
181
- for (let i = 0; i < 3; i++) t = unescapeHtml(t) // 可能被多次转义
182
- t = t.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n))
183
- t = t.replace(/\[打开\]\([^)]*\)/g, '') // 钉钉自动加的链接装饰
184
- return t.replace(/[`*_>#|\\~\-\s]+/g, '')
185
- }
186
-
187
- function unescapeHtml(s) {
188
- return s.replace(/&(amp|lt|gt|quot|#39);/g, (m, e) =>
189
- ({ amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" })[e])
190
- }
191
-
192
- export const same = (a, b) => words(a) === words(b)
package/src/init.js DELETED
@@ -1,84 +0,0 @@
1
- // init —— 在工作区里铺一个数字员工。
2
- //
3
- // 包里带的是【工具 + 行为范式】;本体和运行记录是 init 在【你的工作区】里生成的。
4
- // 所以这个包能发 npm,而你的 agent 是你自己的 git 仓库。
5
-
6
- import { mkdirSync, cpSync, existsSync, writeFileSync, symlinkSync } from 'node:fs'
7
- import { join, dirname } from 'node:path'
8
- import { fileURLToPath } from 'node:url'
9
- import * as cfg from './config.js'
10
-
11
- const PKG = dirname(dirname(fileURLToPath(import.meta.url)))
12
-
13
- export function init(root) {
14
- if (cfg.load(root)) {
15
- console.log('已经初始化过了。看看: dingtalk-agent kb list')
16
- return
17
- }
18
-
19
- // 行为范式(skills)+ 本体模板(ontology/self 三件套)
20
- cpSync(join(PKG, 'skills'), join(root, 'skills'), { recursive: true })
21
- cpSync(join(PKG, 'templates', 'ontology'), join(root, 'ontology'), { recursive: true })
22
- for (const d of ['ontology/sources', 'ontology/wiki', 'runs', '.cache/kb']) {
23
- mkdirSync(join(root, d), { recursive: true })
24
- }
25
-
26
- // AGENTS.md ← skills/AGENTS.md;CLAUDE.md 是它的符号链接
27
- // **任何 Coding Agent 读到的协议完全一致。**
28
- const agents = join(root, 'AGENTS.md')
29
- if (!existsSync(agents)) {
30
- writeFileSync(agents, `<!-- 正本在 skills/AGENTS.md -->\n\n` +
31
- `# 先读这个\n\n` +
32
- `\`\`\`bash\ndingtalk-agent boot\n\`\`\`\n\n` +
33
- `然后读 [skills/AGENTS.md](skills/AGENTS.md) —— 我是谁、我怎么做事。\n\n` +
34
- `**BOOT FAIL 时禁答身份/能力/记忆/知识类问题。** 不要猜,不要找替代路径。\n`)
35
- }
36
- const claude = join(root, 'CLAUDE.md')
37
- if (!existsSync(claude)) {
38
- try { symlinkSync('AGENTS.md', claude) } catch { cpSync(agents, claude) }
39
- }
40
-
41
- const gi = join(root, '.gitignore')
42
- if (!existsSync(gi)) {
43
- writeFileSync(gi, [
44
- '# 知识库缓存:external 的真值在远端,本地这份【可丢弃】',
45
- '.cache/',
46
- '',
47
- '# 运行记录:含对话原文、人名。本地留档,不进仓库',
48
- 'runs/*',
49
- '!runs/.gitkeep',
50
- '',
51
- '# 配置:含硬 ID,一岗一份',
52
- '.dingtalk-agent/',
53
- '',
54
- '# 原始证据里的敏感原文',
55
- 'ontology/sources/private/',
56
- '',
57
- 'node_modules/',
58
- '.DS_Store',
59
- '',
60
- ].join('\n'))
61
- }
62
- writeFileSync(join(root, 'runs', '.gitkeep'), '')
63
-
64
- cfg.save(root, cfg.DEFAULT)
65
-
66
- console.log(`✅ 铺好了
67
-
68
- AGENTS.md 先读这个(CLAUDE.md 是它的符号链接)
69
- skills/ 行为范式:消息 · 任务 · 心跳 · 知识 · 钉钉 · 评测
70
- ontology/self/ ★ 三件套 —— 我是谁 · 工牌 · 工位
71
- ontology/wiki/ 编译层(越干越有经验的地方)
72
- runs/ 运行记录(三个硬 ID,反查得到才算数)
73
-
74
- 接下来三步(**顺序不能换**):
75
-
76
- 1. 填 ontology/self/ 的三件套。**没填完 boot 会拒绝启动。**
77
- 一个「半个记忆」的员工,比一个明确说「我还没醒」的员工危险得多。
78
-
79
- 2. 挂知识库:
80
- dingtalk-agent kb mount --name 公司知识库 --from dingtalk:doc:<folderId> --own external
81
- dingtalk-agent kb mount --name 本体 --from dingtalk:doc:<folderId> --own owned --local ontology
82
-
83
- 3. dingtalk-agent boot ← 它会【真去摸一遍工位】,拉不到就 BOOT FAIL`)
84
- }
package/src/kb.js DELETED
@@ -1,221 +0,0 @@
1
- // 知识库挂载与同步。
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
-
29
- import { mkdirSync, writeFileSync, readFileSync, existsSync, statSync } from 'node:fs'
30
- import { readdirSync } from 'node:fs'
31
- import { join, dirname, relative, basename } from 'node:path'
32
- import { spawnSync } from 'node:child_process'
33
- import * as dws from './dws.js'
34
-
35
- export const OWNED = 'owned', EXTERNAL = 'external'
36
- export const PULL = 'pull', PUSH = 'push', NONE = 'none'
37
-
38
- /** external 缓存超过这么久就算过期。过期 → kb search 会警告并要求先 sync。 */
39
- export const STALE_HOURS = 24
40
-
41
- const now = () => new Date(Date.now() + 8 * 3600e3).toISOString().replace('Z', '+08:00').slice(0, 19)
42
-
43
- /** dingtalk:doc:<folderId> · dingtalk:wiki:<spaceId> · local:<path> */
44
- export function parseSource(s) {
45
- let m = /^dingtalk:(doc|wiki):(.+)$/.exec(s)
46
- if (m) return { kind: m[1], id: m[2] }
47
- m = /^local:(.+)$/.exec(s)
48
- if (m) return { kind: 'local', id: m[1].replace(/^~/, process.env.HOME) }
49
- throw new Error(
50
- `挂载源写错了: ${s}\n` +
51
- ` 支持三种:\n` +
52
- ` dingtalk:doc:<folderId> 文档文件夹(最常见 —— 很多组织根本没建"知识库")\n` +
53
- ` dingtalk:wiki:<spaceId> 钉钉知识库空间\n` +
54
- ` local:<路径> 本地目录(只读挂载,比如另一个仓库的 wiki/)`)
55
- }
56
-
57
- // ════════════════ pull:钉钉 → 本地缓存 ════════════════
58
- function walkDoc(folder, prefix = '') {
59
- const out = []
60
- for (const n of dws.nodes(dws.run(['doc', 'list', '--folder', folder, '--page-size', '50']))) {
61
- const name = n.name || '', nid = n.nodeId
62
- if (n.nodeType === 'folder') { out.push(...walkDoc(nid, join(prefix, name))); continue }
63
- if (n.contentType && n.contentType !== 'ALIDOC') continue // 二进制附件不是知识
64
- out.push({ rel: join(prefix, name.endsWith('.md') ? name : `${name}.md`),
65
- nid, mtime: n.updateTime || 0 })
66
- }
67
- return out
68
- }
69
-
70
- function walkWiki(space, folder, prefix = '') {
71
- const args = ['wiki', 'node', 'list', '--workspace', space, '--limit', '50']
72
- if (folder) args.push('--folder', folder)
73
- const out = []
74
- for (const n of dws.nodes(dws.run(args))) {
75
- const name = n.name || '', nid = n.nodeId
76
- if (n.nodeType === 'folder' || n.hasChildren) out.push(...walkWiki(space, nid, join(prefix, name)))
77
- out.push({ rel: join(prefix, name.endsWith('.md') ? name : `${name}.md`),
78
- nid, mtime: n.updateTime || 0 })
79
- }
80
- return out
81
- }
82
-
83
- /** 钉钉 → 本地缓存。**远端赢**(external 的真值在远端)。增量靠 updateTime。 */
84
- export function pull(kb, root, force = false) {
85
- const { kind, id } = parseSource(kb.from)
86
- if (kind === 'local') { console.log(` local ${kb.name} 就在本地,不用同步`); return }
87
-
88
- const dest = join(root, kb.local)
89
- const items = kind === 'doc' ? walkDoc(id) : walkWiki(id)
90
- if (!items.length) {
91
- throw new Error(
92
- `「${kb.name}」远端一个节点都没有 —— **不许当成【这个知识库是空的】。**\n` +
93
- ` 可能是: ID 写错 / 没权限 / 它真的空。先用 dws 手工确认再说。`)
94
- }
95
- kb._state ??= {}
96
- let pulled = 0, skipped = 0
97
- for (const { rel, nid, mtime } of items) {
98
- const p = join(dest, rel)
99
- if (!force && kb._state[nid]?.mtime === mtime && existsSync(p)) { skipped++; continue }
100
- const md = dws.readDoc(nid)
101
- mkdirSync(dirname(p), { recursive: true })
102
- // 头三行钉住来源 —— **没有指针的知识,用的时候不敢信。**
103
- writeFileSync(p,
104
- `<!-- 来自钉钉 · 只读缓存 · 不要在这里改,改了下次 sync 会被覆盖 -->\n` +
105
- `<!-- nodeId: ${nid} -->\n<!-- 拉取: ${now()} -->\n\n${md}`)
106
- kb._state[nid] = { rel, mtime }
107
- pulled++
108
- }
109
- kb._syncedAt = now()
110
- console.log(` pull ${kb.name.padEnd(14)} 拉了 ${pulled} 篇 · 没变 ${skipped} 篇`)
111
- }
112
-
113
- // ════════════════ push:本地 → 钉钉(人机同源)════════════════
114
- function walkLocal(dir, base = dir) {
115
- const out = []
116
- for (const e of readdirSync(dir, { withFileTypes: true })) {
117
- const p = join(dir, e.name)
118
- if (e.isDirectory()) { out.push(...walkLocal(p, base)); continue }
119
- if (e.name.endsWith('.md')) out.push({ path: p, rel: relative(base, p) })
120
- }
121
- return out
122
- }
123
-
124
- /**
125
- * 本地 → 钉钉。**Git 赢**(owned 的真值在 Git)。
126
- *
127
- * ⚠️ **推之前先查漂移** —— 远端被人改过就【停下来问】,绝不盲覆盖。
128
- * 人在钉钉里改了东西,说明他有话要说。**盲覆盖 = 把人的意见删了。**
129
- */
130
- export function push(kb, root, force = false) {
131
- const { kind, id } = parseSource(kb.from)
132
- if (kind !== 'doc') {
133
- throw new Error('push 目前只支持 dingtalk:doc:<folderId>(文档文件夹)。\n' +
134
- ' 知识库空间的写入面还没验过 —— **不许假装支持。**')
135
- }
136
- const src = join(root, kb.local)
137
- if (!existsSync(src)) throw new Error(`本地目录不存在: ${src}`)
138
-
139
- kb._state ??= {}
140
- const drift = []
141
- let pushed = 0, unchanged = 0
142
-
143
- for (const { path, rel } of walkLocal(src)) {
144
- const body = readFileSync(path, 'utf8')
145
- let nid = Object.keys(kb._state).find((k) => kb._state[k].rel === rel)
146
- if (!nid) {
147
- nid = dws.createDoc(id, basename(rel), path)
148
- kb._state[nid] = { rel, pushedWords: dws.words(body) }
149
- pushed++; console.log(` + 新建 ${rel}`); continue
150
- }
151
- const remote = dws.readDoc(nid)
152
- if (dws.same(remote, body)) { unchanged++; continue }
153
-
154
- // 远端 ≠ 我上次推上去的 → **有人在钉钉里改过**
155
- const last = kb._state[nid].pushedWords
156
- if (last && dws.words(remote) !== last && !force) { drift.push({ rel, nid }); continue }
157
-
158
- dws.updateDoc(nid, path)
159
- if (!dws.same(dws.readDoc(nid), body)) {
160
- throw new Error(`推了 ${rel} 但回读不一致 —— **写操作报错≠失败,回读定成败。**`)
161
- }
162
- kb._state[nid].pushedWords = dws.words(body)
163
- pushed++; console.log(` ↑ 更新 ${rel}`)
164
- }
165
-
166
- if (drift.length) {
167
- console.log(`\n ⚠️ **远端被人改过**,默认拒绝覆盖(${drift.length} 篇):`)
168
- for (const { rel, nid } of drift) console.log(` ${rel} nodeId=${nid}`)
169
- console.log(` 人在钉钉里改了东西,说明他有话要说。**先去看一眼**,别盲覆盖。`)
170
- console.log(` 确认要覆盖: dingtalk-agent kb sync --name ${kb.name} --force`)
171
- const e = new Error('有漂移,拒绝覆盖'); e.exitCode = 1; throw e
172
- }
173
- kb._syncedAt = now()
174
- console.log(` push ${kb.name.padEnd(14)} 推了 ${pushed} 篇 · 一致 ${unchanged} 篇`)
175
- }
176
-
177
- // ════════════════ search:跨所有挂载点 ════════════════
178
- export function stale(kb) {
179
- if (kb.own !== EXTERNAL || kb.sync === NONE) return false
180
- if (!kb._syncedAt) return true
181
- const age = (Date.now() - Date.parse(kb._syncedAt)) / 3600e3
182
- return age > STALE_HOURS
183
- }
184
-
185
- /**
186
- * 跨所有挂载的 KB 搜。**本地 grep 为主** —— 快、确定、能看到上下文。
187
- *
188
- * fail-closed:不新鲜的缓存会【警告】。
189
- * **拿着过期快照回答,比说「我不知道」危险得多。**
190
- */
191
- export function search(cfg, root, kw, top = 20) {
192
- const hits = [], warned = []
193
- for (const kb of cfg.kb || []) {
194
- const base = kb.local.startsWith('/') ? kb.local : join(root, kb.local)
195
- if (!existsSync(base)) continue
196
- if (stale(kb)) warned.push(kb.name)
197
- const r = spawnSync('grep', ['-ril', '--include=*.md', kw, base], { encoding: 'utf8' })
198
- for (const p of (r.stdout || '').split('\n').filter(Boolean)) {
199
- const ctx = spawnSync('grep', ['-i', '-n', '-m', '2', kw, p], { encoding: 'utf8' }).stdout || ''
200
- hits.push({ kb: kb.name, file: relative(root, p), ctx: ctx.trim() })
201
- }
202
- }
203
-
204
- if (warned.length) {
205
- console.log(`⚠️ 这些知识库的缓存【超过 ${STALE_HOURS} 小时没同步】了: ${warned.join('、')}`)
206
- console.log(` **拿着过期快照回答,比说「我不知道」危险得多。**`)
207
- console.log(` 先跑: dingtalk-agent kb sync\n`)
208
- }
209
- if (!hits.length) {
210
- console.log(`没搜到「${kw}」。`)
211
- console.log(`**搜不到就说搜不到 —— 不要编。** 挂了哪些库: dingtalk-agent kb list`)
212
- return []
213
- }
214
- console.log(`命中 ${hits.length} 处:`)
215
- for (const h of hits.slice(0, top)) {
216
- console.log(`\n── [${h.kb}] ${h.file}`)
217
- for (const l of h.ctx.split('\n').slice(0, 2)) console.log(` ${l.slice(0, 110)}`)
218
- }
219
- console.log(`\n**别只看这几行就作答** —— 打开命中的那一页读完再说。`)
220
- return hits
221
- }
package/src/runs.js DELETED
@@ -1,77 +0,0 @@
1
- // 留痕与集中进化 —— runs/YYYY-MM-DD.md,一天一档 jsonl。
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
-
16
- export const FB_OK = '认可', FB_MORE = '追问', FB_FIX = '纠正'
17
- const MARKER = 'AGENT_RUN_LOG_V1'
18
-
19
- const cst = (d = new Date()) => new Date(d.getTime() + 8 * 3600e3)
20
- export const today = () => cst().toISOString().slice(0, 10)
21
- export const now = () => cst().toISOString().replace('Z', '+08:00').slice(0, 19)
22
- export const todayZero = () => today() + 'T00:00:00+08:00'
23
-
24
- /** 【今晚跑过没有】= 数据水位 ≥ 今天 00:00。纯【数据派生】——
25
- * 不记状态、不建标记文件、不看会话记忆。重启/换实例/漏拍都不受影响。 */
26
- export const ranTonight = (wm) => Boolean(wm) && String(wm) >= todayZero()
27
-
28
- const file = (root, day) => join(root, 'runs', `${day}.md`)
29
-
30
- export function read(root, day) {
31
- const p = file(root, day)
32
- if (!existsSync(p)) return []
33
- const out = []
34
- for (const line of readFileSync(p, 'utf8').split('\n')) {
35
- const s = line.trim()
36
- if (!s.startsWith('{')) continue
37
- try { out.push(JSON.parse(s)) }
38
- catch { console.error(` ⚠️ 运行记录有坏行,跳过但【不当成空】: ${s.slice(0, 60)}`) }
39
- }
40
- return out
41
- }
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
- /** 追加一行 + 回读校验。**写了没落上 = 这次没发生过,要说出来。** */
50
- export function append(root, row) {
51
- const day = today()
52
- const rows = read(root, day)
53
- rows.push(row)
54
- write(root, day, rows)
55
- const ok = read(root, day).some((r) => r.t === row.t && r.did === row.did)
56
- if (!ok) console.error(' ⚠️ 运行记录写后回读不到 —— 动作照常,但【留痕失败】')
57
- return ok ? row.t : ''
58
- }
59
-
60
- export function scan(root, days = 1) {
61
- const out = []
62
- for (let i = 0; i < Math.max(1, days); i++) {
63
- const d = new Date(Date.now() - i * 86400e3)
64
- const day = cst(d).toISOString().slice(0, 10)
65
- for (const r of read(root, day)) out.push({ ...r, _day: day })
66
- }
67
- return out.sort((a, b) => (a._day + a.t).localeCompare(b._day + b.t))
68
- }
69
-
70
- export function logq(root, { did, kind = '问答', secs = 0, r = 'pass', ev = '',
71
- ch = '钉钉单聊', asked = '', issue = '', conv = '', msg = '' }) {
72
- const row = { t: now().slice(11, 19), ch, kind, did: String(did).slice(0, 300), secs: +secs, r }
73
- for (const [k, v] of [['asked', asked], ['issue', issue], ['conv', conv], ['msg', msg], ['ev', ev]]) {
74
- if (v) row[k] = String(v).slice(0, 600)
75
- }
76
- return append(root, row)
77
- }