sintbot 1.0.16 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.en.json +99 -0
  2. package/CHANGELOG.json +99 -0
  3. package/README.md +20 -158
  4. package/README.zh-CN.md +20 -166
  5. package/THIRD-PARTY-NOTICES.md +394 -0
  6. package/bin/sintbot-v2.js +363 -0
  7. package/docs/AI-ASSISTED-INSTALL.md +11 -35
  8. package/docs/install.md +61 -167
  9. package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
  10. package/docs/zh-CN/install.md +61 -165
  11. package/install.sh +84 -0
  12. package/package.json +73 -37
  13. package/src/agent/claudecode.js +149 -108
  14. package/src/agent/codexapp.js +458 -0
  15. package/src/agent/codexcode.js +210 -107
  16. package/src/agent/errtag.js +34 -0
  17. package/src/agent/privatecore.js +790 -0
  18. package/src/agent/privatecore.wasm +0 -0
  19. package/src/agent/runner.js +10 -8
  20. package/src/agent/winexec.js +35 -5
  21. package/src/c2install.js +117 -0
  22. package/src/config.js +2 -1
  23. package/src/core/c2cardeffectbridge.js +51 -0
  24. package/src/core/c2cardterminalpath.js +216 -0
  25. package/src/core/c2durableingress.js +148 -0
  26. package/src/core/c2durablejournal.js +1560 -0
  27. package/src/core/c2durableruntime.js +2361 -0
  28. package/src/core/c2eventidentity.js +90 -0
  29. package/src/core/c2globalfault.js +401 -0
  30. package/src/core/c2localeffectexecutor.js +193 -0
  31. package/src/core/c2payloadhandles.js +333 -0
  32. package/src/core/c2persistrelation.js +179 -0
  33. package/src/core/c2persiststate.js +342 -0
  34. package/src/core/c2platformrecoverysender.js +76 -0
  35. package/src/core/c2recoverygate.js +385 -0
  36. package/src/core/c2runnerinput.js +63 -0
  37. package/src/core/c2servicestartgate.js +103 -0
  38. package/src/core/c2sessionbridge.js +72 -0
  39. package/src/core/c2sourcequarantine.js +30 -0
  40. package/src/core/c2startuprecovery.js +105 -0
  41. package/src/core/c2successorbridge.js +151 -0
  42. package/src/core/c2terminaleffectbridge.js +154 -0
  43. package/src/core/c2terminalprune.js +108 -0
  44. package/src/core/c2turnprojection.js +177 -0
  45. package/src/core/enginecheckpoint.js +211 -0
  46. package/src/core/engineeffectcursor.js +394 -0
  47. package/src/core/enginestepcoordinator.js +183 -0
  48. package/src/core/store.js +52 -8
  49. package/src/core/v2engine.js +1469 -0
  50. package/src/platform/feishu.js +324 -67
  51. package/src/plugins.js +1 -9
  52. package/assets/design-styles/acid-pop.md +0 -155
  53. package/assets/design-styles/archive-index.md +0 -161
  54. package/assets/design-styles/art-deco.md +0 -164
  55. package/assets/design-styles/cinematic-noir.md +0 -178
  56. package/assets/design-styles/editorial.md +0 -121
  57. package/assets/design-styles/extra/README.md +0 -23
  58. package/assets/design-styles/extra/constructivism.md +0 -146
  59. package/assets/design-styles/extra/neo-brutalism.md +0 -175
  60. package/assets/design-styles/extra/new-wave.md +0 -166
  61. package/assets/design-styles/extra/nordic-material.md +0 -162
  62. package/assets/design-styles/extra/paris-couture.md +0 -162
  63. package/assets/design-styles/extra/tropical-modern.md +0 -168
  64. package/assets/design-styles/extra/ulm-functional.md +0 -182
  65. package/assets/design-styles/extra/wa-shikimen.md +0 -160
  66. package/assets/design-styles/extra/y2k-chrome.md +0 -172
  67. package/assets/design-styles/geometric-pop.md +0 -122
  68. package/assets/design-styles/hand-drawn.md +0 -117
  69. package/assets/design-styles/islamic-geometry.md +0 -155
  70. package/assets/design-styles/liquid-glass.md +0 -183
  71. package/assets/design-styles/neo-chinese.md +0 -136
  72. package/assets/design-styles/precision-mono.md +0 -156
  73. package/assets/design-styles/quiet-luxury.md +0 -163
  74. package/assets/design-styles/riso-print.md +0 -116
  75. package/assets/design-styles/soft-friendly.md +0 -174
  76. package/assets/design-styles/swiss-grid.md +0 -156
  77. package/assets/design-styles/tech-dark.md +0 -182
  78. package/assets/favicon.svg +0 -6
  79. package/assets/logo.svg +0 -24
  80. package/bin/sbot.js +0 -4716
  81. package/docs/slack-app-manifest.yaml +0 -104
  82. package/sbot.config.example.json +0 -235
  83. package/src/agent/askonce.js +0 -76
  84. package/src/agent/claudemodels.js +0 -60
  85. package/src/agent/claudestatus.js +0 -74
  86. package/src/agent/claudeusage.js +0 -200
  87. package/src/agent/codexstatus.js +0 -181
  88. package/src/agent/imagen.js +0 -458
  89. package/src/agent/quotaerr.js +0 -44
  90. package/src/agent/transienterr.js +0 -32
  91. package/src/cdp.js +0 -254
  92. package/src/cli-secret.js +0 -36
  93. package/src/clilang.js +0 -31
  94. package/src/core/cron.js +0 -182
  95. package/src/core/cronfire.js +0 -120
  96. package/src/core/cronrun.js +0 -116
  97. package/src/core/engine.js +0 -2758
  98. package/src/core/interfaces.js +0 -114
  99. package/src/core/issues.js +0 -185
  100. package/src/core/ledger.js +0 -30
  101. package/src/core/opsalert.js +0 -214
  102. package/src/core/pending.js +0 -123
  103. package/src/core/proxy.js +0 -424
  104. package/src/core/quotaalert.js +0 -79
  105. package/src/core/quotaerr.js +0 -55
  106. package/src/core/scheduler.js +0 -59
  107. package/src/design.js +0 -5161
  108. package/src/gitrepo.js +0 -150
  109. package/src/health.js +0 -353
  110. package/src/installdeps.js +0 -72
  111. package/src/leftover.js +0 -174
  112. package/src/platform/boardnotice.js +0 -77
  113. package/src/platform/boardops.js +0 -36
  114. package/src/platform/boardwatch.js +0 -447
  115. package/src/platform/cli.js +0 -45
  116. package/src/platform/slack.js +0 -1289
  117. package/src/platform/takeovercard.js +0 -37
  118. package/src/platform/wecom.js +0 -938
  119. package/src/platform/wecomcli.js +0 -128
  120. package/src/platform/wecomgate.js +0 -108
  121. package/src/platform/wecomname.js +0 -43
  122. package/src/platform/wecompoll.js +0 -241
  123. package/src/preview.js +0 -581
  124. package/src/projectmemory.js +0 -175
  125. package/src/service.js +0 -317
  126. package/src/ui/auth.js +0 -31
  127. package/src/ui/browser.html +0 -392
  128. package/src/ui/browserroutes.js +0 -234
  129. package/src/ui/console.html +0 -5526
  130. package/src/ui/dashboard.js +0 -168
  131. package/src/ui/feishulogin.js +0 -136
  132. package/src/ui/server.js +0 -2314
  133. package/src/ui/sessions.js +0 -78
  134. package/src/ui/takeover.js +0 -108
  135. package/src/updater.js +0 -243
  136. package/src/upgrade.js +0 -35
  137. package/src/usage.js +0 -144
package/src/leftover.js DELETED
@@ -1,174 +0,0 @@
1
- // 遗留进程:agent 轮次结束后仍活着的后台 shell(v0.11.108)。
2
- //
3
- // 为什么要有这一层:claude 子进程退了,它在轮中用后台方式起的 shell(轮询循环 / nohup 的脚本 /
4
- // 卡住的 expect)并不跟着退 —— 父进程没了就挂到 pid 1 下面,而且 Claude Code 给每条 Bash 开的是
5
- // **自己的进程组**(pgid = 自身 pid),runner 轮次结束的树杀抓不到它们。2026-10-09 在生产机上翻出
6
- // 三个:其中一条 `until node -e …; do sleep 5; done` 每 5 秒调一次线上云函数,跑了 35 天,零 CPU、
7
- // 零报警,一个月前某次 ps 输出里出现过也没人当回事。缺的不是围栏(四个群 1.27 万次 Bash 调用里
8
- // 后台循环零次 —— 不常见),是**看得见**:doctor 一行 + 控制台概览一区 + 一键结束。
9
- //
10
- // 判据(三条同时成立才算):
11
- // ① ppid === 1(父进程已退,被 launchd / init 收养)
12
- // ② 命令行带 `shell-snapshots/snapshot-`(Claude Code 的 Bash 工具起 shell 时 source 的快照,
13
- // 是「这条 shell 是 agent 起的」唯一可靠标记;别的进程不会带它)
14
- // ③ 存活 ≥ minAgeSec(默认 1 小时):刚结束的轮里还在收尾的后台命令不该被报;一小时还在跑的
15
- // 几乎都是忘了停的循环 / 卡住的等待(轮次墙钟上限才 90 分钟)
16
- // ⚠ 只报与「一键结束」,**不自动杀** —— 偶尔也有 agent 故意留下的东西(比如起了个 dev server
17
- // 在看),杀不杀是 operator 的判断;自动杀等于把一次误判变成一次事故。
18
- // ⚠ Windows 没有 ps、也没有这套 shell 快照 ⇒ supported:false,一行不占(与 browser 行同款:
19
- // 没有的东西不吓人)。
20
- // 纯函数(解析 / 判据 / 摘要)与 I/O(ps / kill)分开,前者单测钉死;ps 与 kill 都可注入。
21
-
22
- const { execFile } = require('node:child_process')
23
- const { redact } = require('./core/redact') // 出站脱敏:命令行里可能有 agent 写进去的密钥
24
-
25
- const IS_WIN = process.platform === 'win32'
26
- const SNAPSHOT_RE = /shell-snapshots[\\/]snapshot-/
27
- const DEFAULT_MIN_AGE_SEC = 60 * 60
28
- const PS_ARGS = ['-axo', 'pid=,ppid=,etime=,command=']
29
-
30
- // ps 的 etime 形态:`ss`?不会;`mm:ss` / `hh:mm:ss` / `dd-hh:mm:ss`。解析不了返回 null(别当 0,
31
- // 0 会让「存活多久」这条判据静默失效 —— 所有行都当刚起的,永远报不出来)。
32
- function parseEtime(s) {
33
- const m = String(s || '').trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d{1,2}):(\d{2})$/)
34
- if (!m) return null
35
- const [, d, h, mi, se] = m
36
- return (Number(d || 0) * 86400) + (Number(h || 0) * 3600) + (Number(mi) * 60) + Number(se)
37
- }
38
-
39
- // `ps -axo pid=,ppid=,etime=,command=` 的输出 → 行记录。command 可能含任意空白,只按前三列切。
40
- function parsePs(text) {
41
- const rows = []
42
- for (const line of String(text || '').split('\n')) {
43
- const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/)
44
- if (!m) continue
45
- rows.push({ pid: Number(m[1]), ppid: Number(m[2]), ageSec: parseEtime(m[3]), command: m[4] })
46
- }
47
- return rows
48
- }
49
-
50
- function isLeftover(row, minAgeSec = DEFAULT_MIN_AGE_SEC) {
51
- if (!row || row.ppid !== 1) return false
52
- if (!SNAPSHOT_RE.test(String(row.command || ''))) return false
53
- return row.ageSec != null && row.ageSec >= minAgeSec
54
- }
55
-
56
- // 把 Claude Code 那段 shell 前导(source 快照 && setopt … && { unalias } && eval '<命令>' < /dev/null
57
- // && pwd -P >| /tmp/claude-xxxx-cwd)剥掉,只留 agent 真正写的那条命令 —— 控制台 / doctor 要给人看的
58
- // 是「它在干什么」,不是机器路径(零工具名:快照路径、/tmp 落点都是实现细节)。剥不干净就退回
59
- // 粗剪:去掉快照前导之前的部分;再不行原样截断。
60
- function summarizeCommand(cmd, max = 100) {
61
- let s = String(cmd || '')
62
- const ev = s.match(/\beval '([\s\S]*)' < \/dev\/null/)
63
- // eval 单引号串里嵌的单引号有两种写法:'\'' 与 '"'"',都还原成 '
64
- if (ev) s = ev[1].replace(/'\\''/g, "'").replace(/'"'"'/g, "'")
65
- else {
66
- const i = s.search(SNAPSHOT_RE)
67
- if (i >= 0) {
68
- // 快照之后的 `… || true && ` 链是前导;取最后一个 `&& ` 之后、`< /dev/null` 之前的段
69
- const tail = s.slice(i).replace(/ < \/dev\/null[\s\S]*$/, '')
70
- const parts = tail.split(/ \|\| true && /)
71
- s = parts[parts.length - 1].replace(/^\{[^}]*\}\s*&&\s*/, '')
72
- }
73
- }
74
- s = s.replace(/\s+/g, ' ').trim()
75
- return s.length > max ? s.slice(0, max - 1) + '…' : s
76
- }
77
-
78
- function describeAge(sec) {
79
- if (sec == null) return '?'
80
- if (sec >= 86400) return `${Math.floor(sec / 86400)} 天`
81
- if (sec >= 3600) return `${Math.floor(sec / 3600)} 小时`
82
- return `${Math.max(1, Math.floor(sec / 60))} 分钟`
83
- }
84
-
85
- function runPs() {
86
- return new Promise((resolve, reject) => {
87
- execFile('ps', PS_ARGS, { maxBuffer: 16 * 1024 * 1024 }, (err, out) => (err ? reject(err) : resolve(out)))
88
- })
89
- }
90
-
91
- // 列出遗留进程。{ supported, rows:[{pid, ageSec, age, command, summary}], error? }
92
- // opts.ps 注入(测试);opts.minAgeSec 判据;opts.isWin 注入平台(测试)。
93
- async function list(opts = {}) {
94
- const isWin = opts.isWin == null ? IS_WIN : !!opts.isWin
95
- if (isWin) return { supported: false, rows: [] }
96
- let out
97
- try { out = await (opts.ps || runPs)() } catch (e) { return { supported: true, rows: [], error: (e && e.message) || String(e) } }
98
- // command / summary 都过一遍出站脱敏:agent 写的后台循环可能带 `-H "Authorization: Bearer …"`,
99
- // 这两个字段会进 doctor stdout / 控制台 / 审计账本
100
- const rows = parsePs(out).filter((r) => isLeftover(r, opts.minAgeSec)).map((r) => ({
101
- pid: r.pid, ageSec: r.ageSec, age: describeAge(r.ageSec), command: redact(r.command), summary: redact(summarizeCommand(r.command)),
102
- })).sort((a, b) => b.ageSec - a.ageSec)
103
- return { supported: true, rows }
104
- }
105
-
106
- // 带 TTL 的列表:控制台每次 /api/state 都会问,ps 一次几十毫秒,30 秒内复用。
107
- let CACHE = { at: 0, val: null }
108
- async function cachedList(opts = {}) {
109
- const ttl = opts.ttlMs == null ? 30_000 : opts.ttlMs
110
- const now = (opts.now || Date.now)()
111
- if (CACHE.val && now - CACHE.at < ttl) return CACHE.val
112
- const val = await list(opts)
113
- CACHE = { at: now, val }
114
- return val
115
- }
116
- function clearCache() { CACHE = { at: 0, val: null } }
117
-
118
- // pid 的全部后代(按 ppid 链广度优先),用于组杀失败时兜底。
119
- function descendants(rows, pid) {
120
- const out = []; const seen = new Set([pid]); const q = [pid]
121
- while (q.length) {
122
- const p = q.shift()
123
- for (const r of rows) if (r.ppid === p && !seen.has(r.pid)) { seen.add(r.pid); out.push(r.pid); q.push(r.pid) }
124
- }
125
- return out
126
- }
127
-
128
- // 结束一组遗留进程。三条纪律(对抗审查 10-09 逮出来的):
129
- // ① **动手前按身份复核**:只对此刻仍满足「ppid 1 + 快照标记」的 pid 发信号(不看年龄),其余归 skipped。
130
- // 名单是分钟级之前的快照,pid 会回绕复用 —— 最坏是新起的一轮 Bash shell 顶了这个号、它正好也是
131
- // 组长,照单 SIGKILL -pid 会把活着的轮次整组掐掉。
132
- // ② 先整个进程组 SIGTERM(真实 wrapper 自成一组,子 sleep / expect 一起收),宽限后 SIGKILL 兜底
133
- // (实测 expect 不理 SIGTERM);**组杀打不到(不是组长 / 组已散)就本体 + 全部后代逐个发**,只杀外层
134
- // zsh 会让真正在打接口的 node / curl 被 pid 1 收养、从此没标记再也看不见。
135
- // ③ 杀完清缓存,调用方不必记得。ESRCH(已经不在)不算失败。ps / kill / sleep 可注入。
136
- async function kill(pids, opts = {}) {
137
- const k = opts.kill || ((pid, sig) => process.kill(pid, sig))
138
- const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
139
- const graceMs = opts.graceMs == null ? 1500 : opts.graceMs
140
- const targets = [...new Set(pids.map(Number).filter((p) => Number.isInteger(p) && p > 1))]
141
- let rows
142
- try { rows = parsePs(await (opts.ps || runPs)()) } catch (e) {
143
- return { killed: [], failed: targets.map((pid) => ({ pid, error: 'ps: ' + ((e && e.message) || String(e)) })), skipped: [] }
144
- }
145
- const byPid = new Map(rows.map((r) => [r.pid, r]))
146
- const skipped = targets.filter((pid) => !isLeftover(byPid.get(pid), 0))
147
- const live = targets.filter((pid) => !skipped.includes(pid))
148
- const failed = []
149
- const singles = new Map() // 组杀失败的 pid → 要逐个发信号的 [本体, ...后代](按动手前的 ps 快照算)
150
- const send = (pid, sig) => {
151
- if (!singles.has(pid)) { try { k(-pid, sig); return } catch {} singles.set(pid, [pid, ...descendants(rows, pid)]) }
152
- for (const q of singles.get(pid)) {
153
- try { k(q, sig) } catch (e) { if (!(e && e.code === 'ESRCH')) failed.push({ pid: q, sig, error: (e && e.message) || String(e) }) }
154
- }
155
- }
156
- for (const pid of live) send(pid, 'SIGTERM')
157
- if (live.length && graceMs > 0) await sleep(graceMs)
158
- for (const pid of live) send(pid, 'SIGKILL')
159
- clearCache()
160
- const failedPids = new Set(failed.map((f) => f.pid))
161
- return { killed: live.filter((p) => !failedPids.has(p)), failed, skipped }
162
- }
163
-
164
- // doctor / 控制台健康行共用的一句话。零工具名:不写命令、不写路径。
165
- function describe(res) {
166
- if (!res || !res.supported) return null
167
- if (res.error) return { status: 'warn', text: `⚠ 查不了(${res.error})` }
168
- if (!res.rows.length) return { status: 'ok', text: '无 —— 对话轮次结束后没有留下后台进程' }
169
- const oldest = res.rows[0]
170
- const heads = res.rows.slice(0, 3).map((r) => `${r.age}前起:${r.summary.slice(0, 60)}`).join(' · ')
171
- return { status: 'warn', text: `⚠ ${res.rows.length} 个后台进程在对话轮次结束后仍在跑(最老 ${oldest.age}):${heads}${res.rows.length > 3 ? ' · …' : ''} —— 它们可能还在反复打接口 / 占端口,到概览里看一眼并结束` }
172
- }
173
-
174
- module.exports = { parseEtime, parsePs, isLeftover, summarizeCommand, describeAge, descendants, list, cachedList, clearCache, kill, describe, DEFAULT_MIN_AGE_SEC, SNAPSHOT_RE, PS_ARGS }
@@ -1,77 +0,0 @@
1
- // 画布播报卡回填通道(daemon ↔ CLI 文件桥,2026-07-23)。
2
- //
3
- // 单卡回填(产品决定):点「按标注改图」按钮时 daemon 把这张播报卡的实体 cardId 入队;agent 改完调
4
- // `board-add-image --deliver` 出队 → cardkit card.update 把结果图回填进原卡(处理中 → 结果卡,始终
5
- // 一张卡);agent 没 --deliver 时,轮次结束 daemon 兜底出队把卡收尾(不卡处理中)。
6
- //
7
- // **FIFO 队列而非单槽**(审查修正 M6):第 1 轮进行中用户又点了第 2 张播报卡 ⇒ 两条各自入队;群锁
8
- // 保证轮次线性执行 ⇒ 交付顺序 = 触发顺序,shift 出的恰是本轮的卡,不串。单轮多次 --deliver 时第二次
9
- // 起队列已空 ⇒ 新发结果卡(可接受)。条目带 ts,出队时丢弃超龄陈旧条目(审查修正 L13:防隔天残留
10
- // 被无关交付误用;上限取轮次墙钟的宽余量)。
11
- //
12
- // 单写(daemon 确认入口)单读(CLI --deliver / daemon 收尾),原子写;不上锁(与 boardops 同款纪律)。
13
- const fs = require('fs')
14
- const os = require('os')
15
- const path = require('path')
16
- const { atomicWriteJSON } = require('../core/store')
17
-
18
- const NOTICE_MAX_AGE_MS = 6 * 60 * 60_000 // 陈旧上限 6h(轮次墙钟 2h 的宽余量)
19
- const NOTICE_MAX_QUEUE = 5 // 队列上限(防异常堆积;超出丢最旧)
20
-
21
- function boardNoticeDir() {
22
- return path.join(os.homedir(), '.sbot', 'board-notice')
23
- }
24
-
25
- // chatId 形如 oc_xxx(文件名安全),仍 encodeURIComponent 兜底非常规字符
26
- const noticeFile = (dir, chatId) => path.join(dir, encodeURIComponent(chatId) + '.json')
27
-
28
- function _readQueue(dir, chatId, now) {
29
- let q
30
- try { q = JSON.parse(fs.readFileSync(noticeFile(dir, chatId), 'utf8')).queue } catch { return [] }
31
- if (!Array.isArray(q)) return []
32
- return q.filter((n) => n && typeof n === 'object' && (now - (Number(n.ts) || 0)) <= NOTICE_MAX_AGE_MS)
33
- }
34
-
35
- function _writeQueue(dir, chatId, queue) {
36
- if (!queue.length) { try { fs.unlinkSync(noticeFile(dir, chatId)) } catch {} ; return }
37
- fs.mkdirSync(dir, { recursive: true })
38
- atomicWriteJSON(noticeFile(dir, chatId), { queue })
39
- }
40
-
41
- // 确认入口调用:一条待回填锚点入队(FIFO 尾部;超上限丢最旧)。
42
- function pushNotice(dir, chatId, item, now = Date.now()) {
43
- const q = _readQueue(dir, chatId, now)
44
- q.push({ ...(item || {}), ts: now })
45
- while (q.length > NOTICE_MAX_QUEUE) q.shift()
46
- _writeQueue(dir, chatId, q)
47
- }
48
-
49
- // --deliver / 轮次收尾调用:出队最旧一条(= 当前正交付的轮次;过期条目已滤)。空队返回 null。
50
- function shiftNotice(dir, chatId, now = Date.now()) {
51
- const q = _readQueue(dir, chatId, now)
52
- if (!q.length) { _writeQueue(dir, chatId, q); return null } // 顺带清掉全过期的文件
53
- const head = q.shift()
54
- _writeQueue(dir, chatId, q)
55
- return head
56
- }
57
-
58
- // 只看队头不消费(是否有待回填锚点)。
59
- function peekNotice(dir, chatId, now = Date.now()) {
60
- const q = _readQueue(dir, chatId, now)
61
- return q.length ? q[0] : null
62
- }
63
-
64
- // 按本轮 goId(触发的合成消息 msgId,boardgo:chatId:ts)精确取该轮的锚点 —— **不靠 FIFO 头**。
65
- // detached imagen worker 交付顺序与轮次 done 顺序会错位,靠头出队会张冠李戴/卡死;done 兜底用它
66
- // 精确摘自己这轮的卡(审查 M2)。opts.peek=true 只查不消费。goIds 传本轮全批 msgIds(排队合并)。
67
- function takeNoticeByGo(dir, chatId, goIds, opts = {}, now = Date.now()) {
68
- const q = _readQueue(dir, chatId, now)
69
- const set = new Set(Array.isArray(goIds) ? goIds : [goIds])
70
- const i = q.findIndex((n) => n && n.goId != null && set.has(n.goId))
71
- if (i < 0) { _writeQueue(dir, chatId, q); return null } // 顺带清过期
72
- const hit = q[i]
73
- if (!opts.peek) { q.splice(i, 1); _writeQueue(dir, chatId, q) } else { _writeQueue(dir, chatId, q) }
74
- return hit
75
- }
76
-
77
- module.exports = { boardNoticeDir, pushNotice, shiftNotice, peekNotice, takeNoticeByGo, NOTICE_MAX_AGE_MS, NOTICE_MAX_QUEUE }
@@ -1,36 +0,0 @@
1
- // bot 侧画布写操作留痕(ops journal)—— 轮询自动感知的自编辑抑制数据面(docs/BOARD.md)。
2
- // 事件版有 operator_ids 可辨「谁改的」;轮询版只看得到节点集,故 CLI 的写动作(add-image /
3
- // del-node / create 使用说明便签)成功后把服务端返回的**持久 id** 记在这里,daemon 轮询先
4
- // 重放到基线再 diff ⇒ bot 自己放的图不当「新增」播报;del-node 的 overwrite 会让全部节点
5
- // id 变化,rewrite 留痕带的是重写后全量新 id ⇒ 基线直接重置,不把整板当新内容重放。
6
- // 纯 fs 零依赖;withFileLock 串行化并发写(锁语义同 config 写回);写失败由调用方忽略 ——
7
- // 留痕丢失的最坏后果 = 多播一次 bot 自己的编辑,好过让主动作失败。
8
- const fs = require('fs')
9
- const path = require('path')
10
- const { atomicWriteJSON, withFileLock } = require('../core/store')
11
-
12
- const MAX_OPS = 50 // 每画板只留末 N 条 —— daemon 按 ts 游标消费,陈旧条目无用
13
-
14
- function opsFile(dir, whiteboardId) {
15
- return path.join(dir, String(whiteboardId).replace(/[^A-Za-z0-9_-]/g, '_') + '.json')
16
- }
17
-
18
- // op: { op:'add'|'rewrite', ids:[…] };ts 由本函数补(留痕时刻,同文件内保证单调递增 ——
19
- // daemon 的消费游标是「ts > cursor」,同毫秒两条会丢后一条)。
20
- function appendBoardOp(dir, whiteboardId, op) {
21
- fs.mkdirSync(dir, { recursive: true })
22
- const file = opsFile(dir, whiteboardId)
23
- withFileLock(file, () => {
24
- let ops = []
25
- try { ops = JSON.parse(fs.readFileSync(file, 'utf8')).ops || [] } catch {} // 首次/损坏 → 重建
26
- const last = ops.length ? Number(ops[ops.length - 1].ts) || 0 : 0
27
- ops.push({ ts: Math.max(Date.now(), last + 1), op: op.op, ids: Array.isArray(op.ids) ? op.ids : [] })
28
- atomicWriteJSON(file, { ops: ops.slice(-MAX_OPS) })
29
- })
30
- }
31
-
32
- function readBoardOps(dir, whiteboardId) {
33
- try { return JSON.parse(fs.readFileSync(opsFile(dir, whiteboardId), 'utf8')).ops || [] } catch { return [] }
34
- }
35
-
36
- module.exports = { appendBoardOp, readBoardOps, opsFile, MAX_OPS }