annotatable-html 1.38.3 → 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/README.md +76 -13
- package/agent/annot_poll.js +11 -4
- package/bin/cli.js +105 -21
- 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 +130 -122
- 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 +1 -1
- package/lib/opencode.js +195 -0
- package/lib/poll-process.js +42 -0
- package/lib/serve.js +28 -3
- package/package.json +51 -50
- package/templates/SKILL.md +35 -81
- package/templates/annotatable-doc.html +12 -18
- package/templates/commands/html4agent.md +3 -34
- 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/AGENTS.md +0 -107
package/lib/codex.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Optional Codex notification transport. No settings or credentials are changed.
|
|
4
|
+
* The control socket uses WebSocket; app-server proxy forwards its raw bytes.
|
|
5
|
+
* Node 22+ supplies WebSocket, so the package keeps its zero-dependency install. */
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var os = require('os');
|
|
9
|
+
var net = require('net');
|
|
10
|
+
var cp = require('child_process');
|
|
11
|
+
var runPoll = require('./poll-process.js').runPoll;
|
|
12
|
+
|
|
13
|
+
function binaryFor(opts) {
|
|
14
|
+
if (opts.codexBin) return opts.codexBin;
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
var home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
17
|
+
var managed = path.join(home, 'packages', 'app-server-daemon', 'current', 'bin', 'codex.exe');
|
|
18
|
+
if (fs.existsSync(managed)) return managed;
|
|
19
|
+
}
|
|
20
|
+
return 'codex';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CodexRpc {
|
|
24
|
+
constructor(binary) {
|
|
25
|
+
if (typeof WebSocket !== 'function') throw new Error('Codex 通知需要 Node.js 22+;普通 poll 仍支持 Node.js 14+');
|
|
26
|
+
this.nextId = 0;
|
|
27
|
+
this.pending = new Map();
|
|
28
|
+
this.stderr = '';
|
|
29
|
+
this.child = cp.spawn(binary, ['app-server', 'proxy'], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
30
|
+
this.child.stderr.on('data', chunk => { this.stderr = (this.stderr + chunk).slice(-2000); });
|
|
31
|
+
this.child.on('error', err => this.abort(err));
|
|
32
|
+
this.child.stdin.on('error', err => this.abort(err));
|
|
33
|
+
this.child.on('exit', code => {
|
|
34
|
+
if (!this.closing) this.abort(new Error('Codex proxy 已退出 (' + code + '):' + this.stderr));
|
|
35
|
+
});
|
|
36
|
+
this.bridge = net.createServer(socket => {
|
|
37
|
+
this.socket = socket;
|
|
38
|
+
socket.on('error', err => this.abort(err));
|
|
39
|
+
socket.pipe(this.child.stdin);
|
|
40
|
+
this.child.stdout.pipe(socket);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
abort(err) {
|
|
44
|
+
this.failure = err;
|
|
45
|
+
for (const waiter of this.pending.values()) { clearTimeout(waiter.timer); waiter.reject(err); }
|
|
46
|
+
this.pending.clear();
|
|
47
|
+
if (this.socket) this.socket.destroy();
|
|
48
|
+
}
|
|
49
|
+
request(method, params) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
if (this.failure) { reject(this.failure); return; }
|
|
52
|
+
const id = ++this.nextId;
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
this.pending.delete(id);
|
|
55
|
+
reject(new Error('Codex ' + method + ' 超时'));
|
|
56
|
+
}, 10000);
|
|
57
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
58
|
+
try { this.ws.send(JSON.stringify({ id, method, params })); }
|
|
59
|
+
catch (err) { clearTimeout(timer); this.pending.delete(id); reject(err); }
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async open() {
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
this.bridge.once('error', reject);
|
|
65
|
+
this.bridge.listen(0, '127.0.0.1', resolve);
|
|
66
|
+
});
|
|
67
|
+
if (this.failure) throw this.failure;
|
|
68
|
+
this.ws = new WebSocket('ws://127.0.0.1:' + this.bridge.address().port);
|
|
69
|
+
this.ws.addEventListener('message', event => {
|
|
70
|
+
let msg;
|
|
71
|
+
try { msg = JSON.parse(event.data); } catch (_) { return; }
|
|
72
|
+
const waiter = this.pending.get(msg.id);
|
|
73
|
+
if (!waiter) return;
|
|
74
|
+
this.pending.delete(msg.id);
|
|
75
|
+
clearTimeout(waiter.timer);
|
|
76
|
+
if (msg.error) waiter.reject(new Error(JSON.stringify(msg.error)));
|
|
77
|
+
else waiter.resolve(msg.result);
|
|
78
|
+
});
|
|
79
|
+
this.ws.addEventListener('close', () => {
|
|
80
|
+
if (!this.closing) this.abort(new Error('Codex 控制连接已关闭'));
|
|
81
|
+
});
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
const timer = setTimeout(() => reject(new Error('Codex WebSocket 握手超时:' + this.stderr)), 10000);
|
|
84
|
+
this.ws.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
85
|
+
this.ws.addEventListener('error', () => { clearTimeout(timer); reject(this.failure || new Error('Codex WebSocket 握手失败:' + this.stderr)); }, { once: true });
|
|
86
|
+
});
|
|
87
|
+
await this.request('initialize', {
|
|
88
|
+
clientInfo: { name: 'annotatable_html', title: 'Annotatable HTML', version: require('../package.json').version },
|
|
89
|
+
capabilities: { experimentalApi: true }
|
|
90
|
+
});
|
|
91
|
+
this.ws.send(JSON.stringify({ method: 'initialized', params: {} }));
|
|
92
|
+
}
|
|
93
|
+
async read(threadId, includeTurns) {
|
|
94
|
+
return (await this.request('thread/read', { threadId, includeTurns: !!includeTurns })).thread;
|
|
95
|
+
}
|
|
96
|
+
close() {
|
|
97
|
+
this.closing = true;
|
|
98
|
+
this.abort(new Error('Codex 连接已结束'));
|
|
99
|
+
if (this.ws) this.ws.close();
|
|
100
|
+
this.bridge.close();
|
|
101
|
+
this.child.stdin.end();
|
|
102
|
+
this.child.kill();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function withRpc(opts, action) {
|
|
107
|
+
var rpc = new CodexRpc(binaryFor(opts));
|
|
108
|
+
try { await rpc.open(); return await action(rpc); }
|
|
109
|
+
finally { rpc.close(); }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function pollAnnotations(htmlPath, opts) {
|
|
113
|
+
opts = opts || {};
|
|
114
|
+
var threadId = opts.codexThread || process.env.CODEX_THREAD_ID;
|
|
115
|
+
if (!threadId) throw new Error('缺少 Codex 会话 ID:在目标会话中运行,或传 --codex-thread ID');
|
|
116
|
+
var notifier;
|
|
117
|
+
try {
|
|
118
|
+
notifier = await createNotifier(threadId, opts);
|
|
119
|
+
console.log('CODEX_READY ' + JSON.stringify({ threadId: threadId, transport: notifier.kind }));
|
|
120
|
+
var result = await runPoll(htmlPath, opts);
|
|
121
|
+
if (result.hit) {
|
|
122
|
+
var notified = await notifier.notify(result.hit);
|
|
123
|
+
console.log('CODEX_NOTIFIED ' + JSON.stringify(Object.assign({ file: result.hit.file }, notified)));
|
|
124
|
+
}
|
|
125
|
+
process.exitCode = result.code;
|
|
126
|
+
} finally { if (notifier) notifier.close(); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requireLoaded(thread) {
|
|
130
|
+
if (!thread || !thread.status || !['active', 'idle'].includes(thread.status.type)) {
|
|
131
|
+
throw new Error('目标会话未加载到此 app-server;聊天记录存在不代表可通知。请从托管该会话的 Codex 桌面重新启动监听,勿在其他服务 resume 同一会话');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function createNotifier(threadId, opts) {
|
|
136
|
+
opts = opts || {};
|
|
137
|
+
var mode = opts.codexTransport || 'auto';
|
|
138
|
+
if (!['auto', 'desktop', 'app-server'].includes(mode)) throw new Error('未知 Codex 通知方式:' + mode);
|
|
139
|
+
var pipePath = opts.codexPipe || process.env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
140
|
+
var notifier;
|
|
141
|
+
try {
|
|
142
|
+
if (mode === 'desktop' || (mode === 'auto' && pipePath)) {
|
|
143
|
+
if (!pipePath) throw new Error('缺少 CODEX_APP_TOOLS_PIPE_PATH;请在 Codex 桌面会话中启动监听');
|
|
144
|
+
var turnId = opts.codexTurn || process.env.CODEX_TURN_ID;
|
|
145
|
+
if (!turnId) {
|
|
146
|
+
// Read only the persisted turn id for call provenance. Never resume a
|
|
147
|
+
// thread here: the desktop service remains the sole runtime owner.
|
|
148
|
+
var turns = await withRpc(opts, rpc => rpc.request('thread/turns/list', {
|
|
149
|
+
threadId: threadId, limit: 1, sortDirection: 'desc', itemsView: 'notLoaded'
|
|
150
|
+
}));
|
|
151
|
+
turnId = turns.data && turns.data[0] && turns.data[0].id;
|
|
152
|
+
if (!turnId) throw new Error('无法读取来源轮次;请用 --codex-turn 指定当前轮次 ID');
|
|
153
|
+
}
|
|
154
|
+
notifier = new (require('./codex-desktop.js').DesktopNotifier)(pipePath, threadId, turnId);
|
|
155
|
+
await notifier.open();
|
|
156
|
+
} else {
|
|
157
|
+
var rpc = new CodexRpc(binaryFor(opts));
|
|
158
|
+
notifier = { kind: 'app-server', close: () => rpc.close(), notify: async hit => {
|
|
159
|
+
requireLoaded(await rpc.read(threadId));
|
|
160
|
+
var result = await rpc.request('turn/start', { threadId: threadId, input: [], toolOutput: {
|
|
161
|
+
name: 'annotatable_poll', output: 'POLL_HIT ' + JSON.stringify(hit)
|
|
162
|
+
} });
|
|
163
|
+
return { threadId: threadId, turnId: result.turn.id, transport: 'app-server' };
|
|
164
|
+
} };
|
|
165
|
+
await rpc.open();
|
|
166
|
+
requireLoaded(await rpc.read(threadId));
|
|
167
|
+
}
|
|
168
|
+
return notifier;
|
|
169
|
+
} catch (error) { if (notifier) notifier.close(); throw error; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function dutyBinding(file, options) {
|
|
173
|
+
var threadId = options.codexThread || process.env.CODEX_THREAD_ID;
|
|
174
|
+
if (!/^[a-f0-9-]{36}$/i.test(threadId || '')) throw new Error('续接必须绑定明确的 Codex 会话编号');
|
|
175
|
+
var pipePath = options.codexPipe || process.env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
176
|
+
if (!pipePath) throw new Error('请从目标桌面会话注册页面,不根据最近聊天猜测目标');
|
|
177
|
+
var binary = options.codexBin || process.env.CODEX_APP_RUNTIME_CLI_PATH || binaryFor(options);
|
|
178
|
+
var notifier = await createNotifier(threadId, { codexTransport: 'desktop', codexPipe: pipePath, codexTurn: options.codexTurn, codexBin: binary });
|
|
179
|
+
var rpc, processes = require('./duty-process');
|
|
180
|
+
try {
|
|
181
|
+
var desktop = await processes.pipeOwners(pipePath);
|
|
182
|
+
rpc = new CodexRpc(binary); await rpc.open();
|
|
183
|
+
var settings = require('./codex-headless').readSettings(await rpc.read(threadId, false));
|
|
184
|
+
return { threadId: threadId, binary: binary, codexHome: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), settings: settings,
|
|
185
|
+
desktop: { pipePath: pipePath, turnId: notifier.context.turnId, owners: desktop.owners.map(processes.identity), main: processes.identity(desktop.main) } };
|
|
186
|
+
} finally { notifier.close(); if (rpc) rpc.close(); }
|
|
187
|
+
}
|
|
188
|
+
function createDutyAdapter(getBinding, updateBinding) {
|
|
189
|
+
var notifier, processes = require('./duty-process');
|
|
190
|
+
async function online() {
|
|
191
|
+
if (notifier) {
|
|
192
|
+
try { var r = await notifier.read(); return { available: true, active: r.thread.status.type === 'active', notify: hit => notifier.notify(hit, { managed: true }) }; }
|
|
193
|
+
catch (_) { notifier.close(); notifier = null; }
|
|
194
|
+
}
|
|
195
|
+
var current = getBinding(), successes = [];
|
|
196
|
+
for (var candidate of processes.pipeCandidates(current.desktop.pipePath)) {
|
|
197
|
+
var item;
|
|
198
|
+
try {
|
|
199
|
+
item = await createNotifier(current.threadId, { codexTransport: 'desktop', codexPipe: candidate, codexTurn: current.desktop.turnId, codexBin: current.binary });
|
|
200
|
+
var owner = await processes.pipeOwners(candidate);
|
|
201
|
+
successes.push({ notifier: item, owners: owner.owners.map(processes.identity), main: processes.identity(owner.main), pipe: candidate });
|
|
202
|
+
} catch (_) { if (item) item.close(); }
|
|
203
|
+
}
|
|
204
|
+
if (successes.length !== 1) { successes.forEach(x => x.notifier.close()); return null; }
|
|
205
|
+
var selected = successes[0]; notifier = selected.notifier;
|
|
206
|
+
updateBinding(Object.assign({}, current, { desktop: { pipePath: selected.pipe, owners: selected.owners, main: selected.main, turnId: current.desktop.turnId } }));
|
|
207
|
+
var result = await notifier.read();
|
|
208
|
+
return { available: true, active: result.thread.status.type === 'active', notify: hit => notifier.notify(hit, { managed: true }) };
|
|
209
|
+
}
|
|
210
|
+
return { online: online, released: () => processes.released(getBinding().desktop.owners),
|
|
211
|
+
run: (hit, onStart) => require('./codex-headless').runHeadless(getBinding(), hit, { onStart: onStart }),
|
|
212
|
+
close: () => { if (notifier) notifier.close(); } };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { CodexRpc: CodexRpc, pollAnnotations: pollAnnotations,
|
|
216
|
+
createNotifier: createNotifier, requireLoaded: requireLoaded, dutyBinding: dutyBinding, createDutyAdapter: createDutyAdapter };
|
package/lib/core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/* 可批注 HTML · 核心逻辑
|
|
4
|
-
* 供 bin/cli.js 与 agent/
|
|
4
|
+
* 供 bin/cli.js 与 agent/ 薄封装共用;工作流协议随 annotatable-html skill 分发。 */
|
|
5
5
|
|
|
6
6
|
var fs = require('fs');
|
|
7
7
|
var path = require('path');
|
|
@@ -12,19 +12,13 @@ var LANG = require('./lang-en.js');
|
|
|
12
12
|
* 词典本体(zh 键)先掩码再替换——en 文档运行时还要用它把动态生成的中文翻成英文
|
|
13
13
|
* (boot 后 MutationObserver 消费 LANG_PAIRS,模板里 langTr 只认 DOC_LANG==='en')。 */
|
|
14
14
|
var LANG_BLOCK_ANNOT_EN = [
|
|
15
|
-
'<!-- ═══ ANNOTATION DATA BEGIN —
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'③ Write back with the standard tools annot_reply.js / annotatable reply (appends the ai turn, maintains history snapshots, rev+1); the reply field mirrors the latest AI turn;',
|
|
19
|
-
'④ If a comment contains "stash" or "keep" → file the quoted text into that ANSWER.md staging area; "distill" or "distill to: file" → distill reusable knowledge into docs/Learn/ (or the named file);',
|
|
20
|
-
'⑤ history is the version ledger: since v1.10.0 the file keeps a skeleton (rev/time/op/n, last 60; adjustable in the history panel ⚙, persisted via store histCaps — the CLI tools trim by the same counts; edit entries carry bodyOps strips); the full snapshot ledger lives in .annotatable/<file>.hist.json (tool-maintained, with DOC_ID ownership checks). Users can roll back any version (including body) in the page Version history. Agents append via annot_reply.js / annot_edit.js as usual — never hand-edit the history structure;',
|
|
21
|
-
'⑥ Duty & streaming (v1.21.0): while on duty, append with `annotatable reply doc.html --stream \'chunk\' --id annId` (the page renders typing live); finish with `--stream-end \'full text\'`; exit code 4 = the human pressed ⏹ (stop immediately), 5 = the annotation was deleted (stop too), 7 = no open streaming round (start one with --id). Without --id, a chunk auto-attaches to the most recently STARTED still-open streaming round and never to a finished one. An ai turn with streaming:true means a reply is in progress; top-level control.stop is an interrupt request written by the page button — tools consume it automatically, never hand-edit. After each round, re-mount `annotatable poll` to keep watching ═══ -->'
|
|
15
|
+
'<!-- ═══ ANNOTATION DATA BEGIN — data location for the annotatable-html skill:',
|
|
16
|
+
'Use annotatable read/reply/edit/poll. The tools maintain revisions and history; do not hand-edit store JSON.',
|
|
17
|
+
'The skill defines the workflow. Answer content and teaching methods belong to the user-selected content workflow ═══ -->'
|
|
22
18
|
].join('\n');
|
|
23
19
|
var LANG_BLOCK_LESSON_EN = [
|
|
24
|
-
'<!-- ═══ LESSON DATA BEGIN —
|
|
25
|
-
'
|
|
26
|
-
'Multi-rung or checkpointed entries render on the answer page as staged lesson cards (rung-by-rung unlock); single-rung entries render as one long-answer card.',
|
|
27
|
-
'The annotation store stays slim: to read pending annotations, read the annotations-store above — no need to swallow lesson full texts ═══ -->'
|
|
20
|
+
'<!-- ═══ LESSON DATA BEGIN — legacy extended-answer data. Preserve existing content.',
|
|
21
|
+
'Ordinary annotation replies use reply; this block is not a default teaching workflow ═══ -->'
|
|
28
22
|
].join('\n');
|
|
29
23
|
function applyLangEn(tpl) {
|
|
30
24
|
var pairs = LANG.pairs.slice().sort(function (a, b) { return b[0].length - a[0].length; });
|
|
@@ -510,7 +504,7 @@ function pollAnnotations(htmlPath, opts) {
|
|
|
510
504
|
var payload = { file: htmlPath, rev: s.rev, pending: s.pending, ids: s.ids, created: s.created };
|
|
511
505
|
if (s.replay) payload.replay = s.replay;
|
|
512
506
|
console.log('POLL_HIT ' + JSON.stringify(payload));
|
|
513
|
-
process.exit(0);
|
|
507
|
+
process.exit(opts.exitCode || 0); // v1.38.4 命中退出码可配(issue#8):zcode 等任务退出即唤醒 → 默认 0;Claude Code 等非零才唤醒 → --exit-code 1
|
|
514
508
|
}
|
|
515
509
|
if (st.pending || st.replay) hit(st);
|
|
516
510
|
console.log('已有批注都已回应,等待新批注…(每 ' + intervalSec + 's 检查' + (timeoutSec ? ',最长 ' + timeoutSec + 's' : '') + ',Ctrl+C 退出)');
|
|
@@ -528,7 +522,11 @@ function pollAnnotations(htmlPath, opts) {
|
|
|
528
522
|
}
|
|
529
523
|
if (deadline && Date.now() > deadline) {
|
|
530
524
|
writeWatchBeat(htmlPath, 'handoff', startedAt);
|
|
531
|
-
console.log('TIMEOUT · 待回应=' + s2.pending);
|
|
525
|
+
console.log('TIMEOUT · 待回应=' + s2.pending);
|
|
526
|
+
/* v1.38.4 超时退出码可配(issue#8):默认 3 向后兼容;传 0 = 超时轮空静默退出
|
|
527
|
+
* 不叫醒 agent(Claude Code 等非零即唤醒的 harness 需要)。NaN/未传回 3。 */
|
|
528
|
+
var tec = parseInt(opts.timeoutExitCode, 10);
|
|
529
|
+
process.exit(isNaN(tec) ? 3 : tec);
|
|
532
530
|
}
|
|
533
531
|
}, intervalSec * 1000);
|
|
534
532
|
}
|
|
@@ -875,85 +873,6 @@ function applyAnswers(htmlPath, answersPath) {
|
|
|
875
873
|
if (out.missing.length) console.log('⚠️ 未找到关联的批注id(annId 可能已失效): ' + out.missing.join(', '));
|
|
876
874
|
}
|
|
877
875
|
|
|
878
|
-
/* ---------- teach:从源文档批注生成教学文档脚手架(v1.18.0)----------
|
|
879
|
-
* CLI 没有 AI:本命令把源文档的批注(原句+批语)按主题铺成分节骨架(每节含 原句引用块 +
|
|
880
|
-
* 教学正文占位 + 检查点占位),agent 随后用 annotatable edit 逐节填充(走锚定管线、入版本账本);
|
|
881
|
-
* 产物是完整可批注 HTML(同模板同主题机制),批注循环与源文档相互独立。 */
|
|
882
|
-
function teachDoc(srcPath, opts, pkgRoot) {
|
|
883
|
-
opts = opts || {};
|
|
884
|
-
var src = path.resolve(srcPath || '');
|
|
885
|
-
if (!fs.existsSync(src)) throw new Error('找不到源文档: ' + src);
|
|
886
|
-
var t = fs.readFileSync(src, 'utf8');
|
|
887
|
-
var o = readStore(t);
|
|
888
|
-
if (!o || !o.annotations.length) throw new Error('源文档没有任何批注——先在浏览器里划线批注,再生成教学文档');
|
|
889
|
-
var name = opts.out ||
|
|
890
|
-
path.join(path.dirname(src), path.basename(src).replace(/\.html?$/i, '') + '-教学.html');
|
|
891
|
-
if (!/\.html?$/i.test(name)) name += '.html';
|
|
892
|
-
var dst = path.resolve(name);
|
|
893
|
-
if (fs.existsSync(dst)) throw new Error('文件已存在: ' + dst);
|
|
894
|
-
|
|
895
|
-
var rsl = resolveTemplate(path.dirname(dst), pkgRoot, opts);
|
|
896
|
-
var tpl = rsl.tpl, tplSrc = rsl.src;
|
|
897
|
-
var theme = loadThemeChain(path.dirname(dst), opts);
|
|
898
|
-
tpl = applyTheme(tpl, theme);
|
|
899
|
-
|
|
900
|
-
function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
901
|
-
function cutTitle(s, n) { s = String(s || '').replace(/\s+/g, ' ').trim(); n = n || 26; return s.length > n ? s.slice(0, n) + '…' : s; }
|
|
902
|
-
|
|
903
|
-
var srcTitle = '';
|
|
904
|
-
var tm = t.match(/<title>([\s\S]*?)<\/title>/);
|
|
905
|
-
if (tm) srcTitle = tm[1].replace(/<[^>]+>/g, '').trim();
|
|
906
|
-
var title = (opts.title || srcTitle || path.basename(dst).replace(/\.html?$/i, '')) + ' · 教学版';
|
|
907
|
-
var docSeed = Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
908
|
-
|
|
909
|
-
var body = '<h1>' + esc(title) + '</h1>\n';
|
|
910
|
-
body += '<section data-title="来源与用法" id="sec-src">\n';
|
|
911
|
-
body += '<h2>📋 来源与用法</h2>\n';
|
|
912
|
-
body += '<p>本文档由源文档 <strong>' + esc(path.basename(src)) + '</strong> 的批注汇总生成(' + o.annotations.length + ' 个主题)。两个文档的批注循环相互独立:在这里划线批注 = 对教学本身提问或纠偏;回到原文语境请打开源文件。</p>\n';
|
|
913
|
-
body += '<p>agent 将用 <code>annotatable edit</code> 逐节替换下面的占位段落(锚定自动重算、每节入版本账本可回滚)。批语含「继续」= 推进下一节;「重讲」= 本节换一种讲法。</p>\n';
|
|
914
|
-
body += '</section>\n';
|
|
915
|
-
|
|
916
|
-
/* 每节预置一条任务批注(锚定在占位段上):agent 无需等待人类批注即可 edit 填充,
|
|
917
|
-
填充后该批注自动变成这一节的讨论线(锚定随 newText 重算) */
|
|
918
|
-
var teachAnns = [];
|
|
919
|
-
o.annotations.forEach(function (a, i) {
|
|
920
|
-
var q = '';
|
|
921
|
-
(a.thread || []).forEach(function (x) { if (x.role === 'user' && x.text) q = x.text; });
|
|
922
|
-
if (!q) q = a.comment || '';
|
|
923
|
-
var st = a.status === 'answered' ? '(已回应过——本次教学可更系统)' : '(待回应——回答时引用本文档)';
|
|
924
|
-
var ph = '(agent 待撰写:围绕「' + cutTitle(q || a.quote, 30) + '」分阶展开——从动机讲到机制再落到边界与失效场景,每阶结尾给一个检查点。)' + st;
|
|
925
|
-
body += '<section data-title="' + esc((i + 1) + '. ' + cutTitle(q || a.quote)) + '" id="sec-t' + (i + 1) + '">\n';
|
|
926
|
-
body += '<h2>❓ ' + (i + 1) + '. ' + esc(cutTitle(q || a.quote, 44)) + '</h2>\n';
|
|
927
|
-
body += '<blockquote><p><strong>原句:</strong>' + esc(a.quote) + '</p>' + (q ? '<p><strong>批语:</strong>' + esc(q) + '</p>' : '') + '</blockquote>\n';
|
|
928
|
-
body += '<p>' + esc(ph) + '</p>\n';
|
|
929
|
-
body += '<p><strong>✅ 检查点(占位):</strong>学完本节,不看原文回答:这一节的核心机制是什么?它在哪里失效?</p>\n';
|
|
930
|
-
body += '</section>\n';
|
|
931
|
-
teachAnns.push({
|
|
932
|
-
id: 'teach-' + i + '-' + docSeed, quote: ph, prefix: '', suffix: '', ordinal: 0,
|
|
933
|
-
section: (i + 1) + '. ' + cutTitle(q || a.quote),
|
|
934
|
-
comment: '🎓 教学占位:请把本节教学正文直接写进这个段落(对 agent:edit 本条批注,newText=教学正文)',
|
|
935
|
-
created: Date.now(), status: 'new',
|
|
936
|
-
thread: [{ role: 'user', text: '🎓 教学占位:请把本节教学正文直接写进这个段落', time: Date.now() }]
|
|
937
|
-
});
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
tpl = tpl.replace(/<title>[\s\S]*?<\/title>/, '<title>' + esc(title) + '</title>');
|
|
941
|
-
tpl = tpl.replace(/<article>[\s\S]*?<\/article>/, '<article>\n' + body + '\n </article>');
|
|
942
|
-
var docId = 'doc-' + docSeed + Math.random().toString(36).slice(2, 8);
|
|
943
|
-
tpl = tpl.replace(/var DOC_ID\s*=\s*'[^']*';/, "var DOC_ID = '" + docId + "';");
|
|
944
|
-
tpl = tpl.replace(/var FILE_NAME = '[^']*';/, "var FILE_NAME = '" + path.basename(dst) + "';");
|
|
945
|
-
tpl = stampPageVersion(tpl, pkgRoot);
|
|
946
|
-
/* 预置任务批注写进 store:agent 打开即可 read → edit,无需等人类先批注 */
|
|
947
|
-
tpl = tpl.replace(/(<script id="annotations-store" type="application\/json">\s*)([\s\S]*?)(\s*<\/script>)/, function (all, p1, p2, p3) {
|
|
948
|
-
return p1 + '\n' + JSON.stringify({ rev: 0, annotations: teachAnns, history: [] }).replace(/</g, '\\u003c') + '\n' + p3;
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
fs.writeFileSync(dst, tpl);
|
|
952
|
-
console.log('已从源文档批注生成教学文档: ' + dst);
|
|
953
|
-
console.log(' 来源: ' + src + ' · 主题数: ' + o.annotations.length + ' · 模板: ' + tplSrc + (theme ? ' + 主题(' + THEME_FILE + ')' : ''));
|
|
954
|
-
console.log('下一步: agent 用 annotatable edit 逐节填充占位段落;填充完让人类用浏览器打开批注/推进。');
|
|
955
|
-
return dst;
|
|
956
|
-
}
|
|
957
876
|
|
|
958
877
|
/* ---------- edit:按批注指令修改正文,同步重算锚定 ----------
|
|
959
878
|
* 与模板锚定算法(buildIndex/normalizeWS)对齐的充分近似:
|
|
@@ -1332,17 +1251,66 @@ function applyTheme(tpl, theme) {
|
|
|
1332
1251
|
var rootMatch = theme.match(/:root\s*\{[^}]*\}/);
|
|
1333
1252
|
if (rootMatch) {
|
|
1334
1253
|
rest = theme.replace(rootMatch[0], '');
|
|
1335
|
-
|
|
1254
|
+
// Keep defaults that a partial theme omits; later declarations override them.
|
|
1255
|
+
var overrides = rootMatch[0].slice(rootMatch[0].indexOf('{') + 1, -1);
|
|
1256
|
+
tpl = tpl.replace(/:root\s*\{([^}]*)\}/, function (_, defaults) {
|
|
1257
|
+
return ':root{' + defaults + '\n' + overrides + '\n}';
|
|
1258
|
+
});
|
|
1336
1259
|
}
|
|
1337
|
-
if (rest.trim()) tpl = tpl.replace('</style>', rest + '\n</style>');
|
|
1260
|
+
if (rest.trim()) tpl = tpl.replace('</style>', function () { return rest + '\n</style>'; });
|
|
1338
1261
|
return tpl;
|
|
1339
1262
|
}
|
|
1340
1263
|
|
|
1341
1264
|
/* init:把工作流文件装进目标目录(已存在的不覆盖) */
|
|
1342
|
-
|
|
1265
|
+
/* ---------- v1.38.4 文档侧 kit 拷贝「触碰即同步」(issue#7 根因修复)----------
|
|
1266
|
+
* init 分发的 lib/core.js + agent/annot_*.js 拷贝进文档目录后从不随包升级(initKit
|
|
1267
|
+
* 对已存在文件一律跳过)——v1.29.0 纯划线静默等一切后续改进都触达不到存量项目,
|
|
1268
|
+
* 文档侧值守行为随拷贝版本静止腐烂。实证:某文档项目拷贝停在 v1.18.0,纯划线
|
|
1269
|
+
* 被 poll 判 pending=1 挂起即命中 → 空转循环,而 read 走全局新版却正确静默,
|
|
1270
|
+
* 同一份数据两种判定。与页面层静默升级(v1.36.0)同哲学:永远新鲜的组件(全局包)
|
|
1271
|
+
* 在触碰时把设施拉齐。只同步代码设施(agent/*.js + lib/core.js,无用户数据);
|
|
1272
|
+
* AGENTS.md、模板、主题可能含用户手改,不碰。覆盖前备份 .bak-kit-<时间戳>。
|
|
1273
|
+
* 提示走 stderr:stdout 是 agent 的解析面(read 清单/POLL_HIT 载荷),不掺行。 */
|
|
1274
|
+
var KIT_CODE_FILES = [
|
|
1275
|
+
'agent/annot_read.js', 'agent/annot_reply.js', 'agent/annot_edit.js',
|
|
1276
|
+
'agent/annot_answer.js', 'agent/annot_poll.js', 'lib/core.js', 'lib/lang-en.js'
|
|
1277
|
+
];
|
|
1278
|
+
function syncKitCode(dir, pkgRoot) {
|
|
1279
|
+
var updated = [];
|
|
1280
|
+
KIT_CODE_FILES.forEach(function (rel) {
|
|
1281
|
+
var dst = path.join(dir, rel), src = path.join(pkgRoot, rel), cur, nw;
|
|
1282
|
+
try { cur = fs.readFileSync(dst, 'utf8'); } catch (e) { return; } // 该目录没分发过 kit → 无事可做
|
|
1283
|
+
try { nw = fs.readFileSync(src, 'utf8'); } catch (e) { return; }
|
|
1284
|
+
if (cur === nw) return;
|
|
1285
|
+
fs.writeFileSync(dst + '.bak-kit-' + Date.now(), cur);
|
|
1286
|
+
fs.writeFileSync(dst, nw);
|
|
1287
|
+
updated.push(rel);
|
|
1288
|
+
});
|
|
1289
|
+
if (updated.length) {
|
|
1290
|
+
try {
|
|
1291
|
+
console.error('ℹ 文档侧 agent 设施已同步到当前包版本(' + updated.length + ' 个文件;旧版留 .bak-kit-* 备份)');
|
|
1292
|
+
} catch (e) { /* stdout/stderr 关闭(管道早闭)不阻塞 */ }
|
|
1293
|
+
}
|
|
1294
|
+
return updated;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/* 查找已安装的批注 skill;初始化不再向宿主指令文件写入协议。 */
|
|
1298
|
+
function skillProbe(home) {
|
|
1299
|
+
/* v1.40.0:扫主流 harness 全集——.agents(共享仓)/.zcode/.claude/.codex/.config/opencode */
|
|
1300
|
+
var dirs = ['.agents', '.zcode', '.claude', '.codex', path.join('.config', 'opencode')];
|
|
1301
|
+
for (var i = 0; i < dirs.length; i++) {
|
|
1302
|
+
if (fs.existsSync(path.join(home, dirs[i], 'skills', 'annotatable-html', 'SKILL.md'))) return true;
|
|
1303
|
+
}
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
function initKit(dir, pkgRoot, opts) {
|
|
1307
|
+
if (opts && opts.agentsMd !== undefined) {
|
|
1308
|
+
throw new Error('init 不再写入 AGENTS.md;请运行 annotatable skill 安装批注工作流。');
|
|
1309
|
+
}
|
|
1343
1310
|
dir = dir || process.cwd();
|
|
1311
|
+
var created = [], skipped = [], upgraded = [];
|
|
1312
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1344
1313
|
var items = [
|
|
1345
|
-
['templates/AGENTS.md', 'AGENTS.md'],
|
|
1346
1314
|
['templates/annotatable-doc.html', '可批注文档.html'],
|
|
1347
1315
|
['templates/annotatable.theme.css', THEME_FILE],
|
|
1348
1316
|
['agent/annot_read.js', 'agent/annot_read.js'],
|
|
@@ -1350,13 +1318,25 @@ function initKit(dir, pkgRoot) {
|
|
|
1350
1318
|
['agent/annot_edit.js', 'agent/annot_edit.js'],
|
|
1351
1319
|
['agent/annot_answer.js', 'agent/annot_answer.js'],
|
|
1352
1320
|
['agent/annot_poll.js', 'agent/annot_poll.js'],
|
|
1353
|
-
['lib/core.js', 'lib/core.js']
|
|
1321
|
+
['lib/core.js', 'lib/core.js'],
|
|
1322
|
+
['lib/lang-en.js', 'lib/lang-en.js'] // v1.38.4:core.js require('./lang-en.js'),漏分发=文档侧薄封装加载即崩(e2e 实弹揪出)
|
|
1354
1323
|
];
|
|
1355
|
-
var created = [], skipped = [];
|
|
1356
1324
|
items.forEach(function (it) {
|
|
1357
1325
|
var src = path.join(pkgRoot, it[0]);
|
|
1358
1326
|
var dst = path.join(dir, it[1]);
|
|
1359
|
-
if (fs.existsSync(dst)) {
|
|
1327
|
+
if (fs.existsSync(dst)) {
|
|
1328
|
+
/* v1.38.4:代码设施已存在不再是「跳过」——与包内不同则备份后升级(拷贝静止腐烂
|
|
1329
|
+
* = issue#7 根因);模板/主题可能含用户手改,维持跳过。 */
|
|
1330
|
+
if (KIT_CODE_FILES.indexOf(it[0]) !== -1) {
|
|
1331
|
+
var cur = fs.readFileSync(dst, 'utf8'), nw = fs.readFileSync(src, 'utf8');
|
|
1332
|
+
if (cur !== nw) {
|
|
1333
|
+
fs.writeFileSync(dst + '.bak-kit-' + Date.now(), cur);
|
|
1334
|
+
fs.writeFileSync(dst, nw);
|
|
1335
|
+
upgraded.push(it[1]);
|
|
1336
|
+
}
|
|
1337
|
+
} else skipped.push(it[1]);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1360
1340
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
1361
1341
|
fs.copyFileSync(src, dst);
|
|
1362
1342
|
created.push(it[1]);
|
|
@@ -1364,12 +1344,13 @@ function initKit(dir, pkgRoot) {
|
|
|
1364
1344
|
console.log('初始化完成 → ' + dir);
|
|
1365
1345
|
created.forEach(function (f) { console.log(' + ' + f); });
|
|
1366
1346
|
skipped.forEach(function (f) { console.log(' = 已存在,跳过: ' + f); });
|
|
1347
|
+
upgraded.forEach(function (f) { console.log(' ↑ 已升级(旧版留 .bak-kit-* 备份): ' + f); });
|
|
1367
1348
|
console.log('\n下一步:');
|
|
1368
1349
|
console.log(' 1. 用 Edge/Chrome 打开 可批注文档.html,左下角绑定文件;');
|
|
1369
1350
|
console.log(' 2. 人类划线批注后,agent 运行: node agent/annot_read.js <html路径> 查看待回应批注;');
|
|
1370
1351
|
console.log(' 3. 生成 replies.json 后运行: node agent/annot_reply.js <html路径> replies.json 写回。');
|
|
1371
1352
|
console.log(' 4. 想换外观:编辑 annotatable.theme.css(annotatable new 时自动应用)。');
|
|
1372
|
-
console.log('
|
|
1353
|
+
console.log('批注工作流由 annotatable-html skill 提供;未安装时运行 annotatable skill。');
|
|
1373
1354
|
}
|
|
1374
1355
|
|
|
1375
1356
|
/* new:从模板新建可批注文档
|
|
@@ -1595,11 +1576,13 @@ function staleSkillNotice(pkgRoot, baseDir) {
|
|
|
1595
1576
|
var home = baseDir || os.homedir();
|
|
1596
1577
|
var ver = pkgVersion(pkgRoot);
|
|
1597
1578
|
var stale = [];
|
|
1598
|
-
['.zcode', '.claude'].forEach(function (d) {
|
|
1579
|
+
['.zcode', '.claude', '.codex'].forEach(function (d) {
|
|
1599
1580
|
var f = path.join(home, d, 'skills', 'annotatable-html', 'SKILL.md');
|
|
1600
1581
|
var v = skillVersionOf(f);
|
|
1601
1582
|
if (v !== null && v !== ver) stale.push(f);
|
|
1602
1583
|
});
|
|
1584
|
+
var foc = skillVersionOf(path.join(home, '.config', 'opencode', 'skills', 'annotatable-html', 'SKILL.md'));
|
|
1585
|
+
if (foc !== null && foc !== ver) stale.push(path.join(home, '.config', 'opencode', 'skills', 'annotatable-html'));
|
|
1603
1586
|
if (!stale.length) return '';
|
|
1604
1587
|
return '⚠ 已安装的 skill 副本落后于当前包 v' + ver + '(' + stale.length + ' 份),运行 annotatable skill 可自动升级';
|
|
1605
1588
|
} catch (e) { return ''; }
|
|
@@ -1613,12 +1596,17 @@ function installSkill(opts, pkgRoot, baseDir) {
|
|
|
1613
1596
|
var os = require('os');
|
|
1614
1597
|
var home = baseDir || os.homedir();
|
|
1615
1598
|
var ver = pkgVersion(pkgRoot);
|
|
1599
|
+
/* v1.40.0 兼容主流 harness(对外发布口径):Codex(~/.codex/skills) 与 opencode(~/.config/opencode/skills)
|
|
1600
|
+
* 均采 Agent Skills 开放标准的 SKILL.md 形态,拷入即生效;slash 命令仍是 zcode/claude 公约不投。
|
|
1601
|
+
* 指定任一旗标则只投该家(+agents 共享仓),无旗标默认投全部主流四家。 */
|
|
1602
|
+
var anyHarness = opts.zcode || opts.claude || opts.codex || opts.opencode;
|
|
1616
1603
|
var targets = [];
|
|
1617
|
-
if (opts.zcode ||
|
|
1618
|
-
if (opts.claude ||
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1604
|
+
if (opts.zcode || !anyHarness) targets.push({ name: 'ZCode', base: path.join(home, '.zcode'), cmd: true });
|
|
1605
|
+
if (opts.claude || !anyHarness) targets.push({ name: 'Claude Code', base: path.join(home, '.claude'), cmd: true });
|
|
1606
|
+
if (opts.codex || !anyHarness) targets.push({ name: 'Codex', base: path.join(home, '.codex') });
|
|
1607
|
+
if (opts.opencode || !anyHarness) targets.push({ name: 'opencode', base: path.join(home, '.config', 'opencode'), opencodePlugin: true });
|
|
1608
|
+
/* 共享仓与各 harness 都安装完整 skill;补充协议只放在技能目录内。 */
|
|
1609
|
+
targets.push({ name: 'agents(共享仓)', base: path.join(home, '.agents') });
|
|
1622
1610
|
var srcSkill = path.join(pkgRoot, 'templates', 'SKILL.md');
|
|
1623
1611
|
var cmdDir = path.join(pkgRoot, 'templates', 'commands');
|
|
1624
1612
|
var installed = [], skipped = 0;
|
|
@@ -1634,21 +1622,40 @@ function installSkill(opts, pkgRoot, baseDir) {
|
|
|
1634
1622
|
installed.push(sv === null ? (t.name + ' skill → ' + skillDst)
|
|
1635
1623
|
: (t.name + ' skill 升级 ' + (sv || '无版本标记(旧)') + ' → v' + ver + ':' + skillDst));
|
|
1636
1624
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
var
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
fs.
|
|
1645
|
-
|
|
1646
|
-
installed.push(av === null ? (t.name + ' AGENTS.md → ' + agDst)
|
|
1647
|
-
: (t.name + ' AGENTS.md 升级 ' + (av || '无版本标记(旧)') + ' → v' + ver));
|
|
1625
|
+
var agDst = path.join(t.base, 'skills', 'annotatable-html', 'AGENTS.md');
|
|
1626
|
+
if (fs.existsSync(agDst)) {
|
|
1627
|
+
var legacy = fs.readFileSync(agDst, 'utf8');
|
|
1628
|
+
if (skillVersionOf(agDst) && legacy.indexOf('# 可批注 HTML 工作流') !== -1) {
|
|
1629
|
+
var skillRoot = path.resolve(path.dirname(agDst));
|
|
1630
|
+
var legacyBackup = path.resolve(agDst + '.legacy-' + Date.now() + '.bak');
|
|
1631
|
+
if (path.dirname(legacyBackup) !== skillRoot) throw new Error('技能协议备份路径异常');
|
|
1632
|
+
fs.renameSync(path.resolve(agDst), legacyBackup);
|
|
1633
|
+
installed.push(t.name + ' 旧技能协议已移入备份 → ' + legacyBackup);
|
|
1648
1634
|
}
|
|
1649
1635
|
}
|
|
1636
|
+
['hosts.md', 'operations.md', 'compatibility.md'].forEach(function (name) {
|
|
1637
|
+
var refDst = path.join(path.dirname(skillDst), 'references', name);
|
|
1638
|
+
if (skillVersionOf(refDst) === ver) { skipped++; return; }
|
|
1639
|
+
fs.mkdirSync(path.dirname(refDst), { recursive: true });
|
|
1640
|
+
fs.copyFileSync(path.join(pkgRoot, 'templates', 'references', name), refDst);
|
|
1641
|
+
stampVersion(refDst, ver);
|
|
1642
|
+
installed.push(t.name + ' 按需说明 → ' + refDst);
|
|
1643
|
+
});
|
|
1644
|
+
if (t.opencodePlugin) {
|
|
1645
|
+
var pluginSrc = path.join(pkgRoot, 'templates', 'opencode', 'annotatable-html.js');
|
|
1646
|
+
var pluginDst = path.join(t.base, 'plugins', 'annotatable-html.js');
|
|
1647
|
+
var pluginText = fs.readFileSync(pluginSrc, 'utf8') + '\n// annotatable-html v' + ver + '\n';
|
|
1648
|
+
var oldPlugin = null;
|
|
1649
|
+
try { oldPlugin = fs.readFileSync(pluginDst, 'utf8'); } catch (e) { /* first install */ }
|
|
1650
|
+
if (oldPlugin !== pluginText) {
|
|
1651
|
+
fs.mkdirSync(path.dirname(pluginDst), { recursive: true });
|
|
1652
|
+
if (oldPlugin !== null) fs.writeFileSync(pluginDst + '.bak-' + Date.now(), oldPlugin);
|
|
1653
|
+
fs.writeFileSync(pluginDst, pluginText);
|
|
1654
|
+
installed.push('opencode 会话上下文插件 → ' + pluginDst);
|
|
1655
|
+
} else skipped++;
|
|
1656
|
+
}
|
|
1650
1657
|
// 命令独立于 skill 处理(升级时清理旧命令、安装新命令)
|
|
1651
|
-
if (fs.existsSync(cmdDir) &&
|
|
1658
|
+
if (fs.existsSync(cmdDir) && t.cmd) {
|
|
1652
1659
|
// 清理 v1.3.0 的旧命令文件(已被 html4agent.md 取代)
|
|
1653
1660
|
['读批注.md', '可批注文档.md'].forEach(function (old) {
|
|
1654
1661
|
var oldDst = path.join(t.base, 'commands', old);
|
|
@@ -1714,7 +1721,7 @@ function annotParts(tpl) {
|
|
|
1714
1721
|
}
|
|
1715
1722
|
|
|
1716
1723
|
function annotStoreBlock() {
|
|
1717
|
-
return '<!-- ═══ AI批注数据 BEGIN — agent 读取协议见
|
|
1724
|
+
return '<!-- ═══ AI批注数据 BEGIN — agent 读取协议见 annotatable-html skill ═══ -->\n' +
|
|
1718
1725
|
'<script id="annotations-store" type="application/json">\n' +
|
|
1719
1726
|
JSON.stringify({ rev: 0, annotations: [], history: [] }) + '\n</' + 'script>\n' +
|
|
1720
1727
|
'<!-- ═══ AI批注数据 END ═══ -->\n';
|
|
@@ -1943,6 +1950,7 @@ function upgradeLayerFile(targetPath, pkgRoot, opts) {
|
|
|
1943
1950
|
}
|
|
1944
1951
|
/* agent 触碰即换装:六个读写命令与 serve 启动的统一钩子。任何失败只提示一行,绝不阻塞原操作。 */
|
|
1945
1952
|
function silentUpgrade(htmlPath) {
|
|
1953
|
+
try { syncKitCode(path.dirname(htmlPath), path.join(__dirname, '..')); } catch (e) { /* 设施同步尽力而为 */ }
|
|
1946
1954
|
try {
|
|
1947
1955
|
var r = upgradeLayerFile(htmlPath, path.join(__dirname, '..'));
|
|
1948
1956
|
if (r && r.status === 'upgraded')
|
|
@@ -2166,8 +2174,8 @@ var path = require('path');
|
|
|
2166
2174
|
|
|
2167
2175
|
module.exports = {
|
|
2168
2176
|
readPending: readPending, applyReplies: applyReplies,
|
|
2169
|
-
readAnswers: readAnswers, applyAnswers: applyAnswers, listAnswers: listAnswers,
|
|
2170
|
-
initKit: initKit, newDoc: newDoc,
|
|
2177
|
+
readAnswers: readAnswers, applyAnswers: applyAnswers, listAnswers: listAnswers,
|
|
2178
|
+
initKit: initKit, syncKitCode: syncKitCode, skillProbe: skillProbe, newDoc: newDoc,
|
|
2171
2179
|
mdDoc: mdDoc, mdToBody: mdToBody, mdMathPass: mdMathPass,
|
|
2172
2180
|
exportTheme: exportTheme, exportTemplate: exportTemplate,
|
|
2173
2181
|
templateUse: templateUse, templateReset: templateReset,
|