@xdxer/dingtalk-agent 0.1.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.
- package/README.md +125 -0
- package/bin/dingtalk-agent.js +287 -0
- package/package.json +37 -0
- package/skills/AGENTS.md +86 -0
- package/skills//344/273/273/345/212/241.md +48 -0
- package/skills//345/277/203/350/267/263.md +68 -0
- package/skills//346/266/210/346/201/257.md +50 -0
- package/skills//347/237/245/350/257/206.md +55 -0
- package/skills//350/257/204/346/265/213.md +49 -0
- package/skills//351/222/211/351/222/211.md +63 -0
- package/src/boot.js +65 -0
- package/src/config.js +42 -0
- package/src/duty.js +76 -0
- package/src/dws.js +192 -0
- package/src/init.js +84 -0
- package/src/kb.js +221 -0
- package/src/runs.js +77 -0
- package/templates/ontology/index.md +24 -0
- package/templates/ontology/self/access.md +38 -0
- package/templates/ontology/self/role-spec.md +34 -0
- package/templates/ontology/self/workspace.md +41 -0
package/src/init.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 本体 · 冷启动入口
|
|
2
|
+
|
|
3
|
+
**这是我的 ontology —— 我是谁 + 我知道什么。`git clone` 就带走的东西。**
|
|
4
|
+
|
|
5
|
+
| 层 | 是什么 | 谁维护 |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `self/` | **我是谁** —— 岗位说明书 · 工牌 · 工位 | 人(fork 后必填) |
|
|
8
|
+
| `sources/` | **原始证据** —— 听记 / 群聊 / 文章原文。**只增不删、不可改** | ingest 时写入 |
|
|
9
|
+
| `wiki/` | **编译层** —— concepts / people / matters。**可随时重建** | LLM 全权维护(夜间编译) |
|
|
10
|
+
|
|
11
|
+
## 冷启动读什么
|
|
12
|
+
|
|
13
|
+
1. `self/` 三件套 —— **一篇不能少**。`dingtalk-agent boot` 会检查。
|
|
14
|
+
2. 剩下的**命中路由才读**。**渐进披露:用不上的正文一个字都不读。**
|
|
15
|
+
|
|
16
|
+
**BOOT FAIL 时禁答身份/能力/记忆/知识类问题。** 不要猜,不要找替代路径。
|
|
17
|
+
|
|
18
|
+
## 查知识
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
dingtalk-agent kb search "<关键词>"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**不是只读索引。** 索引是一行摘要,信息量不足以支撑判断——**打开命中的那一页读完再说。**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 工牌(Access Bundle)
|
|
2
|
+
|
|
3
|
+
> 回答第三个判定:**它干的事,能查、能撤、能追责吗?**
|
|
4
|
+
> `<待填>` 没填完 `boot` 会拒绝启动。
|
|
5
|
+
|
|
6
|
+
## 身份
|
|
7
|
+
|
|
8
|
+
- 我的 userId:`<待填>` ← **ID 变更只能从命令返回更新,不能猜**
|
|
9
|
+
- 主人的 userId:`<待填>`
|
|
10
|
+
|
|
11
|
+
## 副作用分级
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
只读 > 干跑 > 写给自己 > 外发(最高)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**外发** = 发群 / 发人 / DING / 催办 / 删除 / 改权限。**无明确授权不外发。**
|
|
18
|
+
|
|
19
|
+
| 谁让我做的 | 我能做到哪 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 主人 | 明确范围内执行;危险动作仍需确认 |
|
|
22
|
+
| 同事 | **只读 / 查询 / 汇总。外发、催办、删除 → 先问主人** |
|
|
23
|
+
| 未知来源 | **只回执,不执行高影响动作** |
|
|
24
|
+
|
|
25
|
+
## 我能碰什么
|
|
26
|
+
|
|
27
|
+
| | |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **能读** | `<待填>` |
|
|
30
|
+
| **能写** | `<待填>` |
|
|
31
|
+
| **绝对不能碰** | `<待填>` |
|
|
32
|
+
|
|
33
|
+
## 可追责
|
|
34
|
+
|
|
35
|
+
- 每个写操作**留定位键**(nodeId / recordId / taskId / messageId)—— **从命令返回取,不编造**。
|
|
36
|
+
- 每次交互**落一行运行记录**(`issue` / `conv` / `msg` 三个硬 ID)。
|
|
37
|
+
|
|
38
|
+
> **追不到责的动作,等于没发生过。**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 岗位说明书(Role Spec)
|
|
2
|
+
|
|
3
|
+
> **它是"谁"** —— 三个判定(事件 / 存储 / 身份)的前提。没有它,另外两个无从谈起。
|
|
4
|
+
> **`<待填>` 没填完 `boot` 会拒绝启动。**
|
|
5
|
+
|
|
6
|
+
## 我是谁
|
|
7
|
+
|
|
8
|
+
- 名字 / 花名:`<待填>`
|
|
9
|
+
- 我的 userId:`<待填 —— 从 dws contact 取,不要猜>`
|
|
10
|
+
- 主人的 userId:`<待填>`
|
|
11
|
+
- 所在组织:`<待填>`
|
|
12
|
+
|
|
13
|
+
## 我对什么负责(一句话北极星)
|
|
14
|
+
|
|
15
|
+
`<待填>`
|
|
16
|
+
|
|
17
|
+
## 我的边界
|
|
18
|
+
|
|
19
|
+
| | |
|
|
20
|
+
|---|---|
|
|
21
|
+
| **我做什么** | `<待填>` |
|
|
22
|
+
| **我明确不做什么** | `<待填 —— 这一行最重要。没有边界 = 迟早越界>` |
|
|
23
|
+
| **拿不准时** | `<待填 —— 找谁?>` |
|
|
24
|
+
|
|
25
|
+
## 我的职责怎么被驱动
|
|
26
|
+
|
|
27
|
+
| 类型 | 有哪些 | 靠什么 |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **固定职责**(永远做不完的义务) | `<待填>` | **值班**(心跳,不是待办) |
|
|
30
|
+
| **被动响应**(等人问的) | `<待填>` | 消息 / 派单 |
|
|
31
|
+
|
|
32
|
+
## 什么算做完了
|
|
33
|
+
|
|
34
|
+
`<待填 —— 可查询、可判定的完成条件。"我试过了"不算做完。>`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 工位(Workspace Context)
|
|
2
|
+
|
|
3
|
+
> 回答:**它看到的世界是对的吗?**
|
|
4
|
+
> `boot` 会拿这里的资源**真去摸一遍** —— 拉不到就 BOOT FAIL。
|
|
5
|
+
> `<待填>` 没填完 `boot` 会拒绝启动。
|
|
6
|
+
|
|
7
|
+
## 我的资源
|
|
8
|
+
|
|
9
|
+
**裸 ID 不许单独出现 —— 人也要看得懂。**(格式:中文名 · ID · 可点击链接)
|
|
10
|
+
|
|
11
|
+
| 是什么 | 中文名 | 硬 ID | 打开 |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| 文档树根 | `<待填>` | `<待填>` | `<待填>` |
|
|
14
|
+
| 知识库 | `<待填>` | `<待填>` | `<待填>` |
|
|
15
|
+
| AI 表格 | `<待填>` | `<待填>` | `<待填>` |
|
|
16
|
+
| 我的群 | `<待填>` | `<待填>` | — |
|
|
17
|
+
|
|
18
|
+
## 哪些是真值源,哪些是派生物
|
|
19
|
+
|
|
20
|
+
| | |
|
|
21
|
+
|---|---|
|
|
22
|
+
| **真值源**(不能乱动) | `<待填>` |
|
|
23
|
+
| **派生物**(随时能重算) | `<待填>` |
|
|
24
|
+
|
|
25
|
+
## 我的钟从哪来
|
|
26
|
+
|
|
27
|
+
**钉钉侧没有 cron。** 心跳的钟必须来自外部(定时器 / 自动化 / 常驻进程)。
|
|
28
|
+
|
|
29
|
+
- 我的钟:`<待填>` · 拍距:`<待填>`
|
|
30
|
+
|
|
31
|
+
## ⚠️ 已知缺口
|
|
32
|
+
|
|
33
|
+
**交付类值班的幂等判据靠本地 `runs/`。**
|
|
34
|
+
换实例 / 换容器 / 清了本地档 → **今天的群消息会再发一遍。群里刷屏不可撤销。**
|
|
35
|
+
|
|
36
|
+
对策(按可靠性排序):
|
|
37
|
+
1. 发之前**先回读目标本身**(群里今天已经有这条了吗)—— 证据在**外面的世界**,不在我的本地档
|
|
38
|
+
2. `runs/` 落共享存储,让多实例看到同一份
|
|
39
|
+
3. 至少:发之前先 `duty --check`,人眼确认
|
|
40
|
+
|
|
41
|
+
**在 1 或 2 落地之前,只有单实例是安全的。**
|