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.
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ // The Windows desktop app owns a separate stdio app-server. Use its inherited
4
+ // app-tools pipe instead of resuming the same thread in an unrelated daemon.
5
+ var net = require('net');
6
+ var os = require('os');
7
+ var crypto = require('crypto');
8
+ var MAX_FRAME = 8 * 1024 * 1024;
9
+
10
+ class DesktopRpc {
11
+ constructor(pipePath) {
12
+ this.pipePath = pipePath;
13
+ this.pending = new Map();
14
+ this.nextId = 0;
15
+ this.buffer = Buffer.alloc(0);
16
+ }
17
+ async open() {
18
+ await new Promise((resolve, reject) => {
19
+ var timer = setTimeout(() => { this.close(); reject(new Error('Codex 桌面连接超时')); }, 10000);
20
+ this.socket = net.createConnection(this.pipePath);
21
+ this.socket.once('connect', () => { clearTimeout(timer); resolve(); });
22
+ this.socket.on('data', chunk => this.receive(chunk));
23
+ this.socket.on('error', error => { clearTimeout(timer); this.abort(error); reject(error); });
24
+ this.socket.on('close', () => {
25
+ clearTimeout(timer);
26
+ var error = new Error('Codex 桌面连接已关闭;请在当前桌面会话重挂监听');
27
+ this.abort(error); reject(error);
28
+ });
29
+ });
30
+ }
31
+ abort(error) {
32
+ if (!this.failure) this.failure = error;
33
+ for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); }
34
+ this.pending.clear();
35
+ if (this.socket) this.socket.destroy();
36
+ }
37
+ receive(chunk) {
38
+ this.buffer = Buffer.concat([this.buffer, chunk]);
39
+ while (this.buffer.length >= 4) {
40
+ var size = os.endianness() === 'LE' ? this.buffer.readUInt32LE(0) : this.buffer.readUInt32BE(0);
41
+ if (size > MAX_FRAME) { this.abort(new Error('Codex 桌面响应超过大小限制')); return; }
42
+ if (this.buffer.length < size + 4) return;
43
+ var message;
44
+ try { message = JSON.parse(this.buffer.subarray(4, size + 4).toString('utf8')); }
45
+ catch (_) { this.abort(new Error('Codex 桌面返回无效 JSON')); return; }
46
+ this.buffer = this.buffer.subarray(size + 4);
47
+ var pending = this.pending.get(message.id);
48
+ if (!pending) continue;
49
+ this.pending.delete(message.id); clearTimeout(pending.timer);
50
+ if (message.error) pending.reject(new Error('Codex 桌面接口失败:' + message.error.message));
51
+ else pending.resolve(message.result);
52
+ }
53
+ }
54
+ request(method, params) {
55
+ if (this.failure) return Promise.reject(this.failure);
56
+ return new Promise((resolve, reject) => {
57
+ var id = ++this.nextId;
58
+ var body = Buffer.from(JSON.stringify({ id: id, jsonrpc: '2.0', method: method, params: params }));
59
+ if (body.length > MAX_FRAME) { reject(new Error('Codex 桌面请求超过大小限制')); return; }
60
+ var header = Buffer.alloc(4);
61
+ if (os.endianness() === 'LE') header.writeUInt32LE(body.length); else header.writeUInt32BE(body.length);
62
+ var timer = setTimeout(() => {
63
+ this.pending.delete(id);
64
+ reject(new Error('Codex 桌面 ' + method + ' 超时;结果不确定,请先核对会话,不要重复发送'));
65
+ }, 15000);
66
+ this.pending.set(id, { resolve: resolve, reject: reject, timer: timer });
67
+ try { this.socket.write(Buffer.concat([header, body])); }
68
+ catch (error) { this.pending.delete(id); clearTimeout(timer); reject(error); }
69
+ });
70
+ }
71
+ async call(tool, args, context) {
72
+ var result = await this.request('tools/call', {
73
+ callerSource: 'codex', namespace: 'codex_app', hostId: 'local',
74
+ callId: 'annotatable-' + crypto.randomUUID(), threadId: context.threadId,
75
+ turnId: context.turnId, tool: tool, arguments: args
76
+ });
77
+ if (!result || result.success !== true) throw new Error('Codex 桌面工具 ' + tool + ' 未成功;批注仍保留在文档中');
78
+ var item = (result.contentItems || []).find(item => item.type === 'inputText');
79
+ if (!item) throw new Error('Codex 桌面工具 ' + tool + ' 缺少结果');
80
+ try { return JSON.parse(item.text); }
81
+ catch (_) { throw new Error('Codex 桌面工具 ' + tool + ' 返回无效结果'); }
82
+ }
83
+ close() { this.abort(new Error('Codex 桌面连接已结束')); }
84
+ }
85
+
86
+ class DesktopNotifier {
87
+ constructor(pipePath, threadId, turnId) {
88
+ if (!pipePath || !threadId || !turnId) throw new Error('Codex 桌面通知缺少连接、会话或来源轮次');
89
+ this.rpc = new DesktopRpc(pipePath);
90
+ this.context = { threadId: threadId, turnId: turnId };
91
+ this.kind = 'desktop';
92
+ }
93
+ async open() {
94
+ await this.rpc.open();
95
+ var result = await this.rpc.request('tools/list', { threadStartKind: 'all' });
96
+ for (const name of ['read_thread', 'send_message_to_thread']) {
97
+ if (!(result.tools || []).some(tool => tool.namespace === 'codex_app' && tool.name === name)) {
98
+ throw new Error('Codex 桌面不提供 ' + name + ';无法恢复自动通知');
99
+ }
100
+ }
101
+ return this.read();
102
+ }
103
+ async read() {
104
+ var result = await this.rpc.call('read_thread', {
105
+ threadId: this.context.threadId, hostId: 'local', turnLimit: 1,
106
+ includeOutputs: false, maxOutputCharsPerItem: 0
107
+ }, this.context);
108
+ if (!result.thread || result.thread.id !== this.context.threadId || result.thread.hostId !== 'local') {
109
+ throw new Error('Codex 桌面返回了其他会话;停止监听');
110
+ }
111
+ return result;
112
+ }
113
+ async notify(hit, options) {
114
+ var prompt = '【批注工具自动通知】用户指定的页面收到待回应批注。请按 annotatable-html 工作流读取完整批注并回复,完成后重新挂同一页面的监听。\n' +
115
+ '这条消息由本机监听程序发出,不是用户新输入。\nPOLL_HIT ' + JSON.stringify(hit);
116
+ if (options && options.managed) {
117
+ prompt = '【指定页面批注自动续接】请读取并回复用户已指定页面的待回应批注。\n' +
118
+ '后台续接程序负责后续监听,不要重挂 poll 或启动另一个控制器。长回复通过 UTF-8 JSON 文件交给批注 CLI,完成后核对落盘。\n' +
119
+ 'POLL_HIT ' + JSON.stringify(hit);
120
+ }
121
+ var result = await this.rpc.call('send_message_to_thread', {
122
+ threadId: this.context.threadId, hostId: 'local', prompt: prompt
123
+ }, this.context);
124
+ if (result.threadId !== this.context.threadId) throw new Error('Codex 桌面未确认目标会话收到通知');
125
+ return { threadId: result.threadId, transport: this.kind };
126
+ }
127
+ close() { this.rpc.close(); }
128
+ }
129
+
130
+ module.exports = { DesktopRpc: DesktopRpc, DesktopNotifier: DesktopNotifier };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var JsonProcess = require('./duty-process').JsonProcess;
6
+ var util = require('util');
7
+
8
+ function canonicalPath(value) { return path.resolve(value).replace(/\\/g, '/').toLowerCase(); }
9
+ function readSettings(thread) {
10
+ if (!thread || !thread.id || !thread.path || !thread.modelProvider || !thread.model) throw new Error('会话缺少可核对的续接设置');
11
+ var fd = fs.openSync(thread.path, 'r'), size, buffer;
12
+ try {
13
+ size = fs.fstatSync(fd).size; buffer = Buffer.alloc(Math.min(size, 4 * 1024 * 1024));
14
+ fs.readSync(fd, buffer, 0, buffer.length, size - buffer.length);
15
+ } finally { fs.closeSync(fd); }
16
+ var tail = buffer.toString('utf8').split(/\r?\n/);
17
+ var context;
18
+ for (var line of tail) {
19
+ try { var row = JSON.parse(line); if (row.type === 'turn_context') context = row.payload; } catch (_) {}
20
+ }
21
+ if (!context || !context.permission_profile || !context.sandbox_policy || !context.approval_policy) {
22
+ throw new Error('无法核对会话原有权限;保留批注,不启动无头模型');
23
+ }
24
+ if (canonicalPath(context.cwd) !== canonicalPath(thread.cwd) || context.model !== thread.model) {
25
+ throw new Error('会话元数据与最近轮次设置不一致');
26
+ }
27
+ return { model: thread.model, modelProvider: thread.modelProvider, cwd: thread.cwd,
28
+ reasoningEffort: context.effort || thread.reasoningEffort || null,
29
+ approvalPolicy: context.approval_policy, approvalsReviewer: context.approvals_reviewer || 'user',
30
+ sandbox: context.sandbox_policy, permissionProfile: context.permission_profile, rolloutPath: thread.path };
31
+ }
32
+ function normalizeSandbox(policy, cwd) {
33
+ var type = { 'workspace-write': 'workspaceWrite', 'read-only': 'readOnly', 'danger-full-access': 'dangerFullAccess', 'external-sandbox': 'externalSandbox' }[policy.type] || policy.type;
34
+ var result = { type: type, network: policy.networkAccess === undefined ? policy.network_access || false : policy.networkAccess };
35
+ if (type === 'workspaceWrite') {
36
+ result.roots = Array.from(new Set([cwd].concat(policy.writableRoots || policy.writable_roots || []).map(canonicalPath))).sort();
37
+ result.excludeTmp = !!(policy.excludeTmpdirEnvVar || policy.exclude_tmpdir_env_var);
38
+ result.excludeSlash = !!(policy.excludeSlashTmp || policy.exclude_slash_tmp);
39
+ }
40
+ return result;
41
+ }
42
+ function assertRestored(saved, restored) {
43
+ var valid = restored.thread && restored.thread.id && restored.model === saved.model && restored.modelProvider === saved.modelProvider &&
44
+ canonicalPath(restored.cwd) === canonicalPath(saved.cwd) &&
45
+ util.isDeepStrictEqual(saved.approvalPolicy, restored.approvalPolicy) && saved.approvalsReviewer === restored.approvalsReviewer &&
46
+ util.isDeepStrictEqual(normalizeSandbox(saved.sandbox, saved.cwd), normalizeSandbox(restored.sandbox, restored.cwd));
47
+ if (!valid) throw new Error('无头恢复后的模型、目录或权限不一致;未启动回复轮次');
48
+ }
49
+
50
+ class StdioRpc extends JsonProcess {
51
+ constructor(binary, home, cwd) {
52
+ var env = require('./duty-process').agentEnv({ CODEX_HOME: home });
53
+ super(binary, ['app-server', '--stdio'], { cwd: cwd, env: env });
54
+ }
55
+ async open() {
56
+ await this.request('initialize', { clientInfo: { name: 'annotatable_duty', version: require('../package.json').version }, capabilities: { experimentalApi: true } });
57
+ this.send({ method: 'initialized', params: {} });
58
+ }
59
+ }
60
+
61
+ async function runHeadless(binding, hit, options) {
62
+ options = options || {};
63
+ if (binding.settings.rolloutPath) {
64
+ var latest = readSettings({ id: binding.threadId, path: binding.settings.rolloutPath, model: binding.settings.model,
65
+ modelProvider: binding.settings.modelProvider, cwd: binding.settings.cwd });
66
+ if (!util.isDeepStrictEqual(latest, binding.settings)) throw new Error('注册后会话设置已改变;请重新注册,不自动覆盖原设置');
67
+ }
68
+ var released = await (options.checkReleased || require('./duty-process').released)(binding.desktop.owners);
69
+ if (!released.released) throw new Error('桌面运行时尚未确认退出;不启动第二份会话');
70
+ var rpc = options.rpc || new StdioRpc(binding.binary, binding.codexHome, binding.settings.cwd);
71
+ var record = { startedAt: Date.now(), threadId: binding.threadId, pid: rpc.child && rpc.child.pid, outcome: 'starting', events: [] };
72
+ if (options.onStart) options.onStart(record);
73
+ var turnId;
74
+ var requestError;
75
+ var terminal;
76
+ var timeout;
77
+ var finished = new Promise((resolve, reject) => { terminal = { resolve: resolve, reject: reject }; });
78
+ // Avoid an unhandled rejection while initialization is still in progress.
79
+ finished.catch(function () {});
80
+ function interrupt(reason) {
81
+ requestError = reason;
82
+ if (turnId) rpc.request('turn/interrupt', { threadId: binding.threadId, turnId: turnId }).catch(function () {});
83
+ }
84
+ rpc.on('serverRequest', message => {
85
+ if (message.method === 'item/tool/call') {
86
+ rpc.send({ id: message.id, result: { success: false, contentItems: [{ type: 'inputText', text: '当前为无头宿主,桌面动态工具不可用。使用内置命令执行工具运行本地批注 CLI;不要申请扩大权限。' }] } });
87
+ record.events.push({ kind: 'desktop-tool-unavailable', tool: message.params.tool });
88
+ } else if (/requestApproval$/.test(message.method)) {
89
+ rpc.send({ id: message.id, result: { decision: 'decline' } });
90
+ interrupt('需要人工审批;批注保留待处理');
91
+ } else {
92
+ rpc.send({ id: message.id, error: { code: -32601, message: 'Unattended request requires a user or an unavailable host' } });
93
+ interrupt('无头宿主无法处理 ' + message.method);
94
+ }
95
+ });
96
+ rpc.on('notification', message => {
97
+ if (message.params && message.params.threadId && message.params.threadId !== binding.threadId) return;
98
+ if (message.method === 'turn/completed') {
99
+ record.turnStatus = message.params.turn.status;
100
+ terminal.resolve(message.params.turn);
101
+ }
102
+ if (message.method === 'item/completed' && message.params.item && message.params.item.type === 'agentMessage') {
103
+ record.finalMessage = message.params.item.text;
104
+ }
105
+ });
106
+ rpc.on('exit', () => terminal.reject(new Error('无头进程在轮次完成前退出')));
107
+ try {
108
+ await rpc.open();
109
+ var restored = await rpc.request('thread/resume', { threadId: binding.threadId, excludeTurns: true, model: binding.settings.model });
110
+ if (!restored.thread || restored.thread.id !== binding.threadId) throw new Error('恢复到了其他会话');
111
+ assertRestored(binding.settings, restored);
112
+ record.restored = { model: restored.model, modelProvider: restored.modelProvider, cwd: restored.cwd,
113
+ approvalPolicy: restored.approvalPolicy, approvalsReviewer: restored.approvalsReviewer, sandbox: restored.sandbox };
114
+ if (!(await (options.checkReleased || require('./duty-process').released)(binding.desktop.owners)).released) throw new Error('桌面已重开;不启动无头回复轮次');
115
+ var prompt = require('./duty').prompt(binding, hit, { headless: true });
116
+ var started = await rpc.request('turn/start', { threadId: binding.threadId, input: [{ type: 'text', text: prompt }], effort: binding.settings.reasoningEffort || undefined });
117
+ turnId = started.turn.id; record.turnId = turnId;
118
+ if (requestError) interrupt(requestError);
119
+ timeout = setTimeout(() => { interrupt('无头处理超时,保留待办'); setTimeout(() => terminal.reject(new Error(requestError)), 10000).unref(); }, options.timeoutMs || 300000);
120
+ var turn = await finished;
121
+ if (requestError || turn.status !== 'completed') throw new Error(requestError || '无头轮次未完成:' + turn.status);
122
+ record.outcome = 'completed';
123
+ return record;
124
+ } catch (error) { record.outcome = 'failed'; record.error = error.message; throw Object.assign(error, { dutyRun: record }); }
125
+ finally { clearTimeout(timeout); await rpc.close(); record.finishedAt = Date.now(); if (options.onFinish) options.onFinish(record); }
126
+ }
127
+
128
+ module.exports = { readSettings: readSettings, normalizeSandbox: normalizeSandbox, assertRestored: assertRestored,
129
+ StdioRpc: StdioRpc, runHeadless: runHeadless, canonicalPath: canonicalPath };
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 };