annotatable-html 0.0.0-stage → 1.45.3
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 +352 -2
- package/agent/annot_answer.js +7 -0
- package/agent/annot_edit.js +6 -0
- package/agent/annot_poll.js +19 -0
- package/agent/annot_read.js +5 -0
- package/agent/annot_reply.js +50 -0
- package/bin/cli.js +323 -0
- package/lib/claude.js +242 -0
- package/lib/codex-desktop.js +130 -0
- package/lib/codex-headless.js +129 -0
- package/lib/codex.js +216 -0
- package/lib/core.js +2206 -0
- package/lib/document.js +56 -0
- package/lib/duty-process.js +190 -0
- package/lib/duty-worker.js +5 -0
- package/lib/duty.js +334 -0
- package/lib/lang-en.js +462 -0
- package/lib/opencode.js +195 -0
- package/lib/poll-process.js +42 -0
- package/lib/serve.js +318 -0
- package/lib/vendor/marked.cjs +2811 -0
- package/lib/vendor/temml.cjs +14710 -0
- package/package.json +51 -6
- package/templates/SKILL.md +53 -0
- package/templates/annotatable-doc.html +5702 -0
- package/templates/annotatable.theme.css +28 -0
- package/templates/commands/html4agent.md +6 -0
- package/templates/opencode/annotatable-html.js +12 -0
- package/templates/references/compatibility.md +14 -0
- package/templates/references/hosts.md +47 -0
- package/templates/references/operations.md +37 -0
- package/templates/theme-studio.html +330 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/* annotatable-html 命令行入口
|
|
5
|
+
* annotatable init [dir] 导出目录内工具、模板与主题
|
|
6
|
+
* annotatable new <名称.html> [--template 路径]
|
|
7
|
+
* 新建可批注文档(自动应用目录内主题/自定义模板)
|
|
8
|
+
* annotatable md <文件.md> [-o 输出.html] [--title 标题] [--template 路径]
|
|
9
|
+
* 一键 Markdown → 可批注 HTML(正文遵照原文,仅加批注层)
|
|
10
|
+
* annotatable theme 导出默认主题文件(annotatable.theme.css)供微调
|
|
11
|
+
* annotatable studio [--port 端口] 打开可视化主题工坊(配色/字体/字号/页宽,实时预览)
|
|
12
|
+
* annotatable template [名称.html] 导出内置模板供深度改造
|
|
13
|
+
* annotatable template --use <文件.html> 把你的 HTML 一键设为全局默认模板(整页保留原样式)
|
|
14
|
+
* annotatable template --reset 撤销全局默认模板,恢复内置
|
|
15
|
+
* annotatable read <file.html> 列出待回应批注
|
|
16
|
+
* annotatable reply <file.html> <replies.json>
|
|
17
|
+
* 写回 AI 回复(追加ai轮次+快照+rev+1)
|
|
18
|
+
* annotatable poll <file.html> [--timeout 秒] [--interval 秒] [--exit-code N] [--exit-code-timeout N]
|
|
19
|
+
* 阻塞等待新批注(配后台任务:出批注即退出唤醒;退出码按 harness 选——zcode 默认 0/3,Claude Code 等非零才唤醒 → --exit-code 1)
|
|
20
|
+
* annotatable poll <file.html> --codex 用官方会话接口通知当前 Codex(Node.js 22+)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
var path = require('path');
|
|
24
|
+
var core = require('../lib/core.js');
|
|
25
|
+
|
|
26
|
+
var args = process.argv.slice(2);
|
|
27
|
+
/* 短旗标 -o 等价 --out。 */
|
|
28
|
+
args = args.map(function (a) { return a === '-o' ? '--out' : a; });
|
|
29
|
+
var cmd = args[0] || 'help';
|
|
30
|
+
var positional = [], flags = {};
|
|
31
|
+
var boolFlags = { zcode: true, claude: true, codex: true, opencode: true, reset: true, stdin: true, go: true, force: true, status: true, stop: true, listen: true, hook: true };
|
|
32
|
+
for (var i = 1; i < args.length; i++) {
|
|
33
|
+
if (args[i].indexOf('--') === 0) {
|
|
34
|
+
var k = args[i].slice(2);
|
|
35
|
+
if (boolFlags[k]) { flags[k] = true; }
|
|
36
|
+
else if (args[i + 1] && args[i + 1].indexOf('--') !== 0) { flags[k] = args[i + 1]; i++; } // 下一个是旗标时不当成值(--stream --id x)
|
|
37
|
+
else { flags[k] = ''; }
|
|
38
|
+
} else positional.push(args[i]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* 长文本(流式段落)经管道进:annotatable reply f.html --stream --stdin < 段落.txt */
|
|
42
|
+
function readStdinSync() {
|
|
43
|
+
try { return require('fs').readFileSync(0, 'utf8'); } catch (e) { return ''; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function usage() {
|
|
47
|
+
console.log([
|
|
48
|
+
'annotatable-html — 可批注 HTML:人类划线批注,AI agent 读批注回答',
|
|
49
|
+
'',
|
|
50
|
+
'用法:',
|
|
51
|
+
' annotatable init [目录] 导出 agent 工具、模板与主题;批注协议通过 skill 安装',
|
|
52
|
+
' annotatable new <名称.html> [--template 路径] [--lang en]',
|
|
53
|
+
' 新建可批注文档(自动应用主题/自定义模板)',
|
|
54
|
+
' annotatable md <文件.md> [-o 输出.html] [--title 标题] [--lang en]',
|
|
55
|
+
' 一键 Markdown → 可批注 HTML(正文遵照原文,仅加批注层)',
|
|
56
|
+
' annotatable theme 导出主题文件 annotatable.theme.css 供微调',
|
|
57
|
+
' annotatable studio [--port 端口] 打开可视化主题工坊(配色/字体/字号/页宽,实时预览)',
|
|
58
|
+
' annotatable template [名称.html] 导出内置模板供深度改造',
|
|
59
|
+
' annotatable template --use <文件.html> 把你的 HTML 一键设为全局默认模板(整页保留原样式)',
|
|
60
|
+
' annotatable template --reset 撤销全局默认模板,恢复内置',
|
|
61
|
+
' annotatable skill [--zcode] [--claude] [--codex] [--opencode] 安装 skill(v1.40.0 默认投主流四家 harness',
|
|
62
|
+
' 的 skill 目录;slash 命令仅 zcode/claude)+ /html4agent 命令(任意目录触发)',
|
|
63
|
+
' annotatable serve [目录] [--port N] 启动本地服务器(全自动保存,全浏览器支持;--port 0=系统挑空闲端口)',
|
|
64
|
+
' annotatable inject <file.html> 给任意 HTML 注入批注层',
|
|
65
|
+
' annotatable regen <活页.html> <图纸.html> [--go] 图纸页安全重建:报表手改清单,--go 上盘(store/DOC_ID 保留)',
|
|
66
|
+
' annotatable upgrade <文件或目录> 旧层页批量静默换新层(read/reply/poll/serve 触达时也会自动做)',
|
|
67
|
+
' annotatable read <file.html> 列出待回应批注(含对话线+结构体检)',
|
|
68
|
+
' annotatable reply <file.html> <replies.json> 写回 AI 回复(json 是文件路径,非内联):[{"id":"..","reply":".."}]',
|
|
69
|
+
' annotatable reply <file.html> --stream \'<段落>\' --id <批注id>',
|
|
70
|
+
' 流式追加一段回答(值守协议;不带 --id 自动接最近打开的流式轮;退出码 4=被打断 5=目标已删 6=人类编辑中,让路 7=没有打开中的流式轮)',
|
|
71
|
+
' annotatable reply <file.html> --stream-end \'<全文>\' [--id <批注id>]',
|
|
72
|
+
' 收尾流式回答(全文替换段落累计,落快照)',
|
|
73
|
+
' annotatable edit <file.html> <edits.json> 按批注指令改正文(json 是文件路径):[{"id":"..","newText":".."}]',
|
|
74
|
+
' annotatable answer <file.html> [json] 兼容既有 answers-store 数据(显式调用;非默认回复流程)',
|
|
75
|
+
' annotatable poll <file.html> [--timeout 秒] [--interval 秒] [--exit-code N] [--exit-code-timeout N]',
|
|
76
|
+
' 阻塞等待新批注:有新批注打印 POLL_HIT 并退出唤醒 agent(默认命中 0/超时 3;',
|
|
77
|
+
' Claude Code 等非零才唤醒的 harness → --exit-code 1,超时轮空不叫人再加 --exit-code-timeout 0),',
|
|
78
|
+
' annotatable poll <file.html> --codex [--codex-thread ID] [--codex-bin 原生CLI路径]',
|
|
79
|
+
' 默认取 CODEX_THREAD_ID;桌面会话优先使用继承的本机工具连接,',
|
|
80
|
+
' --codex-transport auto|desktop|app-server 选择入口;--codex-turn ID 指定来源轮次',
|
|
81
|
+
' annotatable poll <file.html> --opencode [--opencode-url URL] [--opencode-session ID]',
|
|
82
|
+
' OpenCode 异步会话通知;上下文插件自动传递地址和会话 ID',
|
|
83
|
+
' annotatable duty <file.html> --codex|--claude|--opencode [--port N] 注册原会话自动续接(Windows、Node.js 22+)',
|
|
84
|
+
' 原宿主在线通知,确认退出后才无头回复;输出保存服务的页面地址',
|
|
85
|
+
' Claude 用原生后台任务启动;可加 --claude-session ID、--claude-bin 路径',
|
|
86
|
+
' Claude 的项目启动/结束钩子自动接回通知,重开原会话无需手动重挂',
|
|
87
|
+
' annotatable duty <file.html> --status 查看注册、处理者与待办状态,不启动模型',
|
|
88
|
+
' annotatable duty <file.html> --stop 停止续接;已有无头轮次完成后退出',
|
|
89
|
+
'',
|
|
90
|
+
'图纸页维护:纯文字改动直接 edit/手改,别整页重生成;必须重生成(改图/大改版)时走 regen:',
|
|
91
|
+
'先报表(默认),把手改同步进图纸后 --go 上盘。人类编辑模式中,回复退出码 6、regen 退出码 3——让路等收笔。',
|
|
92
|
+
'read/reply/edit/poll 可使用本机 localhost 页面地址;需要新版 annotatable serve 正在运行。',
|
|
93
|
+
'',
|
|
94
|
+
'典型循环: 人类在浏览器批注 → AI 运行 read → 生成 replies.json → 运行 reply → 人类刷新可见。'
|
|
95
|
+
].join('\n'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function runCommand() {
|
|
99
|
+
try {
|
|
100
|
+
/* skill 副本过期自检(skill 命令自身就会刷新升级,不再提示) */
|
|
101
|
+
if (!flags.hook && ['init', 'new', 'md', 'theme', 'template', 'studio', 'inject', 'upgrade', 'regen', 'read', 'reply', 'edit', 'answer', 'poll', 'duty', 'serve'].indexOf(cmd) !== -1) {
|
|
102
|
+
var notice = core.staleSkillNotice(path.join(__dirname, '..'));
|
|
103
|
+
if (notice) (cmd === 'duty' && flags.claude && !flags.status && !flags.stop ? console.error : console.log)(notice);
|
|
104
|
+
}
|
|
105
|
+
if (cmd === 'init') {
|
|
106
|
+
if (Object.keys(flags).some(function (flag) { return /^agents-md(?:=|$)/.test(flag); })) {
|
|
107
|
+
throw new Error('init 不再写入 AGENTS.md;请运行 annotatable skill 安装批注工作流。');
|
|
108
|
+
}
|
|
109
|
+
core.initKit(positional[0] || process.cwd(), path.join(__dirname, '..'));
|
|
110
|
+
} else if (cmd === 'new') {
|
|
111
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
112
|
+
core.newDoc(process.cwd(), positional[0], path.join(__dirname, '..'), { template: flags.template, lang: flags.lang });
|
|
113
|
+
} else if (cmd === 'md') {
|
|
114
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
115
|
+
core.mdDoc(positional[0], { out: flags.out, title: flags.title, template: flags.template, lang: flags.lang }, path.join(__dirname, '..'));
|
|
116
|
+
} else if (cmd === 'theme') {
|
|
117
|
+
core.exportTheme(process.cwd(), path.join(__dirname, '..'));
|
|
118
|
+
} else if (cmd === 'template') {
|
|
119
|
+
if (flags.use) {
|
|
120
|
+
core.templateUse(flags.use, path.join(__dirname, '..'), {});
|
|
121
|
+
} else if (flags.reset) {
|
|
122
|
+
core.templateReset({});
|
|
123
|
+
} else {
|
|
124
|
+
core.exportTemplate(process.cwd(), positional[0], path.join(__dirname, '..'));
|
|
125
|
+
}
|
|
126
|
+
} else if (cmd === 'studio') {
|
|
127
|
+
var sServe = require('../lib/serve.js');
|
|
128
|
+
var sPort = parseInt(flags.port, 10);
|
|
129
|
+
if (isNaN(sPort)) sPort = 8974;
|
|
130
|
+
var sPkg = path.join(__dirname, '..');
|
|
131
|
+
sServe.start(process.cwd(), sPort, { studioProvider: function () { return core.studioHtml(sPkg); } })
|
|
132
|
+
.then(function (info) {
|
|
133
|
+
var url = info.url + '/__annot/studio';
|
|
134
|
+
console.log('🎨 主题工坊已启动: ' + url);
|
|
135
|
+
console.log(' 左侧调配色/字体/字号/页宽,右侧实时预览(批注、导航窗格可直接试)。');
|
|
136
|
+
console.log(' 调好后点「保存到当前目录」或「设为全局默认」,之后 new / md 自动应用。按 Ctrl+C 停止。');
|
|
137
|
+
try {
|
|
138
|
+
if (!process.env.ANNOT_NO_OPEN) { // e2e 等无人值守场景置 1 跳过自动开浏览器
|
|
139
|
+
var opener = process.platform === 'win32' ? 'start "" "' + url + '"'
|
|
140
|
+
: process.platform === 'darwin' ? 'open "' + url + '"' : 'xdg-open "' + url + '"';
|
|
141
|
+
require('child_process').exec(opener, function () { /* 打不开就手动复制地址 */ });
|
|
142
|
+
}
|
|
143
|
+
} catch (e) { /* 同上 */ }
|
|
144
|
+
});
|
|
145
|
+
} else if (cmd === 'skill') {
|
|
146
|
+
core.installSkill({ zcode: flags.zcode, claude: flags.claude, codex: flags.codex, opencode: flags.opencode }, path.join(__dirname, '..'), flags.dir);
|
|
147
|
+
} else if (cmd === 'serve') {
|
|
148
|
+
var fs = require('fs');
|
|
149
|
+
var serve = require('../lib/serve.js');
|
|
150
|
+
var dir = positional[0] || process.cwd();
|
|
151
|
+
var port = parseInt(flags.port, 10);
|
|
152
|
+
if (isNaN(port)) port = 8973; // --port 0 = 系统挑空闲端口(合法值,不能用 || 吞掉)
|
|
153
|
+
serve.start(dir, port).then(function (info) {
|
|
154
|
+
console.log('可批注文档服务器已启动: ' + info.url);
|
|
155
|
+
console.log('伺服目录: ' + info.root);
|
|
156
|
+
core.upgradeDir(info.root); // v1.36.0 开服即扫目录:旧层页静默换新层(有升级会各打一行)
|
|
157
|
+
try {
|
|
158
|
+
fs.readdirSync(info.root).forEach(function (f) {
|
|
159
|
+
if (/\.html?$/i.test(f)) console.log(' 文档 → ' + info.url + '/' + encodeURIComponent(f));
|
|
160
|
+
});
|
|
161
|
+
} catch (e) { /* 目录尚为空或未创建,浏览器访问时再列 */ }
|
|
162
|
+
console.log('服务器模式:批注自动保存进文件,无需绑定/授权。按 Ctrl+C 停止。');
|
|
163
|
+
});
|
|
164
|
+
} else if (cmd === 'inject') {
|
|
165
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
166
|
+
core.injectHtml(positional[0], path.join(__dirname, '..'), { lang: flags.lang });
|
|
167
|
+
} else if (cmd === 'upgrade') {
|
|
168
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
169
|
+
var fsU = require('fs');
|
|
170
|
+
var upT = path.resolve(positional[0]);
|
|
171
|
+
var upIsDir;
|
|
172
|
+
try { upIsDir = fsU.statSync(upT).isDirectory(); } catch (e) { throw new Error('路径不存在: ' + upT); }
|
|
173
|
+
if (upIsDir) {
|
|
174
|
+
var upN = core.upgradeDir(upT);
|
|
175
|
+
console.log(upN ? '✅ 静默升级完成:' + upN + ' 个旧层页已换新层。' : '没有需要升级的旧层页(已是最新或不可自动换装)。');
|
|
176
|
+
} else {
|
|
177
|
+
var upR = core.silentUpgrade(upT);
|
|
178
|
+
if (!upR || upR.status === 'skip') console.log('未升级:' + ((upR && upR.note) || '定位失败') + '(正文未动)');
|
|
179
|
+
else if (upR.status === 'current') console.log('✅ 已是当前层 v' + core.pkgVersion(path.join(__dirname, '..')) + ',无需升级');
|
|
180
|
+
else if (upR.status === 'error') console.log('⚠ ' + (upR.note || '升级失败'));
|
|
181
|
+
}
|
|
182
|
+
} else if (cmd === 'regen') {
|
|
183
|
+
if (!positional[0] || !positional[1]) { usage(); process.exit(2); }
|
|
184
|
+
var rg = core.regenDoc(positional[0], positional[1], { go: !!flags.go, force: !!flags.force });
|
|
185
|
+
if (rg.status === 'wordmode') {
|
|
186
|
+
console.log('WORD_MODE 人类编辑中(wordLock 旗鲜活):重生成挂起,等收笔再跑');
|
|
187
|
+
process.exitCode = 3;
|
|
188
|
+
} else if (rg.status === 'manual-edits') {
|
|
189
|
+
console.log('MANUAL_EDITS 盘上有 ' + rg.lost.length + ' 行手改内容是图纸里没有的——直接上盘会盖掉它们:');
|
|
190
|
+
rg.lost.slice(0, 20).forEach(function (l) { console.log(' · ' + l.slice(0, 90)); });
|
|
191
|
+
console.log(' 把手改同步进图纸、重出 fresh 后再 --go;确要丢弃手改加 --force(此报表已记名)');
|
|
192
|
+
process.exitCode = 2;
|
|
193
|
+
} else if (rg.status === 'clean') {
|
|
194
|
+
console.log('CLEAN 图纸与盘面对齐、无手改残留——加 --go 即可安全上盘');
|
|
195
|
+
} else if (rg.status === 'swapped') {
|
|
196
|
+
console.log('✅ 重生成上盘完成:store rev=' + rg.rev + '(' + rg.ann + ' 条批注)逐字节保留 · 备份 ' + rg.bak);
|
|
197
|
+
} else if (rg.status === 'stale' || rg.status === 'error') {
|
|
198
|
+
console.log('⚠ ' + (rg.note || rg.status));
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
} else if (cmd === 'read') {
|
|
202
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
203
|
+
core.readPending(positional[0]);
|
|
204
|
+
} else if (cmd === 'reply') {
|
|
205
|
+
var streaming = flags.stream !== undefined || flags['stream-end'] !== undefined;
|
|
206
|
+
if (positional.length > (streaming ? 1 : 2)) throw new Error('回复参数被拆成多段;长文本请通过 UTF-8 JSON 文件或 --stdin 传入。');
|
|
207
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
208
|
+
var sText, sEnd = false;
|
|
209
|
+
if (flags.stream !== undefined) { sText = flags.stream; }
|
|
210
|
+
else if (flags['stream-end'] !== undefined) { sText = flags['stream-end']; sEnd = true; }
|
|
211
|
+
if (sText !== undefined) {
|
|
212
|
+
if (flags.stdin || sText === '--stdin') sText = readStdinSync();
|
|
213
|
+
var sr = core.streamReply(positional[0], { text: sText, id: flags.id, end: sEnd });
|
|
214
|
+
if (sr.status === 'ok') {
|
|
215
|
+
console.log((sr.done ? '✅ 回答完成' : sr.started ? '▶️ 流式回答开始' : '➕ 已追加段落') + ' · 批注 ' + sr.annId + ' · rev=' + sr.rev);
|
|
216
|
+
if (sr.closed && sr.closed.length) console.log('ℹ️ 顺手封轮(未收尾的遗留流式轮,文本保留): ' + sr.closed.join(', '));
|
|
217
|
+
} else if (sr.status === 'wordmode') {
|
|
218
|
+
console.log('WORD_MODE 人类编辑中(wordLock 旗鲜活):回答挂起不落盘,等收笔再发');
|
|
219
|
+
process.exitCode = 6;
|
|
220
|
+
} else if (sr.status === 'interrupted') {
|
|
221
|
+
console.log('INTERRUPTED 人类已按⏹停止(批注 ' + sr.annId + ' · rev=' + sr.rev + ')——本轮作答终止,不要再追加');
|
|
222
|
+
if (sr.closed && sr.closed.length) console.log('ℹ️ 顺手封轮(未收尾的遗留流式轮,文本保留): ' + sr.closed.join(', '));
|
|
223
|
+
process.exitCode = 4;
|
|
224
|
+
} else if (sr.status === 'target-gone') {
|
|
225
|
+
console.log('TARGET_GONE 批注 ' + sr.annId + ' 已被删除——本轮作答终止');
|
|
226
|
+
process.exitCode = 5;
|
|
227
|
+
} else if (sr.status === 'empty') {
|
|
228
|
+
console.log('空段落:--stream 需要文本或 --stdin');
|
|
229
|
+
process.exitCode = 2;
|
|
230
|
+
} else {
|
|
231
|
+
console.log('NO_STREAM 没有打开中的流式回答——首段请带 --id 批注id(退出码 7)');
|
|
232
|
+
process.exitCode = 7;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
if (!positional[1]) { usage(); process.exit(2); }
|
|
236
|
+
core.applyReplies(positional[0], positional[1]);
|
|
237
|
+
}
|
|
238
|
+
} else if (cmd === 'edit') {
|
|
239
|
+
if (!positional[0] || !positional[1]) { usage(); process.exit(2); }
|
|
240
|
+
core.applyEdits(positional[0], positional[1]);
|
|
241
|
+
} else if (cmd === 'duty') {
|
|
242
|
+
if (flags.hook) {
|
|
243
|
+
if (!flags.claude || flags.codex || flags.opencode || flags.listen || flags.stop || flags.status) throw new Error('--hook 只用于 Claude 原生事件');
|
|
244
|
+
var chunks = [], bytes = 0;
|
|
245
|
+
process.stdin.on('data', function (chunk) { bytes += chunk.length; if (bytes > 1024 * 1024) { console.error('DUTY_ERROR: hook 输入过大'); process.exit(1); } chunks.push(chunk); });
|
|
246
|
+
process.stdin.on('end', function () {
|
|
247
|
+
Promise.resolve().then(function () { return require('../lib/claude').resumeHook(JSON.parse(Buffer.concat(chunks).toString('utf8'))); })
|
|
248
|
+
.catch(function (error) { console.error('DUTY_ERROR: ' + error.message); process.exitCode = 1; });
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
253
|
+
var duty = require('../lib/duty.js');
|
|
254
|
+
if (flags.status) console.log(JSON.stringify(duty.status(positional[0]), null, 2));
|
|
255
|
+
else if (flags.stop) console.log(JSON.stringify(duty.stop(positional[0]), null, 2));
|
|
256
|
+
else {
|
|
257
|
+
var hosts = ['codex', 'claude', 'opencode'].filter(function (name) { return flags[name]; });
|
|
258
|
+
if (hosts.length !== 1) throw new Error('自动续接需显式选择一个宿主:--codex、--claude 或 --opencode');
|
|
259
|
+
var host = hosts[0];
|
|
260
|
+
if (flags.listen && host !== 'claude') throw new Error('--listen 只用于 Claude 的通知任务');
|
|
261
|
+
(flags.listen ? duty.listen : duty.register)(positional[0], { host: host, events: host === 'claude', codexThread: flags['codex-thread'], codexTurn: flags['codex-turn'],
|
|
262
|
+
codexBin: flags['codex-bin'], codexPipe: flags['codex-pipe'], claudeSession: flags['claude-session'], claudeBin: flags['claude-bin'],
|
|
263
|
+
opencodeUrl: flags['opencode-url'], opencodeSession: flags['opencode-session'], opencodeBin: flags['opencode-bin'], port: flags.port, pageContext: flags.dutyContext })
|
|
264
|
+
.then(function (result) {
|
|
265
|
+
if (result.stopped) { console.error('DUTY_STOPPED 用户已停止,不自动重新启动'); return; }
|
|
266
|
+
(host === 'claude' ? console.error : console.log)(host.toUpperCase() + '_DUTY_READY ' + JSON.stringify(result));
|
|
267
|
+
if (host === 'claude' && !result.alreadyListening) return duty.events(positional[0]);
|
|
268
|
+
})
|
|
269
|
+
.catch(function (error) { console.error('DUTY_ERROR: ' + error.message); process.exitCode = 1; });
|
|
270
|
+
}
|
|
271
|
+
} else if (cmd === 'answer') {
|
|
272
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
273
|
+
if (!positional[1]) core.listAnswers(positional[0]); // 旧数据的显式兼容入口
|
|
274
|
+
else core.applyAnswers(positional[0], positional[1]);
|
|
275
|
+
} else if (cmd === 'teach') {
|
|
276
|
+
throw new Error('教学模式已从批注工具移出;请使用独立教学工作流。批注回复使用 annotatable reply。');
|
|
277
|
+
} else if (cmd === 'poll') {
|
|
278
|
+
if (!positional[0]) { usage(); process.exit(2); }
|
|
279
|
+
if (flags.codex && flags.opencode) throw new Error('poll 一次只能选择 Codex 或 OpenCode');
|
|
280
|
+
var pollOpts = {
|
|
281
|
+
timeout: flags.timeout, interval: flags.interval,
|
|
282
|
+
exitCode: parseInt(flags['exit-code'], 10) || 0,
|
|
283
|
+
timeoutExitCode: flags['exit-code-timeout'] !== undefined ? parseInt(flags['exit-code-timeout'], 10) : undefined
|
|
284
|
+
};
|
|
285
|
+
if (flags.codex) {
|
|
286
|
+
pollOpts.codexThread = flags['codex-thread'];
|
|
287
|
+
pollOpts.codexBin = flags['codex-bin'];
|
|
288
|
+
pollOpts.codexTransport = flags['codex-transport'];
|
|
289
|
+
pollOpts.codexTurn = flags['codex-turn'];
|
|
290
|
+
require('../lib/codex.js').pollAnnotations(positional[0], pollOpts).catch(function (err) {
|
|
291
|
+
console.error('CODEX_NOTIFY_ERROR ' + (err.message || err));
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
});
|
|
294
|
+
} else if (flags.opencode) {
|
|
295
|
+
pollOpts.opencodeUrl = flags['opencode-url'];
|
|
296
|
+
pollOpts.opencodeSession = flags['opencode-session'];
|
|
297
|
+
require('../lib/opencode.js').pollAnnotations(positional[0], pollOpts).catch(function (err) {
|
|
298
|
+
console.error('OPENCODE_NOTIFY_ERROR ' + (err.message || err));
|
|
299
|
+
process.exitCode = 1;
|
|
300
|
+
});
|
|
301
|
+
} else core.pollAnnotations(positional[0], pollOpts);
|
|
302
|
+
} else {
|
|
303
|
+
usage();
|
|
304
|
+
}
|
|
305
|
+
} catch (e) {
|
|
306
|
+
console.error('错误: ' + (e.message || e));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (['read', 'reply', 'edit', 'answer', 'poll', 'duty'].indexOf(cmd) !== -1 && /^https?:\/\//i.test(positional[0] || '')) {
|
|
312
|
+
require('../lib/document.js').resolveDocument(positional[0], { includeContext: cmd === 'duty' }).then(function (file) {
|
|
313
|
+
if (cmd === 'duty') {
|
|
314
|
+
flags.dutyContext = file;
|
|
315
|
+
if (flags.port === undefined) flags.port = file.port;
|
|
316
|
+
positional[0] = file.file;
|
|
317
|
+
} else positional[0] = file;
|
|
318
|
+
runCommand();
|
|
319
|
+
}).catch(function (err) {
|
|
320
|
+
console.error('DOCUMENT_RESOLVE_ERROR ' + (err.message || err));
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
});
|
|
323
|
+
} else runCommand();
|
package/lib/claude.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs'), path = require('path'), os = require('os'), util = require('util');
|
|
4
|
+
var processes = require('./duty-process');
|
|
5
|
+
var canonical = value => path.resolve(value).replace(/\\/g, '/').toLowerCase();
|
|
6
|
+
function registrations(home, sessionId) {
|
|
7
|
+
var dir = path.join(home, 'sessions');
|
|
8
|
+
if (!fs.existsSync(dir)) return [];
|
|
9
|
+
return fs.readdirSync(dir).filter(n => /^\d+\.json$/.test(n)).flatMap(n => {
|
|
10
|
+
try { var r = JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8')); return (!sessionId || r.sessionId === sessionId) ? [r] : []; } catch (_) { return []; }
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function liveRegistrations(home, id) {
|
|
14
|
+
var live = [];
|
|
15
|
+
for (var r of registrations(home, id)) {
|
|
16
|
+
var p = await processes.processInfo(r.pid);
|
|
17
|
+
if (p && /^claude(?:\.exe)?$/i.test(p.Name) && Number(p.CreationDate.replace(/\/Date\((\d+)\)\//, '$1')) <= r.startedAt + 2000) live.push({ registration: r, process: p });
|
|
18
|
+
}
|
|
19
|
+
return live;
|
|
20
|
+
}
|
|
21
|
+
function transcript(home, id) {
|
|
22
|
+
var dir = path.join(home, 'projects');
|
|
23
|
+
var found = fs.existsSync(dir) ? fs.readdirSync(dir).map(n => path.join(dir, n, id + '.jsonl')).filter(n => fs.existsSync(n)) : [];
|
|
24
|
+
if (found.length !== 1) throw new Error('无法唯一定位指定 Claude 会话记录');
|
|
25
|
+
return found[0];
|
|
26
|
+
}
|
|
27
|
+
function savedTurn(file, id) {
|
|
28
|
+
var rows = processes.tailRows(file).filter(r => r.sessionId === id && !r.isSidechain);
|
|
29
|
+
var user = rows.findLast(r => r.type === 'user' && r.permissionMode);
|
|
30
|
+
var assistant = rows.findLast(r => r.type === 'assistant' && r.message && r.message.model);
|
|
31
|
+
if (!user || !assistant || !user.cwd) throw new Error('Claude 会话缺少可核对的模型、目录或权限');
|
|
32
|
+
return { model: assistant.message.model, cwd: user.cwd, permissionMode: user.permissionMode };
|
|
33
|
+
}
|
|
34
|
+
function resumeFlags(argv, cwd) {
|
|
35
|
+
var values = ['--allowedTools', '--allowed-tools', '--disallowedTools', '--disallowed-tools', '--add-dir', '--tools', '--settings', '--setting-sources', '--effort', '--agent', '--mcp-config', '--append-system-prompt', '--system-prompt', '--plugin-dir', '--betas'];
|
|
36
|
+
var boolean = ['--strict-mcp-config']; var result = [], files = [];
|
|
37
|
+
for (var i = 1; i < argv.length; i++) {
|
|
38
|
+
if (boolean.includes(argv[i])) { result.push(argv[i]); continue; }
|
|
39
|
+
var flag = argv[i].split('=')[0]; if (!values.includes(flag)) continue;
|
|
40
|
+
var inline = argv[i].includes('='), items = inline ? [argv[i].slice(flag.length + 1)] : [];
|
|
41
|
+
if (!inline && argv[i + 1] && !argv[i + 1].startsWith('-')) items.push(argv[++i]);
|
|
42
|
+
if (['--allowedTools', '--allowed-tools', '--disallowedTools', '--disallowed-tools', '--add-dir', '--tools', '--mcp-config', '--betas'].includes(flag)) {
|
|
43
|
+
while (!inline && argv[i + 1] && !argv[i + 1].startsWith('-')) items.push(argv[++i]);
|
|
44
|
+
}
|
|
45
|
+
if (!items.length) throw new Error('原 Claude 参数缺少值:' + flag);
|
|
46
|
+
if (flag === '--settings') {
|
|
47
|
+
items = items.map(value => {
|
|
48
|
+
if (value.trim().startsWith('{')) {
|
|
49
|
+
var parsed = JSON.parse(value);
|
|
50
|
+
if (parsed.env) {
|
|
51
|
+
if (Object.keys(parsed.env).some(k => process.env[k] !== String(parsed.env[k]))) throw new Error('请在原 Claude 会话内注册,以保留临时环境设置');
|
|
52
|
+
delete parsed.env; // Credentials stay in inherited memory, never in the binding file.
|
|
53
|
+
}
|
|
54
|
+
return JSON.stringify(parsed);
|
|
55
|
+
}
|
|
56
|
+
var file = path.resolve(cwd, value); files.push(file); return file;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (flag === '--mcp-config') items = items.map(value => {
|
|
60
|
+
if (value.trim().startsWith('{')) throw new Error('临时内联 MCP 配置可能含凭据;请使用配置文件后在原会话重新注册');
|
|
61
|
+
var file = path.resolve(cwd, value); files.push(file); return file;
|
|
62
|
+
});
|
|
63
|
+
result.push(flag, ...items);
|
|
64
|
+
}
|
|
65
|
+
return { args: result, files: files };
|
|
66
|
+
}
|
|
67
|
+
function profileFiles(home, cwd, extra) {
|
|
68
|
+
return processes.configStamps(cwd, ['.claude/settings.json', '.claude/settings.local.json'], [path.join(home, 'settings.json')].concat(extra || []));
|
|
69
|
+
}
|
|
70
|
+
function providerContext(env) {
|
|
71
|
+
var base = new URL(env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com');
|
|
72
|
+
if (base.username || base.password || base.search || base.hash) throw new Error('Claude 提供方地址不能含内嵌凭据或查询参数');
|
|
73
|
+
return { baseURL: base.href, bedrock: env.CLAUDE_CODE_USE_BEDROCK || '', vertex: env.CLAUDE_CODE_USE_VERTEX || '',
|
|
74
|
+
foundry: env.CLAUDE_CODE_USE_FOUNDRY || '', region: env.AWS_REGION || env.AWS_DEFAULT_REGION || '',
|
|
75
|
+
vertexProject: env.ANTHROPIC_VERTEX_PROJECT_ID || '', vertexRegion: env.CLOUD_ML_REGION || '' };
|
|
76
|
+
}
|
|
77
|
+
var hookStatus = 'annotatable-html duty listener';
|
|
78
|
+
function hookCommand() {
|
|
79
|
+
return [process.execPath, path.join(__dirname, '../bin/cli.js'), 'duty', '--claude', '--hook'].map(value =>
|
|
80
|
+
"'" + value.replace(/\\/g, '/').replace(/'/g, "'\"'\"'") + "'").join(' ');
|
|
81
|
+
}
|
|
82
|
+
function updateHooks(binding, install) {
|
|
83
|
+
var core = require('./core'), file = path.join(binding.settings.cwd, '.claude', 'settings.local.json');
|
|
84
|
+
return core.withDocLock(file, function () {
|
|
85
|
+
var before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null, config = before ? JSON.parse(before) : {};
|
|
86
|
+
if (!config || Array.isArray(config) || typeof config !== 'object') throw new Error('Claude 项目设置必须为 JSON 对象');
|
|
87
|
+
if (install && (config.disableAllHooks || config.allowManagedHooksOnly)) throw new Error('原项目策略禁用了本地钩子,无法自动接回通知');
|
|
88
|
+
var events = ['SessionStart', 'Stop'], command = hookCommand(), changed = false;
|
|
89
|
+
config.hooks = config.hooks || {};
|
|
90
|
+
for (var event of events) {
|
|
91
|
+
var entries = config.hooks[event] || [], next = [];
|
|
92
|
+
for (var entry of entries) {
|
|
93
|
+
var hooks = entry.hooks.filter(h => !(h.type === 'command' && h.statusMessage === hookStatus));
|
|
94
|
+
if (hooks.length !== entry.hooks.length) changed = true;
|
|
95
|
+
if (hooks.length) next.push(Object.assign({}, entry, { hooks: hooks }));
|
|
96
|
+
}
|
|
97
|
+
if (install) {
|
|
98
|
+
var entry = { hooks: [{ type: 'command', command: command, asyncRewake: true, timeout: 2147483, statusMessage: hookStatus }] };
|
|
99
|
+
if (event === 'SessionStart') entry.matcher = 'startup|resume';
|
|
100
|
+
next.push(entry); changed = true;
|
|
101
|
+
}
|
|
102
|
+
if (next.length) config.hooks[event] = next; else delete config.hooks[event];
|
|
103
|
+
}
|
|
104
|
+
if (!Object.keys(config.hooks).length) delete config.hooks;
|
|
105
|
+
if (changed) {
|
|
106
|
+
var text = JSON.stringify(config, null, 2) + '\n';
|
|
107
|
+
if (before !== text) core.writeDocAtomic(file, text);
|
|
108
|
+
}
|
|
109
|
+
return { file: file, command: command };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function installResumeHooks(binding) {
|
|
113
|
+
binding.claude.resumeHook = updateHooks(binding, true);
|
|
114
|
+
binding.claude.files = binding.claude.files.map(x => processes.fileStamp(x.file));
|
|
115
|
+
}
|
|
116
|
+
function removeResumeHooks(binding) {
|
|
117
|
+
if (!binding.claude || !binding.claude.resumeHook) return;
|
|
118
|
+
var dir = path.dirname(binding.lockFile), duty = require('./duty');
|
|
119
|
+
for (var name of fs.existsSync(dir) ? fs.readdirSync(dir) : []) {
|
|
120
|
+
if (!/^duty-claude-[a-f0-9-]+\.lock$/i.test(name) || path.join(dir, name) === binding.lockFile) continue;
|
|
121
|
+
var lock = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
122
|
+
if (duty.alive(lock.pid)) return; // A project uses one pair of hooks for all its registered sessions.
|
|
123
|
+
}
|
|
124
|
+
updateHooks(binding, false);
|
|
125
|
+
}
|
|
126
|
+
async function resumeHook(input) {
|
|
127
|
+
if (!input || !['SessionStart', 'Stop'].includes(input.hook_event_name) || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(input.session_id || '') || !input.cwd) return;
|
|
128
|
+
if (process.env.ANNOTATABLE_DUTY_HEADLESS_SESSION === input.session_id) return;
|
|
129
|
+
var lockFile = path.join(input.cwd, '.annotatable', 'duty-claude-' + input.session_id + '.lock');
|
|
130
|
+
if (!fs.existsSync(lockFile)) return;
|
|
131
|
+
var lock = JSON.parse(fs.readFileSync(lockFile, 'utf8')), duty = require('./duty');
|
|
132
|
+
if (!duty.alive(lock.pid) || !lock.bindingFile || !fs.existsSync(lock.bindingFile)) return;
|
|
133
|
+
var binding = JSON.parse(fs.readFileSync(lock.bindingFile, 'utf8'));
|
|
134
|
+
if (binding.host !== 'claude' || binding.threadId !== input.session_id || binding.stopRequested ||
|
|
135
|
+
!binding.claude.resumeHook || canonical(binding.settings.cwd) !== canonical(input.cwd) ||
|
|
136
|
+
canonical(binding.lockFile) !== canonical(lockFile) || canonical(duty.pathsFor(binding.file).binding) !== canonical(lock.bindingFile)) return;
|
|
137
|
+
var deadline = Date.now() + 10000, connected;
|
|
138
|
+
for (;;) {
|
|
139
|
+
try { connected = await duty.listen(binding.file, { claudeSession: input.session_id, events: true, nativeHook: true }); break; }
|
|
140
|
+
catch (error) { if (Date.now() >= deadline) throw error; await new Promise(resolve => setTimeout(resolve, 200)); }
|
|
141
|
+
}
|
|
142
|
+
if (!connected.stopped && !connected.alreadyListening) await duty.events(binding.file, { exitCode: 2, hook: true });
|
|
143
|
+
}
|
|
144
|
+
async function dutyBinding(file, options) {
|
|
145
|
+
var home = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
146
|
+
var id = options.claudeSession || process.env.CLAUDE_CODE_SESSION_ID || process.env.CLAUDE_SESSION_ID;
|
|
147
|
+
if (!id) {
|
|
148
|
+
var chain = await processes.parents(process.pid);
|
|
149
|
+
var candidates = registrations(home).filter(r => chain.some(p => p.ProcessId === r.pid));
|
|
150
|
+
if (candidates.length === 1) id = candidates[0].sessionId;
|
|
151
|
+
}
|
|
152
|
+
if (!/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(id || '')) throw new Error('Claude 续接需要明确的原会话编号;使用 --claude-session ID');
|
|
153
|
+
var matches = await liveRegistrations(home, id);
|
|
154
|
+
if (matches.length !== 1) throw new Error('请在原 Claude 会话仍运行时注册,无法确认唯一处理进程');
|
|
155
|
+
var owner = matches[0], record = transcript(home, id), saved = savedTurn(record, id);
|
|
156
|
+
var ancestry = chain || await processes.parents(process.pid), originalIndex = ancestry.findIndex(p => p.ProcessId === owner.process.ProcessId);
|
|
157
|
+
if (options.events && originalIndex < 0) throw new Error('请从原 Claude 会话的后台命令工具接上通知');
|
|
158
|
+
var bash = ancestry.slice(1, originalIndex < 0 ? 1 : originalIndex).find(p => /^bash(?:\.exe)?$/i.test(p.Name));
|
|
159
|
+
if (canonical(saved.cwd) !== canonical(owner.registration.cwd)) throw new Error('Claude 会话目录不一致');
|
|
160
|
+
var flags = resumeFlags(await processes.argumentsFor(owner.process.ProcessId), saved.cwd);
|
|
161
|
+
return { threadId: id, binary: options.claudeBin || owner.process.ExecutablePath, settings: saved,
|
|
162
|
+
claude: { home: home, transcript: record, args: flags.args, gitBashPath: bash && bash.ExecutablePath || process.env.CLAUDE_CODE_GIT_BASH_PATH || null,
|
|
163
|
+
provider: providerContext(process.env), files: profileFiles(home, saved.cwd, flags.files), owners: [processes.identity(owner.process)] } };
|
|
164
|
+
}
|
|
165
|
+
class ClaudeRpc extends processes.JsonProcess {
|
|
166
|
+
constructor(binding, options) {
|
|
167
|
+
var env = processes.agentEnv({ CLAUDE_CONFIG_DIR: binding.claude.home });
|
|
168
|
+
if (options && options.headless) env.ANNOTATABLE_DUTY_HEADLESS_SESSION = binding.threadId;
|
|
169
|
+
if (binding.claude.gitBashPath) env.CLAUDE_CODE_GIT_BASH_PATH = binding.claude.gitBashPath;
|
|
170
|
+
super(binding.binary, ['-p', options && options.fresh ? '--session-id' : '--resume', binding.threadId, '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
|
|
171
|
+
'--permission-prompt-tool', 'stdio', ...binding.claude.args, '--model', binding.settings.model, '--permission-mode', binding.settings.permissionMode], {
|
|
172
|
+
cwd: binding.settings.cwd, env: env,
|
|
173
|
+
protocol: { request: (id, subtype, params) => ({ type: 'control_request', request_id: id, request: Object.assign({ subtype: subtype }, params) }),
|
|
174
|
+
response: m => m.type === 'control_response' ? { id: m.response.request_id, result: m.response.response, error: m.response.subtype === 'error' ? m.response.error : null } : null,
|
|
175
|
+
isRequest: m => m.type === 'control_request' }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function runHeadless(binding, hit, options) {
|
|
180
|
+
options = options || {};
|
|
181
|
+
if (!util.isDeepStrictEqual(savedTurn(binding.claude.transcript, binding.threadId), binding.settings) ||
|
|
182
|
+
!util.isDeepStrictEqual(binding.claude.files.map(x => processes.fileStamp(x.file)), binding.claude.files)) throw new Error('Claude 模型、权限或配置已改变,请重新注册');
|
|
183
|
+
if (!(await options.released()).released) throw new Error('原 Claude 进程尚未退出');
|
|
184
|
+
var rpc = options.rpc || new ClaudeRpc(binding, { headless: true }), record = { host: 'claude', threadId: binding.threadId, pid: rpc.child.pid, startedAt: Date.now() }, timer;
|
|
185
|
+
if (options.onStart) options.onStart(record);
|
|
186
|
+
var refusal, complete;
|
|
187
|
+
var done = new Promise((resolve, reject) => { complete = { resolve: resolve, reject: reject }; }); done.catch(function () {});
|
|
188
|
+
rpc.on('serverRequest', message => {
|
|
189
|
+
record.approval = { subtype: message.request.subtype, tool: message.request.tool_name || null };
|
|
190
|
+
rpc.send({ type: 'control_response', response: { subtype: 'success', request_id: message.request_id,
|
|
191
|
+
response: { behavior: 'deny', message: '无人值守保留待办,不扩大权限' } } });
|
|
192
|
+
refusal = 'Claude 需要人工审批或原宿主工具,保留待办'; rpc.request('interrupt', {}).catch(function () {});
|
|
193
|
+
});
|
|
194
|
+
rpc.on('notification', message => {
|
|
195
|
+
if (message.session_id && message.session_id !== binding.threadId) { complete.reject(new Error('Claude 返回其他会话')); return; }
|
|
196
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
197
|
+
record.restored = { model: message.model, cwd: message.cwd, permissionMode: message.permissionMode };
|
|
198
|
+
record.shellTools = (message.tools || []).filter(name => ['Bash', 'PowerShell'].includes(name));
|
|
199
|
+
if (message.model !== binding.settings.model || message.permissionMode !== binding.settings.permissionMode || canonical(message.cwd) !== canonical(binding.settings.cwd)) { refusal = 'Claude 恢复设置不一致'; rpc.request('interrupt', {}).catch(function () {}); }
|
|
200
|
+
}
|
|
201
|
+
if (message.type === 'result') complete.resolve(message);
|
|
202
|
+
});
|
|
203
|
+
rpc.on('exit', () => complete.reject(new Error('Claude 在回复结束前退出')));
|
|
204
|
+
try {
|
|
205
|
+
await rpc.request('initialize', {});
|
|
206
|
+
var settings = await rpc.request('get_settings', {});
|
|
207
|
+
if (!settings.applied || settings.applied.model !== binding.settings.model || settings.errors && settings.errors.length) throw new Error('Claude 配置或模型预检失败');
|
|
208
|
+
if (binding.claude.provider && !util.isDeepStrictEqual(providerContext(Object.assign({}, process.env, settings.effective && settings.effective.env)), binding.claude.provider)) throw new Error('Claude 提供方不一致,保留待办');
|
|
209
|
+
if (!(await options.released(rpc.child.pid)).released) throw new Error('原 Claude 会话已重开,暂停无头执行');
|
|
210
|
+
record.effectivePermissions = settings.effective && settings.effective.permissions;
|
|
211
|
+
var turnPrompt = require('./duty').prompt(binding, hit, { headless: true });
|
|
212
|
+
if (binding.claude.gitBashPath) turnPrompt += '原会话使用 Bash;继续用 Bash 运行上述 node 命令,不切换命令工具。';
|
|
213
|
+
rpc.send({ type: 'user', session_id: binding.threadId, parent_tool_use_id: null,
|
|
214
|
+
message: { role: 'user', content: turnPrompt } });
|
|
215
|
+
timer = setTimeout(() => { refusal = 'Claude 回复超时'; rpc.request('interrupt', {}).catch(function () {}); complete.reject(new Error(refusal)); }, options.timeoutMs || 300000);
|
|
216
|
+
var result = await done;
|
|
217
|
+
if (refusal || !record.restored || result.is_error || result.subtype !== 'success') throw new Error(refusal || 'Claude 未成功完成回复');
|
|
218
|
+
record.outcome = 'completed'; return record;
|
|
219
|
+
} catch (error) { record.outcome = 'failed'; record.error = error.message; throw Object.assign(error, { dutyRun: record }); }
|
|
220
|
+
finally { clearTimeout(timer); await rpc.close(); record.finishedAt = Date.now(); }
|
|
221
|
+
}
|
|
222
|
+
function createDutyAdapter(getBinding, updateBinding) {
|
|
223
|
+
async function live() { var b = getBinding(); return liveRegistrations(b.claude.home, b.threadId); }
|
|
224
|
+
async function released(excludePid) {
|
|
225
|
+
var matches = (await live()).filter(x => x.process.ProcessId !== excludePid);
|
|
226
|
+
if (matches.length) return { released: false, reason: 'claude-session-process-alive' };
|
|
227
|
+
return processes.ownersExited(getBinding().claude.owners);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
online: async () => {
|
|
231
|
+
var b = getBinding(), matches = await live(); if (matches.length !== 1) return null;
|
|
232
|
+
if (!processes.sameProcess(b.claude.owners[0], matches[0].process)) updateBinding(Object.assign({}, b, { claude: Object.assign({}, b.claude, { owners: [processes.identity(matches[0].process)] }) }));
|
|
233
|
+
var connected = b.bridge && processes.sameProcess(b.bridge, await processes.processInfo(b.bridge.ProcessId));
|
|
234
|
+
return { available: true, active: !connected, notify: async () => {} }; // The original host consumes its own background task output.
|
|
235
|
+
},
|
|
236
|
+
released: released, run: (hit, onStart) => runHeadless(getBinding(), hit, { onStart: onStart, released: released }),
|
|
237
|
+
close: () => { var b = getBinding(); if (b.stopRequested) removeResumeHooks(b); }
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
module.exports = { dutyBinding: dutyBinding, createDutyAdapter: createDutyAdapter, ClaudeRpc: ClaudeRpc,
|
|
241
|
+
runHeadless: runHeadless, savedTurn: savedTurn, resumeFlags: resumeFlags, liveRegistrations: liveRegistrations,
|
|
242
|
+
installResumeHooks: installResumeHooks, removeResumeHooks: removeResumeHooks, resumeHook: resumeHook };
|