@walkhi/code-relax 0.1.0-beta.11 → 0.1.0-beta.12
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/dist/bin/codex-remote.mjs +13 -3
- package/dist/bin/migrate-relay-db.mjs +104 -0
- package/dist/bin/self-relay-server.mjs +17 -10
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/src/managed-app-server.mjs +13 -4
- package/dist/src/platform/windows/codex-runtime.mjs +138 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/resident-start.js +8 -0
- package/dist/src/platform/windows/resident-startup.ps1 +6 -3
- package/dist/src/self-relay/admin-state.mjs +8 -6
- package/dist/src/self-relay/email-accounts.mjs +125 -0
- package/dist/src/self-relay/lifecycle.mjs +48 -80
- package/dist/src/self-relay/mysql-state.mjs +104 -0
- package/dist/src/self-relay/server.mjs +88 -26
- package/dist/src/server.mjs +745 -726
- package/dist/src/shared-app-server.mjs +285 -285
- package/package.json +2 -1
package/dist/src/server.mjs
CHANGED
|
@@ -4,23 +4,24 @@ import http from 'node:http';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
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';
|
|
12
|
-
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
13
|
-
import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
|
|
14
|
-
import { RuntimeDiscovery } from './runtime-discovery.mjs';
|
|
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';
|
|
12
|
+
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
13
|
+
import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
|
|
14
|
+
import { RuntimeDiscovery } from './runtime-discovery.mjs';
|
|
15
15
|
import { streamSharedThread } from './shared-thread-stream.mjs';
|
|
16
16
|
import appServerEvents from '../shared/app-server-events.cjs';
|
|
17
17
|
import { runtimePaths } from './platform/windows/runtime-paths.mjs';
|
|
18
18
|
import { openControl } from './service-control.mjs';
|
|
19
19
|
import { writeRecord, readRecord } from './service-lifecycle.mjs';
|
|
20
20
|
import { ExecutionHealth } from './execution-health.mjs';
|
|
21
|
-
import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
|
|
21
|
+
import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
|
|
22
|
+
import { createRelayManager } from './self-relay/lifecycle.mjs';
|
|
22
23
|
import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
|
|
23
|
-
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
24
|
+
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
24
25
|
import { SharedCatalog } from './shared-catalog.mjs';
|
|
25
26
|
import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
|
|
26
27
|
import { requestServiceRestart } from './service-restart.mjs';
|
|
@@ -29,43 +30,43 @@ const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
|
29
30
|
const MAX_CACHED_ATTACHMENTS = 32;
|
|
30
31
|
const MAX_CACHED_FILES = 64;
|
|
31
32
|
const MAX_CONTEXT_TAIL_BYTES = 4 * 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;
|
|
33
|
+
const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
34
|
+
const SHARED_HISTORY_PAGE_BYTES = 500 * 1024;
|
|
35
|
+
const SHARED_HISTORY_PAGE_TURNS = 8;
|
|
35
36
|
const MAX_SETTINGS_SCAN_BYTES = 64 * 1024 * 1024;
|
|
36
|
-
const ALLOWED_MODELS = new Set([
|
|
37
|
-
'gpt-6-astra',
|
|
38
|
-
'gpt-6-sol',
|
|
37
|
+
const ALLOWED_MODELS = new Set([
|
|
38
|
+
'gpt-6-astra',
|
|
39
|
+
'gpt-6-sol',
|
|
39
40
|
'gpt-5.6-sol',
|
|
40
41
|
'gpt-5.6-terra',
|
|
41
42
|
'gpt-5.6-luna',
|
|
42
43
|
'gpt-5.5',
|
|
43
44
|
'gpt-5.3-codex-spark',
|
|
44
45
|
]);
|
|
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();
|
|
46
|
+
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
47
|
+
|
|
48
|
+
class RequestError extends Error {
|
|
49
|
+
constructor(statusCode, message, details) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.statusCode = statusCode;
|
|
52
|
+
this.details = details;
|
|
53
|
+
this.code = details?.code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readRuntimeVersion() {
|
|
58
|
+
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
while (true) {
|
|
60
|
+
const manifest = readRecord(path.join(directory, 'package.json'));
|
|
61
|
+
if (manifest?.name === '@walkhi/code-relax') return manifest.version || '未知';
|
|
62
|
+
const parent = path.dirname(directory);
|
|
63
|
+
if (parent === directory) return '未知';
|
|
64
|
+
directory = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const options = parseOptions(process.argv.slice(2));
|
|
69
|
+
const runtimeVersion = readRuntimeVersion();
|
|
69
70
|
|
|
70
71
|
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
|
|
71
72
|
const projectRoot = os.homedir();
|
|
@@ -74,58 +75,59 @@ const markdownItScript = fs.readFileSync(path.join(webRoot, 'vendor', 'markdown-
|
|
|
74
75
|
const markdownSandbox = { atob };
|
|
75
76
|
vm.runInNewContext(markdownItScript.toString(), markdownSandbox);
|
|
76
77
|
const imageMarkdown = markdownSandbox.markdownit({ html: false });
|
|
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();
|
|
78
|
+
const attachmentCache = new Map();
|
|
79
|
+
const fileCache = new Map();
|
|
80
|
+
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
81
|
+
const imageCapabilityKey = crypto.createHash('sha256')
|
|
82
|
+
.update('code-relax-image-capability\0').update(options.token || fileCapabilitySecret).digest();
|
|
82
83
|
const sessionPathCache = new Map();
|
|
83
84
|
const sessionSettingsCache = new Map();
|
|
84
85
|
const liveActivityCache = new Map();
|
|
85
|
-
let sharedServer = null;
|
|
86
|
-
const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
|
|
86
|
+
let sharedServer = null;
|
|
87
|
+
const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
|
|
87
88
|
const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
sharedServer
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
89
|
+
const relayManager = createRelayManager(stateRoot);
|
|
90
|
+
let sharedCatalog = null, executionMode = 'ordinary';
|
|
91
|
+
function selectSharedExecution(enabled) {
|
|
92
|
+
const next = enabled ? managedAppServer.client : null;
|
|
93
|
+
if (sharedServer === next) return;
|
|
94
|
+
sharedServer = next;
|
|
95
|
+
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer) : null;
|
|
96
|
+
}
|
|
97
|
+
const executionHealth = new ExecutionHealth(async () => {
|
|
98
|
+
await managedAppServer.ensure();
|
|
99
|
+
const managed = managedAppServer.snapshot();
|
|
100
|
+
// The managed app-server owns execution. Desktop is another client of the
|
|
101
|
+
// same service, so closing Desktop must not disable phone writes.
|
|
102
|
+
selectSharedExecution(managed.ready);
|
|
103
|
+
executionMode = sharedServer ? 'shared' : 'ordinary';
|
|
104
|
+
const managedRestarting = managed.state === 'restarting';
|
|
105
|
+
const ready = !managedRestarting && sharedServer?.ready === true;
|
|
106
|
+
const state = managedRestarting ? 'restarting'
|
|
107
|
+
: ready ? 'full' : 'unavailable';
|
|
108
|
+
return { state, ready, canWrite: ready, mode: executionMode };
|
|
109
|
+
});
|
|
110
|
+
async function refreshExecution() {
|
|
111
|
+
await executionHealth.refresh();
|
|
112
|
+
return { mode: executionMode };
|
|
113
|
+
}
|
|
114
|
+
async function requireExecutionWritable(threadId) {
|
|
115
|
+
const execution = await executionHealth.refresh();
|
|
116
|
+
if (!execution.canWrite) {
|
|
117
|
+
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
118
|
+
{ code: 'full_control_required', execution });
|
|
119
|
+
}
|
|
120
|
+
if (threadId && sharedServer) sharedServer.requireWritable(threadId);
|
|
121
|
+
return execution;
|
|
122
|
+
}
|
|
123
|
+
managedAppServer.client.on('subscription-error', error => console.error('Codex 内核任务取消订阅失败:', error.message));
|
|
122
124
|
|
|
123
125
|
async function handleRequest(request, response) {
|
|
124
126
|
try {
|
|
125
127
|
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
126
128
|
|
|
127
129
|
if (!request.socketRequest) {
|
|
128
|
-
sendJson(response, 404, { error: '此端口不提供网页。' });
|
|
130
|
+
sendJson(response, 404, { error: '此端口不提供网页。' });
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -150,87 +152,87 @@ async function handleRequest(request, response) {
|
|
|
150
152
|
}
|
|
151
153
|
if (request.method === 'POST' && url.pathname === '/api/shared-recovery') {
|
|
152
154
|
const body = await readJsonBody(request);
|
|
153
|
-
if (body.confirmed !== true || typeof body.operationId !== 'string') throw new RequestError(400, '请确认重新打开当前 Codex 并接入 Codex 内核后再继续。');
|
|
155
|
+
if (body.confirmed !== true || typeof body.operationId !== 'string') throw new RequestError(400, '请确认重新打开当前 Codex 并接入 Codex 内核后再继续。');
|
|
154
156
|
sendJson(response, 202, await requestRecovery(stateRoot, body)); return;
|
|
155
157
|
}
|
|
156
|
-
if (request.method === 'POST' && url.pathname === '/api/service/restart') {
|
|
157
|
-
const body = await readJsonBody(request);
|
|
158
|
-
if (body.confirmed !== true) throw new RequestError(400, '请确认重启工作站服务后再继续。');
|
|
159
|
-
sendJson(response, 202, await requestServiceRestart(stateRoot)); return;
|
|
160
|
-
}
|
|
161
|
-
if (request.method === 'POST' && url.pathname === '/api/managed-app-server/restart') {
|
|
162
|
-
const body = await readJsonBody(request);
|
|
163
|
-
if (body.confirmed !== true || typeof body.operationId !== 'string' || !body.operationId.trim()) {
|
|
164
|
-
throw new RequestError(400, '请先确认中断风险,再重启 Codex 内核。');
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
sendJson(response, 202, managedAppServer.restart(body.operationId.trim()));
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (error?.code === 'managed_app_server_restarting') throw new RequestError(409, error.message);
|
|
170
|
-
throw error;
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
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
|
-
|
|
193
|
-
if (request.method === 'GET' && url.pathname === '/api/health') {
|
|
194
|
-
// Status readers consume the workstation's cached sample. A phone query
|
|
195
|
-
// must not trigger PowerShell, process-identity, or app-server checks.
|
|
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 },
|
|
200
|
-
managedAppServer: managedAppServer.snapshot(),
|
|
201
|
-
...runtimeDiscovery.state,
|
|
202
|
-
sharedAppServerConnected: sharedServer?.ready === true,
|
|
203
|
-
sharedRecoveryAvailable: runtimeDiscovery.state.desktop?.installed === true,
|
|
158
|
+
if (request.method === 'POST' && url.pathname === '/api/service/restart') {
|
|
159
|
+
const body = await readJsonBody(request);
|
|
160
|
+
if (body.confirmed !== true) throw new RequestError(400, '请确认重启工作站服务后再继续。');
|
|
161
|
+
sendJson(response, 202, await requestServiceRestart(stateRoot)); return;
|
|
162
|
+
}
|
|
163
|
+
if (request.method === 'POST' && url.pathname === '/api/managed-app-server/restart') {
|
|
164
|
+
const body = await readJsonBody(request);
|
|
165
|
+
if (body.confirmed !== true || typeof body.operationId !== 'string' || !body.operationId.trim()) {
|
|
166
|
+
throw new RequestError(400, '请先确认中断风险,再重启 Codex 内核。');
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
sendJson(response, 202, managedAppServer.restart(body.operationId.trim()));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error?.code === 'managed_app_server_restarting') throw new RequestError(409, error.message);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (request.method === 'GET' && url.pathname === '/api/official-remote/status') {
|
|
178
|
+
if (!managedAppServer.client.ready) throw new RequestError(503, 'Codex 内核未就绪,无法读取官方工作站状态。');
|
|
179
|
+
const result = await managedAppServer.client.request('remoteControl/status/read', {});
|
|
180
|
+
sendJson(response, 200, { status: result.status, environmentId: result.environmentId });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (request.method === 'POST' && url.pathname === '/api/official-remote/pairing') {
|
|
184
|
+
if (!managedAppServer.client.ready) throw new RequestError(503, 'Codex 内核未就绪,无法生成官方配对码。');
|
|
185
|
+
const status = await managedAppServer.client.request('remoteControl/status/read', {});
|
|
186
|
+
if (status.status !== 'connected') throw new RequestError(409, '当前 Codex 内核未连接 OpenAI Remote,无法配对。');
|
|
187
|
+
const result = await managedAppServer.client.request('remoteControl/pairing/start', { manualCode: true });
|
|
188
|
+
if (!result.manualPairingCode || !result.environmentId) throw new RequestError(502, 'Codex 内核未返回有效的官方配对码。');
|
|
189
|
+
// One-time code: return only to the authenticated station controller; never persist or log it.
|
|
190
|
+
sendJson(response, 200, { manualPairingCode: result.manualPairingCode,
|
|
191
|
+
environmentId: result.environmentId, expiresAt: result.expiresAt });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (request.method === 'GET' && url.pathname === '/api/health') {
|
|
196
|
+
// Status readers consume the workstation's cached sample. A phone query
|
|
197
|
+
// must not trigger PowerShell, process-identity, or app-server checks.
|
|
198
|
+
sendJson(response, 200, {
|
|
199
|
+
status: 'ok', resident: true, version: runtimeVersion,
|
|
200
|
+
execution: { ...executionHealth.state, ready: executionHealth.state.ready && sharedServer?.ready === true,
|
|
201
|
+
mode: executionMode },
|
|
202
|
+
managedAppServer: managedAppServer.snapshot(),
|
|
203
|
+
...runtimeDiscovery.state,
|
|
204
|
+
sharedAppServerConnected: sharedServer?.ready === true,
|
|
205
|
+
sharedRecoveryAvailable: runtimeDiscovery.state.desktop?.installed === true,
|
|
204
206
|
mode: options.lan ? 'lan' : 'loopback',
|
|
205
207
|
tokenRequired: Boolean(options.token),
|
|
206
208
|
nativeImageTransport: sharedServer ? 'shared-app-server' : null,
|
|
207
|
-
transportMode: sharedServer?.ready === true ? 'lan-shared' : 'unavailable',
|
|
209
|
+
transportMode: sharedServer?.ready === true ? 'lan-shared' : 'unavailable',
|
|
208
210
|
});
|
|
209
211
|
return;
|
|
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')}` });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
215
|
+
if (request.method === 'GET' && imageMatch) {
|
|
216
|
+
let image = attachmentCache.get(imageMatch[1])
|
|
217
|
+
|| recoverLocalImage(imageMatch[1], url.searchParams.get('recovery') || '');
|
|
218
|
+
const contextThreadId = url.searchParams.get('threadId') || '';
|
|
219
|
+
const contextTurnId = url.searchParams.get('turnId') || '';
|
|
220
|
+
if (!image && sharedServer && contextThreadId && contextTurnId
|
|
221
|
+
&& contextThreadId.length <= 128 && contextTurnId.length <= 128) {
|
|
222
|
+
try {
|
|
223
|
+
normalizeTurn(await sharedServer.turnContext(contextThreadId, contextTurnId));
|
|
224
|
+
image = attachmentCache.get(imageMatch[1]);
|
|
225
|
+
} catch {}
|
|
226
|
+
}
|
|
227
|
+
if (!image) throw new RequestError(404, '这张图片已失效,无法从工作站重新读取。');
|
|
228
|
+
const thumbnail = url.searchParams.get('variant') === 'thumbnail';
|
|
229
|
+
const asset = thumbnail ? await attachmentThumbnail(image) : { buffer: image.buffer, mimeType: image.mimeType };
|
|
230
|
+
sendJson(response, 200, { url: `data:${asset.mimeType};base64,${asset.buffer.toString('base64')}` });
|
|
229
231
|
return;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
const fileMatch = /^\/api\/files\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
233
|
-
if (request.method === 'GET' && fileMatch) {
|
|
235
|
+
if (request.method === 'GET' && fileMatch) {
|
|
234
236
|
const file = fileCache.get(fileMatch[1]);
|
|
235
237
|
if (!file) throw new RequestError(404, '文件授权已失效,请重新读取对话。');
|
|
236
238
|
let stat;
|
|
@@ -241,47 +243,47 @@ async function handleRequest(request, response) {
|
|
|
241
243
|
}
|
|
242
244
|
if (stat.size > MAX_ATTACHMENT_BYTES) throw new RequestError(413, '暂只支持下载不超过 16 MiB 的文件。');
|
|
243
245
|
sendJson(response, 200, { name: file.name, size: file.size, dataBase64: fs.readFileSync(file.path).toString('base64') });
|
|
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
|
-
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (url.pathname.startsWith('/api/app-server/') || url.pathname === '/api/wait') {
|
|
250
|
+
throw new RequestError(410, '此连接方式已停用。请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
251
|
+
}
|
|
252
|
+
const taskPath = /^\/api\/threads\/([^/]+)/.exec(url.pathname);
|
|
253
|
+
if (taskPath && (url.pathname.endsWith('/takeover') || url.pathname === `/api/threads/${taskPath[1]}`)) {
|
|
254
|
+
throw new RequestError(410, '此连接方式已停用。', { code: 'unsupported_control' });
|
|
255
|
+
}
|
|
256
|
+
if (url.searchParams.has('hostId') && url.searchParams.get('hostId') !== 'local') {
|
|
257
|
+
throw new RequestError(410, '此对话的连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
258
|
+
}
|
|
259
|
+
if (!executionHealth.state.ready || sharedServer?.ready !== true) {
|
|
260
|
+
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
261
|
+
{ code: 'full_control_required' });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (request.method === 'GET' && url.pathname === '/api/models') {
|
|
265
|
+
sendJson(response, 200, await sharedServer.models());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
266
268
|
|
|
267
269
|
if (request.method === 'GET' && url.pathname === '/api/threads') {
|
|
268
270
|
const limit = boundedInteger(url.searchParams.get('limit'), 20, 1, 200);
|
|
269
271
|
const archived = url.searchParams.get('archived') === 'true';
|
|
270
|
-
sendJson(response, 200, await sharedCatalog.threads(limit, archived));
|
|
272
|
+
sendJson(response, 200, await sharedCatalog.threads(limit, archived));
|
|
271
273
|
return;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
if (request.method === 'GET' && url.pathname === '/api/projects') {
|
|
275
|
-
sendJson(response, 200, { projects: (await sharedCatalog.projects()).projects });
|
|
277
|
+
sendJson(response, 200, { projects: (await sharedCatalog.projects()).projects });
|
|
276
278
|
return;
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
if (request.method === 'GET' && url.pathname === '/api/usage-limits') {
|
|
280
|
-
sendJson(response, 200, await sharedServer.usage());
|
|
282
|
+
sendJson(response, 200, await sharedServer.usage());
|
|
281
283
|
return;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
|
-
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
286
|
+
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
285
287
|
const execution = sharedServer;
|
|
286
288
|
response.startEvents();
|
|
287
289
|
const emit = value => { if (!response.destroyed) response.event('usage', value); };
|
|
@@ -294,186 +296,186 @@ async function handleRequest(request, response) {
|
|
|
294
296
|
}
|
|
295
297
|
|
|
296
298
|
const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
|
|
297
|
-
if (request.method === 'POST' && createThreadMatch) {
|
|
298
|
-
await requireExecutionWritable();
|
|
299
|
+
if (request.method === 'POST' && createThreadMatch) {
|
|
300
|
+
await requireExecutionWritable();
|
|
299
301
|
const projectId = decodeURIComponent(createThreadMatch[1]);
|
|
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;
|
|
302
|
+
const body = await readJsonBody(request);
|
|
303
|
+
if (body.transport === 'app-server' || (body.hostId && body.hostId !== 'local')) {
|
|
304
|
+
throw new RequestError(410, '此连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
305
|
+
}
|
|
306
|
+
const projects = (await sharedCatalog.projects()).projects;
|
|
307
|
+
const project = projects.find(item => item.projectId === projectId);
|
|
308
|
+
if (projectId !== '__unassigned__' && !project) throw new RequestError(404, '没有找到对应项目。');
|
|
309
|
+
const execution = sharedServer;
|
|
310
|
+
await execution.connect();
|
|
311
|
+
const rpc = (method, params) => execution.request(method, params);
|
|
312
|
+
let target = appServerEvents.projectlessThreadTarget(projectRoot);
|
|
313
|
+
if (project) {
|
|
314
|
+
const nativeProjects = await appServerEvents.readProjects(rpc);
|
|
315
|
+
const nativeId = nativeProjects.find(item => item.id === project.projectId)?.id;
|
|
316
|
+
if (!nativeId) throw new RequestError(404, 'Codex 内核中没有找到对应项目,请刷新项目列表。');
|
|
317
|
+
target = { projectId:nativeId, cwd:project.path };
|
|
318
|
+
}
|
|
319
|
+
const { input } = appServerTurnArguments('', body);
|
|
320
|
+
const created = await appServerEvents.createThread(rpc, target, input, body);
|
|
321
|
+
const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
322
|
+
void generateAndSetThreadTitle({
|
|
323
|
+
request: rpc,
|
|
324
|
+
subscribe: listener => {
|
|
325
|
+
execution.on('notification', listener);
|
|
326
|
+
return () => execution.off('notification', listener);
|
|
327
|
+
},
|
|
328
|
+
threadId: created.threadId,
|
|
329
|
+
prompt,
|
|
330
|
+
cwd: target.cwd || null,
|
|
331
|
+
}).catch(error => console.error(`生成对话标题失败:${error.message}`));
|
|
332
|
+
sendJson(response, 201, { ...created, hostId:'local' });
|
|
333
|
+
return;
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
335
|
-
if (request.method === 'GET' && timelineMatch) {
|
|
336
|
-
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
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
|
-
}
|
|
336
|
+
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
337
|
+
if (request.method === 'GET' && timelineMatch) {
|
|
338
|
+
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
339
|
+
const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
|
|
340
|
+
const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
|
|
341
|
+
const knownTurnId = url.searchParams.get('knownTurnId') || '';
|
|
342
|
+
const knownTurnStatus = url.searchParams.get('knownTurnStatus') || '';
|
|
343
|
+
const knownHistoryView = url.searchParams.get('knownHistoryView') || '';
|
|
344
|
+
const latest = timeline.turns.at(-1);
|
|
345
|
+
const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
|
|
346
|
+
const sameHistoryView = knownHistoryView === timeline.historyView;
|
|
347
|
+
const unchanged = sameHistoryView
|
|
348
|
+
&& knownIndex === timeline.turns.length - 1
|
|
349
|
+
&& timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
|
|
350
|
+
&& knownTurnStatus && knownTurnStatus !== 'inProgress'
|
|
351
|
+
&& (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
|
|
352
|
+
const partial = !unchanged && sameHistoryView && knownIndex >= 0
|
|
353
|
+
? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
|
|
354
|
+
: timeline;
|
|
355
|
+
sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
356
358
|
|
|
357
359
|
const approvalMatch = /^\/api\/threads\/([^/]+)\/approvals$/.exec(url.pathname);
|
|
358
|
-
if (request.method === 'POST' && approvalMatch) {
|
|
359
|
-
const threadId = decodeURIComponent(approvalMatch[1]);
|
|
360
|
-
await requireExecutionWritable(threadId);
|
|
361
|
-
const body = await readJsonBody(request);
|
|
360
|
+
if (request.method === 'POST' && approvalMatch) {
|
|
361
|
+
const threadId = decodeURIComponent(approvalMatch[1]);
|
|
362
|
+
await requireExecutionWritable(threadId);
|
|
363
|
+
const body = await readJsonBody(request);
|
|
362
364
|
sendJson(response, 200, await sharedServer.respondApproval(threadId, body)); return;
|
|
363
365
|
}
|
|
364
366
|
|
|
365
367
|
const eventsMatch = /^\/api\/threads\/([^/]+)\/events$/.exec(url.pathname);
|
|
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
|
-
}
|
|
368
|
+
if (request.method === 'GET' && eventsMatch) {
|
|
369
|
+
const threadId = decodeURIComponent(eventsMatch[1]);
|
|
370
|
+
const after = Number(url.searchParams.get('afterSeq'));
|
|
371
|
+
const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
|
|
372
|
+
? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
|
|
373
|
+
const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
|
|
374
|
+
const known = url.searchParams.get('knownTurnId') ? {
|
|
375
|
+
updatedAt: Number.isFinite(knownUpdatedAt) ? knownUpdatedAt : 0,
|
|
376
|
+
turnId: url.searchParams.get('knownTurnId'),
|
|
377
|
+
turnStatus: url.searchParams.get('knownTurnStatus') || '',
|
|
378
|
+
} : undefined;
|
|
379
|
+
await streamSharedThread(response, threadId, sharedServer,
|
|
380
|
+
normalizeSharedTimeline, requestedSync, known);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
381
383
|
|
|
382
384
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
383
385
|
if (request.method === 'GET' && contextMatch) {
|
|
384
386
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
385
387
|
const current = readContextUsage(threadId);
|
|
386
|
-
const native = await sharedServer.readContext(threadId);
|
|
388
|
+
const native = await sharedServer.readContext(threadId);
|
|
387
389
|
sendJson(response, 200, { ...current, ...(native.model ? { model: native.model, thinking: native.thinking, serviceTier: native.serviceTier,
|
|
388
390
|
permissionMode: native.permissionMode, permissionModes: native.permissionModes } : {}), ...(native.available ? native : {}) });
|
|
389
391
|
return;
|
|
390
392
|
}
|
|
391
393
|
|
|
392
394
|
const sharedAction = /^\/api\/threads\/([^/]+)\/(history|settings|permissions)$/.exec(url.pathname);
|
|
393
|
-
if (sharedAction) {
|
|
395
|
+
if (sharedAction) {
|
|
394
396
|
const id = decodeURIComponent(sharedAction[1]);
|
|
395
397
|
if (request.method === 'GET' && sharedAction[2] === 'history') {
|
|
396
|
-
sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'), sharedHistoryPageOptions())));
|
|
398
|
+
sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'), sharedHistoryPageOptions())));
|
|
397
399
|
return;
|
|
398
400
|
}
|
|
399
|
-
if (request.method === 'POST' && sharedAction[2] === 'settings') {
|
|
400
|
-
await requireExecutionWritable(id);
|
|
401
|
+
if (request.method === 'POST' && sharedAction[2] === 'settings') {
|
|
402
|
+
await requireExecutionWritable(id);
|
|
401
403
|
const body = await readJsonBody(request);
|
|
402
404
|
sendJson(response, 200, await sharedServer.settings(id, body.model, body.thinking, body.serviceTier));
|
|
403
405
|
return;
|
|
404
406
|
}
|
|
405
|
-
if (request.method === 'POST' && sharedAction[2] === 'permissions') {
|
|
406
|
-
await requireExecutionWritable(id);
|
|
407
|
+
if (request.method === 'POST' && sharedAction[2] === 'permissions') {
|
|
408
|
+
await requireExecutionWritable(id);
|
|
407
409
|
const body = await readJsonBody(request);
|
|
408
410
|
sendJson(response, 200, await sharedServer.permissions(id, body.mode));
|
|
409
411
|
return;
|
|
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);
|
|
425
|
-
if (request.method === 'POST' && archiveMatch) {
|
|
426
|
-
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
427
|
-
await requireExecutionWritable(threadId);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const sharedOperationGroup = /^\/api\/threads\/([^/]+)\/turns\/([^/]+)\/groups\/([^/]+)\/items$/.exec(url.pathname);
|
|
416
|
+
if (request.method === 'GET' && sharedOperationGroup) {
|
|
417
|
+
const threadId = decodeURIComponent(sharedOperationGroup[1]);
|
|
418
|
+
const turnId = decodeURIComponent(sharedOperationGroup[2]);
|
|
419
|
+
const groupId = decodeURIComponent(sharedOperationGroup[3]);
|
|
420
|
+
const page = await sharedServer.operationGroupItems(threadId, turnId, groupId, url.searchParams.get('offset'));
|
|
421
|
+
sendJson(response, 200, { turnId: page.turnId, groupId: page.groupId,
|
|
422
|
+
entries: page.items.map(normalizeItem).filter(Boolean), nextOffset: page.nextOffset, total: page.total });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
427
|
+
if (request.method === 'POST' && archiveMatch) {
|
|
428
|
+
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
429
|
+
await requireExecutionWritable(threadId);
|
|
428
430
|
await sharedServer.request(`thread/${archiveMatch[2]}`, { threadId });
|
|
429
431
|
sendJson(response, 200, { threadId, archived: archiveMatch[2] === 'archive' });
|
|
430
432
|
return;
|
|
431
433
|
}
|
|
432
434
|
|
|
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; }
|
|
439
|
-
if (request.method === 'POST') {
|
|
440
|
-
await requireExecutionWritable(threadId);
|
|
441
|
-
const args = appServerTurnArguments(threadId, await readJsonBody(request));
|
|
442
|
-
sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
435
|
+
const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
|
|
436
|
+
if (queueMatch) {
|
|
437
|
+
const threadId = decodeURIComponent(queueMatch[1]);
|
|
438
|
+
await sharedServer.connect();
|
|
439
|
+
const rpc = (method, params) => sharedServer.request(method, params);
|
|
440
|
+
if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
|
|
441
|
+
if (request.method === 'POST') {
|
|
442
|
+
await requireExecutionWritable(threadId);
|
|
443
|
+
const args = appServerTurnArguments(threadId, await readJsonBody(request));
|
|
444
|
+
sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
445
447
|
|
|
446
448
|
const queueItemMatch = /^\/api\/threads\/([^/]+)\/queue\/([a-f0-9-]+)(?:\/(steer))?$/.exec(url.pathname);
|
|
447
449
|
if (queueItemMatch) {
|
|
448
450
|
const threadId = decodeURIComponent(queueItemMatch[1]);
|
|
449
451
|
const itemId = queueItemMatch[2];
|
|
450
|
-
await sharedServer.connect();
|
|
451
|
-
const action = request.method === 'DELETE' ? 'delete' : queueItemMatch[3] === 'steer' ? 'steer' : null;
|
|
452
|
-
if (!action) throw new RequestError(405, '不支持的队列操作');
|
|
453
|
-
await requireExecutionWritable(threadId);
|
|
454
|
-
sendJson(response, 200, await appServerEvents.changeQueue((method, params) => sharedServer.request(method, params), threadId, itemId, action)); return;
|
|
455
|
-
}
|
|
452
|
+
await sharedServer.connect();
|
|
453
|
+
const action = request.method === 'DELETE' ? 'delete' : queueItemMatch[3] === 'steer' ? 'steer' : null;
|
|
454
|
+
if (!action) throw new RequestError(405, '不支持的队列操作');
|
|
455
|
+
await requireExecutionWritable(threadId);
|
|
456
|
+
sendJson(response, 200, await appServerEvents.changeQueue((method, params) => sharedServer.request(method, params), threadId, itemId, action)); return;
|
|
457
|
+
}
|
|
456
458
|
|
|
457
|
-
const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
|
|
458
|
-
if (request.method === 'POST' && interruptMatch) {
|
|
459
|
-
const threadId = decodeURIComponent(interruptMatch[1]);
|
|
460
|
-
await requireExecutionWritable(threadId);
|
|
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;
|
|
459
|
+
const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
|
|
460
|
+
if (request.method === 'POST' && interruptMatch) {
|
|
461
|
+
const threadId = decodeURIComponent(interruptMatch[1]);
|
|
462
|
+
await requireExecutionWritable(threadId);
|
|
463
|
+
const body = await readJsonBody(request);
|
|
464
|
+
if (!body.turnId) throw new RequestError(400, '缺少活动轮次 ID。');
|
|
465
|
+
await sharedServer.interrupt(threadId, body.turnId);
|
|
466
|
+
sendJson(response, 200, { requested: true });
|
|
467
|
+
return;
|
|
466
468
|
}
|
|
467
469
|
|
|
468
470
|
const messageMatch = /^\/api\/threads\/([^/]+)\/messages$/.exec(url.pathname);
|
|
469
|
-
if (request.method === 'POST' && messageMatch) {
|
|
470
|
-
const threadId = decodeURIComponent(messageMatch[1]);
|
|
471
|
-
await requireExecutionWritable(threadId);
|
|
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
|
-
}
|
|
471
|
+
if (request.method === 'POST' && messageMatch) {
|
|
472
|
+
const threadId = decodeURIComponent(messageMatch[1]);
|
|
473
|
+
await requireExecutionWritable(threadId);
|
|
474
|
+
const body = await readJsonBody(request);
|
|
475
|
+
if (body.hostId && body.hostId !== 'local') throw new RequestError(410, '此对话的连接方式已停用。', { code: 'unsupported_control' });
|
|
476
|
+
sendJson(response, 200, { success: true, data: await sharedServer.send(appServerTurnArguments(threadId, body)) });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
477
479
|
|
|
478
480
|
sendJson(response, 404, { error: '未找到请求的资源。' });
|
|
479
481
|
} catch (error) {
|
|
@@ -481,27 +483,27 @@ async function handleRequest(request, response) {
|
|
|
481
483
|
if (!response.writableEnded) response.end();
|
|
482
484
|
return;
|
|
483
485
|
}
|
|
484
|
-
const status = error instanceof RequestError || error instanceof ThreadWriterConflictError ? (error.statusCode || 409) : 502;
|
|
485
|
-
sendJson(response, status, {
|
|
486
|
-
error: error instanceof Error ? error.message : String(error),
|
|
487
|
-
code: error?.code,
|
|
488
|
-
threadId: error?.threadId,
|
|
489
|
-
readOnly: error?.readOnly,
|
|
490
|
-
details: error?.details,
|
|
486
|
+
const status = error instanceof RequestError || error instanceof ThreadWriterConflictError ? (error.statusCode || 409) : 502;
|
|
487
|
+
sendJson(response, status, {
|
|
488
|
+
error: error instanceof Error ? error.message : String(error),
|
|
489
|
+
code: error?.code,
|
|
490
|
+
threadId: error?.threadId,
|
|
491
|
+
readOnly: error?.readOnly,
|
|
492
|
+
details: error?.details,
|
|
491
493
|
});
|
|
492
494
|
}
|
|
493
495
|
}
|
|
494
496
|
|
|
495
497
|
const server = http.createServer(handleRequest);
|
|
496
|
-
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
497
|
-
uploadsRoot,
|
|
498
|
-
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
498
|
+
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
499
|
+
uploadsRoot,
|
|
500
|
+
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
499
501
|
});
|
|
500
502
|
|
|
501
|
-
let control;
|
|
502
|
-
void managedAppServer.start();
|
|
503
|
-
executionHealth.start();
|
|
504
|
-
runtimeDiscovery.start();
|
|
503
|
+
let control;
|
|
504
|
+
void managedAppServer.start();
|
|
505
|
+
executionHealth.start();
|
|
506
|
+
runtimeDiscovery.start();
|
|
505
507
|
server.on('error', error => {
|
|
506
508
|
console.error('Bridge 监听失败:', error.message);
|
|
507
509
|
process.exit(1);
|
|
@@ -509,14 +511,19 @@ server.on('error', error => {
|
|
|
509
511
|
server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
510
512
|
try {
|
|
511
513
|
if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
|
|
512
|
-
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
513
|
-
secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
|
|
514
|
+
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
515
|
+
secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
|
|
514
516
|
'refresh-execution': refreshExecution,
|
|
515
|
-
'
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
517
|
+
'relay-start': () => relayManager.run('relay-start'),
|
|
518
|
+
'relay-status': () => relayManager.run('relay-status'),
|
|
519
|
+
'relay-stop': () => relayManager.run('relay-stop'),
|
|
520
|
+
'relay-pair': () => relayManager.run('relay-pair'),
|
|
521
|
+
'relay-revoke': () => relayManager.run('relay-revoke'),
|
|
522
|
+
'restart-managed-app-server': () => {
|
|
523
|
+
const state = managedAppServer.snapshot();
|
|
524
|
+
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
525
|
+
},
|
|
526
|
+
} });
|
|
520
527
|
writeRecord(process.env.CODEX_REMOTE_READY_PATH, { instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
521
528
|
pid: process.pid, controlPort: control.port });
|
|
522
529
|
}
|
|
@@ -536,9 +543,10 @@ async function shutDown() {
|
|
|
536
543
|
shuttingDown = true;
|
|
537
544
|
lanSocket.close();
|
|
538
545
|
server.close();
|
|
539
|
-
executionHealth?.stop();
|
|
546
|
+
executionHealth?.stop();
|
|
540
547
|
runtimeDiscovery.stop();
|
|
541
|
-
|
|
548
|
+
await relayManager.close();
|
|
549
|
+
managedAppServer.close();
|
|
542
550
|
await control?.close();
|
|
543
551
|
process.exit(0);
|
|
544
552
|
}
|
|
@@ -715,73 +723,73 @@ function appServerTurnArguments(threadId, body) {
|
|
|
715
723
|
return value;
|
|
716
724
|
}
|
|
717
725
|
|
|
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 {
|
|
726
|
+
function normalizeTimeline(data) {
|
|
727
|
+
const source = data && typeof data === 'object' ? data : {};
|
|
728
|
+
const sourceTurns = Array.isArray(source.turns) ? source.turns : source.thread?.turns;
|
|
729
|
+
const turns = Array.isArray(sourceTurns) ? sourceTurns.map(normalizeTurn) : [];
|
|
730
|
+
return {
|
|
723
731
|
schemaVersion: 1,
|
|
724
732
|
thread: normalizeThread(source.thread),
|
|
725
733
|
turns,
|
|
726
734
|
};
|
|
727
735
|
}
|
|
728
736
|
|
|
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-
|
|
733
|
-
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
734
|
-
}
|
|
737
|
+
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
738
|
+
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
|
|
739
|
+
return { ...timeline,
|
|
740
|
+
historyView: 'grouped-lazy-skeleton-v13',
|
|
741
|
+
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
742
|
+
}
|
|
735
743
|
|
|
736
|
-
function normalizeThread(thread) {
|
|
744
|
+
function normalizeThread(thread) {
|
|
737
745
|
const source = thread && typeof thread === 'object' ? thread : {};
|
|
738
746
|
return {
|
|
739
|
-
id: source.id || '',
|
|
740
|
-
title: source.name || source.title || source.preview || '',
|
|
741
|
-
status: statusText(source.status),
|
|
742
|
-
updatedAt: Number(source.updatedAt) || 0,
|
|
743
|
-
recencyAt: Number(source.recencyAt ?? source.updatedAt) || 0,
|
|
744
|
-
projectId: source.projectId || '',
|
|
747
|
+
id: source.id || '',
|
|
748
|
+
title: source.name || source.title || source.preview || '',
|
|
749
|
+
status: statusText(source.status),
|
|
750
|
+
updatedAt: Number(source.updatedAt) || 0,
|
|
751
|
+
recencyAt: Number(source.recencyAt ?? source.updatedAt) || 0,
|
|
752
|
+
projectId: source.projectId || '',
|
|
745
753
|
cwd: source.cwd || '',
|
|
746
754
|
hostId: source.hostId || '',
|
|
747
|
-
transport: source.transport || 'shared-app-server',
|
|
748
|
-
readOnly: source.readOnly === true,
|
|
749
|
-
readOnlyReason: source.readOnlyReason || '',
|
|
755
|
+
transport: source.transport || 'shared-app-server',
|
|
756
|
+
readOnly: source.readOnly === true,
|
|
757
|
+
readOnlyReason: source.readOnlyReason || '',
|
|
750
758
|
};
|
|
751
759
|
}
|
|
752
760
|
|
|
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),
|
|
761
|
+
function normalizeTurn(turn) {
|
|
762
|
+
const source = turn && typeof turn === 'object' ? turn : {};
|
|
763
|
+
const turnId = source.id || '';
|
|
764
|
+
const entries = collapseSubAgentEntries(Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : []);
|
|
765
|
+
for (const entry of entries) {
|
|
766
|
+
const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
|
|
767
|
+
for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
id: turnId,
|
|
771
|
+
status: statusText(source.status),
|
|
764
772
|
startedAt: source.startedAt || null,
|
|
765
773
|
completedAt: source.completedAt || 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
|
-
}
|
|
774
|
+
durationMs: source.durationMs ?? null,
|
|
775
|
+
error: source.error ?? null,
|
|
776
|
+
entries,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function normalizeItem(item) {
|
|
781
|
+
if (!item || typeof item !== 'object') return null;
|
|
782
|
+
|
|
783
|
+
if (item.type === 'lazyOperationGroup') {
|
|
784
|
+
return {
|
|
785
|
+
type: 'activity',
|
|
786
|
+
id: item.id || '',
|
|
787
|
+
kind: 'lazyOperationGroup',
|
|
788
|
+
turnId: item.turnId || '',
|
|
789
|
+
groupId: item.groupId || '',
|
|
790
|
+
operationTypes: Array.isArray(item.operationTypes) ? item.operationTypes : [],
|
|
791
|
+
};
|
|
792
|
+
}
|
|
785
793
|
|
|
786
794
|
if (item.type === 'contextCompaction') {
|
|
787
795
|
return {
|
|
@@ -807,9 +815,9 @@ function normalizeItem(item) {
|
|
|
807
815
|
}
|
|
808
816
|
}
|
|
809
817
|
|
|
810
|
-
if (item.type === 'userMessage') {
|
|
811
|
-
const { text, files } = extractUserText(item.content);
|
|
812
|
-
const attachments = [...normalizeAttachments(item.content), ...files];
|
|
818
|
+
if (item.type === 'userMessage') {
|
|
819
|
+
const { text, files } = extractUserText(item.content);
|
|
820
|
+
const attachments = [...normalizeAttachments(item.content), ...files];
|
|
813
821
|
if (!text && !attachments.length) return null;
|
|
814
822
|
return {
|
|
815
823
|
clientId: item.clientId,
|
|
@@ -835,248 +843,259 @@ function normalizeItem(item) {
|
|
|
835
843
|
};
|
|
836
844
|
}
|
|
837
845
|
|
|
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
|
-
}
|
|
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
|
-
|
|
866
|
-
if (item.type === 'mcpToolCall') {
|
|
867
|
-
return {
|
|
868
|
-
type: 'activity',
|
|
869
|
-
id: item.id || '',
|
|
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 : [],
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
|
|
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
|
-
|
|
846
|
+
if (item.type === 'reasoning') {
|
|
847
|
+
const detail = Array.isArray(item.summary)
|
|
848
|
+
? item.summary.map(summaryText).filter(Boolean).join('\n')
|
|
849
|
+
: summaryText(item.summary);
|
|
850
|
+
return {
|
|
851
|
+
type: 'activity',
|
|
852
|
+
id: item.id || '',
|
|
853
|
+
kind: 'reasoning',
|
|
854
|
+
title: '思考摘要',
|
|
855
|
+
detail,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (item.type === 'commandExecution') {
|
|
860
|
+
return {
|
|
861
|
+
type: 'activity',
|
|
862
|
+
id: item.id || '',
|
|
863
|
+
kind: 'command',
|
|
864
|
+
title: '命令',
|
|
865
|
+
titleOnly: true,
|
|
866
|
+
status: statusText(item.status),
|
|
867
|
+
command: item.command,
|
|
868
|
+
cwd: item.cwd,
|
|
869
|
+
exitCode: item.exitCode,
|
|
870
|
+
durationMs: item.durationMs,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (item.type === 'mcpToolCall') {
|
|
875
|
+
return {
|
|
876
|
+
type: 'activity',
|
|
877
|
+
id: item.id || '',
|
|
878
|
+
kind: 'tool',
|
|
879
|
+
title: '工具',
|
|
880
|
+
titleOnly: true,
|
|
881
|
+
status: statusText(item.status),
|
|
882
|
+
server: item.server,
|
|
883
|
+
tool: item.tool,
|
|
884
|
+
arguments: item.arguments,
|
|
885
|
+
durationMs: item.durationMs,
|
|
886
|
+
error: item.error,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (item.type === 'dynamicToolCall') {
|
|
891
|
+
return {
|
|
892
|
+
type: 'activity',
|
|
893
|
+
id: item.id || '',
|
|
894
|
+
kind: 'tool',
|
|
895
|
+
title: '工具',
|
|
896
|
+
titleOnly: true,
|
|
897
|
+
status: statusText(item.status),
|
|
898
|
+
namespace: item.namespace,
|
|
899
|
+
tool: item.tool,
|
|
900
|
+
arguments: item.arguments,
|
|
901
|
+
durationMs: item.durationMs,
|
|
902
|
+
success: item.success,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (item.type === 'fileChange') {
|
|
907
|
+
return {
|
|
908
|
+
type: 'activity',
|
|
909
|
+
id: item.id || '',
|
|
910
|
+
kind: 'fileChange',
|
|
911
|
+
title: '文件修改',
|
|
912
|
+
titleOnly: true,
|
|
913
|
+
status: statusText(item.status),
|
|
914
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (item.type === 'webSearch') {
|
|
919
|
+
return {
|
|
920
|
+
type: 'activity',
|
|
921
|
+
id: item.id || '',
|
|
922
|
+
kind: 'webSearch',
|
|
923
|
+
title: '网页搜索',
|
|
924
|
+
titleOnly: true,
|
|
925
|
+
query: item.query || item.action?.query || '',
|
|
926
|
+
...(item.action && typeof item.action === 'object' ? { action: item.action } : {}),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
922
930
|
if (item.type === 'imageView') {
|
|
923
|
-
|
|
924
|
-
type: '
|
|
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: '生成的图片' })
|
|
931
|
+
const image = typeof item.path === 'string' && item.path
|
|
932
|
+
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
939
933
|
: null;
|
|
934
|
+
if (image?.available) {
|
|
935
|
+
return {
|
|
936
|
+
type: 'message',
|
|
937
|
+
id: item.id || '',
|
|
938
|
+
role: 'assistant',
|
|
939
|
+
phase: 'commentary',
|
|
940
|
+
text: '',
|
|
941
|
+
attachments: [image],
|
|
942
|
+
};
|
|
943
|
+
}
|
|
940
944
|
return {
|
|
941
|
-
type: 'activity',
|
|
942
|
-
id: item.id || '',
|
|
943
|
-
kind: '
|
|
944
|
-
title: '
|
|
945
|
-
titleOnly: true,
|
|
946
|
-
|
|
945
|
+
type: 'activity',
|
|
946
|
+
id: item.id || '',
|
|
947
|
+
kind: 'imageView',
|
|
948
|
+
title: '查看图片',
|
|
949
|
+
titleOnly: true,
|
|
950
|
+
path: item.path || '',
|
|
947
951
|
image,
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (item.type === 'imageGeneration') {
|
|
956
|
+
const image = typeof item.savedPath === 'string' && item.savedPath
|
|
957
|
+
? normalizeAttachment({ type: 'localImage', path: item.savedPath, name: '生成的图片' })
|
|
958
|
+
: null;
|
|
959
|
+
return {
|
|
960
|
+
type: 'activity',
|
|
961
|
+
id: item.id || '',
|
|
962
|
+
kind: 'imageGeneration',
|
|
963
|
+
title: '生成图片',
|
|
964
|
+
titleOnly: true,
|
|
965
|
+
status: statusText(item.status),
|
|
966
|
+
image,
|
|
967
|
+
failure: item.failure || null,
|
|
968
|
+
transparentBackground: item.transparentBackground === true,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (item.type === 'subAgentActivity') {
|
|
973
|
+
return {
|
|
974
|
+
type: 'activity',
|
|
975
|
+
id: item.id || '',
|
|
976
|
+
kind: 'subAgent',
|
|
977
|
+
title: '子任务',
|
|
978
|
+
titleOnly: true,
|
|
979
|
+
status: statusText(item.kind),
|
|
980
|
+
agentPath: item.agentPath || '',
|
|
981
|
+
agentThreadId: item.agentThreadId || '',
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (item.type === 'collabAgentToolCall') {
|
|
986
|
+
return {
|
|
987
|
+
type: 'activity',
|
|
988
|
+
id: item.id || '',
|
|
989
|
+
kind: 'collaboration',
|
|
990
|
+
title: '任务协作',
|
|
991
|
+
titleOnly: true,
|
|
992
|
+
status: statusText(item.status),
|
|
993
|
+
tool: item.tool || '',
|
|
994
|
+
prompt: item.prompt || '',
|
|
995
|
+
senderThreadId: item.senderThreadId || '',
|
|
996
|
+
receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
|
|
997
|
+
model: item.model || '',
|
|
998
|
+
reasoningEffort: item.reasoningEffort || '',
|
|
999
|
+
agentsStates: item.agentsStates && typeof item.agentsStates === 'object' ? item.agentsStates : {},
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
985
1004
|
type: 'activity',
|
|
986
1005
|
id: item.id || '',
|
|
987
1006
|
kind: item.type || 'event',
|
|
988
1007
|
title: item.type || '事件',
|
|
989
1008
|
status: statusText(item.status),
|
|
990
1009
|
detail: JSON.stringify(item, null, 2),
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
|
|
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;
|
|
1006
|
-
};
|
|
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;
|
|
1029
|
-
}
|
|
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;
|
|
1050
|
-
}
|
|
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))),
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function normalizeAttachments(content) {
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function collapseSubAgentEntries(entries) {
|
|
1014
|
+
const agents = new Map();
|
|
1015
|
+
let firstIndex = -1;
|
|
1016
|
+
const retained = [];
|
|
1017
|
+
const ensureAgent = threadId => {
|
|
1018
|
+
if (!threadId) return null;
|
|
1019
|
+
let agent = agents.get(threadId);
|
|
1020
|
+
if (!agent) {
|
|
1021
|
+
agent = { threadId, name: '', status: 'pendingInit', objective: '', model: '', reasoningEffort: '' };
|
|
1022
|
+
agents.set(threadId, agent);
|
|
1023
|
+
}
|
|
1024
|
+
return agent;
|
|
1025
|
+
};
|
|
1026
|
+
const setProtocolStatus = (agent, status) => {
|
|
1027
|
+
if (!agent || !status || status === 'unknown') return;
|
|
1028
|
+
const terminal = ['interrupted', 'completed', 'errored', 'shutdown', 'notFound'];
|
|
1029
|
+
if (terminal.includes(agent.status) && ['pendingInit', 'running'].includes(status)) return;
|
|
1030
|
+
if (status === 'shutdown' && agent.status === 'completed') return;
|
|
1031
|
+
agent.status = status;
|
|
1032
|
+
};
|
|
1033
|
+
for (const entry of entries) {
|
|
1034
|
+
if (entry.kind === 'subAgent') {
|
|
1035
|
+
if (firstIndex < 0) firstIndex = retained.length;
|
|
1036
|
+
const agent = ensureAgent(entry.agentThreadId);
|
|
1037
|
+
if (!agent) continue;
|
|
1038
|
+
agent.name = subAgentDisplayName(entry.agentPath) || agent.name;
|
|
1039
|
+
const activityStatus = {
|
|
1040
|
+
started: 'running',
|
|
1041
|
+
interacted: 'running',
|
|
1042
|
+
interrupted: 'interrupted',
|
|
1043
|
+
completed: 'completed',
|
|
1044
|
+
}[entry.status];
|
|
1045
|
+
agent.activity = entry.status;
|
|
1046
|
+
setProtocolStatus(agent, activityStatus);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (entry.kind === 'collaboration') {
|
|
1050
|
+
const receiverIds = entry.receiverThreadIds || [];
|
|
1051
|
+
const createsAgent = entry.tool === 'spawnAgent';
|
|
1052
|
+
const updatesKnownAgent = receiverIds.some(threadId => agents.has(threadId));
|
|
1053
|
+
if (!createsAgent && !updatesKnownAgent) {
|
|
1054
|
+
retained.push(entry);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (firstIndex < 0) firstIndex = retained.length;
|
|
1058
|
+
for (const threadId of receiverIds) {
|
|
1059
|
+
const agent = ensureAgent(threadId);
|
|
1060
|
+
if (!agent) continue;
|
|
1061
|
+
if (entry.prompt) agent.objective = entry.prompt;
|
|
1062
|
+
if (entry.model) agent.model = entry.model;
|
|
1063
|
+
if (entry.reasoningEffort) agent.reasoningEffort = entry.reasoningEffort;
|
|
1064
|
+
setProtocolStatus(agent, statusText(entry.agentsStates?.[threadId]?.status));
|
|
1065
|
+
if (entry.status === 'failed' && !entry.agentsStates?.[threadId]?.status) agent.status = 'errored';
|
|
1066
|
+
if (entry.status === 'interrupted' && !entry.agentsStates?.[threadId]?.status) agent.status = 'interrupted';
|
|
1067
|
+
}
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
retained.push(entry);
|
|
1071
|
+
}
|
|
1072
|
+
if (!agents.size) return retained;
|
|
1073
|
+
retained.splice(firstIndex < 0 ? retained.length : firstIndex, 0, {
|
|
1074
|
+
type: 'activity',
|
|
1075
|
+
id: `subagents:${[...agents.keys()].join(':')}`,
|
|
1076
|
+
kind: 'subAgentGroup',
|
|
1077
|
+
title: '子 Agent',
|
|
1078
|
+
agents: [...agents.values()],
|
|
1079
|
+
});
|
|
1080
|
+
return retained;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function subAgentDisplayName(agentPath) {
|
|
1084
|
+
const segment = String(agentPath || '').split('/').map(value => value.trim()).filter(value => value && value !== 'root').at(-1);
|
|
1085
|
+
if (!segment) return '';
|
|
1086
|
+
const text = segment.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1087
|
+
return text ? text[0].toUpperCase() + text.slice(1) : '';
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function sharedHistoryPageOptions() {
|
|
1091
|
+
return {
|
|
1092
|
+
maxTurns: SHARED_HISTORY_PAGE_TURNS,
|
|
1093
|
+
maxBytes: SHARED_HISTORY_PAGE_BYTES,
|
|
1094
|
+
measure: value => Buffer.byteLength(JSON.stringify(normalizeSharedTimeline(value))),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function normalizeAttachments(content) {
|
|
1080
1099
|
if (!Array.isArray(content)) return [];
|
|
1081
1100
|
const items = content.filter(item => item && !['text', 'input_text'].includes(item.type)).map(item => ({ ...item }));
|
|
1082
1101
|
const text = content.filter(item => ['text', 'input_text'].includes(item?.type)).map(item => item.text || '').join('\n');
|
|
@@ -1119,19 +1138,19 @@ function markdownImageAssets(text) {
|
|
|
1119
1138
|
return assets;
|
|
1120
1139
|
}
|
|
1121
1140
|
|
|
1122
|
-
function markdownFileAssets(text) {
|
|
1141
|
+
function markdownFileAssets(text) {
|
|
1123
1142
|
const assets = Object.create(null);
|
|
1124
1143
|
function visit(tokens) {
|
|
1125
1144
|
for (const token of tokens) {
|
|
1126
1145
|
if (token.type === 'link_open') {
|
|
1127
1146
|
const value = token.attrGet('href');
|
|
1128
1147
|
if (value && !Object.hasOwn(assets, value)) {
|
|
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);
|
|
1148
|
+
let localPath;
|
|
1149
|
+
try { localPath = decodeURIComponent(value); } catch { localPath = ''; }
|
|
1150
|
+
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1151
|
+
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1152
|
+
localPath = localPath.replace(/:(\d+)(?::\d+)?$/, '');
|
|
1153
|
+
const asset = rememberLocalFile(localPath);
|
|
1135
1154
|
if (asset) assets[value] = { remotePath: asset.id, name: asset.name, size: asset.size };
|
|
1136
1155
|
}
|
|
1137
1156
|
}
|
|
@@ -1174,7 +1193,7 @@ function splitUploadedImages(text) {
|
|
|
1174
1193
|
const paths = children.map(token => {
|
|
1175
1194
|
try { return path.resolve(decodeURIComponent(token.attrGet('src') || '')); } catch { return ''; }
|
|
1176
1195
|
});
|
|
1177
|
-
if (paths.some(file => path.dirname(file).toLowerCase() !== uploadsRoot.toLowerCase()
|
|
1196
|
+
if (paths.some(file => path.dirname(file).toLowerCase() !== uploadsRoot.toLowerCase()
|
|
1178
1197
|
|| !/^[a-f0-9]{64}\.(png|webp)$/.test(path.basename(file)))) continue;
|
|
1179
1198
|
for (const file of paths) attachments.push(normalizeAttachment({ type: 'localImage', path: file, name: '移动端图片' }));
|
|
1180
1199
|
for (let line = block.map[0]; line < block.map[1]; line++) removed.add(line);
|
|
@@ -1201,23 +1220,23 @@ function normalizeAttachment(item) {
|
|
|
1201
1220
|
name,
|
|
1202
1221
|
mimeType: cached?.mimeType || item.mimeType || '',
|
|
1203
1222
|
available: Boolean(cached),
|
|
1204
|
-
remotePath: cached ? cached.id : '',
|
|
1205
|
-
recoveryToken: cached?.recoveryToken || '',
|
|
1223
|
+
remotePath: cached ? cached.id : '',
|
|
1224
|
+
recoveryToken: cached?.recoveryToken || '',
|
|
1206
1225
|
url: '',
|
|
1207
1226
|
thumbnailUrl: '',
|
|
1208
1227
|
};
|
|
1209
1228
|
}
|
|
1210
1229
|
|
|
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;
|
|
1230
|
+
function cacheLocalImage(filePath, name) {
|
|
1231
|
+
try {
|
|
1232
|
+
const resolved = path.resolve(filePath);
|
|
1233
|
+
const stat = fs.statSync(resolved);
|
|
1234
|
+
const mimeType = imageMimeType(resolved);
|
|
1235
|
+
if (!stat.isFile() || !mimeType || stat.size > MAX_ATTACHMENT_BYTES) return null;
|
|
1236
|
+
const cached = rememberAttachment(fs.readFileSync(resolved), mimeType, name,
|
|
1237
|
+
localImageRecoveryToken(resolved, stat));
|
|
1238
|
+
void attachmentThumbnail(cached).catch(() => {});
|
|
1239
|
+
return cached;
|
|
1221
1240
|
} catch {
|
|
1222
1241
|
return null;
|
|
1223
1242
|
}
|
|
@@ -1235,65 +1254,65 @@ function cacheDataImage(dataUrl, name) {
|
|
|
1235
1254
|
}
|
|
1236
1255
|
}
|
|
1237
1256
|
|
|
1238
|
-
function rememberAttachment(buffer, mimeType, name, recoveryToken = '') {
|
|
1239
|
-
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1240
|
-
if (!attachmentCache.has(id)) {
|
|
1257
|
+
function rememberAttachment(buffer, mimeType, name, recoveryToken = '') {
|
|
1258
|
+
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1259
|
+
if (!attachmentCache.has(id)) {
|
|
1241
1260
|
while (attachmentCache.size >= MAX_CACHED_ATTACHMENTS) {
|
|
1242
1261
|
attachmentCache.delete(attachmentCache.keys().next().value);
|
|
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
|
-
}
|
|
1262
|
+
}
|
|
1263
|
+
attachmentCache.set(id, { id, buffer, mimeType, name, recoveryToken });
|
|
1264
|
+
} else if (recoveryToken && !attachmentCache.get(id).recoveryToken) {
|
|
1265
|
+
attachmentCache.get(id).recoveryToken = recoveryToken;
|
|
1266
|
+
}
|
|
1267
|
+
return attachmentCache.get(id);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function localImageRecoveryToken(filePath, stat) {
|
|
1271
|
+
const iv = crypto.randomBytes(12);
|
|
1272
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', imageCapabilityKey, iv);
|
|
1273
|
+
const payload = Buffer.from(JSON.stringify([filePath, stat.size, stat.mtimeMs]));
|
|
1274
|
+
const encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
1275
|
+
return Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString('base64url');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function recoverLocalImage(id, token) {
|
|
1279
|
+
if (!token || token.length > 2048) return null;
|
|
1280
|
+
try {
|
|
1281
|
+
const value = Buffer.from(token, 'base64url');
|
|
1282
|
+
if (value.length < 29) return null;
|
|
1283
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', imageCapabilityKey, value.subarray(0, 12));
|
|
1284
|
+
decipher.setAuthTag(value.subarray(12, 28));
|
|
1285
|
+
const [filePath, size, mtimeMs] = JSON.parse(Buffer.concat([
|
|
1286
|
+
decipher.update(value.subarray(28)), decipher.final(),
|
|
1287
|
+
]).toString('utf8'));
|
|
1288
|
+
if (typeof filePath !== 'string' || !path.isAbsolute(filePath)
|
|
1289
|
+
|| !Number.isSafeInteger(size) || size <= 0 || size > MAX_ATTACHMENT_BYTES
|
|
1290
|
+
|| typeof mtimeMs !== 'number' || !Number.isFinite(mtimeMs)) return null;
|
|
1291
|
+
const stat = fs.statSync(filePath);
|
|
1292
|
+
const mimeType = imageMimeType(filePath);
|
|
1293
|
+
if (!stat.isFile() || stat.size !== size || stat.mtimeMs !== mtimeMs || !mimeType) return null;
|
|
1294
|
+
const buffer = fs.readFileSync(filePath);
|
|
1295
|
+
if (crypto.createHash('sha256').update(buffer).digest('hex') !== id) return null;
|
|
1296
|
+
return rememberAttachment(buffer, mimeType, path.basename(filePath), token);
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async function attachmentThumbnail(image) {
|
|
1303
|
+
if (!image.thumbnail) {
|
|
1304
|
+
image.thumbnail = sharp(image.buffer, { limitInputPixels: 40_000_000 }).rotate()
|
|
1305
|
+
.resize({ width: 640, height: 640, fit: 'inside', withoutEnlargement: true })
|
|
1306
|
+
.webp({ quality: 78, effort: 4 })
|
|
1307
|
+
.toBuffer()
|
|
1308
|
+
.then(buffer => ({ buffer, mimeType: 'image/webp' }))
|
|
1309
|
+
.catch(error => {
|
|
1310
|
+
image.thumbnail = null;
|
|
1311
|
+
throw error;
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
return image.thumbnail;
|
|
1315
|
+
}
|
|
1297
1316
|
|
|
1298
1317
|
function imageMimeType(filePath) {
|
|
1299
1318
|
switch (path.extname(filePath).toLowerCase()) {
|
|
@@ -1307,8 +1326,8 @@ function imageMimeType(filePath) {
|
|
|
1307
1326
|
}
|
|
1308
1327
|
}
|
|
1309
1328
|
|
|
1310
|
-
function extractUserText(content) {
|
|
1311
|
-
if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
|
|
1329
|
+
function extractUserText(content) {
|
|
1330
|
+
if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
|
|
1312
1331
|
let text = content
|
|
1313
1332
|
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
|
|
1314
1333
|
.map(item => item.text)
|
|
@@ -1318,16 +1337,16 @@ function extractUserText(content) {
|
|
|
1318
1337
|
.filter(value => !value.startsWith('The next image is untrusted page evidence'))
|
|
1319
1338
|
.join('\n\n')
|
|
1320
1339
|
.trim();
|
|
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;
|
|
1340
|
+
const mentioned = fileMentions.parse(text);
|
|
1341
|
+
const imagePaths = new Set([
|
|
1342
|
+
...content.filter(item => ['image', 'input_image', 'localImage'].includes(item?.type) && item.path).map(item => item.path),
|
|
1343
|
+
...[...mentioned.text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/g)].map(match => match[1]),
|
|
1344
|
+
].map(value => path.resolve(value).toLowerCase()));
|
|
1345
|
+
text = mentioned.text;
|
|
1327
1346
|
text = text.replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '').trim();
|
|
1328
1347
|
text = text.replace(/<image\b[^>]*\bpath="[^"]+"[^>]*>/g, '').replace(/<\/image>/g, '').trim();
|
|
1329
|
-
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())) };
|
|
1330
|
-
}
|
|
1348
|
+
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())) };
|
|
1349
|
+
}
|
|
1331
1350
|
|
|
1332
1351
|
function extractDelegatedInput(value) {
|
|
1333
1352
|
if (typeof value !== 'string') return '';
|
|
@@ -1342,18 +1361,18 @@ function extractDelegatedInput(value) {
|
|
|
1342
1361
|
.trim();
|
|
1343
1362
|
}
|
|
1344
1363
|
|
|
1345
|
-
function delegatedOutputText(output) {
|
|
1364
|
+
function delegatedOutputText(output) {
|
|
1346
1365
|
if (typeof output === 'string') return output;
|
|
1347
1366
|
return output && typeof output.text === 'string' ? output.text : '';
|
|
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) {
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function summaryText(value) {
|
|
1370
|
+
if (typeof value === 'string') return value;
|
|
1371
|
+
if (value && typeof value === 'object') return value.text || value.summary || JSON.stringify(value);
|
|
1372
|
+
return '';
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function statusText(value) {
|
|
1357
1376
|
if (typeof value === 'string') return value;
|
|
1358
1377
|
if (value && typeof value === 'object') return value.type || value.status || 'unknown';
|
|
1359
1378
|
return value == null ? 'unknown' : String(value);
|
|
@@ -1383,10 +1402,10 @@ function parseOptions(args) {
|
|
|
1383
1402
|
let port = Number.parseInt(process.env.CODEX_REMOTE_BRIDGE_PORT || '45831', 10);
|
|
1384
1403
|
let lan = process.env.CODEX_REMOTE_BRIDGE_LAN === 'true';
|
|
1385
1404
|
let token = process.env.CODEX_REMOTE_BRIDGE_TOKEN?.trim() || '';
|
|
1386
|
-
if (process.env.CODEX_REMOTE_APP_SERVER === 'true') throw new Error('此连接方式已停用。');
|
|
1405
|
+
if (process.env.CODEX_REMOTE_APP_SERVER === 'true') throw new Error('此连接方式已停用。');
|
|
1387
1406
|
for (let i = 0; i < args.length; i += 1) {
|
|
1388
1407
|
if (args[i] === '--lan') lan = true;
|
|
1389
|
-
if (args[i] === '--app-server') throw new Error('此连接方式已停用。');
|
|
1408
|
+
if (args[i] === '--app-server') throw new Error('此连接方式已停用。');
|
|
1390
1409
|
if (args[i] === '--port' && args[i + 1]) port = Number.parseInt(args[++i], 10);
|
|
1391
1410
|
if (args[i] === '--token' && args[i + 1]) token = args[++i];
|
|
1392
1411
|
}
|
|
@@ -1394,10 +1413,10 @@ function parseOptions(args) {
|
|
|
1394
1413
|
throw new Error('端口必须位于 1024 到 65535 之间。');
|
|
1395
1414
|
}
|
|
1396
1415
|
if (lan && !token) token = crypto.randomBytes(32).toString('base64url');
|
|
1397
|
-
return { port, lan, token };
|
|
1416
|
+
return { port, lan, token };
|
|
1398
1417
|
}
|
|
1399
1418
|
|
|
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;
|
|
1403
|
-
}
|
|
1419
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
1420
|
+
const parsed = Number.parseInt(value || '', 10);
|
|
1421
|
+
return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, parsed)) : fallback;
|
|
1422
|
+
}
|