@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.11
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 +53 -64
- package/dist/bin/codex-remote.mjs +90 -71
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +64 -24
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +16 -21
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +59 -10
- package/dist/src/managed-app-server.mjs +36 -14
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +6 -8
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/codex-runtime.mjs +117 -0
- package/dist/src/platform/windows/desktop-host.mjs +17 -3
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -16
- package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +71 -0
- package/dist/src/self-relay/admin-server.mjs +61 -0
- package/dist/src/self-relay/admin-state.mjs +122 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +107 -20
- package/dist/src/self-relay/demo.mjs +4 -5
- package/dist/src/self-relay/lifecycle.mjs +58 -70
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +224 -29
- package/dist/src/server.mjs +616 -1299
- package/dist/src/service-doctor.mjs +14 -26
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/service-lifecycle.mjs +12 -12
- package/dist/src/shared-app-server.mjs +206 -46
- package/dist/src/shared-catalog.mjs +5 -25
- package/dist/src/shared-recovery.mjs +11 -9
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +16 -18
- package/tools/postinstall.mjs +140 -3
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -355
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -13
- package/dist/src/thread-catalog.mjs +0 -42
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -57
- package/dist/web/chat-transport.js +0 -196
- package/dist/web/chat.css +0 -736
- package/dist/web/chat.js +0 -5149
- package/dist/web/client-platform.js +0 -11
- package/dist/web/harmony-platform.js +0 -51
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -146
- package/dist/web/index.html +0 -222
- package/dist/web/lan-session.js +0 -122
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -83
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -119
- package/dist/web/server-requests.js +0 -67
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
package/dist/src/server.mjs
CHANGED
|
@@ -1,151 +1,131 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import http from 'node:http';
|
|
4
|
-
import net from 'node:net';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import os from 'node:os';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import vm from 'node:vm';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
7
|
+
import vm from 'node:vm';
|
|
8
|
+
import sharp from 'sharp';
|
|
9
|
+
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
10
|
+
import fileMentions from '../shared/file-mentions.cjs';
|
|
11
|
+
import { validateInlineImage } from './message-images.mjs';
|
|
11
12
|
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
12
13
|
import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
|
|
14
|
+
import { RuntimeDiscovery } from './runtime-discovery.mjs';
|
|
13
15
|
import { streamSharedThread } from './shared-thread-stream.mjs';
|
|
14
|
-
import { AppServerClient } from './app-server-client.mjs';
|
|
15
|
-
import { AppServerTaskStore, AppServerTaskTransport } from './app-server-tasks.mjs';
|
|
16
|
-
import { supplementThreadCatalog } from './thread-catalog.mjs';
|
|
17
16
|
import appServerEvents from '../shared/app-server-events.cjs';
|
|
18
17
|
import { runtimePaths } from './platform/windows/runtime-paths.mjs';
|
|
19
18
|
import { openControl } from './service-control.mjs';
|
|
20
19
|
import { writeRecord, readRecord } from './service-lifecycle.mjs';
|
|
21
20
|
import { ExecutionHealth } from './execution-health.mjs';
|
|
22
|
-
import { activeDebugPort, serveDebugResource } from './debug-ui.mjs';
|
|
23
21
|
import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
|
|
24
22
|
import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
|
|
25
|
-
import { DesktopMonitor } from './platform/windows/desktop-monitor.mjs';
|
|
26
23
|
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
27
24
|
import { SharedCatalog } from './shared-catalog.mjs';
|
|
28
25
|
import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
|
|
29
26
|
import { requestServiceRestart } from './service-restart.mjs';
|
|
30
27
|
|
|
31
|
-
const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
32
28
|
const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
33
29
|
const MAX_CACHED_ATTACHMENTS = 32;
|
|
34
30
|
const MAX_CACHED_FILES = 64;
|
|
35
31
|
const MAX_CONTEXT_TAIL_BYTES = 4 * 1024 * 1024;
|
|
36
|
-
const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
32
|
+
const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
33
|
+
const SHARED_HISTORY_PAGE_BYTES = 500 * 1024;
|
|
34
|
+
const SHARED_HISTORY_PAGE_TURNS = 8;
|
|
37
35
|
const MAX_SETTINGS_SCAN_BYTES = 64 * 1024 * 1024;
|
|
38
|
-
const ALLOWED_MODELS = new Set([
|
|
39
|
-
'gpt-6-astra',
|
|
36
|
+
const ALLOWED_MODELS = new Set([
|
|
37
|
+
'gpt-6-astra',
|
|
38
|
+
'gpt-6-sol',
|
|
40
39
|
'gpt-5.6-sol',
|
|
41
40
|
'gpt-5.6-terra',
|
|
42
41
|
'gpt-5.6-luna',
|
|
43
42
|
'gpt-5.5',
|
|
44
43
|
'gpt-5.3-codex-spark',
|
|
45
44
|
]);
|
|
46
|
-
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
46
|
+
|
|
47
|
+
class RequestError extends Error {
|
|
48
|
+
constructor(statusCode, message, details) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.statusCode = statusCode;
|
|
51
|
+
this.details = details;
|
|
52
|
+
this.code = details?.code;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readRuntimeVersion() {
|
|
57
|
+
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
while (true) {
|
|
59
|
+
const manifest = readRecord(path.join(directory, 'package.json'));
|
|
60
|
+
if (manifest?.name === '@walkhi/code-relax') return manifest.version || '未知';
|
|
61
|
+
const parent = path.dirname(directory);
|
|
62
|
+
if (parent === directory) return '未知';
|
|
63
|
+
directory = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const options = parseOptions(process.argv.slice(2));
|
|
68
|
+
const runtimeVersion = readRuntimeVersion();
|
|
61
69
|
|
|
62
70
|
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
|
|
63
|
-
const repositoryRoot = path.resolve(webRoot, '..', '..');
|
|
64
71
|
const projectRoot = os.homedir();
|
|
65
72
|
const { codexDataRoot, stateRoot, sessionsRoot, uploadsRoot } = runtimePaths();
|
|
66
73
|
const markdownItScript = fs.readFileSync(path.join(webRoot, 'vendor', 'markdown-it.umd.min.js'));
|
|
67
74
|
const markdownSandbox = { atob };
|
|
68
75
|
vm.runInNewContext(markdownItScript.toString(), markdownSandbox);
|
|
69
76
|
const imageMarkdown = markdownSandbox.markdownit({ html: false });
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const allowedUploadsRoots = [uploadsRoot, legacyUploadsRoot,
|
|
76
|
-
...JSON.parse(process.env.CODEX_REMOTE_LEGACY_UPLOADS_ROOTS || '[]')];
|
|
77
|
+
const attachmentCache = new Map();
|
|
78
|
+
const fileCache = new Map();
|
|
79
|
+
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
80
|
+
const imageCapabilityKey = crypto.createHash('sha256')
|
|
81
|
+
.update('code-relax-image-capability\0').update(options.token || fileCapabilitySecret).digest();
|
|
77
82
|
const sessionPathCache = new Map();
|
|
78
83
|
const sessionSettingsCache = new Map();
|
|
79
84
|
const liveActivityCache = new Map();
|
|
80
|
-
const messageQueues = new Map();
|
|
81
85
|
let sharedServer = null;
|
|
82
|
-
const desktopMonitor = new DesktopMonitor({
|
|
83
|
-
read: (tool, args, pipe) => callTool(tool, args, 5000, pipe),
|
|
84
|
-
});
|
|
85
86
|
const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
|
|
87
|
+
const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
|
|
86
88
|
let sharedCatalog = null, executionMode = 'ordinary';
|
|
87
89
|
function selectSharedExecution(enabled) {
|
|
88
90
|
const next = enabled ? managedAppServer.client : null;
|
|
89
91
|
if (sharedServer === next) return;
|
|
90
92
|
sharedServer = next;
|
|
91
|
-
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer
|
|
93
|
+
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer) : null;
|
|
92
94
|
}
|
|
93
95
|
const executionHealth = new ExecutionHealth(async () => {
|
|
94
96
|
await managedAppServer.ensure();
|
|
95
|
-
const [desktop, attachedUrl] = await Promise.all([desktopHost.inspect(), desktopHost.sharedServiceUrl()]);
|
|
96
|
-
const attached = Boolean(desktop.running && attachedUrl === managedAppServerUrl);
|
|
97
97
|
const managed = managedAppServer.snapshot();
|
|
98
98
|
// The managed app-server owns execution. Desktop is another client of the
|
|
99
99
|
// same service, so closing Desktop must not disable phone writes.
|
|
100
100
|
selectSharedExecution(managed.ready);
|
|
101
101
|
executionMode = sharedServer ? 'shared' : 'ordinary';
|
|
102
|
-
const basicReady = Boolean(desktop.running && sourceThreadId && desktopMonitor.pipe);
|
|
103
102
|
const managedRestarting = managed.state === 'restarting';
|
|
104
|
-
const ready = !managedRestarting &&
|
|
103
|
+
const ready = !managedRestarting && sharedServer?.ready === true;
|
|
105
104
|
const state = managedRestarting ? 'restarting'
|
|
106
|
-
: ready ?
|
|
107
|
-
return { state, ready, canWrite: ready, mode: executionMode
|
|
108
|
-
desktop: { state: !desktop.running ? 'stopped' : attached ? 'managed' : 'ordinary', attachedToManagedServer: attached } };
|
|
105
|
+
: ready ? 'full' : 'unavailable';
|
|
106
|
+
return { state, ready, canWrite: ready, mode: executionMode };
|
|
109
107
|
});
|
|
110
108
|
async function refreshExecution() {
|
|
111
|
-
sourceThreadId = readRecord(path.join(stateRoot, 'bridge-startup-context.json'))?.sourceThreadId || sourceThreadId;
|
|
112
109
|
await executionHealth.refresh();
|
|
113
110
|
return { mode: executionMode };
|
|
114
111
|
}
|
|
115
112
|
async function requireExecutionWritable(threadId) {
|
|
116
|
-
const recovery = readRecord(path.join(stateRoot, 'shared-recovery.json'));
|
|
117
|
-
if (['scheduled', 'running'].includes(recovery?.status)) {
|
|
118
|
-
throw new RequestError(409, 'Codex 正在重新启动,当前不能提交写操作。', { code: 'desktop_recovering' });
|
|
119
|
-
}
|
|
120
113
|
const execution = await executionHealth.refresh();
|
|
121
114
|
if (!execution.canWrite) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
: 'Codex Desktop 尚未建立基础控制连接,请稍后重新检测。';
|
|
125
|
-
throw new RequestError(409, message, { code: 'execution_not_writable', execution });
|
|
115
|
+
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
116
|
+
{ code: 'full_control_required', execution });
|
|
126
117
|
}
|
|
127
118
|
if (threadId && sharedServer) sharedServer.requireWritable(threadId);
|
|
128
119
|
return execution;
|
|
129
120
|
}
|
|
130
|
-
managedAppServer.client.on('subscription-error', error => console.error('
|
|
131
|
-
const queueWorkers = new Map();
|
|
132
|
-
const appServer = new AppServerClient({ cwd: projectRoot });
|
|
133
|
-
const appServerTasks = new AppServerTaskTransport({
|
|
134
|
-
client: appServer,
|
|
135
|
-
store: new AppServerTaskStore(path.join(stateRoot, 'app-server-tasks.json')),
|
|
136
|
-
});
|
|
137
|
-
appServer.on('protocol-error', error => console.error(`app-server 协议错误:${error.message}`));
|
|
138
|
-
appServer.on('server-request', request => {
|
|
139
|
-
console.log(`app-server 等待客户端处理反向请求:${request.method} (${request.id})`);
|
|
140
|
-
});
|
|
121
|
+
managedAppServer.client.on('subscription-error', error => console.error('Codex 内核任务取消订阅失败:', error.message));
|
|
141
122
|
|
|
142
123
|
async function handleRequest(request, response) {
|
|
143
124
|
try {
|
|
144
125
|
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
145
126
|
|
|
146
127
|
if (!request.socketRequest) {
|
|
147
|
-
|
|
148
|
-
sendJson(response, 404, { error: 'Bridge 调试页面未开启,或请求的资源不存在。' });
|
|
128
|
+
sendJson(response, 404, { error: '此端口不提供网页。' });
|
|
149
129
|
return;
|
|
150
130
|
}
|
|
151
131
|
|
|
@@ -170,7 +150,7 @@ async function handleRequest(request, response) {
|
|
|
170
150
|
}
|
|
171
151
|
if (request.method === 'POST' && url.pathname === '/api/shared-recovery') {
|
|
172
152
|
const body = await readJsonBody(request);
|
|
173
|
-
if (body.confirmed !== true || typeof body.operationId !== 'string') throw new RequestError(400, '请确认重新打开当前 Codex
|
|
153
|
+
if (body.confirmed !== true || typeof body.operationId !== 'string') throw new RequestError(400, '请确认重新打开当前 Codex 并接入 Codex 内核后再继续。');
|
|
174
154
|
sendJson(response, 202, await requestRecovery(stateRoot, body)); return;
|
|
175
155
|
}
|
|
176
156
|
if (request.method === 'POST' && url.pathname === '/api/service/restart') {
|
|
@@ -192,49 +172,65 @@ async function handleRequest(request, response) {
|
|
|
192
172
|
return;
|
|
193
173
|
}
|
|
194
174
|
|
|
175
|
+
if (request.method === 'GET' && url.pathname === '/api/official-remote/status') {
|
|
176
|
+
if (!managedAppServer.client.ready) throw new RequestError(503, 'Codex 内核未就绪,无法读取官方工作站状态。');
|
|
177
|
+
const result = await managedAppServer.client.request('remoteControl/status/read', {});
|
|
178
|
+
sendJson(response, 200, { status: result.status, environmentId: result.environmentId });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (request.method === 'POST' && url.pathname === '/api/official-remote/pairing') {
|
|
182
|
+
if (!managedAppServer.client.ready) throw new RequestError(503, 'Codex 内核未就绪,无法生成官方配对码。');
|
|
183
|
+
const status = await managedAppServer.client.request('remoteControl/status/read', {});
|
|
184
|
+
if (status.status !== 'connected') throw new RequestError(409, '当前 Codex 内核未连接 OpenAI Remote,无法配对。');
|
|
185
|
+
const result = await managedAppServer.client.request('remoteControl/pairing/start', { manualCode: true });
|
|
186
|
+
if (!result.manualPairingCode || !result.environmentId) throw new RequestError(502, 'Codex 内核未返回有效的官方配对码。');
|
|
187
|
+
// One-time code: return only to the authenticated station controller; never persist or log it.
|
|
188
|
+
sendJson(response, 200, { manualPairingCode: result.manualPairingCode,
|
|
189
|
+
environmentId: result.environmentId, expiresAt: result.expiresAt });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
195
193
|
if (request.method === 'GET' && url.pathname === '/api/health') {
|
|
196
194
|
// Status readers consume the workstation's cached sample. A phone query
|
|
197
195
|
// must not trigger PowerShell, process-identity, or app-server checks.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
execution: { ...executionHealth.state, ready: executionHealth.state.ready && (!sharedServer || sharedServer.ready),
|
|
203
|
-
...(recovering ? { state: 'recovering' } : {}), mode: executionMode },
|
|
196
|
+
sendJson(response, 200, {
|
|
197
|
+
status: 'ok', resident: true, version: runtimeVersion,
|
|
198
|
+
execution: { ...executionHealth.state, ready: executionHealth.state.ready && sharedServer?.ready === true,
|
|
199
|
+
mode: executionMode },
|
|
204
200
|
managedAppServer: managedAppServer.snapshot(),
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
debugUiPort: await activeDebugPort(stateRoot, options.port),
|
|
209
|
-
desktopConnected: Boolean(desktopMonitor.pipe),
|
|
210
|
-
sharedAppServerConnected: sharedServer?.ready === true,
|
|
211
|
-
sharedRecoveryAvailable: true,
|
|
212
|
-
desktopOrderingReady: Boolean(desktopMonitor.snapshot),
|
|
213
|
-
toolCount: 0,
|
|
201
|
+
...runtimeDiscovery.state,
|
|
202
|
+
sharedAppServerConnected: sharedServer?.ready === true,
|
|
203
|
+
sharedRecoveryAvailable: runtimeDiscovery.state.desktop?.installed === true,
|
|
214
204
|
mode: options.lan ? 'lan' : 'loopback',
|
|
215
205
|
tokenRequired: Boolean(options.token),
|
|
216
206
|
nativeImageTransport: sharedServer ? 'shared-app-server' : null,
|
|
217
|
-
transportMode: sharedServer ? 'lan-shared' : '
|
|
218
|
-
appServer: {
|
|
219
|
-
state: appServer.state,
|
|
220
|
-
cliVersion: appServer.cliVersion,
|
|
221
|
-
taskCount: appServerTasks.store.list().length,
|
|
222
|
-
},
|
|
207
|
+
transportMode: sharedServer?.ready === true ? 'lan-shared' : 'unavailable',
|
|
223
208
|
});
|
|
224
209
|
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
213
|
+
if (request.method === 'GET' && imageMatch) {
|
|
214
|
+
let image = attachmentCache.get(imageMatch[1])
|
|
215
|
+
|| recoverLocalImage(imageMatch[1], url.searchParams.get('recovery') || '');
|
|
216
|
+
const contextThreadId = url.searchParams.get('threadId') || '';
|
|
217
|
+
const contextTurnId = url.searchParams.get('turnId') || '';
|
|
218
|
+
if (!image && sharedServer && contextThreadId && contextTurnId
|
|
219
|
+
&& contextThreadId.length <= 128 && contextTurnId.length <= 128) {
|
|
220
|
+
try {
|
|
221
|
+
normalizeTurn(await sharedServer.turnContext(contextThreadId, contextTurnId));
|
|
222
|
+
image = attachmentCache.get(imageMatch[1]);
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
if (!image) throw new RequestError(404, '这张图片已失效,无法从工作站重新读取。');
|
|
226
|
+
const thumbnail = url.searchParams.get('variant') === 'thumbnail';
|
|
227
|
+
const asset = thumbnail ? await attachmentThumbnail(image) : { buffer: image.buffer, mimeType: image.mimeType };
|
|
228
|
+
sendJson(response, 200, { url: `data:${asset.mimeType};base64,${asset.buffer.toString('base64')}` });
|
|
233
229
|
return;
|
|
234
230
|
}
|
|
235
231
|
|
|
236
232
|
const fileMatch = /^\/api\/files\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
237
|
-
if (request.method === 'GET' && fileMatch) {
|
|
233
|
+
if (request.method === 'GET' && fileMatch) {
|
|
238
234
|
const file = fileCache.get(fileMatch[1]);
|
|
239
235
|
if (!file) throw new RequestError(404, '文件授权已失效,请重新读取对话。');
|
|
240
236
|
let stat;
|
|
@@ -245,67 +241,47 @@ async function handleRequest(request, response) {
|
|
|
245
241
|
}
|
|
246
242
|
if (stat.size > MAX_ATTACHMENT_BYTES) throw new RequestError(413, '暂只支持下载不超过 16 MiB 的文件。');
|
|
247
243
|
sendJson(response, 200, { name: file.name, size: file.size, dataBase64: fs.readFileSync(file.path).toString('base64') });
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (url.pathname.startsWith('/api/app-server/') || url.pathname === '/api/wait') {
|
|
248
|
+
throw new RequestError(410, '此连接方式已停用。请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
249
|
+
}
|
|
250
|
+
const taskPath = /^\/api\/threads\/([^/]+)/.exec(url.pathname);
|
|
251
|
+
if (taskPath && (url.pathname.endsWith('/takeover') || url.pathname === `/api/threads/${taskPath[1]}`)) {
|
|
252
|
+
throw new RequestError(410, '此连接方式已停用。', { code: 'unsupported_control' });
|
|
253
|
+
}
|
|
254
|
+
if (url.searchParams.has('hostId') && url.searchParams.get('hostId') !== 'local') {
|
|
255
|
+
throw new RequestError(410, '此对话的连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
256
|
+
}
|
|
257
|
+
if (!executionHealth.state.ready || sharedServer?.ready !== true) {
|
|
258
|
+
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
259
|
+
{ code: 'full_control_required' });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (request.method === 'GET' && url.pathname === '/api/models') {
|
|
263
|
+
sendJson(response, 200, await sharedServer.models());
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
256
266
|
|
|
257
267
|
if (request.method === 'GET' && url.pathname === '/api/threads') {
|
|
258
268
|
const limit = boundedInteger(url.searchParams.get('limit'), 20, 1, 200);
|
|
259
269
|
const archived = url.searchParams.get('archived') === 'true';
|
|
260
|
-
|
|
261
|
-
const sharedSnapshot = Boolean(catalog);
|
|
262
|
-
let desktopCatalog;
|
|
263
|
-
if (catalog) desktopCatalog = await catalog.threads(limit, archived);
|
|
264
|
-
else {
|
|
265
|
-
if (archived) throw new RequestError(400, '当前连接不支持归档对话。请启用完整控制。');
|
|
266
|
-
const listing = await callTool('list_threads', { limit: Math.min(limit, 50) });
|
|
267
|
-
if (!listing.success) { sendToolResult(response, listing); return; }
|
|
268
|
-
const projects = await callTool('list_projects', {});
|
|
269
|
-
if (!projects.success) { sendToolResult(response, projects); return; }
|
|
270
|
-
const catalogProjects = projects.data.projects || [];
|
|
271
|
-
desktopCatalog = { ...supplementThreadCatalog(listing.data, catalogProjects, codexDataRoot, limit),
|
|
272
|
-
projects: catalogProjects };
|
|
273
|
-
}
|
|
274
|
-
let appServerThreads = archived ? [] : appServerTasks.store.list().map(record => ({
|
|
275
|
-
id: record.id,
|
|
276
|
-
kind: 'codex',
|
|
277
|
-
hostId: 'app-server',
|
|
278
|
-
title: record.title,
|
|
279
|
-
cwd: record.cwd,
|
|
280
|
-
projectId: record.projectId,
|
|
281
|
-
updatedAt: record.updatedAt,
|
|
282
|
-
status: 'unknown',
|
|
283
|
-
transport: 'app-server',
|
|
284
|
-
}));
|
|
285
|
-
if (appServerThreads.length) {
|
|
286
|
-
try { appServerThreads = await appServerTasks.listTasks(); } catch {}
|
|
287
|
-
}
|
|
288
|
-
sendJson(response, 200, mergeThreadCatalog(desktopCatalog, appServerThreads, limit, sharedSnapshot));
|
|
270
|
+
sendJson(response, 200, await sharedCatalog.threads(limit, archived));
|
|
289
271
|
return;
|
|
290
272
|
}
|
|
291
273
|
|
|
292
274
|
if (request.method === 'GET' && url.pathname === '/api/projects') {
|
|
293
|
-
|
|
294
|
-
sendJson(response, 200, { projects: (await sharedCatalog.projects()).projects });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const result = await callTool('list_projects', {});
|
|
298
|
-
sendToolResult(response, result);
|
|
275
|
+
sendJson(response, 200, { projects: (await sharedCatalog.projects()).projects });
|
|
299
276
|
return;
|
|
300
277
|
}
|
|
301
278
|
|
|
302
279
|
if (request.method === 'GET' && url.pathname === '/api/usage-limits') {
|
|
303
|
-
|
|
304
|
-
else sendToolResult(response, await callTool('get_usage_limits', {}));
|
|
280
|
+
sendJson(response, 200, await sharedServer.usage());
|
|
305
281
|
return;
|
|
306
282
|
}
|
|
307
283
|
|
|
308
|
-
if (request.method === 'GET' && url.pathname === '/api/usage-events'
|
|
284
|
+
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
309
285
|
const execution = sharedServer;
|
|
310
286
|
response.startEvents();
|
|
311
287
|
const emit = value => { if (!response.destroyed) response.event('usage', value); };
|
|
@@ -317,196 +293,107 @@ async function handleRequest(request, response) {
|
|
|
317
293
|
return;
|
|
318
294
|
}
|
|
319
295
|
|
|
320
|
-
if (request.method === 'GET' && url.pathname === '/api/app-server/status') {
|
|
321
|
-
sendJson(response, 200, appServerBridgeStatus());
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (request.method === 'POST' && url.pathname === '/api/app-server/start') {
|
|
326
|
-
await requireExecutionWritable();
|
|
327
|
-
await appServer.start();
|
|
328
|
-
sendJson(response, 200, appServerBridgeStatus());
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
296
|
const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
|
|
333
297
|
if (request.method === 'POST' && createThreadMatch) {
|
|
334
298
|
await requireExecutionWritable();
|
|
335
299
|
const projectId = decodeURIComponent(createThreadMatch[1]);
|
|
336
|
-
const body =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const rpc = (method, params) => execution.request(method, params);
|
|
369
|
-
let target = appServerEvents.projectlessThreadTarget(projectRoot);
|
|
370
|
-
if (project) {
|
|
371
|
-
const nativeProjects = await appServerEvents.readProjects(rpc);
|
|
372
|
-
const nativeId = nativeProjects.find(item => item.id === project.projectId)?.id;
|
|
373
|
-
if (!nativeId) throw new RequestError(404, '完整控制中没有找到对应项目,请刷新项目列表。');
|
|
374
|
-
target = { projectId:nativeId, cwd:project.path };
|
|
375
|
-
}
|
|
376
|
-
const { input } = appServerTurnArguments('', body);
|
|
377
|
-
const created = await appServerEvents.createThread(rpc, target, input, body);
|
|
378
|
-
const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
379
|
-
void generateAndSetThreadTitle({
|
|
380
|
-
request: rpc,
|
|
381
|
-
subscribe: listener => {
|
|
382
|
-
execution.on('notification', listener);
|
|
383
|
-
return () => execution.off('notification', listener);
|
|
384
|
-
},
|
|
385
|
-
threadId: created.threadId,
|
|
386
|
-
prompt,
|
|
387
|
-
cwd: target.cwd || null,
|
|
388
|
-
}).catch(error => console.error(`生成对话标题失败:${error.message}`));
|
|
389
|
-
sendJson(response, 201, { ...created, hostId:'local' });
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const argumentsValue = await createThreadArguments(projectId, body, projects);
|
|
393
|
-
sendToolResult(response, await callTool('create_thread', argumentsValue, 90_000));
|
|
394
|
-
return;
|
|
300
|
+
const body = await readJsonBody(request);
|
|
301
|
+
if (body.transport === 'app-server' || (body.hostId && body.hostId !== 'local')) {
|
|
302
|
+
throw new RequestError(410, '此连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
303
|
+
}
|
|
304
|
+
const projects = (await sharedCatalog.projects()).projects;
|
|
305
|
+
const project = projects.find(item => item.projectId === projectId);
|
|
306
|
+
if (projectId !== '__unassigned__' && !project) throw new RequestError(404, '没有找到对应项目。');
|
|
307
|
+
const execution = sharedServer;
|
|
308
|
+
await execution.connect();
|
|
309
|
+
const rpc = (method, params) => execution.request(method, params);
|
|
310
|
+
let target = appServerEvents.projectlessThreadTarget(projectRoot);
|
|
311
|
+
if (project) {
|
|
312
|
+
const nativeProjects = await appServerEvents.readProjects(rpc);
|
|
313
|
+
const nativeId = nativeProjects.find(item => item.id === project.projectId)?.id;
|
|
314
|
+
if (!nativeId) throw new RequestError(404, 'Codex 内核中没有找到对应项目,请刷新项目列表。');
|
|
315
|
+
target = { projectId:nativeId, cwd:project.path };
|
|
316
|
+
}
|
|
317
|
+
const { input } = appServerTurnArguments('', body);
|
|
318
|
+
const created = await appServerEvents.createThread(rpc, target, input, body);
|
|
319
|
+
const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
320
|
+
void generateAndSetThreadTitle({
|
|
321
|
+
request: rpc,
|
|
322
|
+
subscribe: listener => {
|
|
323
|
+
execution.on('notification', listener);
|
|
324
|
+
return () => execution.off('notification', listener);
|
|
325
|
+
},
|
|
326
|
+
threadId: created.threadId,
|
|
327
|
+
prompt,
|
|
328
|
+
cwd: target.cwd || null,
|
|
329
|
+
}).catch(error => console.error(`生成对话标题失败:${error.message}`));
|
|
330
|
+
sendJson(response, 201, { ...created, hostId:'local' });
|
|
331
|
+
return;
|
|
395
332
|
}
|
|
396
333
|
|
|
397
|
-
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
334
|
+
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
398
335
|
if (request.method === 'GET' && timelineMatch) {
|
|
399
336
|
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
includeTurns: true,
|
|
420
|
-
turnLimit: boundedInteger(url.searchParams.get('turnLimit'), 10, 1, 10),
|
|
421
|
-
});
|
|
422
|
-
sendJson(response, 200, normalizeAppServerTimeline(thread));
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const argumentsValue = readThreadArguments(url, threadId, 10);
|
|
426
|
-
const result = await callTool('read_thread', argumentsValue);
|
|
427
|
-
if (result.success) {
|
|
428
|
-
const timeline = normalizeTimeline(result.data);
|
|
429
|
-
enrichTimelineWithLocalActivity(timeline, threadId);
|
|
430
|
-
sendJson(response, 200, timeline);
|
|
431
|
-
} else {
|
|
432
|
-
sendJson(response, 502, { error: 'Codex Desktop 工具返回失败。', details: result.data });
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
337
|
+
const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
|
|
338
|
+
const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
|
|
339
|
+
const knownTurnId = url.searchParams.get('knownTurnId') || '';
|
|
340
|
+
const knownTurnStatus = url.searchParams.get('knownTurnStatus') || '';
|
|
341
|
+
const knownHistoryView = url.searchParams.get('knownHistoryView') || '';
|
|
342
|
+
const latest = timeline.turns.at(-1);
|
|
343
|
+
const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
|
|
344
|
+
const sameHistoryView = knownHistoryView === timeline.historyView;
|
|
345
|
+
const unchanged = sameHistoryView
|
|
346
|
+
&& knownIndex === timeline.turns.length - 1
|
|
347
|
+
&& timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
|
|
348
|
+
&& knownTurnStatus && knownTurnStatus !== 'inProgress'
|
|
349
|
+
&& (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
|
|
350
|
+
const partial = !unchanged && sameHistoryView && knownIndex >= 0
|
|
351
|
+
? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
|
|
352
|
+
: timeline;
|
|
353
|
+
sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
436
356
|
|
|
437
357
|
const approvalMatch = /^\/api\/threads\/([^/]+)\/approvals$/.exec(url.pathname);
|
|
438
358
|
if (request.method === 'POST' && approvalMatch) {
|
|
439
359
|
const threadId = decodeURIComponent(approvalMatch[1]);
|
|
440
360
|
await requireExecutionWritable(threadId);
|
|
441
|
-
|
|
442
|
-
sendJson(response, 409, { error: '当前执行模式不支持手机审批,请在工作站处理。' }); return;
|
|
443
|
-
}
|
|
444
|
-
const body = await readJsonBody(request);
|
|
361
|
+
const body = await readJsonBody(request);
|
|
445
362
|
sendJson(response, 200, await sharedServer.respondApproval(threadId, body)); return;
|
|
446
363
|
}
|
|
447
364
|
|
|
448
365
|
const eventsMatch = /^\/api\/threads\/([^/]+)\/events$/.exec(url.pathname);
|
|
449
|
-
if (request.method === 'GET' && eventsMatch) {
|
|
450
|
-
const threadId = decodeURIComponent(eventsMatch[1]);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (appServerTasks.owns(threadId)) {
|
|
465
|
-
await streamAppServerThreadEvents(request, response, threadId);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
await streamThreadEvents(
|
|
469
|
-
request,
|
|
470
|
-
response,
|
|
471
|
-
threadId,
|
|
472
|
-
url.searchParams.get('hostId')?.trim(),
|
|
473
|
-
);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const navigateMatch = /^\/api\/threads\/([^/]+)\/navigate$/.exec(url.pathname);
|
|
478
|
-
if (request.method === 'POST' && navigateMatch) {
|
|
479
|
-
const threadId = decodeURIComponent(navigateMatch[1]);
|
|
480
|
-
if (sharedServer && !desktopMonitor.pipe) {
|
|
481
|
-
sendJson(response, 200, { opened: false, reason: 'Desktop 工具通道尚未就绪' });
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (appServerTasks.owns(threadId)) {
|
|
485
|
-
sendJson(response, 200, { opened: false, transport: 'app-server' });
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
sendToolResult(response, await callTool('navigate_to_codex_page', {
|
|
489
|
-
threadId,
|
|
490
|
-
}, 15_000));
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
366
|
+
if (request.method === 'GET' && eventsMatch) {
|
|
367
|
+
const threadId = decodeURIComponent(eventsMatch[1]);
|
|
368
|
+
const after = Number(url.searchParams.get('afterSeq'));
|
|
369
|
+
const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
|
|
370
|
+
? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
|
|
371
|
+
const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
|
|
372
|
+
const known = url.searchParams.get('knownTurnId') ? {
|
|
373
|
+
updatedAt: Number.isFinite(knownUpdatedAt) ? knownUpdatedAt : 0,
|
|
374
|
+
turnId: url.searchParams.get('knownTurnId'),
|
|
375
|
+
turnStatus: url.searchParams.get('knownTurnStatus') || '',
|
|
376
|
+
} : undefined;
|
|
377
|
+
await streamSharedThread(response, threadId, sharedServer,
|
|
378
|
+
normalizeSharedTimeline, requestedSync, known);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
493
381
|
|
|
494
382
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
495
383
|
if (request.method === 'GET' && contextMatch) {
|
|
496
384
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
497
385
|
const current = readContextUsage(threadId);
|
|
498
|
-
const native =
|
|
386
|
+
const native = await sharedServer.readContext(threadId);
|
|
499
387
|
sendJson(response, 200, { ...current, ...(native.model ? { model: native.model, thinking: native.thinking, serviceTier: native.serviceTier,
|
|
500
388
|
permissionMode: native.permissionMode, permissionModes: native.permissionModes } : {}), ...(native.available ? native : {}) });
|
|
501
389
|
return;
|
|
502
390
|
}
|
|
503
391
|
|
|
504
392
|
const sharedAction = /^\/api\/threads\/([^/]+)\/(history|settings|permissions)$/.exec(url.pathname);
|
|
505
|
-
if (sharedAction) {
|
|
393
|
+
if (sharedAction) {
|
|
506
394
|
const id = decodeURIComponent(sharedAction[1]);
|
|
507
|
-
if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(id)) throw new RequestError(400, '此连接不支持该操作。');
|
|
508
395
|
if (request.method === 'GET' && sharedAction[2] === 'history') {
|
|
509
|
-
sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'))));
|
|
396
|
+
sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'), sharedHistoryPageOptions())));
|
|
510
397
|
return;
|
|
511
398
|
}
|
|
512
399
|
if (request.method === 'POST' && sharedAction[2] === 'settings') {
|
|
@@ -520,194 +407,73 @@ async function handleRequest(request, response) {
|
|
|
520
407
|
const body = await readJsonBody(request);
|
|
521
408
|
sendJson(response, 200, await sharedServer.permissions(id, body.mode));
|
|
522
409
|
return;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const sharedOperationGroup = /^\/api\/threads\/([^/]+)\/turns\/([^/]+)\/groups\/([^/]+)\/items$/.exec(url.pathname);
|
|
414
|
+
if (request.method === 'GET' && sharedOperationGroup) {
|
|
415
|
+
const threadId = decodeURIComponent(sharedOperationGroup[1]);
|
|
416
|
+
const turnId = decodeURIComponent(sharedOperationGroup[2]);
|
|
417
|
+
const groupId = decodeURIComponent(sharedOperationGroup[3]);
|
|
418
|
+
const page = await sharedServer.operationGroupItems(threadId, turnId, groupId, url.searchParams.get('offset'));
|
|
419
|
+
sendJson(response, 200, { turnId: page.turnId, groupId: page.groupId,
|
|
420
|
+
entries: page.items.map(normalizeItem).filter(Boolean), nextOffset: page.nextOffset, total: page.total });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
527
425
|
if (request.method === 'POST' && archiveMatch) {
|
|
528
426
|
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
529
427
|
await requireExecutionWritable(threadId);
|
|
530
|
-
if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(threadId)) {
|
|
531
|
-
throw new RequestError(400, '当前连接不支持归档对话。请启用完整控制。');
|
|
532
|
-
}
|
|
533
428
|
await sharedServer.request(`thread/${archiveMatch[2]}`, { threadId });
|
|
534
429
|
sendJson(response, 200, { threadId, archived: archiveMatch[2] === 'archive' });
|
|
535
430
|
return;
|
|
536
431
|
}
|
|
537
432
|
|
|
538
|
-
const
|
|
539
|
-
if (
|
|
540
|
-
const threadId = decodeURIComponent(
|
|
541
|
-
await
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
await appServerTasks.deleteTask(threadId);
|
|
546
|
-
messageQueues.delete(threadId);
|
|
547
|
-
sendJson(response, 200, { deleted: true, threadId, transport: 'app-server' });
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
if (request.method === 'GET' && readMatch) {
|
|
551
|
-
const threadId = decodeURIComponent(readMatch[1]);
|
|
552
|
-
if (appServerTasks.owns(threadId)) {
|
|
553
|
-
sendJson(response, 200, { thread: await appServerTasks.readTask(threadId, {
|
|
554
|
-
includeTurns: true,
|
|
555
|
-
turnLimit: boundedInteger(url.searchParams.get('turnLimit'), 10, 1, 10),
|
|
556
|
-
}) });
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
const argumentsValue = readThreadArguments(url, threadId, 10, 12_000);
|
|
560
|
-
sendToolResult(response, await callTool('read_thread', argumentsValue));
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
|
|
565
|
-
if (queueMatch) {
|
|
566
|
-
const threadId = decodeURIComponent(queueMatch[1]);
|
|
567
|
-
if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
|
|
568
|
-
await sharedServer.connect();
|
|
569
|
-
const rpc = (method, params) => sharedServer.request(method, params);
|
|
570
|
-
if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
|
|
571
|
-
if (request.method === 'POST') {
|
|
572
|
-
await requireExecutionWritable(threadId);
|
|
573
|
-
const args = appServerTurnArguments(threadId, await readJsonBody(request));
|
|
574
|
-
sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
if (request.method === 'GET') {
|
|
578
|
-
sendJson(response, 200, { items: publicQueue(threadId) });
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
433
|
+
const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
|
|
434
|
+
if (queueMatch) {
|
|
435
|
+
const threadId = decodeURIComponent(queueMatch[1]);
|
|
436
|
+
await sharedServer.connect();
|
|
437
|
+
const rpc = (method, params) => sharedServer.request(method, params);
|
|
438
|
+
if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
|
|
581
439
|
if (request.method === 'POST') {
|
|
582
440
|
await requireExecutionWritable(threadId);
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const item = {
|
|
588
|
-
id: crypto.randomUUID(),
|
|
589
|
-
...argumentsValue,
|
|
590
|
-
transport: appServerTasks.owns(threadId) ? 'app-server' : 'desktop-tools',
|
|
591
|
-
displayPrompt: messageDisplayText(body),
|
|
592
|
-
text: typeof body.prompt === 'string' ? body.prompt.trim() : '',
|
|
593
|
-
attachments: (body.images || []).map(id => ({ type:'image', available:true, url:id.startsWith('data:') ? id : `/api/attachments/${id}`, thumbnailUrl:id.startsWith('data:') ? id : `/api/attachments/${id}/thumbnail` })),
|
|
594
|
-
createdAt: new Date().toISOString(),
|
|
595
|
-
error: '',
|
|
596
|
-
};
|
|
597
|
-
const queue = messageQueues.get(threadId) || [];
|
|
598
|
-
queue.push(item);
|
|
599
|
-
messageQueues.set(threadId, queue);
|
|
600
|
-
ensureQueueWorker(threadId);
|
|
601
|
-
sendJson(response, 202, { items: publicQueue(threadId) });
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
441
|
+
const args = appServerTurnArguments(threadId, await readJsonBody(request));
|
|
442
|
+
sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
605
445
|
|
|
606
446
|
const queueItemMatch = /^\/api\/threads\/([^/]+)\/queue\/([a-f0-9-]+)(?:\/(steer))?$/.exec(url.pathname);
|
|
607
447
|
if (queueItemMatch) {
|
|
608
448
|
const threadId = decodeURIComponent(queueItemMatch[1]);
|
|
609
449
|
const itemId = queueItemMatch[2];
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (!action) throw new RequestError(405, '不支持的队列操作');
|
|
614
|
-
await requireExecutionWritable(threadId);
|
|
615
|
-
sendJson(response, 200, await appServerEvents.changeQueue((method, params) => sharedServer.request(method, params), threadId, itemId, action)); return;
|
|
616
|
-
}
|
|
617
|
-
const queue = messageQueues.get(threadId) || [];
|
|
618
|
-
const index = queue.findIndex(item => item.id === itemId);
|
|
619
|
-
if (index < 0) {
|
|
620
|
-
sendJson(response, 404, { error: '没有找到该排队消息。' });
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
if (request.method === 'DELETE' && !queueItemMatch[3]) {
|
|
624
|
-
await requireExecutionWritable(threadId);
|
|
625
|
-
queue.splice(index, 1);
|
|
626
|
-
sendJson(response, 200, { items: publicQueue(threadId) });
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (request.method === 'POST' && queueItemMatch[3] === 'steer') {
|
|
630
|
-
await requireExecutionWritable(threadId);
|
|
631
|
-
const [item] = queue.splice(index, 1);
|
|
632
|
-
const result = await steerQueuedItem(threadId, item);
|
|
633
|
-
if (!result.success) {
|
|
634
|
-
queue.splice(index, 0, item);
|
|
635
|
-
sendJson(response, 502, { error: '插队发送失败。', details: result.data });
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
sendJson(response, 200, { items: publicQueue(threadId) });
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const takeoverMatch = /^\/api\/threads\/([^/]+)\/takeover$/.exec(url.pathname);
|
|
644
|
-
if (request.method === 'POST' && takeoverMatch) {
|
|
645
|
-
const threadId = decodeURIComponent(takeoverMatch[1]);
|
|
450
|
+
await sharedServer.connect();
|
|
451
|
+
const action = request.method === 'DELETE' ? 'delete' : queueItemMatch[3] === 'steer' ? 'steer' : null;
|
|
452
|
+
if (!action) throw new RequestError(405, '不支持的队列操作');
|
|
646
453
|
await requireExecutionWritable(threadId);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
if (messageQueues.get(threadId)?.length || queueWorkers.has(threadId)) throw new RequestError(409, '请先处理此聊天的排队消息。');
|
|
652
|
-
const result = await callTool('read_thread', { threadId, turnLimit: 1, includeOutputs: false });
|
|
653
|
-
if (!result.success) throw new RequestError(502, '无法确认 Desktop 聊天状态。');
|
|
654
|
-
const timeline = normalizeTimeline(result.data);
|
|
655
|
-
const thread = timeline.thread;
|
|
656
|
-
if (thread.hostId && thread.hostId !== 'local') throw new RequestError(400, '只支持本机聊天。');
|
|
657
|
-
if (!['idle', 'notLoaded'].includes(thread.status) || timeline.turns.some(t => t.status === 'inProgress')) throw new RequestError(409, '只允许接管已空闲的聊天,请等当前轮次结束。');
|
|
658
|
-
if (!findSessionPath(threadId)) throw new RequestError(400, '未找到此聊天的本机历史。');
|
|
659
|
-
sendJson(response, 200, { thread: await appServerTasks.adoptTask({ ...thread, id: threadId }) });
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
454
|
+
sendJson(response, 200, await appServerEvents.changeQueue((method, params) => sharedServer.request(method, params), threadId, itemId, action)); return;
|
|
455
|
+
}
|
|
662
456
|
|
|
663
|
-
const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
|
|
457
|
+
const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
|
|
664
458
|
if (request.method === 'POST' && interruptMatch) {
|
|
665
459
|
const threadId = decodeURIComponent(interruptMatch[1]);
|
|
666
460
|
await requireExecutionWritable(threadId);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (!appServerTasks.owns(threadId)) throw new RequestError(409, '请先接管空闲聊天。');
|
|
675
|
-
const body = await readJsonBody(request);
|
|
676
|
-
const thread = await appServerTasks.readTask(threadId, { turnLimit: 1 });
|
|
677
|
-
const active = thread.turns?.at(-1);
|
|
678
|
-
if (!body.turnId || active?.id !== body.turnId || statusText(active.status) !== 'inProgress') throw new RequestError(409, '此轮次已经结束或发生变化。');
|
|
679
|
-
// Stop also clears pending messages so the queue cannot restart work.
|
|
680
|
-
messageQueues.delete(threadId);
|
|
681
|
-
await appServerTasks.interruptTurn(threadId, body.turnId);
|
|
682
|
-
sendJson(response, 200, { requested: true });
|
|
683
|
-
return;
|
|
461
|
+
const body = await readJsonBody(request);
|
|
462
|
+
if (!body.turnId) throw new RequestError(400, '缺少活动轮次 ID。');
|
|
463
|
+
await sharedServer.interrupt(threadId, body.turnId);
|
|
464
|
+
sendJson(response, 200, { requested: true });
|
|
465
|
+
return;
|
|
684
466
|
}
|
|
685
467
|
|
|
686
468
|
const messageMatch = /^\/api\/threads\/([^/]+)\/messages$/.exec(url.pathname);
|
|
687
469
|
if (request.method === 'POST' && messageMatch) {
|
|
688
470
|
const threadId = decodeURIComponent(messageMatch[1]);
|
|
689
471
|
await requireExecutionWritable(threadId);
|
|
690
|
-
const body = await readJsonBody(request);
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const current = await appServerTasks.readTask(threadId, { turnLimit: 1 });
|
|
696
|
-
const active = current.turns?.at(-1);
|
|
697
|
-
sendJson(response, 200, statusText(active?.status) === 'inProgress'
|
|
698
|
-
? await appServerTasks.steerTurn(threadId, active.id, args.input)
|
|
699
|
-
: await appServerTasks.startTurn(threadId, args));
|
|
700
|
-
} else {
|
|
701
|
-
sendToolResult(response, await callTool('send_message_to_thread', await messageArguments(threadId, body)));
|
|
702
|
-
}
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (request.method === 'POST' && url.pathname === '/api/wait') {
|
|
707
|
-
const body = await readJsonBody(request);
|
|
708
|
-
sendToolResult(response, await callTool('wait_threads', body, 130_000));
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
472
|
+
const body = await readJsonBody(request);
|
|
473
|
+
if (body.hostId && body.hostId !== 'local') throw new RequestError(410, '此对话的连接方式已停用。', { code: 'unsupported_control' });
|
|
474
|
+
sendJson(response, 200, { success: true, data: await sharedServer.send(appServerTurnArguments(threadId, body)) });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
711
477
|
|
|
712
478
|
sendJson(response, 404, { error: '未找到请求的资源。' });
|
|
713
479
|
} catch (error) {
|
|
@@ -727,23 +493,15 @@ async function handleRequest(request, response) {
|
|
|
727
493
|
}
|
|
728
494
|
|
|
729
495
|
const server = http.createServer(handleRequest);
|
|
730
|
-
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
731
|
-
|
|
496
|
+
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
497
|
+
uploadsRoot,
|
|
498
|
+
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
732
499
|
});
|
|
733
500
|
|
|
734
|
-
if (options.appServer) {
|
|
735
|
-
try {
|
|
736
|
-
await appServer.start();
|
|
737
|
-
} catch (error) {
|
|
738
|
-
console.error(`app-server 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
739
|
-
process.exit(2);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
501
|
let control;
|
|
744
|
-
desktopMonitor.start();
|
|
745
502
|
void managedAppServer.start();
|
|
746
503
|
executionHealth.start();
|
|
504
|
+
runtimeDiscovery.start();
|
|
747
505
|
server.on('error', error => {
|
|
748
506
|
console.error('Bridge 监听失败:', error.message);
|
|
749
507
|
process.exit(1);
|
|
@@ -751,8 +509,14 @@ server.on('error', error => {
|
|
|
751
509
|
server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
752
510
|
try {
|
|
753
511
|
if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
|
|
754
|
-
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
755
|
-
secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
|
|
512
|
+
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
513
|
+
secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
|
|
514
|
+
'refresh-execution': refreshExecution,
|
|
515
|
+
'restart-managed-app-server': () => {
|
|
516
|
+
const state = managedAppServer.snapshot();
|
|
517
|
+
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
518
|
+
},
|
|
519
|
+
} });
|
|
756
520
|
writeRecord(process.env.CODEX_REMOTE_READY_PATH, { instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
757
521
|
pid: process.pid, controlPort: control.port });
|
|
758
522
|
}
|
|
@@ -772,147 +536,15 @@ async function shutDown() {
|
|
|
772
536
|
shuttingDown = true;
|
|
773
537
|
lanSocket.close();
|
|
774
538
|
server.close();
|
|
775
|
-
desktopMonitor?.stop();
|
|
776
539
|
executionHealth?.stop();
|
|
540
|
+
runtimeDiscovery.stop();
|
|
777
541
|
managedAppServer.close();
|
|
778
|
-
await appServer.close();
|
|
779
542
|
await control?.close();
|
|
780
543
|
process.exit(0);
|
|
781
544
|
}
|
|
782
545
|
process.on('SIGTERM', shutDown);
|
|
783
546
|
process.on('SIGINT', shutDown);
|
|
784
547
|
|
|
785
|
-
async function callTool(tool, argumentsValue, timeoutMs = 60_000, targetPipe) {
|
|
786
|
-
if (!sourceThreadId) throw new RequestError(503, '基础控制尚未绑定调用上下文;请在 Codex 对话启动一次 Code Relax。电脑管理连接仍可使用。');
|
|
787
|
-
if (!ALLOWED_TOOLS.has(tool)) {
|
|
788
|
-
throw new RequestError(403, `工具 ${tool} 不在 Bridge 允许列表中。`);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const invocationId = crypto.randomUUID();
|
|
792
|
-
const rpc = await callDesktop('tools/call', {
|
|
793
|
-
arguments: argumentsValue,
|
|
794
|
-
callId: `codex-remote-${invocationId}`,
|
|
795
|
-
namespace: 'codex_app',
|
|
796
|
-
threadId: sourceThreadId,
|
|
797
|
-
tool,
|
|
798
|
-
turnId: `codex-remote-turn-${invocationId}`,
|
|
799
|
-
}, timeoutMs, targetPipe);
|
|
800
|
-
|
|
801
|
-
const result = rpc.result;
|
|
802
|
-
if (!result || typeof result.success !== 'boolean') {
|
|
803
|
-
throw new BridgeError('Codex Desktop 没有返回有效的工具结果。', rpc);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const items = Array.isArray(result.contentItems) ? result.contentItems : [];
|
|
807
|
-
let data = items;
|
|
808
|
-
if (items.length === 1 && items[0]?.type === 'inputText' && typeof items[0].text === 'string') {
|
|
809
|
-
try {
|
|
810
|
-
data = JSON.parse(items[0].text);
|
|
811
|
-
} catch {
|
|
812
|
-
data = items[0].text;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
return { success: result.success, data };
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function callDesktop(method, params, timeoutMs, targetPipe = desktopMonitor?.pipe || pipePath) {
|
|
820
|
-
if (!targetPipe) return Promise.reject(new RequestError(503, 'Desktop 工具通道尚未就绪。'));
|
|
821
|
-
return new Promise((resolve, reject) => {
|
|
822
|
-
const socket = net.createConnection(targetPipe);
|
|
823
|
-
const requestId = ++nextRequestId;
|
|
824
|
-
const payload = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: requestId, method, params }), 'utf8');
|
|
825
|
-
const header = Buffer.allocUnsafe(4);
|
|
826
|
-
header.writeUInt32LE(payload.length, 0);
|
|
827
|
-
let pending = Buffer.alloc(0);
|
|
828
|
-
let expectedLength = null;
|
|
829
|
-
let settled = false;
|
|
830
|
-
|
|
831
|
-
const finish = (callback, value) => {
|
|
832
|
-
if (settled) return;
|
|
833
|
-
settled = true;
|
|
834
|
-
clearTimeout(timer);
|
|
835
|
-
socket.destroy();
|
|
836
|
-
callback(value);
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
const timer = setTimeout(() => {
|
|
840
|
-
finish(reject, new BridgeError('等待 Codex Desktop 响应超时。'));
|
|
841
|
-
}, timeoutMs);
|
|
842
|
-
|
|
843
|
-
socket.once('connect', () => socket.write(Buffer.concat([header, payload])));
|
|
844
|
-
socket.on('data', chunk => {
|
|
845
|
-
pending = Buffer.concat([pending, chunk]);
|
|
846
|
-
if (expectedLength === null && pending.length >= 4) {
|
|
847
|
-
expectedLength = pending.readUInt32LE(0);
|
|
848
|
-
pending = pending.subarray(4);
|
|
849
|
-
if (expectedLength <= 0 || expectedLength > MAX_FRAME_BYTES) {
|
|
850
|
-
finish(reject, new BridgeError(`Codex Desktop 返回了无效帧长度:${expectedLength}。`));
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (expectedLength !== null && pending.length >= expectedLength) {
|
|
856
|
-
try {
|
|
857
|
-
const responseValue = JSON.parse(pending.subarray(0, expectedLength).toString('utf8'));
|
|
858
|
-
if (responseValue.error) {
|
|
859
|
-
finish(reject, new BridgeError('Codex Desktop 工具调用失败。', responseValue.error));
|
|
860
|
-
} else if (responseValue.id !== requestId) {
|
|
861
|
-
finish(reject, new BridgeError('Codex Desktop 返回了不匹配的请求 ID。'));
|
|
862
|
-
} else {
|
|
863
|
-
finish(resolve, responseValue);
|
|
864
|
-
}
|
|
865
|
-
} catch (error) {
|
|
866
|
-
finish(reject, new BridgeError('无法解析 Codex Desktop 响应。', undefined, error));
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
socket.once('error', error => finish(reject, new BridgeError(
|
|
871
|
-
'无法连接 Codex Desktop Pipe。桌面版可能已重启,请重新启动 Bridge。',
|
|
872
|
-
undefined,
|
|
873
|
-
error,
|
|
874
|
-
)));
|
|
875
|
-
socket.once('close', () => {
|
|
876
|
-
if (!settled) finish(reject, new BridgeError('Codex Desktop Pipe 在返回结果前关闭。'));
|
|
877
|
-
});
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function sendToolResult(response, result) {
|
|
882
|
-
if (result.success) {
|
|
883
|
-
sendJson(response, 200, result.data);
|
|
884
|
-
} else {
|
|
885
|
-
sendJson(response, 502, { error: 'Codex Desktop 工具返回失败。', details: result.data });
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function appServerBridgeStatus() {
|
|
890
|
-
return {
|
|
891
|
-
...appServer.snapshot(),
|
|
892
|
-
platformFamily: appServer.initializeResult?.platformFamily || null,
|
|
893
|
-
platformOs: appServer.initializeResult?.platformOs || null,
|
|
894
|
-
taskCount: appServerTasks.store.list().length,
|
|
895
|
-
ownershipError: appServerTasks.store.loadError?.message || null,
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function readThreadArguments(url, threadId, defaultTurnLimit, defaultOutputLimit = 4_000) {
|
|
900
|
-
const argumentsValue = {
|
|
901
|
-
threadId,
|
|
902
|
-
turnLimit: boundedInteger(url.searchParams.get('turnLimit'), defaultTurnLimit, 1, 10),
|
|
903
|
-
includeOutputs: booleanValue(url.searchParams.get('includeOutputs'), true),
|
|
904
|
-
maxOutputCharsPerItem: boundedInteger(
|
|
905
|
-
url.searchParams.get('maxOutputCharsPerItem'),
|
|
906
|
-
defaultOutputLimit,
|
|
907
|
-
500,
|
|
908
|
-
100_000,
|
|
909
|
-
),
|
|
910
|
-
};
|
|
911
|
-
const hostId = url.searchParams.get('hostId')?.trim();
|
|
912
|
-
if (hostId) argumentsValue.hostId = hostId;
|
|
913
|
-
return argumentsValue;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
548
|
function readContextUsage(threadId) {
|
|
917
549
|
if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) {
|
|
918
550
|
return { available: false };
|
|
@@ -1035,108 +667,6 @@ function findSessionPath(threadId) {
|
|
|
1035
667
|
return null;
|
|
1036
668
|
}
|
|
1037
669
|
|
|
1038
|
-
function enrichTimelineWithLocalActivity(timeline, threadId) {
|
|
1039
|
-
const turn = timeline.turns.at(-1);
|
|
1040
|
-
if (!turn || turn.status !== 'inProgress') return;
|
|
1041
|
-
if (turn.entries.some(entry => entry?.type === 'activity' && entry.status === 'inProgress')) return;
|
|
1042
|
-
const pending = readPendingLocalActivity(threadId);
|
|
1043
|
-
if (!pending || (pending.turnId && pending.turnId !== turn.id)) return;
|
|
1044
|
-
turn.entries.push(pending.entry);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function readPendingLocalActivity(threadId) {
|
|
1048
|
-
const sessionPath = findSessionPath(threadId);
|
|
1049
|
-
if (!sessionPath) return null;
|
|
1050
|
-
let handle;
|
|
1051
|
-
try {
|
|
1052
|
-
const stat = fs.statSync(sessionPath);
|
|
1053
|
-
const cacheKey = `${stat.size}:${stat.mtimeMs}`;
|
|
1054
|
-
const cached = liveActivityCache.get(sessionPath);
|
|
1055
|
-
if (cached?.key === cacheKey) return cached.value;
|
|
1056
|
-
const readLength = Math.min(stat.size, MAX_LIVE_ACTIVITY_TAIL_BYTES);
|
|
1057
|
-
const buffer = Buffer.allocUnsafe(readLength);
|
|
1058
|
-
handle = fs.openSync(sessionPath, 'r');
|
|
1059
|
-
fs.readSync(handle, buffer, 0, readLength, stat.size - readLength);
|
|
1060
|
-
const lines = buffer.toString('utf8').split(/\r?\n/);
|
|
1061
|
-
if (readLength < stat.size) lines.shift();
|
|
1062
|
-
const pending = new Map();
|
|
1063
|
-
for (const line of lines) {
|
|
1064
|
-
if (!line.includes('"custom_tool_call')) continue;
|
|
1065
|
-
let event;
|
|
1066
|
-
try { event = JSON.parse(line); } catch { continue; }
|
|
1067
|
-
if (event?.type !== 'response_item') continue;
|
|
1068
|
-
const payload = event.payload;
|
|
1069
|
-
const callId = payload?.call_id;
|
|
1070
|
-
if (!callId) continue;
|
|
1071
|
-
if (payload.type === 'custom_tool_call') pending.set(callId, payload);
|
|
1072
|
-
if (payload.type === 'custom_tool_call_output') pending.delete(callId);
|
|
1073
|
-
}
|
|
1074
|
-
const payload = [...pending.values()].at(-1);
|
|
1075
|
-
const value = payload ? pendingActivity(payload) : null;
|
|
1076
|
-
liveActivityCache.set(sessionPath, { key: cacheKey, value });
|
|
1077
|
-
return value;
|
|
1078
|
-
} catch {
|
|
1079
|
-
liveActivityCache.delete(sessionPath);
|
|
1080
|
-
return null;
|
|
1081
|
-
} finally {
|
|
1082
|
-
if (handle !== undefined) fs.closeSync(handle);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function pendingActivity(payload) {
|
|
1087
|
-
const input = typeof payload.input === 'string' ? payload.input : '';
|
|
1088
|
-
const nestedTool = input.match(/tools\.([A-Za-z0-9_]+)\s*\(/)?.[1] || payload.name || 'tool';
|
|
1089
|
-
const id = `live:${payload.call_id}`;
|
|
1090
|
-
const common = { type: 'activity', id, status: 'inProgress', transient: true };
|
|
1091
|
-
const turnId = payload.internal_chat_message_metadata_passthrough?.turn_id || '';
|
|
1092
|
-
if (nestedTool === 'exec_command') {
|
|
1093
|
-
return {
|
|
1094
|
-
turnId,
|
|
1095
|
-
entry: {
|
|
1096
|
-
...common,
|
|
1097
|
-
kind: 'command',
|
|
1098
|
-
title: '命令',
|
|
1099
|
-
command: jsonStringField(input, 'cmd'),
|
|
1100
|
-
cwd: jsonStringField(input, 'workdir'),
|
|
1101
|
-
},
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
if (nestedTool === 'apply_patch') {
|
|
1105
|
-
return { turnId, entry: { ...common, kind: 'fileChange', title: '文件修改', changes: [] } };
|
|
1106
|
-
}
|
|
1107
|
-
if (nestedTool === 'web__run') {
|
|
1108
|
-
return {
|
|
1109
|
-
turnId,
|
|
1110
|
-
entry: { ...common, kind: 'webSearch', title: '网页搜索', query: jsonStringField(input, 'q') },
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
if (nestedTool === 'view_image') {
|
|
1114
|
-
return {
|
|
1115
|
-
turnId,
|
|
1116
|
-
entry: { ...common, kind: 'imageView', title: '查看图片', path: jsonStringField(input, 'path') },
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
const parts = nestedTool.replace(/^mcp__/, '').split('__');
|
|
1120
|
-
return {
|
|
1121
|
-
turnId,
|
|
1122
|
-
entry: {
|
|
1123
|
-
...common,
|
|
1124
|
-
kind: 'tool',
|
|
1125
|
-
title: '工具',
|
|
1126
|
-
server: parts.length > 1 ? parts.shift() : 'functions',
|
|
1127
|
-
tool: parts.join('/') || nestedTool,
|
|
1128
|
-
arguments: {},
|
|
1129
|
-
},
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
function jsonStringField(source, field) {
|
|
1134
|
-
const pattern = new RegExp(`(?:["']${field}["']|${field})\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`);
|
|
1135
|
-
const match = pattern.exec(source);
|
|
1136
|
-
if (!match) return '';
|
|
1137
|
-
try { return JSON.parse(match[1]); } catch { return ''; }
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
670
|
function messagePayload(body) {
|
|
1141
671
|
const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
|
|
1142
672
|
if (body.images !== undefined && (!Array.isArray(body.images) || body.images.length > 4)) {
|
|
@@ -1173,26 +703,6 @@ function messagePayload(body) {
|
|
|
1173
703
|
return payload;
|
|
1174
704
|
}
|
|
1175
705
|
|
|
1176
|
-
async function messageArguments(threadId, body) {
|
|
1177
|
-
const payload = messagePayload(body);
|
|
1178
|
-
payload.images = await Promise.all(payload.images.map(image => image.startsWith('data:') ? persistLegacyImage(image, uploadsRoot) : image));
|
|
1179
|
-
let prompt = payload.prompt;
|
|
1180
|
-
if (payload.images.length) {
|
|
1181
|
-
prompt += (prompt ? '\n\n' : '') + payload.images
|
|
1182
|
-
.map((file, index) => `})`)
|
|
1183
|
-
.join('\n\n');
|
|
1184
|
-
}
|
|
1185
|
-
const argumentsValue = { threadId, prompt };
|
|
1186
|
-
if (payload.hostId) argumentsValue.hostId = payload.hostId;
|
|
1187
|
-
if (payload.model) argumentsValue.model = payload.model;
|
|
1188
|
-
if (payload.thinking) argumentsValue.thinking = payload.thinking;
|
|
1189
|
-
return argumentsValue;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function usesSharedTask(body) {
|
|
1193
|
-
return Boolean(sharedServer && (!body.hostId || body.hostId === 'local'));
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
706
|
function appServerTurnArguments(threadId, body) {
|
|
1197
707
|
const payload = messagePayload(body);
|
|
1198
708
|
const input = [];
|
|
@@ -1205,332 +715,23 @@ function appServerTurnArguments(threadId, body) {
|
|
|
1205
715
|
return value;
|
|
1206
716
|
}
|
|
1207
717
|
|
|
1208
|
-
function
|
|
1209
|
-
const
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
function createThreadBody(body) {
|
|
1215
|
-
const payload = messagePayload(body);
|
|
1216
|
-
return { ...body, prompt: payload.prompt, transport: body.transport === 'app-server' ? 'app-server' : 'desktop-tools' };
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
function appServerProjectTarget(projectId, projects) {
|
|
1220
|
-
if (projectId === '__unassigned__') return appServerEvents.projectlessThreadTarget(projectRoot);
|
|
1221
|
-
const project = projects.find(item => item?.projectId === projectId);
|
|
1222
|
-
if (!project) throw new RequestError(404, '没有找到对应项目。');
|
|
1223
|
-
if (project.hostId && project.hostId !== 'local') {
|
|
1224
|
-
throw new RequestError(400, '独立 app-server 当前只支持本机项目。');
|
|
1225
|
-
}
|
|
1226
|
-
if (typeof project.path !== 'string' || !path.isAbsolute(project.path)) {
|
|
1227
|
-
throw new RequestError(400, '项目没有可用的本机路径。');
|
|
1228
|
-
}
|
|
1229
|
-
return { projectId, cwd: project.path };
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function mergeThreadCatalog(catalog, appThreads, limit, sharedSnapshot = Boolean(sharedServer)) {
|
|
1233
|
-
const owned = new Map(appThreads.map(thread => [thread.id, thread]));
|
|
1234
|
-
const pinnedThreads = (catalog.pinnedThreads || []).map(thread => owned.get(thread.id) || thread);
|
|
1235
|
-
const pinnedIds = new Set(pinnedThreads.map(thread => thread.id));
|
|
1236
|
-
if (sharedSnapshot) {
|
|
1237
|
-
const listed = new Set([...pinnedIds, ...(catalog.threads || []).map(thread => thread.id)]);
|
|
1238
|
-
return { ...catalog, pinnedThreads, threads: [
|
|
1239
|
-
...(catalog.threads || []).map(thread => owned.get(thread.id) || thread),
|
|
1240
|
-
...appThreads.filter(thread => !listed.has(thread.id)),
|
|
1241
|
-
].slice(0, limit) };
|
|
1242
|
-
}
|
|
1243
|
-
const desktopThreads = (catalog.threads || []).filter(thread => !owned.has(thread.id));
|
|
1244
|
-
const merged = [...desktopThreads, ...appThreads.filter(thread => !pinnedIds.has(thread.id))]
|
|
1245
|
-
.sort((left, right) => Number(right.updatedAt || 0) - Number(left.updatedAt || 0))
|
|
1246
|
-
.slice(0, limit);
|
|
1247
|
-
return { ...catalog, pinnedThreads, threads: merged };
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
async function createThreadArguments(projectId, body, projects) {
|
|
1251
|
-
const { threadId, hostId, ...message } = await messageArguments('', body);
|
|
1252
|
-
if (projectId === '__unassigned__') return { ...message, target: { type:'projectless' } };
|
|
1253
|
-
const project = projects.find(item => item?.projectId === projectId);
|
|
1254
|
-
if (!project) throw new RequestError(404, '没有找到对应项目。');
|
|
1255
|
-
return { ...message, target:{ type:'project', projectId, environment:{ type:'local' } } };
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
function publicQueue(threadId) {
|
|
1259
|
-
return (messageQueues.get(threadId) || []).map(item => ({
|
|
1260
|
-
id: item.id,
|
|
1261
|
-
prompt: item.displayPrompt || item.prompt,
|
|
1262
|
-
text: item.text, attachments:item.attachments || [],
|
|
1263
|
-
model: item.model || '',
|
|
1264
|
-
thinking: item.thinking || item.effort || '',
|
|
1265
|
-
createdAt: item.createdAt,
|
|
1266
|
-
error: item.error || '',
|
|
1267
|
-
}));
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function queueArguments(item) {
|
|
1271
|
-
const value = { threadId: item.threadId, prompt: item.prompt };
|
|
1272
|
-
if (item.hostId) value.hostId = item.hostId;
|
|
1273
|
-
if (item.model) value.model = item.model;
|
|
1274
|
-
if (item.thinking) value.thinking = item.thinking;
|
|
1275
|
-
return value;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
async function sendQueuedItem(threadId, item) {
|
|
1279
|
-
await requireExecutionWritable(threadId);
|
|
1280
|
-
if (item.transport === 'app-server') {
|
|
1281
|
-
try {
|
|
1282
|
-
return { success: true, data: await appServerTasks.startTurn(threadId, item) };
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
return { success: false, data: error instanceof Error ? error.message : String(error) };
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
return callTool('send_message_to_thread', queueArguments(item));
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
async function steerQueuedItem(threadId, item) {
|
|
1291
|
-
if (item.transport !== 'app-server') return callTool('send_message_to_thread', queueArguments(item));
|
|
1292
|
-
try {
|
|
1293
|
-
const thread = await appServerTasks.readTask(threadId, { includeTurns: true, turnLimit: 1 });
|
|
1294
|
-
const activeTurn = thread.turns?.at(-1);
|
|
1295
|
-
if (!activeTurn || statusText(activeTurn.status) !== 'inProgress') {
|
|
1296
|
-
throw new Error('当前没有可引导的 app-server 活动轮次。');
|
|
1297
|
-
}
|
|
1298
|
-
return {
|
|
1299
|
-
success: true,
|
|
1300
|
-
data: await appServerTasks.steerTurn(threadId, activeTurn.id, item.input),
|
|
1301
|
-
};
|
|
1302
|
-
} catch (error) {
|
|
1303
|
-
return { success: false, data: error instanceof Error ? error.message : String(error) };
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
function ensureQueueWorker(threadId) {
|
|
1308
|
-
if (queueWorkers.has(threadId)) return;
|
|
1309
|
-
const worker = runQueueWorker(threadId)
|
|
1310
|
-
.catch(error => {
|
|
1311
|
-
const item = messageQueues.get(threadId)?.[0];
|
|
1312
|
-
if (item) item.error = error instanceof Error ? error.message : String(error);
|
|
1313
|
-
})
|
|
1314
|
-
.finally(() => queueWorkers.delete(threadId));
|
|
1315
|
-
queueWorkers.set(threadId, worker);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
async function runQueueWorker(threadId) {
|
|
1319
|
-
while (true) {
|
|
1320
|
-
const queue = messageQueues.get(threadId) || [];
|
|
1321
|
-
const item = queue[0];
|
|
1322
|
-
if (!item || item.error) return;
|
|
1323
|
-
let timeline;
|
|
1324
|
-
if (item.transport === 'app-server') {
|
|
1325
|
-
const thread = await appServerTasks.readTask(threadId, { includeTurns: true, turnLimit: 1 });
|
|
1326
|
-
timeline = normalizeAppServerTimeline(thread);
|
|
1327
|
-
} else {
|
|
1328
|
-
const readArguments = {
|
|
1329
|
-
threadId,
|
|
1330
|
-
turnLimit: 1,
|
|
1331
|
-
includeOutputs: false,
|
|
1332
|
-
maxOutputCharsPerItem: 500,
|
|
1333
|
-
};
|
|
1334
|
-
if (item.hostId) readArguments.hostId = item.hostId;
|
|
1335
|
-
const threadResult = await callTool('read_thread', readArguments, 15_000);
|
|
1336
|
-
if (!threadResult.success) throw new BridgeError('读取排队任务状态失败。', threadResult.data);
|
|
1337
|
-
timeline = normalizeTimeline(threadResult.data);
|
|
1338
|
-
}
|
|
1339
|
-
const active = timeline.thread.status === 'active' || timeline.turns.at(-1)?.status === 'inProgress';
|
|
1340
|
-
if (active) {
|
|
1341
|
-
await delay(2_000);
|
|
1342
|
-
continue;
|
|
1343
|
-
}
|
|
1344
|
-
if (messageQueues.get(threadId) !== queue || queue[0]?.id !== item.id) continue;
|
|
1345
|
-
const sendResult = await sendQueuedItem(threadId, item);
|
|
1346
|
-
if (!sendResult.success) throw new BridgeError('发送排队消息失败。', sendResult.data);
|
|
1347
|
-
if (queue[0]?.id === item.id) queue.shift();
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
async function streamAppServerThreadEvents(request, response, threadId) {
|
|
1352
|
-
response.startEvents();
|
|
1353
|
-
|
|
1354
|
-
let closed = false;
|
|
1355
|
-
let pushPending = false;
|
|
1356
|
-
let pushing = false;
|
|
1357
|
-
let previousHash = '';
|
|
1358
|
-
let updateTimer = null;
|
|
1359
|
-
let resolveClosed;
|
|
1360
|
-
const closedPromise = new Promise(resolve => { resolveClosed = resolve; });
|
|
1361
|
-
response.once('close', () => { closed = true; resolveClosed(); });
|
|
1362
|
-
|
|
1363
|
-
const pushUpdate = async () => {
|
|
1364
|
-
if (closed) return;
|
|
1365
|
-
if (pushing) { pushPending = true; return; }
|
|
1366
|
-
pushing = true;
|
|
1367
|
-
try {
|
|
1368
|
-
const thread = await appServerTasks.readTask(threadId, { includeTurns: true, turnLimit: 2 });
|
|
1369
|
-
const timeline = normalizeAppServerTimeline(thread);
|
|
1370
|
-
const update = {
|
|
1371
|
-
thread: timeline.thread,
|
|
1372
|
-
turns: timeline.turns.slice(-2),
|
|
1373
|
-
turn: timeline.turns.at(-1) || null,
|
|
1374
|
-
queue: publicQueue(threadId),
|
|
1375
|
-
};
|
|
1376
|
-
const serialized = JSON.stringify(update);
|
|
1377
|
-
const hash = crypto.createHash('sha256').update(serialized).digest('base64url');
|
|
1378
|
-
if (hash !== previousHash) {
|
|
1379
|
-
sendEvent(response, 'update', update);
|
|
1380
|
-
previousHash = hash;
|
|
1381
|
-
}
|
|
1382
|
-
} catch (error) {
|
|
1383
|
-
sendEvent(response, 'bridge-error', { error: error instanceof Error ? error.message : String(error) });
|
|
1384
|
-
} finally {
|
|
1385
|
-
pushing = false;
|
|
1386
|
-
if (pushPending && !closed) {
|
|
1387
|
-
pushPending = false;
|
|
1388
|
-
void pushUpdate();
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
};
|
|
1392
|
-
const scheduleUpdate = event => {
|
|
1393
|
-
if (closed || event.threadId !== threadId || updateTimer) return;
|
|
1394
|
-
updateTimer = setTimeout(() => {
|
|
1395
|
-
updateTimer = null;
|
|
1396
|
-
void pushUpdate();
|
|
1397
|
-
}, 60);
|
|
1398
|
-
};
|
|
1399
|
-
appServerTasks.on('thread-event', scheduleUpdate);
|
|
1400
|
-
await pushUpdate();
|
|
1401
|
-
|
|
1402
|
-
await closedPromise;
|
|
1403
|
-
closed = true;
|
|
1404
|
-
if (updateTimer) clearTimeout(updateTimer);
|
|
1405
|
-
appServerTasks.off('thread-event', scheduleUpdate);
|
|
1406
|
-
if (!response.writableEnded) response.end();
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
async function streamThreadEvents(request, response, threadId, hostId) {
|
|
1410
|
-
response.startEvents();
|
|
1411
|
-
|
|
1412
|
-
let closed = false;
|
|
1413
|
-
let previousHash = '';
|
|
1414
|
-
response.once('close', () => { closed = true; });
|
|
1415
|
-
|
|
1416
|
-
while (!closed) {
|
|
1417
|
-
let intervalMs = 2_500;
|
|
1418
|
-
try {
|
|
1419
|
-
const argumentsValue = {
|
|
1420
|
-
threadId,
|
|
1421
|
-
turnLimit: 2,
|
|
1422
|
-
includeOutputs: true,
|
|
1423
|
-
maxOutputCharsPerItem: 4_000,
|
|
1424
|
-
};
|
|
1425
|
-
if (hostId) argumentsValue.hostId = hostId;
|
|
1426
|
-
const result = await callTool('read_thread', argumentsValue);
|
|
1427
|
-
if (!result.success) {
|
|
1428
|
-
throw new BridgeError('Codex Desktop 工具返回失败。', result.data);
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
const timeline = normalizeTimeline(result.data);
|
|
1432
|
-
enrichTimelineWithLocalActivity(timeline, threadId);
|
|
1433
|
-
const update = {
|
|
1434
|
-
thread: timeline.thread,
|
|
1435
|
-
turns: timeline.turns.slice(-2),
|
|
1436
|
-
turn: timeline.turns.at(-1) || null,
|
|
1437
|
-
queue: publicQueue(threadId),
|
|
1438
|
-
};
|
|
1439
|
-
const serialized = JSON.stringify(update);
|
|
1440
|
-
const hash = crypto.createHash('sha256').update(serialized).digest('base64url');
|
|
1441
|
-
if (hash !== previousHash) {
|
|
1442
|
-
sendEvent(response, 'update', update);
|
|
1443
|
-
previousHash = hash;
|
|
1444
|
-
}
|
|
1445
|
-
if (timeline.thread.status === 'active' || update.turn?.status === 'inProgress') {
|
|
1446
|
-
intervalMs = 1_000;
|
|
1447
|
-
}
|
|
1448
|
-
} catch (error) {
|
|
1449
|
-
sendEvent(response, 'bridge-error', {
|
|
1450
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1451
|
-
});
|
|
1452
|
-
intervalMs = 3_000;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
await delay(intervalMs);
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
if (!response.writableEnded) response.end();
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
function sendEvent(response, eventName, value) {
|
|
1462
|
-
if (response.destroyed || response.writableEnded) return;
|
|
1463
|
-
response.event(eventName, value);
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function normalizeTimeline(data, reverseTurns = true) {
|
|
1467
|
-
const source = data && typeof data === 'object' ? data : {};
|
|
1468
|
-
const sourceTurns = Array.isArray(source.turns) ? source.turns : source.thread?.turns;
|
|
1469
|
-
const turns = Array.isArray(sourceTurns) ? sourceTurns.map(normalizeTurn) : [];
|
|
1470
|
-
if (reverseTurns) turns.reverse();
|
|
1471
|
-
if (reverseTurns) restoreEmptyTurnMessages(source.thread?.id, turns);
|
|
1472
|
-
return {
|
|
718
|
+
function normalizeTimeline(data) {
|
|
719
|
+
const source = data && typeof data === 'object' ? data : {};
|
|
720
|
+
const sourceTurns = Array.isArray(source.turns) ? source.turns : source.thread?.turns;
|
|
721
|
+
const turns = Array.isArray(sourceTurns) ? sourceTurns.map(normalizeTurn) : [];
|
|
722
|
+
return {
|
|
1473
723
|
schemaVersion: 1,
|
|
1474
724
|
thread: normalizeThread(source.thread),
|
|
1475
725
|
turns,
|
|
1476
726
|
};
|
|
1477
727
|
}
|
|
1478
728
|
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
return
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
projectId: record?.projectId || '',
|
|
1486
|
-
hostId: 'app-server',
|
|
1487
|
-
transport: 'app-server',
|
|
1488
|
-
},
|
|
1489
|
-
}, false);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
1493
|
-
return { ...normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } }, false),
|
|
1494
|
-
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
function restoreEmptyTurnMessages(threadId, turns) {
|
|
1498
|
-
const missing = new Map(turns.filter(turn => turn.entries.length === 0).map(turn => [turn.id, turn]));
|
|
1499
|
-
if (!missing.size || !threadId) return;
|
|
1500
|
-
const sessionPath = findSessionPath(threadId);
|
|
1501
|
-
if (!sessionPath) return;
|
|
1502
|
-
const handle = fs.openSync(sessionPath, 'r');
|
|
1503
|
-
try {
|
|
1504
|
-
const size = fs.fstatSync(handle).size;
|
|
1505
|
-
const length = Math.min(size, MAX_SETTINGS_SCAN_BYTES);
|
|
1506
|
-
const buffer = Buffer.allocUnsafe(length);
|
|
1507
|
-
fs.readSync(handle, buffer, 0, length, size - length);
|
|
1508
|
-
const lines = buffer.toString('utf8').split(/\r?\n/);
|
|
1509
|
-
if (length < size) lines.shift();
|
|
1510
|
-
let turnId;
|
|
1511
|
-
for (const line of lines) {
|
|
1512
|
-
if (!line.trim()) continue;
|
|
1513
|
-
let event;
|
|
1514
|
-
try { event = JSON.parse(line); } catch { continue; }
|
|
1515
|
-
const item = event.payload;
|
|
1516
|
-
if (event.type === 'event_msg' && item?.type === 'task_started') turnId = item.turn_id;
|
|
1517
|
-
if (event.type === 'turn_context') turnId = item?.turn_id;
|
|
1518
|
-
const turn = missing.get(turnId);
|
|
1519
|
-
if (!turn || event.type !== 'response_item') continue;
|
|
1520
|
-
let entry;
|
|
1521
|
-
if (item.type === 'function_call_output' && item.namespace === 'codex_app' && ['create_thread', 'send_message_to_thread'].includes(item.name)) {
|
|
1522
|
-
const text = extractDelegatedInput(item.output);
|
|
1523
|
-
if (text) entry = { type: 'message', id: item.id, role: 'user', source: 'remote', ...splitUploadedImages(text) };
|
|
1524
|
-
} else if (item.type === 'message' && ['user', 'assistant'].includes(item.role) && item.channel !== 'analysis') {
|
|
1525
|
-
const text = (item.content || []).filter(part => ['input_text', 'output_text'].includes(part.type)).map(part => part.text || '').join('\n');
|
|
1526
|
-
if (text) entry = item.role === 'user'
|
|
1527
|
-
? normalizeItem({ type: 'userMessage', id: item.id, content: item.content.map(part => ({ ...part, type: part.type === 'input_text' ? 'text' : part.type })) })
|
|
1528
|
-
: normalizeItem({ type: 'agentMessage', id: item.id, phase: item.phase || item.channel, text });
|
|
1529
|
-
}
|
|
1530
|
-
if (entry) turn.entries.push(entry);
|
|
1531
|
-
}
|
|
1532
|
-
} finally { fs.closeSync(handle); }
|
|
1533
|
-
}
|
|
729
|
+
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
730
|
+
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
|
|
731
|
+
return { ...timeline,
|
|
732
|
+
historyView: 'grouped-lazy-skeleton-v12',
|
|
733
|
+
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
734
|
+
}
|
|
1534
735
|
|
|
1535
736
|
function normalizeThread(thread) {
|
|
1536
737
|
const source = thread && typeof thread === 'object' ? thread : {};
|
|
@@ -1539,30 +740,48 @@ function normalizeThread(thread) {
|
|
|
1539
740
|
title: source.name || source.title || source.preview || '',
|
|
1540
741
|
status: statusText(source.status),
|
|
1541
742
|
updatedAt: Number(source.updatedAt) || 0,
|
|
743
|
+
recencyAt: Number(source.recencyAt ?? source.updatedAt) || 0,
|
|
1542
744
|
projectId: source.projectId || '',
|
|
1543
745
|
cwd: source.cwd || '',
|
|
1544
746
|
hostId: source.hostId || '',
|
|
1545
|
-
transport: source.transport ||
|
|
747
|
+
transport: source.transport || 'shared-app-server',
|
|
1546
748
|
readOnly: source.readOnly === true,
|
|
1547
749
|
readOnlyReason: source.readOnlyReason || '',
|
|
1548
750
|
};
|
|
1549
751
|
}
|
|
1550
752
|
|
|
1551
|
-
function normalizeTurn(turn) {
|
|
1552
|
-
const source = turn && typeof turn === 'object' ? turn : {};
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
753
|
+
function normalizeTurn(turn) {
|
|
754
|
+
const source = turn && typeof turn === 'object' ? turn : {};
|
|
755
|
+
const turnId = source.id || '';
|
|
756
|
+
const entries = collapseSubAgentEntries(Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : []);
|
|
757
|
+
for (const entry of entries) {
|
|
758
|
+
const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
|
|
759
|
+
for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
id: turnId,
|
|
763
|
+
status: statusText(source.status),
|
|
1556
764
|
startedAt: source.startedAt || null,
|
|
1557
765
|
completedAt: source.completedAt || null,
|
|
1558
|
-
durationMs: source.durationMs ?? null,
|
|
1559
|
-
error: source.error ?? null,
|
|
1560
|
-
entries
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function normalizeItem(item) {
|
|
1565
|
-
if (!item || typeof item !== 'object') return null;
|
|
766
|
+
durationMs: source.durationMs ?? null,
|
|
767
|
+
error: source.error ?? null,
|
|
768
|
+
entries,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function normalizeItem(item) {
|
|
773
|
+
if (!item || typeof item !== 'object') return null;
|
|
774
|
+
|
|
775
|
+
if (item.type === 'lazyOperationGroup') {
|
|
776
|
+
return {
|
|
777
|
+
type: 'activity',
|
|
778
|
+
id: item.id || '',
|
|
779
|
+
kind: 'lazyOperationGroup',
|
|
780
|
+
turnId: item.turnId || '',
|
|
781
|
+
groupId: item.groupId || '',
|
|
782
|
+
operationTypes: Array.isArray(item.operationTypes) ? item.operationTypes : [],
|
|
783
|
+
};
|
|
784
|
+
}
|
|
1566
785
|
|
|
1567
786
|
if (item.type === 'contextCompaction') {
|
|
1568
787
|
return {
|
|
@@ -1588,9 +807,9 @@ function normalizeItem(item) {
|
|
|
1588
807
|
}
|
|
1589
808
|
}
|
|
1590
809
|
|
|
1591
|
-
if (item.type === 'userMessage') {
|
|
1592
|
-
const text = extractUserText(item.content);
|
|
1593
|
-
const attachments = normalizeAttachments(item.content);
|
|
810
|
+
if (item.type === 'userMessage') {
|
|
811
|
+
const { text, files } = extractUserText(item.content);
|
|
812
|
+
const attachments = [...normalizeAttachments(item.content), ...files];
|
|
1594
813
|
if (!text && !attachments.length) return null;
|
|
1595
814
|
return {
|
|
1596
815
|
clientId: item.clientId,
|
|
@@ -1616,132 +835,151 @@ function normalizeItem(item) {
|
|
|
1616
835
|
};
|
|
1617
836
|
}
|
|
1618
837
|
|
|
1619
|
-
if (item.type === 'reasoning') {
|
|
1620
|
-
const detail = Array.isArray(item.summary)
|
|
1621
|
-
? item.summary.map(summaryText).filter(Boolean).join('\n')
|
|
1622
|
-
: summaryText(item.summary);
|
|
1623
|
-
return {
|
|
1624
|
-
type: 'activity',
|
|
1625
|
-
id: item.id || '',
|
|
1626
|
-
kind: 'reasoning',
|
|
1627
|
-
title: '思考摘要',
|
|
1628
|
-
detail,
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
if (item.type === 'commandExecution') {
|
|
1633
|
-
return {
|
|
1634
|
-
type: 'activity',
|
|
1635
|
-
id: item.id || '',
|
|
1636
|
-
kind: 'command',
|
|
1637
|
-
title: '命令',
|
|
1638
|
-
status: statusText(item.status),
|
|
1639
|
-
command: item.command || '',
|
|
1640
|
-
cwd: item.cwd || '',
|
|
1641
|
-
exitCode: item.exitCode ?? null,
|
|
1642
|
-
durationMs: item.durationMs ?? null,
|
|
1643
|
-
output: item.aggregatedOutput || item.output?.text || '',
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
838
|
+
if (item.type === 'reasoning') {
|
|
839
|
+
const detail = Array.isArray(item.summary)
|
|
840
|
+
? item.summary.map(summaryText).filter(Boolean).join('\n')
|
|
841
|
+
: summaryText(item.summary);
|
|
842
|
+
return {
|
|
843
|
+
type: 'activity',
|
|
844
|
+
id: item.id || '',
|
|
845
|
+
kind: 'reasoning',
|
|
846
|
+
title: '思考摘要',
|
|
847
|
+
detail,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
1646
850
|
|
|
851
|
+
if (item.type === 'commandExecution') {
|
|
852
|
+
return {
|
|
853
|
+
type: 'activity',
|
|
854
|
+
id: item.id || '',
|
|
855
|
+
kind: 'command',
|
|
856
|
+
title: '命令',
|
|
857
|
+
titleOnly: true,
|
|
858
|
+
status: statusText(item.status),
|
|
859
|
+
command: item.command,
|
|
860
|
+
cwd: item.cwd,
|
|
861
|
+
exitCode: item.exitCode,
|
|
862
|
+
durationMs: item.durationMs,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
1647
866
|
if (item.type === 'mcpToolCall') {
|
|
1648
|
-
const result = normalizeToolResult(item.result, item.error);
|
|
1649
867
|
return {
|
|
1650
868
|
type: 'activity',
|
|
1651
869
|
id: item.id || '',
|
|
1652
|
-
kind: 'tool',
|
|
1653
|
-
title: '工具',
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
870
|
+
kind: 'tool',
|
|
871
|
+
title: '工具',
|
|
872
|
+
titleOnly: true,
|
|
873
|
+
status: statusText(item.status),
|
|
874
|
+
server: item.server,
|
|
875
|
+
tool: item.tool,
|
|
876
|
+
arguments: item.arguments,
|
|
877
|
+
durationMs: item.durationMs,
|
|
878
|
+
error: item.error,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (item.type === 'dynamicToolCall') {
|
|
883
|
+
return {
|
|
884
|
+
type: 'activity',
|
|
885
|
+
id: item.id || '',
|
|
886
|
+
kind: 'tool',
|
|
887
|
+
title: '工具',
|
|
888
|
+
titleOnly: true,
|
|
889
|
+
status: statusText(item.status),
|
|
890
|
+
namespace: item.namespace,
|
|
891
|
+
tool: item.tool,
|
|
892
|
+
arguments: item.arguments,
|
|
893
|
+
durationMs: item.durationMs,
|
|
894
|
+
success: item.success,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (item.type === 'fileChange') {
|
|
899
|
+
return {
|
|
900
|
+
type: 'activity',
|
|
901
|
+
id: item.id || '',
|
|
902
|
+
kind: 'fileChange',
|
|
903
|
+
title: '文件修改',
|
|
904
|
+
titleOnly: true,
|
|
905
|
+
status: statusText(item.status),
|
|
906
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
1663
907
|
};
|
|
1664
908
|
}
|
|
1665
909
|
|
|
1666
|
-
if (item.type === '
|
|
1667
|
-
return {
|
|
1668
|
-
type: 'activity',
|
|
1669
|
-
id: item.id || '',
|
|
1670
|
-
kind: '
|
|
1671
|
-
title: '
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
status: statusText(item.status),
|
|
1740
|
-
tool: item.tool || '',
|
|
1741
|
-
prompt: item.prompt || '',
|
|
1742
|
-
receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
910
|
+
if (item.type === 'webSearch') {
|
|
911
|
+
return {
|
|
912
|
+
type: 'activity',
|
|
913
|
+
id: item.id || '',
|
|
914
|
+
kind: 'webSearch',
|
|
915
|
+
title: '网页搜索',
|
|
916
|
+
titleOnly: true,
|
|
917
|
+
query: item.query || item.action?.query || '',
|
|
918
|
+
...(item.action && typeof item.action === 'object' ? { action: item.action } : {}),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (item.type === 'imageView') {
|
|
923
|
+
return {
|
|
924
|
+
type: 'activity',
|
|
925
|
+
id: item.id || '',
|
|
926
|
+
kind: 'imageView',
|
|
927
|
+
title: '查看图片',
|
|
928
|
+
titleOnly: true,
|
|
929
|
+
path: item.path || '',
|
|
930
|
+
image: typeof item.path === 'string' && item.path
|
|
931
|
+
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
932
|
+
: null,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (item.type === 'imageGeneration') {
|
|
937
|
+
const image = typeof item.savedPath === 'string' && item.savedPath
|
|
938
|
+
? normalizeAttachment({ type: 'localImage', path: item.savedPath, name: '生成的图片' })
|
|
939
|
+
: null;
|
|
940
|
+
return {
|
|
941
|
+
type: 'activity',
|
|
942
|
+
id: item.id || '',
|
|
943
|
+
kind: 'imageGeneration',
|
|
944
|
+
title: '生成图片',
|
|
945
|
+
titleOnly: true,
|
|
946
|
+
status: statusText(item.status),
|
|
947
|
+
image,
|
|
948
|
+
failure: item.failure || null,
|
|
949
|
+
transparentBackground: item.transparentBackground === true,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (item.type === 'subAgentActivity') {
|
|
954
|
+
return {
|
|
955
|
+
type: 'activity',
|
|
956
|
+
id: item.id || '',
|
|
957
|
+
kind: 'subAgent',
|
|
958
|
+
title: '子任务',
|
|
959
|
+
titleOnly: true,
|
|
960
|
+
status: statusText(item.kind),
|
|
961
|
+
agentPath: item.agentPath || '',
|
|
962
|
+
agentThreadId: item.agentThreadId || '',
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (item.type === 'collabAgentToolCall') {
|
|
967
|
+
return {
|
|
968
|
+
type: 'activity',
|
|
969
|
+
id: item.id || '',
|
|
970
|
+
kind: 'collaboration',
|
|
971
|
+
title: '任务协作',
|
|
972
|
+
titleOnly: true,
|
|
973
|
+
status: statusText(item.status),
|
|
974
|
+
tool: item.tool || '',
|
|
975
|
+
prompt: item.prompt || '',
|
|
976
|
+
senderThreadId: item.senderThreadId || '',
|
|
977
|
+
receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
|
|
978
|
+
model: item.model || '',
|
|
979
|
+
reasoningEffort: item.reasoningEffort || '',
|
|
980
|
+
agentsStates: item.agentsStates && typeof item.agentsStates === 'object' ? item.agentsStates : {},
|
|
981
|
+
};
|
|
982
|
+
}
|
|
1745
983
|
|
|
1746
984
|
return {
|
|
1747
985
|
type: 'activity',
|
|
@@ -1753,44 +991,92 @@ function normalizeItem(item) {
|
|
|
1753
991
|
};
|
|
1754
992
|
}
|
|
1755
993
|
|
|
1756
|
-
function
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
if (
|
|
994
|
+
function collapseSubAgentEntries(entries) {
|
|
995
|
+
const agents = new Map();
|
|
996
|
+
let firstIndex = -1;
|
|
997
|
+
const retained = [];
|
|
998
|
+
const ensureAgent = threadId => {
|
|
999
|
+
if (!threadId) return null;
|
|
1000
|
+
let agent = agents.get(threadId);
|
|
1001
|
+
if (!agent) {
|
|
1002
|
+
agent = { threadId, name: '', status: 'pendingInit', objective: '', model: '', reasoningEffort: '' };
|
|
1003
|
+
agents.set(threadId, agent);
|
|
1004
|
+
}
|
|
1005
|
+
return agent;
|
|
1764
1006
|
};
|
|
1765
|
-
const
|
|
1766
|
-
if (
|
|
1767
|
-
|
|
1768
|
-
if (
|
|
1769
|
-
if (
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1007
|
+
const setProtocolStatus = (agent, status) => {
|
|
1008
|
+
if (!agent || !status || status === 'unknown') return;
|
|
1009
|
+
const terminal = ['interrupted', 'completed', 'errored', 'shutdown', 'notFound'];
|
|
1010
|
+
if (terminal.includes(agent.status) && ['pendingInit', 'running'].includes(status)) return;
|
|
1011
|
+
if (status === 'shutdown' && agent.status === 'completed') return;
|
|
1012
|
+
agent.status = status;
|
|
1013
|
+
};
|
|
1014
|
+
for (const entry of entries) {
|
|
1015
|
+
if (entry.kind === 'subAgent') {
|
|
1016
|
+
if (firstIndex < 0) firstIndex = retained.length;
|
|
1017
|
+
const agent = ensureAgent(entry.agentThreadId);
|
|
1018
|
+
if (!agent) continue;
|
|
1019
|
+
agent.name = subAgentDisplayName(entry.agentPath) || agent.name;
|
|
1020
|
+
const activityStatus = {
|
|
1021
|
+
started: 'running',
|
|
1022
|
+
interacted: 'running',
|
|
1023
|
+
interrupted: 'interrupted',
|
|
1024
|
+
completed: 'completed',
|
|
1025
|
+
}[entry.status];
|
|
1026
|
+
agent.activity = entry.status;
|
|
1027
|
+
setProtocolStatus(agent, activityStatus);
|
|
1028
|
+
continue;
|
|
1774
1029
|
}
|
|
1775
|
-
if (
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1030
|
+
if (entry.kind === 'collaboration') {
|
|
1031
|
+
const receiverIds = entry.receiverThreadIds || [];
|
|
1032
|
+
const createsAgent = entry.tool === 'spawnAgent';
|
|
1033
|
+
const updatesKnownAgent = receiverIds.some(threadId => agents.has(threadId));
|
|
1034
|
+
if (!createsAgent && !updatesKnownAgent) {
|
|
1035
|
+
retained.push(entry);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (firstIndex < 0) firstIndex = retained.length;
|
|
1039
|
+
for (const threadId of receiverIds) {
|
|
1040
|
+
const agent = ensureAgent(threadId);
|
|
1041
|
+
if (!agent) continue;
|
|
1042
|
+
if (entry.prompt) agent.objective = entry.prompt;
|
|
1043
|
+
if (entry.model) agent.model = entry.model;
|
|
1044
|
+
if (entry.reasoningEffort) agent.reasoningEffort = entry.reasoningEffort;
|
|
1045
|
+
setProtocolStatus(agent, statusText(entry.agentsStates?.[threadId]?.status));
|
|
1046
|
+
if (entry.status === 'failed' && !entry.agentsStates?.[threadId]?.status) agent.status = 'errored';
|
|
1047
|
+
if (entry.status === 'interrupted' && !entry.agentsStates?.[threadId]?.status) agent.status = 'interrupted';
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1780
1050
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1051
|
+
retained.push(entry);
|
|
1052
|
+
}
|
|
1053
|
+
if (!agents.size) return retained;
|
|
1054
|
+
retained.splice(firstIndex < 0 ? retained.length : firstIndex, 0, {
|
|
1055
|
+
type: 'activity',
|
|
1056
|
+
id: `subagents:${[...agents.keys()].join(':')}`,
|
|
1057
|
+
kind: 'subAgentGroup',
|
|
1058
|
+
title: '子 Agent',
|
|
1059
|
+
agents: [...agents.values()],
|
|
1060
|
+
});
|
|
1061
|
+
return retained;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function subAgentDisplayName(agentPath) {
|
|
1065
|
+
const segment = String(agentPath || '').split('/').map(value => value.trim()).filter(value => value && value !== 'root').at(-1);
|
|
1066
|
+
if (!segment) return '';
|
|
1067
|
+
const text = segment.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1068
|
+
return text ? text[0].toUpperCase() + text.slice(1) : '';
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function sharedHistoryPageOptions() {
|
|
1072
|
+
return {
|
|
1073
|
+
maxTurns: SHARED_HISTORY_PAGE_TURNS,
|
|
1074
|
+
maxBytes: SHARED_HISTORY_PAGE_BYTES,
|
|
1075
|
+
measure: value => Buffer.byteLength(JSON.stringify(normalizeSharedTimeline(value))),
|
|
1785
1076
|
};
|
|
1786
|
-
visit(result);
|
|
1787
|
-
const resultError = result?.type === 'error' ? result.error : null;
|
|
1788
|
-
const errorValue = fallbackError || resultError;
|
|
1789
|
-
const error = typeof errorValue === 'string' ? errorValue : errorValue?.message || errorValue?.text || '';
|
|
1790
|
-
return { text: text.join('\n\n'), error, resources, mediaCount };
|
|
1791
1077
|
}
|
|
1792
|
-
|
|
1793
|
-
function normalizeAttachments(content) {
|
|
1078
|
+
|
|
1079
|
+
function normalizeAttachments(content) {
|
|
1794
1080
|
if (!Array.isArray(content)) return [];
|
|
1795
1081
|
const items = content.filter(item => item && !['text', 'input_text'].includes(item.type)).map(item => ({ ...item }));
|
|
1796
1082
|
const text = content.filter(item => ['text', 'input_text'].includes(item?.type)).map(item => item.text || '').join('\n');
|
|
@@ -1833,18 +1119,19 @@ function markdownImageAssets(text) {
|
|
|
1833
1119
|
return assets;
|
|
1834
1120
|
}
|
|
1835
1121
|
|
|
1836
|
-
function markdownFileAssets(text) {
|
|
1122
|
+
function markdownFileAssets(text) {
|
|
1837
1123
|
const assets = Object.create(null);
|
|
1838
1124
|
function visit(tokens) {
|
|
1839
1125
|
for (const token of tokens) {
|
|
1840
1126
|
if (token.type === 'link_open') {
|
|
1841
1127
|
const value = token.attrGet('href');
|
|
1842
1128
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1843
|
-
let localPath;
|
|
1844
|
-
try { localPath = decodeURIComponent(value); } catch { localPath = ''; }
|
|
1845
|
-
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1846
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1847
|
-
|
|
1129
|
+
let localPath;
|
|
1130
|
+
try { localPath = decodeURIComponent(value); } catch { localPath = ''; }
|
|
1131
|
+
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1132
|
+
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1133
|
+
localPath = localPath.replace(/:(\d+)(?::\d+)?$/, '');
|
|
1134
|
+
const asset = rememberLocalFile(localPath);
|
|
1848
1135
|
if (asset) assets[value] = { remotePath: asset.id, name: asset.name, size: asset.size };
|
|
1849
1136
|
}
|
|
1850
1137
|
}
|
|
@@ -1887,9 +1174,9 @@ function splitUploadedImages(text) {
|
|
|
1887
1174
|
const paths = children.map(token => {
|
|
1888
1175
|
try { return path.resolve(decodeURIComponent(token.attrGet('src') || '')); } catch { return ''; }
|
|
1889
1176
|
});
|
|
1890
|
-
if (paths.some(file =>
|
|
1177
|
+
if (paths.some(file => path.dirname(file).toLowerCase() !== uploadsRoot.toLowerCase()
|
|
1891
1178
|
|| !/^[a-f0-9]{64}\.(png|webp)$/.test(path.basename(file)))) continue;
|
|
1892
|
-
for (const file of paths) attachments.push(normalizeAttachment({ type: 'localImage', path: file, name: '
|
|
1179
|
+
for (const file of paths) attachments.push(normalizeAttachment({ type: 'localImage', path: file, name: '移动端图片' }));
|
|
1893
1180
|
for (let line = block.map[0]; line < block.map[1]; line++) removed.add(line);
|
|
1894
1181
|
}
|
|
1895
1182
|
const visibleText = lines.filter((line, index) => !removed.has(index)).join('\n').trim();
|
|
@@ -1914,18 +1201,23 @@ function normalizeAttachment(item) {
|
|
|
1914
1201
|
name,
|
|
1915
1202
|
mimeType: cached?.mimeType || item.mimeType || '',
|
|
1916
1203
|
available: Boolean(cached),
|
|
1917
|
-
remotePath: cached ? cached.id : '',
|
|
1204
|
+
remotePath: cached ? cached.id : '',
|
|
1205
|
+
recoveryToken: cached?.recoveryToken || '',
|
|
1918
1206
|
url: '',
|
|
1919
1207
|
thumbnailUrl: '',
|
|
1920
1208
|
};
|
|
1921
1209
|
}
|
|
1922
1210
|
|
|
1923
|
-
function cacheLocalImage(filePath, name) {
|
|
1924
|
-
try {
|
|
1925
|
-
const
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1211
|
+
function cacheLocalImage(filePath, name) {
|
|
1212
|
+
try {
|
|
1213
|
+
const resolved = path.resolve(filePath);
|
|
1214
|
+
const stat = fs.statSync(resolved);
|
|
1215
|
+
const mimeType = imageMimeType(resolved);
|
|
1216
|
+
if (!stat.isFile() || !mimeType || stat.size > MAX_ATTACHMENT_BYTES) return null;
|
|
1217
|
+
const cached = rememberAttachment(fs.readFileSync(resolved), mimeType, name,
|
|
1218
|
+
localImageRecoveryToken(resolved, stat));
|
|
1219
|
+
void attachmentThumbnail(cached).catch(() => {});
|
|
1220
|
+
return cached;
|
|
1929
1221
|
} catch {
|
|
1930
1222
|
return null;
|
|
1931
1223
|
}
|
|
@@ -1943,16 +1235,65 @@ function cacheDataImage(dataUrl, name) {
|
|
|
1943
1235
|
}
|
|
1944
1236
|
}
|
|
1945
1237
|
|
|
1946
|
-
function rememberAttachment(buffer, mimeType, name) {
|
|
1947
|
-
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1948
|
-
if (!attachmentCache.has(id)) {
|
|
1238
|
+
function rememberAttachment(buffer, mimeType, name, recoveryToken = '') {
|
|
1239
|
+
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1240
|
+
if (!attachmentCache.has(id)) {
|
|
1949
1241
|
while (attachmentCache.size >= MAX_CACHED_ATTACHMENTS) {
|
|
1950
1242
|
attachmentCache.delete(attachmentCache.keys().next().value);
|
|
1951
|
-
}
|
|
1952
|
-
attachmentCache.set(id, { id, buffer, mimeType, name });
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
}
|
|
1243
|
+
}
|
|
1244
|
+
attachmentCache.set(id, { id, buffer, mimeType, name, recoveryToken });
|
|
1245
|
+
} else if (recoveryToken && !attachmentCache.get(id).recoveryToken) {
|
|
1246
|
+
attachmentCache.get(id).recoveryToken = recoveryToken;
|
|
1247
|
+
}
|
|
1248
|
+
return attachmentCache.get(id);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function localImageRecoveryToken(filePath, stat) {
|
|
1252
|
+
const iv = crypto.randomBytes(12);
|
|
1253
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', imageCapabilityKey, iv);
|
|
1254
|
+
const payload = Buffer.from(JSON.stringify([filePath, stat.size, stat.mtimeMs]));
|
|
1255
|
+
const encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
1256
|
+
return Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString('base64url');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function recoverLocalImage(id, token) {
|
|
1260
|
+
if (!token || token.length > 2048) return null;
|
|
1261
|
+
try {
|
|
1262
|
+
const value = Buffer.from(token, 'base64url');
|
|
1263
|
+
if (value.length < 29) return null;
|
|
1264
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', imageCapabilityKey, value.subarray(0, 12));
|
|
1265
|
+
decipher.setAuthTag(value.subarray(12, 28));
|
|
1266
|
+
const [filePath, size, mtimeMs] = JSON.parse(Buffer.concat([
|
|
1267
|
+
decipher.update(value.subarray(28)), decipher.final(),
|
|
1268
|
+
]).toString('utf8'));
|
|
1269
|
+
if (typeof filePath !== 'string' || !path.isAbsolute(filePath)
|
|
1270
|
+
|| !Number.isSafeInteger(size) || size <= 0 || size > MAX_ATTACHMENT_BYTES
|
|
1271
|
+
|| typeof mtimeMs !== 'number' || !Number.isFinite(mtimeMs)) return null;
|
|
1272
|
+
const stat = fs.statSync(filePath);
|
|
1273
|
+
const mimeType = imageMimeType(filePath);
|
|
1274
|
+
if (!stat.isFile() || stat.size !== size || stat.mtimeMs !== mtimeMs || !mimeType) return null;
|
|
1275
|
+
const buffer = fs.readFileSync(filePath);
|
|
1276
|
+
if (crypto.createHash('sha256').update(buffer).digest('hex') !== id) return null;
|
|
1277
|
+
return rememberAttachment(buffer, mimeType, path.basename(filePath), token);
|
|
1278
|
+
} catch {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async function attachmentThumbnail(image) {
|
|
1284
|
+
if (!image.thumbnail) {
|
|
1285
|
+
image.thumbnail = sharp(image.buffer, { limitInputPixels: 40_000_000 }).rotate()
|
|
1286
|
+
.resize({ width: 640, height: 640, fit: 'inside', withoutEnlargement: true })
|
|
1287
|
+
.webp({ quality: 78, effort: 4 })
|
|
1288
|
+
.toBuffer()
|
|
1289
|
+
.then(buffer => ({ buffer, mimeType: 'image/webp' }))
|
|
1290
|
+
.catch(error => {
|
|
1291
|
+
image.thumbnail = null;
|
|
1292
|
+
throw error;
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
return image.thumbnail;
|
|
1296
|
+
}
|
|
1956
1297
|
|
|
1957
1298
|
function imageMimeType(filePath) {
|
|
1958
1299
|
switch (path.extname(filePath).toLowerCase()) {
|
|
@@ -1966,8 +1307,8 @@ function imageMimeType(filePath) {
|
|
|
1966
1307
|
}
|
|
1967
1308
|
}
|
|
1968
1309
|
|
|
1969
|
-
function extractUserText(content) {
|
|
1970
|
-
if (!Array.isArray(content)) return '(包含图片或界面批注)';
|
|
1310
|
+
function extractUserText(content) {
|
|
1311
|
+
if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
|
|
1971
1312
|
let text = content
|
|
1972
1313
|
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
|
|
1973
1314
|
.map(item => item.text)
|
|
@@ -1977,14 +1318,16 @@ function extractUserText(content) {
|
|
|
1977
1318
|
.filter(value => !value.startsWith('The next image is untrusted page evidence'))
|
|
1978
1319
|
.join('\n\n')
|
|
1979
1320
|
.trim();
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1321
|
+
const mentioned = fileMentions.parse(text);
|
|
1322
|
+
const imagePaths = new Set([
|
|
1323
|
+
...content.filter(item => ['image', 'input_image', 'localImage'].includes(item?.type) && item.path).map(item => item.path),
|
|
1324
|
+
...[...mentioned.text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/g)].map(match => match[1]),
|
|
1325
|
+
].map(value => path.resolve(value).toLowerCase()));
|
|
1326
|
+
text = mentioned.text;
|
|
1984
1327
|
text = text.replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '').trim();
|
|
1985
1328
|
text = text.replace(/<image\b[^>]*\bpath="[^"]+"[^>]*>/g, '').replace(/<\/image>/g, '').trim();
|
|
1986
|
-
return text;
|
|
1987
|
-
}
|
|
1329
|
+
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())) };
|
|
1330
|
+
}
|
|
1988
1331
|
|
|
1989
1332
|
function extractDelegatedInput(value) {
|
|
1990
1333
|
if (typeof value !== 'string') return '';
|
|
@@ -1999,27 +1342,23 @@ function extractDelegatedInput(value) {
|
|
|
1999
1342
|
.trim();
|
|
2000
1343
|
}
|
|
2001
1344
|
|
|
2002
|
-
function delegatedOutputText(output) {
|
|
1345
|
+
function delegatedOutputText(output) {
|
|
2003
1346
|
if (typeof output === 'string') return output;
|
|
2004
1347
|
return output && typeof output.text === 'string' ? output.text : '';
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
function summaryText(value) {
|
|
2008
|
-
if (typeof value === 'string') return value;
|
|
2009
|
-
if (value && typeof value === 'object') return value.text || value.summary || JSON.stringify(value);
|
|
2010
|
-
return '';
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function statusText(value) {
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function summaryText(value) {
|
|
1351
|
+
if (typeof value === 'string') return value;
|
|
1352
|
+
if (value && typeof value === 'object') return value.text || value.summary || JSON.stringify(value);
|
|
1353
|
+
return '';
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function statusText(value) {
|
|
2014
1357
|
if (typeof value === 'string') return value;
|
|
2015
1358
|
if (value && typeof value === 'object') return value.type || value.status || 'unknown';
|
|
2016
1359
|
return value == null ? 'unknown' : String(value);
|
|
2017
1360
|
}
|
|
2018
1361
|
|
|
2019
|
-
function delay(milliseconds) {
|
|
2020
|
-
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
1362
|
function sendJson(response, statusCode, value) {
|
|
2024
1363
|
if (response.json) { response.json(statusCode, value); return; }
|
|
2025
1364
|
sendBuffer(response, statusCode, 'application/json; charset=utf-8', Buffer.from(JSON.stringify(value)));
|
|
@@ -2044,10 +1383,10 @@ function parseOptions(args) {
|
|
|
2044
1383
|
let port = Number.parseInt(process.env.CODEX_REMOTE_BRIDGE_PORT || '45831', 10);
|
|
2045
1384
|
let lan = process.env.CODEX_REMOTE_BRIDGE_LAN === 'true';
|
|
2046
1385
|
let token = process.env.CODEX_REMOTE_BRIDGE_TOKEN?.trim() || '';
|
|
2047
|
-
|
|
1386
|
+
if (process.env.CODEX_REMOTE_APP_SERVER === 'true') throw new Error('此连接方式已停用。');
|
|
2048
1387
|
for (let i = 0; i < args.length; i += 1) {
|
|
2049
1388
|
if (args[i] === '--lan') lan = true;
|
|
2050
|
-
if (args[i] === '--app-server')
|
|
1389
|
+
if (args[i] === '--app-server') throw new Error('此连接方式已停用。');
|
|
2051
1390
|
if (args[i] === '--port' && args[i + 1]) port = Number.parseInt(args[++i], 10);
|
|
2052
1391
|
if (args[i] === '--token' && args[i + 1]) token = args[++i];
|
|
2053
1392
|
}
|
|
@@ -2055,32 +1394,10 @@ function parseOptions(args) {
|
|
|
2055
1394
|
throw new Error('端口必须位于 1024 到 65535 之间。');
|
|
2056
1395
|
}
|
|
2057
1396
|
if (lan && !token) token = crypto.randomBytes(32).toString('base64url');
|
|
2058
|
-
return { port, lan, token
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
function boundedInteger(value, fallback, minimum, maximum) {
|
|
2062
|
-
const parsed = Number.parseInt(value || '', 10);
|
|
2063
|
-
return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, parsed)) : fallback;
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
function booleanValue(value, fallback) {
|
|
2067
|
-
if (value === 'true') return true;
|
|
2068
|
-
if (value === 'false') return false;
|
|
2069
|
-
return fallback;
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
class BridgeError extends Error {
|
|
2073
|
-
constructor(message, details, options) {
|
|
2074
|
-
super(message, options ? { cause: options } : undefined);
|
|
2075
|
-
this.details = details;
|
|
2076
|
-
}
|
|
1397
|
+
return { port, lan, token };
|
|
2077
1398
|
}
|
|
2078
1399
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
this.statusCode = statusCode;
|
|
2083
|
-
this.details = details;
|
|
2084
|
-
this.code = details?.code;
|
|
2085
|
-
}
|
|
1400
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
1401
|
+
const parsed = Number.parseInt(value || '', 10);
|
|
1402
|
+
return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, parsed)) : fallback;
|
|
2086
1403
|
}
|