agentquad 0.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.
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/dist-web/assets/index-CMaXwixo.js +1234 -0
- package/dist-web/assets/index-DBHApzV1.css +32 -0
- package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +14 -0
- package/package.json +88 -0
- package/src/ask-user-buttons.js +142 -0
- package/src/claude-transcript.js +203 -0
- package/src/cli.js +1040 -0
- package/src/codex-event-emitter.js +111 -0
- package/src/codex-prompt-detector.js +53 -0
- package/src/codex-sidecar.js +52 -0
- package/src/codex-transcript.js +74 -0
- package/src/config.js +692 -0
- package/src/data/claude-code-commands.json +52 -0
- package/src/db.js +1503 -0
- package/src/dispatch.js +13 -0
- package/src/export/todoMarkdown.js +246 -0
- package/src/first-run-wizard.js +82 -0
- package/src/git/gitStatus.js +139 -0
- package/src/lark-api-client.js +205 -0
- package/src/lark-bot.js +510 -0
- package/src/lark-card.js +88 -0
- package/src/lark-config-service.js +16 -0
- package/src/lark-event-client.js +107 -0
- package/src/lark-image.js +99 -0
- package/src/lark-markdown.js +51 -0
- package/src/lark-video.js +163 -0
- package/src/mcp/audit.js +34 -0
- package/src/mcp/server.js +83 -0
- package/src/mcp/tools/destructive/index.js +252 -0
- package/src/mcp/tools/openclaw/index.js +405 -0
- package/src/mcp/tools/read/index.js +269 -0
- package/src/mcp/tools/write/index.js +157 -0
- package/src/openclaw-bridge.js +566 -0
- package/src/openclaw-hook-installer.js +338 -0
- package/src/openclaw-hook.js +908 -0
- package/src/openclaw-wizard.js +2442 -0
- package/src/pending-questions.js +297 -0
- package/src/pricing.js +45 -0
- package/src/prompt-render.js +36 -0
- package/src/pty.js +992 -0
- package/src/routes/ai-terminal.js +1228 -0
- package/src/routes/git.js +89 -0
- package/src/routes/openclaw-hook.js +67 -0
- package/src/routes/openclaw-inbound.js +36 -0
- package/src/routes/recurringRules.js +80 -0
- package/src/routes/reports.js +50 -0
- package/src/routes/search.js +46 -0
- package/src/routes/stats.js +31 -0
- package/src/routes/telegram-config.js +152 -0
- package/src/routes/telegram-sync.js +221 -0
- package/src/routes/templates.js +63 -0
- package/src/routes/todos.js +649 -0
- package/src/routes/transcripts.js +75 -0
- package/src/routes/uploads.js +107 -0
- package/src/routes/wiki.js +142 -0
- package/src/search/fts.js +209 -0
- package/src/search/index.js +199 -0
- package/src/search/transcripts.js +148 -0
- package/src/server.js +1791 -0
- package/src/session-input-dispatcher.js +256 -0
- package/src/stats/markdown.js +42 -0
- package/src/stats/report.js +207 -0
- package/src/summarize.js +84 -0
- package/src/system-rules.js +52 -0
- package/src/telegram-bot.js +875 -0
- package/src/telegram-commands.js +149 -0
- package/src/telegram-config-service.js +84 -0
- package/src/telegram-image.js +95 -0
- package/src/telegram-loading-status.js +112 -0
- package/src/telegram-markdown.js +82 -0
- package/src/telegram-reaction-tracker.js +69 -0
- package/src/telegram-video.js +75 -0
- package/src/templates/claude-hooks/notify.js +103 -0
- package/src/transcript.js +305 -0
- package/src/transcripts/blocks.js +56 -0
- package/src/transcripts/index.js +222 -0
- package/src/transcripts/indexer.js +34 -0
- package/src/transcripts/matcher.js +70 -0
- package/src/transcripts/scanner.js +259 -0
- package/src/usage-footer.js +170 -0
- package/src/usage-parser.js +132 -0
- package/src/wiki/guide.js +44 -0
- package/src/wiki/index.js +232 -0
- package/src/wiki/redact.js +34 -0
- package/src/wiki/sources.js +122 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Rendered verbatim into ~/.agentquad/wiki/WIKI_GUIDE.md on first init.
|
|
2
|
+
// Users are free to edit the file afterwards; we only write it if it's missing.
|
|
3
|
+
export const WIKI_GUIDE_CONTENT = `# Wiki 维护指南(LLM 读这个)
|
|
4
|
+
|
|
5
|
+
## 你的职责
|
|
6
|
+
每次被调用时,\`sources/\` 下会有一批新的 todo 素材文件。你的任务是:读完新 sources,把其中可沉淀的知识融入 \`topics/\` / \`projects/\` / \`index.md\`,让 wiki 保持有条理、可检索。
|
|
7
|
+
|
|
8
|
+
## 硬规则
|
|
9
|
+
- \`sources/*.md\` 是输入,**永远不要修改它们**
|
|
10
|
+
- 页面命名:kebab-case,例如 \`topics/cloudbase-cloud-function-deploy.md\`
|
|
11
|
+
- 页面间用相对 markdown 链接互相引用(例如 \`[CloudBase 部署](../topics/cloudbase-cloud-function-deploy.md)\`)
|
|
12
|
+
- 每个页面专注一个主题,不要让单页膨胀到难读
|
|
13
|
+
|
|
14
|
+
## 决策流程
|
|
15
|
+
对每个新 source,问自己:
|
|
16
|
+
1. 这条 todo 揭示了什么**可复用**的知识?(踩过的坑、通用模式、项目结构摘要、外部工具配置)
|
|
17
|
+
2. 对应 topic 页是否已经存在?
|
|
18
|
+
- 存在 → 在合适的段落追加;合并类似条目
|
|
19
|
+
- 不存在 → 新建 topic 页
|
|
20
|
+
3. 这条 todo 有 workDir(项目路径)吗?
|
|
21
|
+
- 有 → 同时更新 \`projects/<projectName>.md\`:项目概述、该项目沉淀过的主要知识点列表(带链接指向 topic)
|
|
22
|
+
4. 如果这条 todo 只是琐碎任务(比如"写邮件"、"买东西"),可以跳过,不强行产出内容
|
|
23
|
+
|
|
24
|
+
## 更新 index.md
|
|
25
|
+
\`index.md\` 是顶级目录。每次都确保:
|
|
26
|
+
- 列出 topics/ 下所有页面(按主题分类)
|
|
27
|
+
- 列出 projects/ 下所有页面
|
|
28
|
+
- 最近 7 天的变更可以用一个 "Recent" 段落点出
|
|
29
|
+
|
|
30
|
+
## 追加 log.md
|
|
31
|
+
最后一步:往 log.md 追加一个 \`## YYYY-MM-DD HH:MM\` 段落,写清楚你这次改了/新增了哪些页,每条一句话。
|
|
32
|
+
|
|
33
|
+
## 语言
|
|
34
|
+
中文优先,代码/命令/路径保留原文。
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
export const EMPTY_INDEX_CONTENT = `# Wiki Index
|
|
38
|
+
|
|
39
|
+
还没有沉淀任何主题。去 AgentQuad 里点「沉淀到记忆」按钮开始。
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
export const EMPTY_LOG_CONTENT = `# Wiki 更新日志
|
|
43
|
+
|
|
44
|
+
`
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
2
|
+
import { execFile, spawn } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { WIKI_GUIDE_CONTENT, EMPTY_INDEX_CONTENT, EMPTY_LOG_CONTENT } from './guide.js'
|
|
6
|
+
import { loadTranscript as defaultLoadTranscript } from '../transcript.js'
|
|
7
|
+
import { summarizeTurns as defaultSummarize } from '../summarize.js'
|
|
8
|
+
import { redact as defaultRedact } from './redact.js'
|
|
9
|
+
import { buildSourceMarkdown, sourceFileName } from './sources.js'
|
|
10
|
+
|
|
11
|
+
const execFileP = promisify(execFile)
|
|
12
|
+
|
|
13
|
+
function isGitRepo(dir) { return existsSync(join(dir, '.git')) }
|
|
14
|
+
function isNonEmptyDir(dir) {
|
|
15
|
+
if (!existsSync(dir)) return false
|
|
16
|
+
try { return readdirSync(dir).length > 0 } catch { return false }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function gitInit(wikiDir) {
|
|
20
|
+
await execFileP('git', ['init', '-q'], { cwd: wikiDir })
|
|
21
|
+
await execFileP('git', ['add', '-A'], { cwd: wikiDir })
|
|
22
|
+
try {
|
|
23
|
+
await execFileP('git', ['commit', '-q', '-m', 'wiki: initial commit'], { cwd: wikiDir })
|
|
24
|
+
} catch {
|
|
25
|
+
await execFileP(
|
|
26
|
+
'git',
|
|
27
|
+
['-c', 'user.email=agentquad@local', '-c', 'user.name=agentquad', 'commit', '-q', '-m', 'wiki: initial commit'],
|
|
28
|
+
{ cwd: wikiDir },
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function gitCommit(wikiDir, message) {
|
|
34
|
+
await execFileP('git', ['add', '-A'], { cwd: wikiDir })
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execFileP('git', ['status', '--porcelain'], { cwd: wikiDir })
|
|
37
|
+
if (!stdout.trim()) return { committed: false }
|
|
38
|
+
} catch {}
|
|
39
|
+
try {
|
|
40
|
+
await execFileP('git', ['commit', '-q', '-m', message], { cwd: wikiDir })
|
|
41
|
+
} catch {
|
|
42
|
+
await execFileP(
|
|
43
|
+
'git',
|
|
44
|
+
['-c', 'user.email=agentquad@local', '-c', 'user.name=agentquad', 'commit', '-q', '-m', message],
|
|
45
|
+
{ cwd: wikiDir },
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return { committed: true }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultExecClaude({ command, bin, args = [], cwd, stdin, timeoutMs }) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const cmd = bin || command
|
|
54
|
+
const child = spawn(cmd, [...args, '-p', '--output-format', 'text'], {
|
|
55
|
+
cwd, stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
})
|
|
57
|
+
let stdout = ''
|
|
58
|
+
let stderr = ''
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
try { child.kill('SIGTERM') } catch {}
|
|
61
|
+
reject(new Error(`claude timeout after ${timeoutMs}ms`))
|
|
62
|
+
}, timeoutMs)
|
|
63
|
+
child.stdout.on('data', d => { stdout += d.toString() })
|
|
64
|
+
child.stderr.on('data', d => { stderr += d.toString() })
|
|
65
|
+
child.on('error', e => { clearTimeout(timer); reject(e) })
|
|
66
|
+
child.on('close', code => { clearTimeout(timer); resolve({ exitCode: code, stdout, stderr }) })
|
|
67
|
+
if (stdin != null) {
|
|
68
|
+
child.stdin.write(stdin)
|
|
69
|
+
child.stdin.end()
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildClaudePrompt(newSourceFiles) {
|
|
75
|
+
const list = newSourceFiles.map(f => `- sources/${f}`).join('\n')
|
|
76
|
+
return `请严格按照 WIKI_GUIDE.md 的规则维护本 wiki。先读 WIKI_GUIDE.md,再读下面这批新的 sources,然后更新 topics/ projects/ index.md 和 log.md:
|
|
77
|
+
|
|
78
|
+
${list}
|
|
79
|
+
|
|
80
|
+
约束重申:
|
|
81
|
+
- 不要修改 sources/*.md
|
|
82
|
+
- 只产出 markdown 文件修改,不要输出总结到终端`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createWikiService({
|
|
86
|
+
db,
|
|
87
|
+
logDir,
|
|
88
|
+
wikiDir,
|
|
89
|
+
getTools,
|
|
90
|
+
maxTailTurns = 20,
|
|
91
|
+
timeoutMs = 600_000,
|
|
92
|
+
redactEnabled = true,
|
|
93
|
+
loadTranscript = (session) => defaultLoadTranscript({
|
|
94
|
+
tool: session.tool,
|
|
95
|
+
nativeSessionId: session.nativeSessionId,
|
|
96
|
+
cwd: session.cwd || null,
|
|
97
|
+
sessionId: session.sessionId,
|
|
98
|
+
logDir,
|
|
99
|
+
}),
|
|
100
|
+
summarize = defaultSummarize,
|
|
101
|
+
execClaude = null,
|
|
102
|
+
}) {
|
|
103
|
+
let running = false
|
|
104
|
+
let lastInitState = 'unknown'
|
|
105
|
+
|
|
106
|
+
async function init() {
|
|
107
|
+
if (existsSync(wikiDir) && isNonEmptyDir(wikiDir) && !isGitRepo(wikiDir)) {
|
|
108
|
+
lastInitState = 'exists-not-git'
|
|
109
|
+
return { state: 'exists-not-git', wikiDir }
|
|
110
|
+
}
|
|
111
|
+
if (!existsSync(wikiDir)) mkdirSync(wikiDir, { recursive: true })
|
|
112
|
+
mkdirSync(join(wikiDir, 'sources'), { recursive: true })
|
|
113
|
+
mkdirSync(join(wikiDir, 'topics'), { recursive: true })
|
|
114
|
+
mkdirSync(join(wikiDir, 'projects'), { recursive: true })
|
|
115
|
+
|
|
116
|
+
const guidePath = join(wikiDir, 'WIKI_GUIDE.md')
|
|
117
|
+
if (!existsSync(guidePath)) writeFileSync(guidePath, WIKI_GUIDE_CONTENT)
|
|
118
|
+
if (!existsSync(join(wikiDir, 'index.md'))) writeFileSync(join(wikiDir, 'index.md'), EMPTY_INDEX_CONTENT)
|
|
119
|
+
if (!existsSync(join(wikiDir, 'log.md'))) writeFileSync(join(wikiDir, 'log.md'), EMPTY_LOG_CONTENT)
|
|
120
|
+
|
|
121
|
+
if (!isGitRepo(wikiDir)) {
|
|
122
|
+
try { await gitInit(wikiDir) } catch (e) {
|
|
123
|
+
lastInitState = 'git-failed'
|
|
124
|
+
return { state: 'git-failed', wikiDir, error: e.message }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
lastInitState = 'ready'
|
|
128
|
+
return { state: 'ready', wikiDir }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function status() {
|
|
132
|
+
const runs = db.listWikiRuns({ limit: 1 })
|
|
133
|
+
return {
|
|
134
|
+
wikiDir,
|
|
135
|
+
initState: lastInitState,
|
|
136
|
+
lastRun: runs[0] || null,
|
|
137
|
+
pendingTodoCount: db.listUnappliedDoneTodos().length,
|
|
138
|
+
running,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function pending() {
|
|
143
|
+
return db.listUnappliedDoneTodos().map(t => ({
|
|
144
|
+
id: t.id, title: t.title, workDir: t.workDir,
|
|
145
|
+
quadrant: t.quadrant, completedAt: t.updatedAt,
|
|
146
|
+
}))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runOnce({ todoIds, dryRun = false } = {}) {
|
|
150
|
+
if (!Array.isArray(todoIds) || todoIds.length === 0) {
|
|
151
|
+
throw new Error('todoIds must be a non-empty array')
|
|
152
|
+
}
|
|
153
|
+
if (running) throw new Error('wiki run already running')
|
|
154
|
+
running = true
|
|
155
|
+
const run = db.createWikiRun({ todoCount: todoIds.length, dryRun: dryRun ? 1 : 0 })
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const writtenFiles = []
|
|
159
|
+
for (const todoId of todoIds) {
|
|
160
|
+
const todo = db.getTodo(todoId)
|
|
161
|
+
if (!todo) throw new Error(`todo_not_found: ${todoId}`)
|
|
162
|
+
const comments = db.listComments(todoId)
|
|
163
|
+
const redactFn = redactEnabled ? defaultRedact : (s) => String(s ?? '')
|
|
164
|
+
const md = await buildSourceMarkdown({
|
|
165
|
+
todo, comments,
|
|
166
|
+
loadTranscript,
|
|
167
|
+
summarize,
|
|
168
|
+
redact: redactFn,
|
|
169
|
+
maxTailTurns,
|
|
170
|
+
})
|
|
171
|
+
const filename = sourceFileName(todo)
|
|
172
|
+
const abs = join(wikiDir, 'sources', filename)
|
|
173
|
+
writeFileSync(abs, md)
|
|
174
|
+
writtenFiles.push(filename)
|
|
175
|
+
db.upsertWikiCoverage(run.id, todoId, `sources/${filename}`, false)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (dryRun) {
|
|
179
|
+
db.completeWikiRun(run.id, { exitCode: 0, note: `dry-run: ${writtenFiles.length} sources` })
|
|
180
|
+
appendFileSync(join(wikiDir, 'log.md'),
|
|
181
|
+
`\n- [${new Date().toISOString()}] dry-run, wrote ${writtenFiles.length} source(s)\n`)
|
|
182
|
+
return { dryRun: true, runId: run.id, sourcesWritten: writtenFiles.length, exitCode: 0 }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const tools = getTools()
|
|
186
|
+
const tool = tools.claude || {}
|
|
187
|
+
const runner = execClaude || defaultExecClaude
|
|
188
|
+
const prompt = buildClaudePrompt(writtenFiles)
|
|
189
|
+
const result = await runner({
|
|
190
|
+
command: tool.command || 'claude',
|
|
191
|
+
bin: tool.bin,
|
|
192
|
+
args: tool.args || [],
|
|
193
|
+
cwd: wikiDir,
|
|
194
|
+
stdin: prompt,
|
|
195
|
+
timeoutMs,
|
|
196
|
+
})
|
|
197
|
+
if (result.exitCode !== 0) {
|
|
198
|
+
throw new Error(`claude exited ${result.exitCode}: ${String(result.stderr || '').slice(0, 400)}`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const now = new Date()
|
|
202
|
+
const tag = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}`
|
|
203
|
+
await gitCommit(wikiDir, `wiki: batch ${tag} (${todoIds.length} todos)`)
|
|
204
|
+
|
|
205
|
+
db.markCoverageApplied(run.id)
|
|
206
|
+
db.completeWikiRun(run.id, { exitCode: 0, note: `batch: ${writtenFiles.length} sources` })
|
|
207
|
+
appendFileSync(join(wikiDir, 'log.md'),
|
|
208
|
+
`\n- [${now.toISOString()}] batch run #${run.id}: ${writtenFiles.length} source(s), exit 0\n`)
|
|
209
|
+
|
|
210
|
+
return { dryRun: false, runId: run.id, sourcesWritten: writtenFiles.length, exitCode: 0 }
|
|
211
|
+
} catch (e) {
|
|
212
|
+
db.failWikiRun(run.id, e.message)
|
|
213
|
+
throw e
|
|
214
|
+
} finally {
|
|
215
|
+
running = false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function markOrphansAsFailed() {
|
|
220
|
+
let n = 0
|
|
221
|
+
for (const orphan of db.findOrphanWikiRuns()) {
|
|
222
|
+
db.failWikiRun(orphan.id, 'AgentQuad process died mid-run')
|
|
223
|
+
n++
|
|
224
|
+
}
|
|
225
|
+
return n
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
init, status, pending, runOnce, markOrphansAsFailed,
|
|
230
|
+
listRuns: (limit = 20) => db.listWikiRuns({ limit }),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// API key / secret redaction for wiki source markdown.
|
|
2
|
+
// Catches obvious leak patterns; not a security guarantee — user should never
|
|
3
|
+
// paste real production secrets into todo descriptions, this is a seatbelt.
|
|
4
|
+
|
|
5
|
+
const PATTERNS = [
|
|
6
|
+
// Anthropic / OpenAI sk- style keys
|
|
7
|
+
/\bsk-[A-Za-z0-9_\-]{20,}\b/g,
|
|
8
|
+
// AWS access key id
|
|
9
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
10
|
+
// GitHub tokens (personal, oauth, server-to-server, refresh)
|
|
11
|
+
/\bgh[pousr]_[A-Za-z0-9]{30,}\b/g,
|
|
12
|
+
// Google API key
|
|
13
|
+
/\bAIza[0-9A-Za-z_\-]{30,}\b/g,
|
|
14
|
+
// Slack tokens
|
|
15
|
+
/\bxox[baprs]-[A-Za-z0-9\-]{10,}\b/g,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
// env-style SECRET_KEY=..., API_TOKEN=..., etc. Replace value but keep key.
|
|
19
|
+
const ENV_LINE = /\b([A-Z][A-Z0-9_]{2,}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD))\s*=\s*\S+/g
|
|
20
|
+
|
|
21
|
+
// inline key: "value", api_key: 'value', password = "value"
|
|
22
|
+
const INLINE_KV = /\b(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|token)\b\s*[:=]\s*['"]?[^\s'",}]{6,}/gi
|
|
23
|
+
|
|
24
|
+
export function redact(input) {
|
|
25
|
+
if (input == null) return ''
|
|
26
|
+
let s = typeof input === 'string' ? input : String(input)
|
|
27
|
+
for (const re of PATTERNS) s = s.replace(re, '[REDACTED]')
|
|
28
|
+
s = s.replace(ENV_LINE, (_, key) => `${key}=[REDACTED]`)
|
|
29
|
+
s = s.replace(INLINE_KV, (match, key) => {
|
|
30
|
+
const sep = match.includes(':') ? ':' : '='
|
|
31
|
+
return `${key}${sep} [REDACTED]`
|
|
32
|
+
})
|
|
33
|
+
return s
|
|
34
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function pad(n) { return String(n).padStart(2, '0') }
|
|
2
|
+
|
|
3
|
+
function toDate(ts) {
|
|
4
|
+
const d = new Date(ts)
|
|
5
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toDateTime(ts) {
|
|
9
|
+
const d = new Date(ts)
|
|
10
|
+
return `${toDate(ts)} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function shortId(id) {
|
|
14
|
+
return String(id).slice(0, 8)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hoursBetween(startMs, endMs) {
|
|
18
|
+
if (!startMs || !endMs) return null
|
|
19
|
+
return +((endMs - startMs) / 3_600_000).toFixed(2)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sourceFileName(todo, nowMs = Date.now()) {
|
|
23
|
+
return `${toDate(nowMs)}-${shortId(todo.id)}.md`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderTurn(turn) {
|
|
27
|
+
const roleMap = {
|
|
28
|
+
user: '用户',
|
|
29
|
+
assistant: 'AI',
|
|
30
|
+
thinking: '思考',
|
|
31
|
+
tool_use: `工具调用(${turn.toolName || ''})`,
|
|
32
|
+
tool_result: '工具输出',
|
|
33
|
+
raw: '原始',
|
|
34
|
+
}
|
|
35
|
+
const role = roleMap[turn.role] || turn.role
|
|
36
|
+
const content = String(turn.content || '')
|
|
37
|
+
return `【${role}】${content}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function buildSourceMarkdown({
|
|
41
|
+
todo,
|
|
42
|
+
comments = [],
|
|
43
|
+
loadTranscript,
|
|
44
|
+
summarize,
|
|
45
|
+
redact,
|
|
46
|
+
maxTailTurns = 20,
|
|
47
|
+
maxBytes = 128 * 1024,
|
|
48
|
+
now = Date.now(),
|
|
49
|
+
}) {
|
|
50
|
+
const lines = []
|
|
51
|
+
|
|
52
|
+
const duration = hoursBetween(todo.createdAt, todo.updatedAt)
|
|
53
|
+
lines.push('---')
|
|
54
|
+
lines.push(`todoId: ${todo.id}`)
|
|
55
|
+
lines.push(`title: ${todo.title.replace(/\n/g, ' ')}`)
|
|
56
|
+
lines.push(`quadrant: ${todo.quadrant}`)
|
|
57
|
+
lines.push(`workDir: ${todo.workDir || '-'}`)
|
|
58
|
+
lines.push(`createdAt: ${new Date(todo.createdAt).toISOString()}`)
|
|
59
|
+
lines.push(`completedAt: ${new Date(todo.updatedAt).toISOString()}`)
|
|
60
|
+
if (duration != null) lines.push(`durationHours: ${duration}`)
|
|
61
|
+
lines.push('---')
|
|
62
|
+
lines.push('')
|
|
63
|
+
lines.push(`# ${todo.title}`)
|
|
64
|
+
lines.push('')
|
|
65
|
+
|
|
66
|
+
if (todo.description && todo.description.trim()) {
|
|
67
|
+
lines.push('## 描述')
|
|
68
|
+
lines.push(redact(todo.description))
|
|
69
|
+
lines.push('')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (comments.length) {
|
|
73
|
+
lines.push(`## 评论(${comments.length})`)
|
|
74
|
+
for (const c of comments) {
|
|
75
|
+
lines.push(`- [${toDateTime(c.createdAt)}] ${redact(c.content)}`)
|
|
76
|
+
}
|
|
77
|
+
lines.push('')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sessions = Array.isArray(todo.aiSessions) ? todo.aiSessions : []
|
|
81
|
+
if (sessions.length) {
|
|
82
|
+
lines.push('## AI 会话')
|
|
83
|
+
let idx = 0
|
|
84
|
+
for (const s of sessions) {
|
|
85
|
+
idx += 1
|
|
86
|
+
const parsed = (await loadTranscript(s)) || { source: 'empty', turns: [] }
|
|
87
|
+
const turns = Array.isArray(parsed.turns) ? parsed.turns : []
|
|
88
|
+
const completed = s.completedAt ? toDateTime(s.completedAt) : '-'
|
|
89
|
+
lines.push(`### Session ${idx} — ${s.tool}(${turns.length} 轮,完成时间 ${completed})`)
|
|
90
|
+
|
|
91
|
+
let summary = ''
|
|
92
|
+
if (turns.length) {
|
|
93
|
+
try {
|
|
94
|
+
summary = await summarize(turns, { tool: s.tool })
|
|
95
|
+
} catch (e) {
|
|
96
|
+
summary = `(摘要失败:${e.message})`
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (summary) {
|
|
100
|
+
lines.push(`**摘要**:${redact(summary)}`)
|
|
101
|
+
lines.push('')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tail = turns.slice(-maxTailTurns)
|
|
105
|
+
if (tail.length) {
|
|
106
|
+
lines.push(`**最后 ${tail.length} 轮原文**:`)
|
|
107
|
+
lines.push('')
|
|
108
|
+
for (const t of tail) {
|
|
109
|
+
lines.push(redact(renderTurn(t)))
|
|
110
|
+
lines.push('')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let out = lines.join('\n')
|
|
117
|
+
if (Buffer.byteLength(out, 'utf8') > maxBytes) {
|
|
118
|
+
const head = out.slice(0, maxBytes - 200)
|
|
119
|
+
out = `${head}\n\n...(内容过长已截断,原始 transcript 保留在本地)...\n`
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|