@walkhi/code-relax 0.1.0-beta.8 → 0.1.0-beta.9

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.
Files changed (41) hide show
  1. package/README.md +10 -11
  2. package/dist/bin/codex-remote.mjs +11 -12
  3. package/dist/bin/self-relay-demo.mjs +1 -1
  4. package/dist/shared/app-server-events.cjs +5 -3
  5. package/dist/src/desktop-launcher.mjs +7 -10
  6. package/dist/src/message-images.mjs +0 -18
  7. package/dist/src/onboarding.mjs +3 -4
  8. package/dist/src/platform/README.md +1 -1
  9. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
  10. package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
  11. package/dist/src/platform/windows/service-host.mjs +4 -7
  12. package/dist/src/self-relay/admin-page.mjs +54 -0
  13. package/dist/src/self-relay/admin-state.mjs +25 -12
  14. package/dist/src/self-relay/demo.mjs +2 -3
  15. package/dist/src/self-relay/lifecycle.mjs +6 -6
  16. package/dist/src/self-relay/server.mjs +7 -11
  17. package/dist/src/server.mjs +147 -1030
  18. package/dist/src/service-doctor.mjs +3 -3
  19. package/dist/src/service-lifecycle.mjs +2 -7
  20. package/dist/src/shared-app-server.mjs +13 -11
  21. package/dist/src/shared-recovery.mjs +5 -5
  22. package/dist/src/shared-thread-stream.mjs +1 -1
  23. package/dist/web/capabilities.js +1 -8
  24. package/dist/web/chat-transport.js +6 -12
  25. package/dist/web/chat.css +13 -14
  26. package/dist/web/chat.js +23 -41
  27. package/dist/web/community.css +15 -10
  28. package/dist/web/community.html +4 -5
  29. package/dist/web/composer-controller.js +0 -4
  30. package/dist/web/index.html +7 -39
  31. package/dist/web/resources.json +1 -1
  32. package/dist/web/task-list-view.js +1 -8
  33. package/dist/web/timeline-reducer.js +0 -4
  34. package/package.json +16 -18
  35. package/dist/src/app-server-client.mjs +0 -468
  36. package/dist/src/app-server-tasks.mjs +0 -358
  37. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  38. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  39. package/dist/src/thread-catalog.mjs +0 -47
  40. package/dist/tools/find-desktop-pipe.mjs +0 -10
  41. package/dist/web/community-view.js +0 -20
@@ -1,20 +1,16 @@
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
7
  import vm from 'node:vm';
9
8
  import sharp from 'sharp';
10
9
  import { attachLanSocket } from './lan-socket-server.mjs';
11
- import { validateInlineImage, persistLegacyImage } from './message-images.mjs';
10
+ import { validateInlineImage } from './message-images.mjs';
12
11
  import { ThreadWriterConflictError } from './shared-app-server.mjs';
13
12
  import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
14
13
  import { streamSharedThread } from './shared-thread-stream.mjs';
15
- import { AppServerClient } from './app-server-client.mjs';
16
- import { AppServerTaskStore, AppServerTaskTransport } from './app-server-tasks.mjs';
17
- import { supplementThreadCatalog } from './thread-catalog.mjs';
18
14
  import appServerEvents from '../shared/app-server-events.cjs';
19
15
  import { runtimePaths } from './platform/windows/runtime-paths.mjs';
20
16
  import { openControl } from './service-control.mjs';
@@ -23,13 +19,11 @@ import { ExecutionHealth } from './execution-health.mjs';
23
19
  import { activeDebugPort, serveDebugResource } from './debug-ui.mjs';
24
20
  import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
25
21
  import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
26
- import { DesktopMonitor } from './platform/windows/desktop-monitor.mjs';
27
22
  import { desktopHost } from './platform/windows/desktop-host.mjs';
28
23
  import { SharedCatalog } from './shared-catalog.mjs';
29
24
  import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
30
25
  import { requestServiceRestart } from './service-restart.mjs';
31
26
 
32
- const MAX_FRAME_BYTES = 32 * 1024 * 1024;
33
27
  const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
34
28
  const MAX_CACHED_ATTACHMENTS = 32;
35
29
  const MAX_CACHED_FILES = 64;
@@ -47,15 +41,6 @@ const ALLOWED_MODELS = new Set([
47
41
  'gpt-5.3-codex-spark',
48
42
  ]);
49
43
  const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
50
- const ALLOWED_TOOLS = new Set([
51
- 'create_thread',
52
- 'get_usage_limits',
53
- 'list_projects',
54
- 'list_threads',
55
- 'read_thread',
56
- 'send_message_to_thread',
57
- 'wait_threads',
58
- ]);
59
44
 
60
45
  function readRuntimeVersion() {
61
46
  let directory = path.dirname(fileURLToPath(import.meta.url));
@@ -70,8 +55,6 @@ function readRuntimeVersion() {
70
55
 
71
56
  const options = parseOptions(process.argv.slice(2));
72
57
  const runtimeVersion = readRuntimeVersion();
73
- const pipePath = process.env.CODEX_APP_TOOLS_PIPE_PATH?.trim();
74
- let sourceThreadId = (process.env.CODEX_THREAD_ID || process.env.CODEX_SESSION_ID)?.trim();
75
58
 
76
59
  const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
77
60
  const repositoryRoot = path.resolve(webRoot, '..', '..');
@@ -81,7 +64,6 @@ const markdownItScript = fs.readFileSync(path.join(webRoot, 'vendor', 'markdown-
81
64
  const markdownSandbox = { atob };
82
65
  vm.runInNewContext(markdownItScript.toString(), markdownSandbox);
83
66
  const imageMarkdown = markdownSandbox.markdownit({ html: false });
84
- let nextRequestId = 0;
85
67
  const attachmentCache = new Map();
86
68
  const fileCache = new Map();
87
69
  const fileCapabilitySecret = crypto.randomBytes(32);
@@ -93,11 +75,7 @@ const allowedUploadsRoots = [uploadsRoot, legacyUploadsRoot,
93
75
  const sessionPathCache = new Map();
94
76
  const sessionSettingsCache = new Map();
95
77
  const liveActivityCache = new Map();
96
- const messageQueues = new Map();
97
78
  let sharedServer = null;
98
- const desktopMonitor = new DesktopMonitor({
99
- read: (tool, args, pipe) => callTool(tool, args, 5000, pipe),
100
- });
101
79
  const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
102
80
  let sharedCatalog = null, executionMode = 'ordinary';
103
81
  function selectSharedExecution(enabled) {
@@ -115,16 +93,14 @@ const executionHealth = new ExecutionHealth(async () => {
115
93
  // same service, so closing Desktop must not disable phone writes.
116
94
  selectSharedExecution(managed.ready);
117
95
  executionMode = sharedServer ? 'shared' : 'ordinary';
118
- const basicReady = Boolean(desktop.running && sourceThreadId && desktopMonitor.pipe);
119
96
  const managedRestarting = managed.state === 'restarting';
120
- const ready = !managedRestarting && (sharedServer ? sharedServer.ready === true : basicReady);
97
+ const ready = !managedRestarting && sharedServer?.ready === true;
121
98
  const state = managedRestarting ? 'restarting'
122
- : ready ? (sharedServer ? 'full' : 'basic') : !desktop.running ? 'offline' : 'unavailable';
99
+ : ready ? 'full' : !desktop.running ? 'offline' : 'unavailable';
123
100
  return { state, ready, canWrite: ready, mode: executionMode,
124
101
  desktop: { state: !desktop.running ? 'stopped' : attached ? 'managed' : 'ordinary', attachedToManagedServer: attached } };
125
102
  });
126
103
  async function refreshExecution() {
127
- sourceThreadId = readRecord(path.join(stateRoot, 'bridge-startup-context.json'))?.sourceThreadId || sourceThreadId;
128
104
  await executionHealth.refresh();
129
105
  return { mode: executionMode };
130
106
  }
@@ -135,25 +111,13 @@ async function requireExecutionWritable(threadId) {
135
111
  }
136
112
  const execution = await executionHealth.refresh();
137
113
  if (!execution.canWrite) {
138
- const message = execution.mode === 'shared'
139
- ? 'Codex 内核尚未建立可写连接,请稍后重新检测。'
140
- : 'Codex Desktop 尚未建立基础控制连接,请稍后重新检测。';
141
- throw new RequestError(409, message, { code: 'execution_not_writable', execution });
114
+ throw new RequestError(503, 'Codex 内核未就绪。请在电脑上重启 Codex;若仍失败,运行 relax doctor。',
115
+ { code: 'full_control_required', execution });
142
116
  }
143
117
  if (threadId && sharedServer) sharedServer.requireWritable(threadId);
144
118
  return execution;
145
119
  }
146
- managedAppServer.client.on('subscription-error', error => console.error('完整控制任务取消订阅失败:', error.message));
147
- const queueWorkers = new Map();
148
- const appServer = new AppServerClient({ cwd: projectRoot });
149
- const appServerTasks = new AppServerTaskTransport({
150
- client: appServer,
151
- store: new AppServerTaskStore(path.join(stateRoot, 'app-server-tasks.json')),
152
- });
153
- appServer.on('protocol-error', error => console.error(`app-server 协议错误:${error.message}`));
154
- appServer.on('server-request', request => {
155
- console.log(`app-server 等待客户端处理反向请求:${request.method} (${request.id})`);
156
- });
120
+ managedAppServer.client.on('subscription-error', error => console.error('Codex 内核任务取消订阅失败:', error.message));
157
121
 
158
122
  async function handleRequest(request, response) {
159
123
  try {
@@ -186,7 +150,7 @@ async function handleRequest(request, response) {
186
150
  }
187
151
  if (request.method === 'POST' && url.pathname === '/api/shared-recovery') {
188
152
  const body = await readJsonBody(request);
189
- 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 内核后再继续。');
190
154
  sendJson(response, 202, await requestRecovery(stateRoot, body)); return;
191
155
  }
192
156
  if (request.method === 'POST' && url.pathname === '/api/service/restart') {
@@ -215,32 +179,23 @@ async function handleRequest(request, response) {
215
179
  const recovering = ['scheduled', 'running'].includes(recovery?.status);
216
180
  sendJson(response, 200, {
217
181
  status: 'ok', resident: true, version: runtimeVersion,
218
- execution: { ...executionHealth.state, ready: executionHealth.state.ready && (!sharedServer || sharedServer.ready),
182
+ execution: { ...executionHealth.state, ready: executionHealth.state.ready && sharedServer?.ready === true,
219
183
  ...(recovering ? { state: 'recovering' } : {}), mode: executionMode },
220
184
  managedAppServer: managedAppServer.snapshot(),
221
185
  desktop: executionHealth.state.desktop || { state: 'stopped', attachedToManagedServer: false },
222
186
  desktopRestartRecommended: executionHealth.state.desktop?.state === 'ordinary' && managedAppServer.snapshot().ready,
223
187
  recovery: recovery ? { operationId: recovery.operationId, status: recovery.status, stage: recovery.stage, error: recovery.error } : null,
224
188
  debugUiPort: await activeDebugPort(stateRoot, options.port),
225
- desktopConnected: Boolean(desktopMonitor.pipe),
226
- sharedAppServerConnected: sharedServer?.ready === true,
227
- sharedRecoveryAvailable: true,
228
- desktopOrderingReady: Boolean(desktopMonitor.snapshot),
229
- toolCount: 0,
189
+ sharedAppServerConnected: sharedServer?.ready === true,
190
+ sharedRecoveryAvailable: true,
230
191
  mode: options.lan ? 'lan' : 'loopback',
231
192
  tokenRequired: Boolean(options.token),
232
193
  nativeImageTransport: sharedServer ? 'shared-app-server' : null,
233
- transportMode: sharedServer ? 'lan-shared' : 'lan-legacy',
234
- appServer: {
235
- state: appServer.state,
236
- cliVersion: appServer.cliVersion,
237
- taskCount: appServerTasks.store.list().length,
238
- },
194
+ transportMode: sharedServer?.ready === true ? 'lan-shared' : 'unavailable',
239
195
  });
240
196
  return;
241
- }
242
-
243
-
197
+ }
198
+
244
199
  const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
245
200
  if (request.method === 'GET' && imageMatch) {
246
201
  let image = attachmentCache.get(imageMatch[1])
@@ -262,7 +217,7 @@ async function handleRequest(request, response) {
262
217
  }
263
218
 
264
219
  const fileMatch = /^\/api\/files\/([a-f0-9]{64})$/.exec(url.pathname);
265
- if (request.method === 'GET' && fileMatch) {
220
+ if (request.method === 'GET' && fileMatch) {
266
221
  const file = fileCache.get(fileMatch[1]);
267
222
  if (!file) throw new RequestError(404, '文件授权已失效,请重新读取对话。');
268
223
  let stat;
@@ -273,68 +228,47 @@ async function handleRequest(request, response) {
273
228
  }
274
229
  if (stat.size > MAX_ATTACHMENT_BYTES) throw new RequestError(413, '暂只支持下载不超过 16 MiB 的文件。');
275
230
  sendJson(response, 200, { name: file.name, size: file.size, dataBase64: fs.readFileSync(file.path).toString('base64') });
276
- return;
277
- }
278
-
279
- if (request.method === 'GET' && url.pathname === '/api/models') {
280
- if (!usesSharedTask({ hostId: url.searchParams.get('hostId') })) throw new RequestError(400, '当前连接不支持即时模型设置。');
281
- sendJson(response, 200, await sharedServer.models());
282
- return;
283
- }
231
+ return;
232
+ }
233
+
234
+ if (url.pathname.startsWith('/api/app-server/') || url.pathname === '/api/wait') {
235
+ throw new RequestError(410, '此连接方式已停用。请检查 Codex 内核状态。', { code: 'unsupported_control' });
236
+ }
237
+ const taskPath = /^\/api\/threads\/([^/]+)/.exec(url.pathname);
238
+ if (taskPath && (url.pathname.endsWith('/takeover') || url.pathname === `/api/threads/${taskPath[1]}`)) {
239
+ throw new RequestError(410, '此连接方式已停用。', { code: 'unsupported_control' });
240
+ }
241
+ if (url.searchParams.has('hostId') && url.searchParams.get('hostId') !== 'local') {
242
+ throw new RequestError(410, '此对话的连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
243
+ }
244
+ if (!executionHealth.state.ready || sharedServer?.ready !== true) {
245
+ throw new RequestError(503, 'Codex 内核未就绪。请在电脑上重启 Codex;若仍失败,运行 relax doctor。',
246
+ { code: 'full_control_required' });
247
+ }
248
+
249
+ if (request.method === 'GET' && url.pathname === '/api/models') {
250
+ sendJson(response, 200, await sharedServer.models());
251
+ return;
252
+ }
284
253
 
285
254
  if (request.method === 'GET' && url.pathname === '/api/threads') {
286
255
  const limit = boundedInteger(url.searchParams.get('limit'), 20, 1, 200);
287
256
  const archived = url.searchParams.get('archived') === 'true';
288
- const catalog = sharedCatalog;
289
- const sharedSnapshot = Boolean(catalog);
290
- let desktopCatalog;
291
- if (catalog) desktopCatalog = await catalog.threads(limit, archived);
292
- else {
293
- if (archived) throw new RequestError(400, '当前连接不支持归档对话。请启用完整控制。');
294
- const listing = await callTool('list_threads', { limit: Math.min(limit, 50) });
295
- if (!listing.success) { sendToolResult(response, listing); return; }
296
- const projects = await callTool('list_projects', {});
297
- if (!projects.success) { sendToolResult(response, projects); return; }
298
- const catalogProjects = projects.data.projects || [];
299
- desktopCatalog = { ...supplementThreadCatalog(listing.data, catalogProjects, codexDataRoot, limit),
300
- projects: catalogProjects };
301
- }
302
- let appServerThreads = archived ? [] : appServerTasks.store.list().map(record => ({
303
- id: record.id,
304
- kind: 'codex',
305
- hostId: 'app-server',
306
- title: record.title,
307
- cwd: record.cwd,
308
- projectId: record.projectId,
309
- updatedAt: record.updatedAt,
310
- recencyAt: record.recencyAt ?? record.updatedAt,
311
- status: 'unknown',
312
- transport: 'app-server',
313
- }));
314
- if (appServerThreads.length) {
315
- try { appServerThreads = await appServerTasks.listTasks(); } catch {}
316
- }
317
- sendJson(response, 200, mergeThreadCatalog(desktopCatalog, appServerThreads, limit, sharedSnapshot));
257
+ sendJson(response, 200, await sharedCatalog.threads(limit, archived));
318
258
  return;
319
259
  }
320
260
 
321
261
  if (request.method === 'GET' && url.pathname === '/api/projects') {
322
- if (sharedCatalog) {
323
- sendJson(response, 200, { projects: (await sharedCatalog.projects()).projects });
324
- return;
325
- }
326
- const result = await callTool('list_projects', {});
327
- sendToolResult(response, result);
262
+ sendJson(response, 200, { projects: (await sharedCatalog.projects()).projects });
328
263
  return;
329
264
  }
330
265
 
331
266
  if (request.method === 'GET' && url.pathname === '/api/usage-limits') {
332
- if (sharedServer) sendJson(response, 200, await sharedServer.usage());
333
- else sendToolResult(response, await callTool('get_usage_limits', {}));
267
+ sendJson(response, 200, await sharedServer.usage());
334
268
  return;
335
269
  }
336
270
 
337
- if (request.method === 'GET' && url.pathname === '/api/usage-events' && sharedServer) {
271
+ if (request.method === 'GET' && url.pathname === '/api/usage-events') {
338
272
  const execution = sharedServer;
339
273
  response.startEvents();
340
274
  const emit = value => { if (!response.destroyed) response.event('usage', value); };
@@ -346,171 +280,96 @@ async function handleRequest(request, response) {
346
280
  return;
347
281
  }
348
282
 
349
- if (request.method === 'GET' && url.pathname === '/api/app-server/status') {
350
- sendJson(response, 200, appServerBridgeStatus());
351
- return;
352
- }
353
-
354
- if (request.method === 'POST' && url.pathname === '/api/app-server/start') {
355
- await requireExecutionWritable();
356
- await appServer.start();
357
- sendJson(response, 200, appServerBridgeStatus());
358
- return;
359
- }
360
-
361
283
  const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
362
284
  if (request.method === 'POST' && createThreadMatch) {
363
285
  await requireExecutionWritable();
364
286
  const projectId = decodeURIComponent(createThreadMatch[1]);
365
- const body = createThreadBody(await readJsonBody(request));
366
- let projects = [];
367
- if (body.transport !== 'app-server' || projectId !== '__unassigned__') {
368
- const projectsResult = sharedCatalog
369
- ? { success: true, data: { projects: (await sharedCatalog.projects()).projects } }
370
- : await callTool('list_projects', {});
371
- if (!projectsResult.success) {
372
- sendJson(response, 502, { error: '无法读取 Codex Desktop 项目。', details: projectsResult.data });
373
- return;
374
- }
375
- projects = Array.isArray(projectsResult.data?.projects) ? projectsResult.data.projects : [];
376
- }
377
- if (body.transport === 'app-server') {
378
- const target = appServerProjectTarget(projectId, projects);
379
- const result = await appServerTasks.createTask({
380
- cwd: target.cwd,
381
- projectId: target.projectId,
382
- input: appServerTurnArguments('', body).input,
383
- });
384
- sendJson(response, 201, {
385
- threadId: result.record.id,
386
- hostId: 'app-server',
387
- transport: 'app-server',
388
- turnId: result.turn.id,
389
- });
390
- return;
391
- }
392
- const project = projects.find(item => item.projectId === projectId);
393
- if (usesSharedTask({ hostId:project?.hostId || body.hostId })) {
394
- if (projectId !== '__unassigned__' && !project) throw new RequestError(404, '没有找到对应项目。');
395
- const execution = sharedServer;
396
- await execution.connect();
397
- const rpc = (method, params) => execution.request(method, params);
398
- let target = appServerEvents.projectlessThreadTarget(projectRoot);
399
- if (project) {
400
- const nativeProjects = await appServerEvents.readProjects(rpc);
401
- const nativeId = nativeProjects.find(item => item.id === project.projectId)?.id;
402
- if (!nativeId) throw new RequestError(404, '完整控制中没有找到对应项目,请刷新项目列表。');
403
- target = { projectId:nativeId, cwd:project.path };
404
- }
405
- const { input } = appServerTurnArguments('', body);
406
- const created = await appServerEvents.createThread(rpc, target, input, body);
407
- const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
408
- void generateAndSetThreadTitle({
409
- request: rpc,
410
- subscribe: listener => {
411
- execution.on('notification', listener);
412
- return () => execution.off('notification', listener);
413
- },
414
- threadId: created.threadId,
415
- prompt,
416
- cwd: target.cwd || null,
417
- }).catch(error => console.error(`生成对话标题失败:${error.message}`));
418
- sendJson(response, 201, { ...created, hostId:'local' });
419
- return;
420
- }
421
- const argumentsValue = await createThreadArguments(projectId, body, projects);
422
- sendToolResult(response, await callTool('create_thread', argumentsValue, 90_000));
423
- return;
287
+ const body = await readJsonBody(request);
288
+ if (body.transport === 'app-server' || (body.hostId && body.hostId !== 'local')) {
289
+ throw new RequestError(410, '此连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
290
+ }
291
+ const projects = (await sharedCatalog.projects()).projects;
292
+ const project = projects.find(item => item.projectId === projectId);
293
+ if (projectId !== '__unassigned__' && !project) throw new RequestError(404, '没有找到对应项目。');
294
+ const execution = sharedServer;
295
+ await execution.connect();
296
+ const rpc = (method, params) => execution.request(method, params);
297
+ let target = appServerEvents.projectlessThreadTarget(projectRoot);
298
+ if (project) {
299
+ const nativeProjects = await appServerEvents.readProjects(rpc);
300
+ const nativeId = nativeProjects.find(item => item.id === project.projectId)?.id;
301
+ if (!nativeId) throw new RequestError(404, 'Codex 内核中没有找到对应项目,请刷新项目列表。');
302
+ target = { projectId:nativeId, cwd:project.path };
303
+ }
304
+ const { input } = appServerTurnArguments('', body);
305
+ const created = await appServerEvents.createThread(rpc, target, input, body);
306
+ const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
307
+ void generateAndSetThreadTitle({
308
+ request: rpc,
309
+ subscribe: listener => {
310
+ execution.on('notification', listener);
311
+ return () => execution.off('notification', listener);
312
+ },
313
+ threadId: created.threadId,
314
+ prompt,
315
+ cwd: target.cwd || null,
316
+ }).catch(error => console.error(`生成对话标题失败:${error.message}`));
317
+ sendJson(response, 201, { ...created, hostId:'local' });
318
+ return;
424
319
  }
425
320
 
426
- const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
321
+ const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
427
322
  if (request.method === 'GET' && timelineMatch) {
428
323
  const threadId = decodeURIComponent(timelineMatch[1]);
429
- if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
430
- const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
431
- const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
432
- const knownTurnId = url.searchParams.get('knownTurnId') || '';
433
- const knownTurnStatus = url.searchParams.get('knownTurnStatus') || '';
434
- const knownHistoryView = url.searchParams.get('knownHistoryView') || '';
435
- const latest = timeline.turns.at(-1);
436
- const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
437
- const sameHistoryView = knownHistoryView === timeline.historyView;
438
- const unchanged = sameHistoryView
439
- && knownIndex === timeline.turns.length - 1
440
- && timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
441
- && knownTurnStatus && knownTurnStatus !== 'inProgress'
442
- && (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
443
- const partial = !unchanged && sameHistoryView && knownIndex >= 0
444
- ? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
445
- : timeline;
446
- sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
447
- return;
448
- }
449
- if (appServerTasks.owns(threadId)) {
450
- const thread = await appServerTasks.readTask(threadId, {
451
- includeTurns: true,
452
- turnLimit: boundedInteger(url.searchParams.get('turnLimit'), 10, 1, 10),
453
- });
454
- sendJson(response, 200, normalizeAppServerTimeline(thread));
455
- return;
456
- }
457
- const argumentsValue = readThreadArguments(url, threadId, 10);
458
- const result = await callTool('read_thread', argumentsValue);
459
- if (result.success) {
460
- const timeline = normalizeTimeline(result.data);
461
- enrichTimelineWithLocalActivity(timeline, threadId);
462
- sendJson(response, 200, timeline);
463
- } else {
464
- sendJson(response, 502, { error: 'Codex Desktop 工具返回失败。', details: result.data });
465
- }
466
- return;
467
- }
324
+ const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
325
+ const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
326
+ const knownTurnId = url.searchParams.get('knownTurnId') || '';
327
+ const knownTurnStatus = url.searchParams.get('knownTurnStatus') || '';
328
+ const knownHistoryView = url.searchParams.get('knownHistoryView') || '';
329
+ const latest = timeline.turns.at(-1);
330
+ const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
331
+ const sameHistoryView = knownHistoryView === timeline.historyView;
332
+ const unchanged = sameHistoryView
333
+ && knownIndex === timeline.turns.length - 1
334
+ && timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
335
+ && knownTurnStatus && knownTurnStatus !== 'inProgress'
336
+ && (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
337
+ const partial = !unchanged && sameHistoryView && knownIndex >= 0
338
+ ? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
339
+ : timeline;
340
+ sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
341
+ return;
342
+ }
468
343
 
469
344
  const approvalMatch = /^\/api\/threads\/([^/]+)\/approvals$/.exec(url.pathname);
470
345
  if (request.method === 'POST' && approvalMatch) {
471
346
  const threadId = decodeURIComponent(approvalMatch[1]);
472
347
  await requireExecutionWritable(threadId);
473
- if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(threadId)) {
474
- sendJson(response, 409, { error: '当前执行模式不支持手机审批,请在工作站处理。' }); return;
475
- }
476
- const body = await readJsonBody(request);
348
+ const body = await readJsonBody(request);
477
349
  sendJson(response, 200, await sharedServer.respondApproval(threadId, body)); return;
478
350
  }
479
351
 
480
352
  const eventsMatch = /^\/api\/threads\/([^/]+)\/events$/.exec(url.pathname);
481
- if (request.method === 'GET' && eventsMatch) {
482
- const threadId = decodeURIComponent(eventsMatch[1]);
483
- if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
484
- const after = Number(url.searchParams.get('afterSeq'));
485
- const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
486
- ? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
487
- const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
488
- const known = url.searchParams.get('knownTurnId') ? {
489
- updatedAt: Number.isFinite(knownUpdatedAt) ? knownUpdatedAt : 0,
490
- turnId: url.searchParams.get('knownTurnId'),
491
- turnStatus: url.searchParams.get('knownTurnStatus') || '',
492
- } : undefined;
493
- await streamSharedThread(response, threadId, sharedServer, normalizeSharedTimeline, requestedSync, known);
494
- return;
495
- }
496
- if (appServerTasks.owns(threadId)) {
497
- await streamAppServerThreadEvents(request, response, threadId);
498
- return;
499
- }
500
- await streamThreadEvents(
501
- request,
502
- response,
503
- threadId,
504
- url.searchParams.get('hostId')?.trim(),
505
- );
506
- return;
507
- }
353
+ if (request.method === 'GET' && eventsMatch) {
354
+ const threadId = decodeURIComponent(eventsMatch[1]);
355
+ const after = Number(url.searchParams.get('afterSeq'));
356
+ const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
357
+ ? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
358
+ const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
359
+ const known = url.searchParams.get('knownTurnId') ? {
360
+ updatedAt: Number.isFinite(knownUpdatedAt) ? knownUpdatedAt : 0,
361
+ turnId: url.searchParams.get('knownTurnId'),
362
+ turnStatus: url.searchParams.get('knownTurnStatus') || '',
363
+ } : undefined;
364
+ await streamSharedThread(response, threadId, sharedServer, normalizeSharedTimeline, requestedSync, known);
365
+ return;
366
+ }
508
367
 
509
368
  const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
510
369
  if (request.method === 'GET' && contextMatch) {
511
370
  const threadId = decodeURIComponent(contextMatch[1]);
512
371
  const current = readContextUsage(threadId);
513
- const native = usesSharedTask({ hostId: url.searchParams.get('hostId') }) ? await sharedServer.readContext(threadId) : {};
372
+ const native = await sharedServer.readContext(threadId);
514
373
  sendJson(response, 200, { ...current, ...(native.model ? { model: native.model, thinking: native.thinking, serviceTier: native.serviceTier,
515
374
  permissionMode: native.permissionMode, permissionModes: native.permissionModes } : {}), ...(native.available ? native : {}) });
516
375
  return;
@@ -519,7 +378,6 @@ async function handleRequest(request, response) {
519
378
  const sharedAction = /^\/api\/threads\/([^/]+)\/(history|settings|permissions)$/.exec(url.pathname);
520
379
  if (sharedAction) {
521
380
  const id = decodeURIComponent(sharedAction[1]);
522
- if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(id)) throw new RequestError(400, '此连接不支持该操作。');
523
381
  if (request.method === 'GET' && sharedAction[2] === 'history') {
524
382
  sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'), sharedHistoryPageOptions())));
525
383
  return;
@@ -543,9 +401,6 @@ async function handleRequest(request, response) {
543
401
  const threadId = decodeURIComponent(sharedOperationGroup[1]);
544
402
  const turnId = decodeURIComponent(sharedOperationGroup[2]);
545
403
  const groupId = decodeURIComponent(sharedOperationGroup[3]);
546
- if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(threadId)) {
547
- throw new RequestError(400, '此连接不支持操作组分页。');
548
- }
549
404
  const page = await sharedServer.operationGroupItems(threadId, turnId, groupId, url.searchParams.get('offset'));
550
405
  sendJson(response, 200, { turnId: page.turnId, groupId: page.groupId,
551
406
  entries: page.items.map(normalizeItem).filter(Boolean), nextOffset: page.nextOffset, total: page.total });
@@ -556,187 +411,55 @@ async function handleRequest(request, response) {
556
411
  if (request.method === 'POST' && archiveMatch) {
557
412
  const threadId = decodeURIComponent(archiveMatch[1]);
558
413
  await requireExecutionWritable(threadId);
559
- if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(threadId)) {
560
- throw new RequestError(400, '当前连接不支持归档对话。请启用完整控制。');
561
- }
562
414
  await sharedServer.request(`thread/${archiveMatch[2]}`, { threadId });
563
415
  sendJson(response, 200, { threadId, archived: archiveMatch[2] === 'archive' });
564
416
  return;
565
417
  }
566
418
 
567
- const readMatch = /^\/api\/threads\/([^/]+)$/.exec(url.pathname);
568
- if (request.method === 'DELETE' && readMatch) {
569
- const threadId = decodeURIComponent(readMatch[1]);
570
- await requireExecutionWritable(threadId);
571
- if (!appServerTasks.owns(threadId)) {
572
- throw new RequestError(400, '仅支持删除由独立 app-server 通道创建的任务。');
573
- }
574
- await appServerTasks.deleteTask(threadId);
575
- messageQueues.delete(threadId);
576
- sendJson(response, 200, { deleted: true, threadId, transport: 'app-server' });
577
- return;
578
- }
579
- if (request.method === 'GET' && readMatch) {
580
- const threadId = decodeURIComponent(readMatch[1]);
581
- if (appServerTasks.owns(threadId)) {
582
- sendJson(response, 200, { thread: await appServerTasks.readTask(threadId, {
583
- includeTurns: true,
584
- turnLimit: boundedInteger(url.searchParams.get('turnLimit'), 10, 1, 10),
585
- }) });
586
- return;
587
- }
588
- const argumentsValue = readThreadArguments(url, threadId, 10, 12_000);
589
- sendToolResult(response, await callTool('read_thread', argumentsValue));
590
- return;
591
- }
592
-
593
- const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
594
- if (queueMatch) {
595
- const threadId = decodeURIComponent(queueMatch[1]);
596
- if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
597
- await sharedServer.connect();
598
- const rpc = (method, params) => sharedServer.request(method, params);
599
- if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
600
- if (request.method === 'POST') {
601
- await requireExecutionWritable(threadId);
602
- const args = appServerTurnArguments(threadId, await readJsonBody(request));
603
- sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
604
- }
605
- }
606
- if (request.method === 'GET') {
607
- sendJson(response, 200, { items: publicQueue(threadId) });
608
- return;
609
- }
419
+ const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
420
+ if (queueMatch) {
421
+ const threadId = decodeURIComponent(queueMatch[1]);
422
+ await sharedServer.connect();
423
+ const rpc = (method, params) => sharedServer.request(method, params);
424
+ if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
610
425
  if (request.method === 'POST') {
611
426
  await requireExecutionWritable(threadId);
612
- const body = await readJsonBody(request);
613
- const argumentsValue = appServerTasks.owns(threadId)
614
- ? appServerTurnArguments(threadId, body)
615
- : await messageArguments(threadId, body);
616
- const item = {
617
- id: crypto.randomUUID(),
618
- ...argumentsValue,
619
- transport: appServerTasks.owns(threadId) ? 'app-server' : 'desktop-tools',
620
- displayPrompt: messageDisplayText(body),
621
- text: typeof body.prompt === 'string' ? body.prompt.trim() : '',
622
- 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` })),
623
- createdAt: new Date().toISOString(),
624
- error: '',
625
- };
626
- const queue = messageQueues.get(threadId) || [];
627
- queue.push(item);
628
- messageQueues.set(threadId, queue);
629
- ensureQueueWorker(threadId);
630
- sendJson(response, 202, { items: publicQueue(threadId) });
631
- return;
632
- }
633
- }
427
+ const args = appServerTurnArguments(threadId, await readJsonBody(request));
428
+ sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
429
+ }
430
+ }
634
431
 
635
432
  const queueItemMatch = /^\/api\/threads\/([^/]+)\/queue\/([a-f0-9-]+)(?:\/(steer))?$/.exec(url.pathname);
636
433
  if (queueItemMatch) {
637
434
  const threadId = decodeURIComponent(queueItemMatch[1]);
638
435
  const itemId = queueItemMatch[2];
639
- if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
640
- await sharedServer.connect();
641
- const action = request.method === 'DELETE' ? 'delete' : queueItemMatch[3] === 'steer' ? 'steer' : null;
642
- if (!action) throw new RequestError(405, '不支持的队列操作');
643
- await requireExecutionWritable(threadId);
644
- sendJson(response, 200, await appServerEvents.changeQueue((method, params) => sharedServer.request(method, params), threadId, itemId, action)); return;
645
- }
646
- const queue = messageQueues.get(threadId) || [];
647
- const index = queue.findIndex(item => item.id === itemId);
648
- if (index < 0) {
649
- sendJson(response, 404, { error: '没有找到该排队消息。' });
650
- return;
651
- }
652
- if (request.method === 'DELETE' && !queueItemMatch[3]) {
653
- await requireExecutionWritable(threadId);
654
- queue.splice(index, 1);
655
- sendJson(response, 200, { items: publicQueue(threadId) });
656
- return;
657
- }
658
- if (request.method === 'POST' && queueItemMatch[3] === 'steer') {
659
- await requireExecutionWritable(threadId);
660
- const [item] = queue.splice(index, 1);
661
- const result = await steerQueuedItem(threadId, item);
662
- if (!result.success) {
663
- queue.splice(index, 0, item);
664
- sendJson(response, 502, { error: '插队发送失败。', details: result.data });
665
- return;
666
- }
667
- sendJson(response, 200, { items: publicQueue(threadId) });
668
- return;
669
- }
670
- }
671
-
672
- const takeoverMatch = /^\/api\/threads\/([^/]+)\/takeover$/.exec(url.pathname);
673
- if (request.method === 'POST' && takeoverMatch) {
674
- const threadId = decodeURIComponent(takeoverMatch[1]);
436
+ await sharedServer.connect();
437
+ const action = request.method === 'DELETE' ? 'delete' : queueItemMatch[3] === 'steer' ? 'steer' : null;
438
+ if (!action) throw new RequestError(405, '不支持的队列操作');
675
439
  await requireExecutionWritable(threadId);
676
- if (appServerTasks.owns(threadId)) {
677
- sendJson(response, 200, { thread: (await appServerTasks.listTasks()).find(t => t.id === threadId) });
678
- return;
679
- }
680
- if (messageQueues.get(threadId)?.length || queueWorkers.has(threadId)) throw new RequestError(409, '请先处理此聊天的排队消息。');
681
- const result = await callTool('read_thread', { threadId, turnLimit: 1, includeOutputs: false });
682
- if (!result.success) throw new RequestError(502, '无法确认 Desktop 聊天状态。');
683
- const timeline = normalizeTimeline(result.data);
684
- const thread = timeline.thread;
685
- if (thread.hostId && thread.hostId !== 'local') throw new RequestError(400, '只支持本机聊天。');
686
- if (!['idle', 'notLoaded'].includes(thread.status) || timeline.turns.some(t => t.status === 'inProgress')) throw new RequestError(409, '只允许接管已空闲的聊天,请等当前轮次结束。');
687
- if (!findSessionPath(threadId)) throw new RequestError(400, '未找到此聊天的本机历史。');
688
- sendJson(response, 200, { thread: await appServerTasks.adoptTask({ ...thread, id: threadId }) });
689
- return;
690
- }
440
+ sendJson(response, 200, await appServerEvents.changeQueue((method, params) => sharedServer.request(method, params), threadId, itemId, action)); return;
441
+ }
691
442
 
692
- const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
443
+ const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
693
444
  if (request.method === 'POST' && interruptMatch) {
694
445
  const threadId = decodeURIComponent(interruptMatch[1]);
695
446
  await requireExecutionWritable(threadId);
696
- if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
697
- const body = await readJsonBody(request);
698
- if (!body.turnId) throw new RequestError(400, '缺少活动轮次 ID。');
699
- await sharedServer.interrupt(threadId, body.turnId);
700
- sendJson(response, 200, { requested: true });
701
- return;
702
- }
703
- if (!appServerTasks.owns(threadId)) throw new RequestError(409, '请先接管空闲聊天。');
704
- const body = await readJsonBody(request);
705
- const thread = await appServerTasks.readTask(threadId, { turnLimit: 1 });
706
- const active = thread.turns?.at(-1);
707
- if (!body.turnId || active?.id !== body.turnId || statusText(active.status) !== 'inProgress') throw new RequestError(409, '此轮次已经结束或发生变化。');
708
- // Stop also clears pending messages so the queue cannot restart work.
709
- messageQueues.delete(threadId);
710
- await appServerTasks.interruptTurn(threadId, body.turnId);
711
- sendJson(response, 200, { requested: true });
712
- return;
447
+ const body = await readJsonBody(request);
448
+ if (!body.turnId) throw new RequestError(400, '缺少活动轮次 ID。');
449
+ await sharedServer.interrupt(threadId, body.turnId);
450
+ sendJson(response, 200, { requested: true });
451
+ return;
713
452
  }
714
453
 
715
454
  const messageMatch = /^\/api\/threads\/([^/]+)\/messages$/.exec(url.pathname);
716
455
  if (request.method === 'POST' && messageMatch) {
717
456
  const threadId = decodeURIComponent(messageMatch[1]);
718
457
  await requireExecutionWritable(threadId);
719
- const body = await readJsonBody(request);
720
- if (!appServerTasks.owns(threadId) && usesSharedTask(body)) {
721
- sendJson(response, 200, { success: true, data: await sharedServer.send(appServerTurnArguments(threadId, body)) });
722
- } else if (appServerTasks.owns(threadId)) {
723
- const args = appServerTurnArguments(threadId, body);
724
- const current = await appServerTasks.readTask(threadId, { turnLimit: 1 });
725
- const active = current.turns?.at(-1);
726
- sendJson(response, 200, statusText(active?.status) === 'inProgress'
727
- ? await appServerTasks.steerTurn(threadId, active.id, args.input)
728
- : await appServerTasks.startTurn(threadId, args));
729
- } else {
730
- sendToolResult(response, await callTool('send_message_to_thread', await messageArguments(threadId, body)));
731
- }
732
- return;
733
- }
734
-
735
- if (request.method === 'POST' && url.pathname === '/api/wait') {
736
- const body = await readJsonBody(request);
737
- sendToolResult(response, await callTool('wait_threads', body, 130_000));
738
- return;
739
- }
458
+ const body = await readJsonBody(request);
459
+ if (body.hostId && body.hostId !== 'local') throw new RequestError(410, '此对话的连接方式已停用。', { code: 'unsupported_control' });
460
+ sendJson(response, 200, { success: true, data: await sharedServer.send(appServerTurnArguments(threadId, body)) });
461
+ return;
462
+ }
740
463
 
741
464
  sendJson(response, 404, { error: '未找到请求的资源。' });
742
465
  } catch (error) {
@@ -760,17 +483,7 @@ const lanSocket = attachLanSocket(server, { token: options.token, dispatch: hand
760
483
  authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
761
484
  });
762
485
 
763
- if (options.appServer) {
764
- try {
765
- await appServer.start();
766
- } catch (error) {
767
- console.error(`app-server 启动失败:${error instanceof Error ? error.message : String(error)}`);
768
- process.exit(2);
769
- }
770
- }
771
-
772
486
  let control;
773
- desktopMonitor.start();
774
487
  void managedAppServer.start();
775
488
  executionHealth.start();
776
489
  server.on('error', error => {
@@ -807,147 +520,14 @@ async function shutDown() {
807
520
  shuttingDown = true;
808
521
  lanSocket.close();
809
522
  server.close();
810
- desktopMonitor?.stop();
811
523
  executionHealth?.stop();
812
524
  managedAppServer.close();
813
- await appServer.close();
814
525
  await control?.close();
815
526
  process.exit(0);
816
527
  }
817
528
  process.on('SIGTERM', shutDown);
818
529
  process.on('SIGINT', shutDown);
819
530
 
820
- async function callTool(tool, argumentsValue, timeoutMs = 60_000, targetPipe) {
821
- if (!sourceThreadId) throw new RequestError(503, '基础控制尚未绑定调用上下文;请在 Codex 对话启动一次 Code Relax。电脑管理连接仍可使用。');
822
- if (!ALLOWED_TOOLS.has(tool)) {
823
- throw new RequestError(403, `工具 ${tool} 不在 Bridge 允许列表中。`);
824
- }
825
-
826
- const invocationId = crypto.randomUUID();
827
- const rpc = await callDesktop('tools/call', {
828
- arguments: argumentsValue,
829
- callId: `codex-remote-${invocationId}`,
830
- namespace: 'codex_app',
831
- threadId: sourceThreadId,
832
- tool,
833
- turnId: `codex-remote-turn-${invocationId}`,
834
- }, timeoutMs, targetPipe);
835
-
836
- const result = rpc.result;
837
- if (!result || typeof result.success !== 'boolean') {
838
- throw new BridgeError('Codex Desktop 没有返回有效的工具结果。', rpc);
839
- }
840
-
841
- const items = Array.isArray(result.contentItems) ? result.contentItems : [];
842
- let data = items;
843
- if (items.length === 1 && items[0]?.type === 'inputText' && typeof items[0].text === 'string') {
844
- try {
845
- data = JSON.parse(items[0].text);
846
- } catch {
847
- data = items[0].text;
848
- }
849
- }
850
-
851
- return { success: result.success, data };
852
- }
853
-
854
- function callDesktop(method, params, timeoutMs, targetPipe = desktopMonitor?.pipe || pipePath) {
855
- if (!targetPipe) return Promise.reject(new RequestError(503, 'Desktop 工具通道尚未就绪。'));
856
- return new Promise((resolve, reject) => {
857
- const socket = net.createConnection(targetPipe);
858
- const requestId = ++nextRequestId;
859
- const payload = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: requestId, method, params }), 'utf8');
860
- const header = Buffer.allocUnsafe(4);
861
- header.writeUInt32LE(payload.length, 0);
862
- let pending = Buffer.alloc(0);
863
- let expectedLength = null;
864
- let settled = false;
865
-
866
- const finish = (callback, value) => {
867
- if (settled) return;
868
- settled = true;
869
- clearTimeout(timer);
870
- socket.destroy();
871
- callback(value);
872
- };
873
-
874
- const timer = setTimeout(() => {
875
- finish(reject, new BridgeError('等待 Codex Desktop 响应超时。'));
876
- }, timeoutMs);
877
-
878
- socket.once('connect', () => socket.write(Buffer.concat([header, payload])));
879
- socket.on('data', chunk => {
880
- pending = Buffer.concat([pending, chunk]);
881
- if (expectedLength === null && pending.length >= 4) {
882
- expectedLength = pending.readUInt32LE(0);
883
- pending = pending.subarray(4);
884
- if (expectedLength <= 0 || expectedLength > MAX_FRAME_BYTES) {
885
- finish(reject, new BridgeError(`Codex Desktop 返回了无效帧长度:${expectedLength}。`));
886
- return;
887
- }
888
- }
889
-
890
- if (expectedLength !== null && pending.length >= expectedLength) {
891
- try {
892
- const responseValue = JSON.parse(pending.subarray(0, expectedLength).toString('utf8'));
893
- if (responseValue.error) {
894
- finish(reject, new BridgeError('Codex Desktop 工具调用失败。', responseValue.error));
895
- } else if (responseValue.id !== requestId) {
896
- finish(reject, new BridgeError('Codex Desktop 返回了不匹配的请求 ID。'));
897
- } else {
898
- finish(resolve, responseValue);
899
- }
900
- } catch (error) {
901
- finish(reject, new BridgeError('无法解析 Codex Desktop 响应。', undefined, error));
902
- }
903
- }
904
- });
905
- socket.once('error', error => finish(reject, new BridgeError(
906
- '无法连接 Codex Desktop Pipe。桌面版可能已重启,请重新启动 Bridge。',
907
- undefined,
908
- error,
909
- )));
910
- socket.once('close', () => {
911
- if (!settled) finish(reject, new BridgeError('Codex Desktop Pipe 在返回结果前关闭。'));
912
- });
913
- });
914
- }
915
-
916
- function sendToolResult(response, result) {
917
- if (result.success) {
918
- sendJson(response, 200, result.data);
919
- } else {
920
- sendJson(response, 502, { error: 'Codex Desktop 工具返回失败。', details: result.data });
921
- }
922
- }
923
-
924
- function appServerBridgeStatus() {
925
- return {
926
- ...appServer.snapshot(),
927
- platformFamily: appServer.initializeResult?.platformFamily || null,
928
- platformOs: appServer.initializeResult?.platformOs || null,
929
- taskCount: appServerTasks.store.list().length,
930
- ownershipError: appServerTasks.store.loadError?.message || null,
931
- };
932
- }
933
-
934
- function readThreadArguments(url, threadId, defaultTurnLimit, defaultOutputLimit = 4_000) {
935
- const argumentsValue = {
936
- threadId,
937
- turnLimit: boundedInteger(url.searchParams.get('turnLimit'), defaultTurnLimit, 1, 10),
938
- includeOutputs: booleanValue(url.searchParams.get('includeOutputs'), true),
939
- maxOutputCharsPerItem: boundedInteger(
940
- url.searchParams.get('maxOutputCharsPerItem'),
941
- defaultOutputLimit,
942
- 500,
943
- 100_000,
944
- ),
945
- };
946
- const hostId = url.searchParams.get('hostId')?.trim();
947
- if (hostId) argumentsValue.hostId = hostId;
948
- return argumentsValue;
949
- }
950
-
951
531
  function readContextUsage(threadId) {
952
532
  if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) {
953
533
  return { available: false };
@@ -1070,108 +650,6 @@ function findSessionPath(threadId) {
1070
650
  return null;
1071
651
  }
1072
652
 
1073
- function enrichTimelineWithLocalActivity(timeline, threadId) {
1074
- const turn = timeline.turns.at(-1);
1075
- if (!turn || turn.status !== 'inProgress') return;
1076
- if (turn.entries.some(entry => entry?.type === 'activity' && entry.status === 'inProgress')) return;
1077
- const pending = readPendingLocalActivity(threadId);
1078
- if (!pending || (pending.turnId && pending.turnId !== turn.id)) return;
1079
- turn.entries.push(pending.entry);
1080
- }
1081
-
1082
- function readPendingLocalActivity(threadId) {
1083
- const sessionPath = findSessionPath(threadId);
1084
- if (!sessionPath) return null;
1085
- let handle;
1086
- try {
1087
- const stat = fs.statSync(sessionPath);
1088
- const cacheKey = `${stat.size}:${stat.mtimeMs}`;
1089
- const cached = liveActivityCache.get(sessionPath);
1090
- if (cached?.key === cacheKey) return cached.value;
1091
- const readLength = Math.min(stat.size, MAX_LIVE_ACTIVITY_TAIL_BYTES);
1092
- const buffer = Buffer.allocUnsafe(readLength);
1093
- handle = fs.openSync(sessionPath, 'r');
1094
- fs.readSync(handle, buffer, 0, readLength, stat.size - readLength);
1095
- const lines = buffer.toString('utf8').split(/\r?\n/);
1096
- if (readLength < stat.size) lines.shift();
1097
- const pending = new Map();
1098
- for (const line of lines) {
1099
- if (!line.includes('"custom_tool_call')) continue;
1100
- let event;
1101
- try { event = JSON.parse(line); } catch { continue; }
1102
- if (event?.type !== 'response_item') continue;
1103
- const payload = event.payload;
1104
- const callId = payload?.call_id;
1105
- if (!callId) continue;
1106
- if (payload.type === 'custom_tool_call') pending.set(callId, payload);
1107
- if (payload.type === 'custom_tool_call_output') pending.delete(callId);
1108
- }
1109
- const payload = [...pending.values()].at(-1);
1110
- const value = payload ? pendingActivity(payload) : null;
1111
- liveActivityCache.set(sessionPath, { key: cacheKey, value });
1112
- return value;
1113
- } catch {
1114
- liveActivityCache.delete(sessionPath);
1115
- return null;
1116
- } finally {
1117
- if (handle !== undefined) fs.closeSync(handle);
1118
- }
1119
- }
1120
-
1121
- function pendingActivity(payload) {
1122
- const input = typeof payload.input === 'string' ? payload.input : '';
1123
- const nestedTool = input.match(/tools\.([A-Za-z0-9_]+)\s*\(/)?.[1] || payload.name || 'tool';
1124
- const id = `live:${payload.call_id}`;
1125
- const common = { type: 'activity', id, status: 'inProgress', transient: true };
1126
- const turnId = payload.internal_chat_message_metadata_passthrough?.turn_id || '';
1127
- if (nestedTool === 'exec_command') {
1128
- return {
1129
- turnId,
1130
- entry: {
1131
- ...common,
1132
- kind: 'command',
1133
- title: '命令',
1134
- command: jsonStringField(input, 'cmd'),
1135
- cwd: jsonStringField(input, 'workdir'),
1136
- },
1137
- };
1138
- }
1139
- if (nestedTool === 'apply_patch') {
1140
- return { turnId, entry: { ...common, kind: 'fileChange', title: '文件修改', changes: [] } };
1141
- }
1142
- if (nestedTool === 'web__run') {
1143
- return {
1144
- turnId,
1145
- entry: { ...common, kind: 'webSearch', title: '网页搜索', query: jsonStringField(input, 'q') },
1146
- };
1147
- }
1148
- if (nestedTool === 'view_image') {
1149
- return {
1150
- turnId,
1151
- entry: { ...common, kind: 'imageView', title: '查看图片', path: jsonStringField(input, 'path') },
1152
- };
1153
- }
1154
- const parts = nestedTool.replace(/^mcp__/, '').split('__');
1155
- return {
1156
- turnId,
1157
- entry: {
1158
- ...common,
1159
- kind: 'tool',
1160
- title: '工具',
1161
- server: parts.length > 1 ? parts.shift() : 'functions',
1162
- tool: parts.join('/') || nestedTool,
1163
- arguments: {},
1164
- },
1165
- };
1166
- }
1167
-
1168
- function jsonStringField(source, field) {
1169
- const pattern = new RegExp(`(?:["']${field}["']|${field})\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`);
1170
- const match = pattern.exec(source);
1171
- if (!match) return '';
1172
- try { return JSON.parse(match[1]); } catch { return ''; }
1173
- }
1174
-
1175
653
  function messagePayload(body) {
1176
654
  const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
1177
655
  if (body.images !== undefined && (!Array.isArray(body.images) || body.images.length > 4)) {
@@ -1208,26 +686,6 @@ function messagePayload(body) {
1208
686
  return payload;
1209
687
  }
1210
688
 
1211
- async function messageArguments(threadId, body) {
1212
- const payload = messagePayload(body);
1213
- payload.images = await Promise.all(payload.images.map(image => image.startsWith('data:') ? persistLegacyImage(image, uploadsRoot) : image));
1214
- let prompt = payload.prompt;
1215
- if (payload.images.length) {
1216
- prompt += (prompt ? '\n\n' : '') + payload.images
1217
- .map((file, index) => `![手机图片 ${index + 1}](${file.replaceAll('\\', '/')})`)
1218
- .join('\n\n');
1219
- }
1220
- const argumentsValue = { threadId, prompt };
1221
- if (payload.hostId) argumentsValue.hostId = payload.hostId;
1222
- if (payload.model) argumentsValue.model = payload.model;
1223
- if (payload.thinking) argumentsValue.thinking = payload.thinking;
1224
- return argumentsValue;
1225
- }
1226
-
1227
- function usesSharedTask(body) {
1228
- return Boolean(sharedServer && (!body.hostId || body.hostId === 'local'));
1229
- }
1230
-
1231
689
  function appServerTurnArguments(threadId, body) {
1232
690
  const payload = messagePayload(body);
1233
691
  const input = [];
@@ -1240,334 +698,23 @@ function appServerTurnArguments(threadId, body) {
1240
698
  return value;
1241
699
  }
1242
700
 
1243
- function messageDisplayText(body) {
1244
- const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
1245
- const imageCount = Array.isArray(body.images) ? body.images.length : 0;
1246
- return prompt || (imageCount ? `[${imageCount} 张图片]` : '');
1247
- }
1248
-
1249
- function createThreadBody(body) {
1250
- const payload = messagePayload(body);
1251
- return { ...body, prompt: payload.prompt, transport: body.transport === 'app-server' ? 'app-server' : 'desktop-tools' };
1252
- }
1253
-
1254
- function appServerProjectTarget(projectId, projects) {
1255
- if (projectId === '__unassigned__') return appServerEvents.projectlessThreadTarget(projectRoot);
1256
- const project = projects.find(item => item?.projectId === projectId);
1257
- if (!project) throw new RequestError(404, '没有找到对应项目。');
1258
- if (project.hostId && project.hostId !== 'local') {
1259
- throw new RequestError(400, '独立 app-server 当前只支持本机项目。');
1260
- }
1261
- if (typeof project.path !== 'string' || !path.isAbsolute(project.path)) {
1262
- throw new RequestError(400, '项目没有可用的本机路径。');
1263
- }
1264
- return { projectId, cwd: project.path };
1265
- }
1266
-
1267
- function mergeThreadCatalog(catalog, appThreads, limit, sharedSnapshot = Boolean(sharedServer)) {
1268
- const owned = new Map(appThreads.map(thread => [thread.id, thread]));
1269
- const pinnedThreads = (catalog.pinnedThreads || []).map(thread => owned.get(thread.id) || thread);
1270
- const pinnedIds = new Set(pinnedThreads.map(thread => thread.id));
1271
- if (sharedSnapshot) {
1272
- const listed = new Set([...pinnedIds, ...(catalog.threads || []).map(thread => thread.id)]);
1273
- const threads = (catalog.threads || []).map(thread => owned.get(thread.id) || thread);
1274
- threads.push(...appThreads.filter(thread => !listed.has(thread.id)));
1275
- threads.sort((left, right) => threadRecency(right) - threadRecency(left));
1276
- return { ...catalog, pinnedThreads: [], threads: threads.slice(0, limit) };
1277
- }
1278
- const desktopThreads = (catalog.threads || []).filter(thread => !owned.has(thread.id));
1279
- const merged = [...desktopThreads, ...appThreads.filter(thread => !pinnedIds.has(thread.id))]
1280
- .sort((left, right) => threadRecency(right) - threadRecency(left))
1281
- .slice(0, limit);
1282
- return { ...catalog, pinnedThreads, threads: merged };
1283
- }
1284
-
1285
- async function createThreadArguments(projectId, body, projects) {
1286
- const { threadId, hostId, ...message } = await messageArguments('', body);
1287
- if (projectId === '__unassigned__') return { ...message, target: { type:'projectless' } };
1288
- const project = projects.find(item => item?.projectId === projectId);
1289
- if (!project) throw new RequestError(404, '没有找到对应项目。');
1290
- return { ...message, target:{ type:'project', projectId, environment:{ type:'local' } } };
1291
- }
1292
-
1293
- function publicQueue(threadId) {
1294
- return (messageQueues.get(threadId) || []).map(item => ({
1295
- id: item.id,
1296
- prompt: item.displayPrompt || item.prompt,
1297
- text: item.text, attachments:item.attachments || [],
1298
- model: item.model || '',
1299
- thinking: item.thinking || item.effort || '',
1300
- createdAt: item.createdAt,
1301
- error: item.error || '',
1302
- }));
1303
- }
1304
-
1305
- function queueArguments(item) {
1306
- const value = { threadId: item.threadId, prompt: item.prompt };
1307
- if (item.hostId) value.hostId = item.hostId;
1308
- if (item.model) value.model = item.model;
1309
- if (item.thinking) value.thinking = item.thinking;
1310
- return value;
1311
- }
1312
-
1313
- async function sendQueuedItem(threadId, item) {
1314
- await requireExecutionWritable(threadId);
1315
- if (item.transport === 'app-server') {
1316
- try {
1317
- return { success: true, data: await appServerTasks.startTurn(threadId, item) };
1318
- } catch (error) {
1319
- return { success: false, data: error instanceof Error ? error.message : String(error) };
1320
- }
1321
- }
1322
- return callTool('send_message_to_thread', queueArguments(item));
1323
- }
1324
-
1325
- async function steerQueuedItem(threadId, item) {
1326
- if (item.transport !== 'app-server') return callTool('send_message_to_thread', queueArguments(item));
1327
- try {
1328
- const thread = await appServerTasks.readTask(threadId, { includeTurns: true, turnLimit: 1 });
1329
- const activeTurn = thread.turns?.at(-1);
1330
- if (!activeTurn || statusText(activeTurn.status) !== 'inProgress') {
1331
- throw new Error('当前没有可引导的 app-server 活动轮次。');
1332
- }
1333
- return {
1334
- success: true,
1335
- data: await appServerTasks.steerTurn(threadId, activeTurn.id, item.input),
1336
- };
1337
- } catch (error) {
1338
- return { success: false, data: error instanceof Error ? error.message : String(error) };
1339
- }
1340
- }
1341
-
1342
- function ensureQueueWorker(threadId) {
1343
- if (queueWorkers.has(threadId)) return;
1344
- const worker = runQueueWorker(threadId)
1345
- .catch(error => {
1346
- const item = messageQueues.get(threadId)?.[0];
1347
- if (item) item.error = error instanceof Error ? error.message : String(error);
1348
- })
1349
- .finally(() => queueWorkers.delete(threadId));
1350
- queueWorkers.set(threadId, worker);
1351
- }
1352
-
1353
- async function runQueueWorker(threadId) {
1354
- while (true) {
1355
- const queue = messageQueues.get(threadId) || [];
1356
- const item = queue[0];
1357
- if (!item || item.error) return;
1358
- let timeline;
1359
- if (item.transport === 'app-server') {
1360
- const thread = await appServerTasks.readTask(threadId, { includeTurns: true, turnLimit: 1 });
1361
- timeline = normalizeAppServerTimeline(thread);
1362
- } else {
1363
- const readArguments = {
1364
- threadId,
1365
- turnLimit: 1,
1366
- includeOutputs: false,
1367
- maxOutputCharsPerItem: 500,
1368
- };
1369
- if (item.hostId) readArguments.hostId = item.hostId;
1370
- const threadResult = await callTool('read_thread', readArguments, 15_000);
1371
- if (!threadResult.success) throw new BridgeError('读取排队任务状态失败。', threadResult.data);
1372
- timeline = normalizeTimeline(threadResult.data);
1373
- }
1374
- const active = timeline.thread.status === 'active' || timeline.turns.at(-1)?.status === 'inProgress';
1375
- if (active) {
1376
- await delay(2_000);
1377
- continue;
1378
- }
1379
- if (messageQueues.get(threadId) !== queue || queue[0]?.id !== item.id) continue;
1380
- const sendResult = await sendQueuedItem(threadId, item);
1381
- if (!sendResult.success) throw new BridgeError('发送排队消息失败。', sendResult.data);
1382
- if (queue[0]?.id === item.id) queue.shift();
1383
- }
1384
- }
1385
-
1386
- async function streamAppServerThreadEvents(request, response, threadId) {
1387
- response.startEvents();
1388
-
1389
- let closed = false;
1390
- let pushPending = false;
1391
- let pushing = false;
1392
- let previousHash = '';
1393
- let updateTimer = null;
1394
- let resolveClosed;
1395
- const closedPromise = new Promise(resolve => { resolveClosed = resolve; });
1396
- response.once('close', () => { closed = true; resolveClosed(); });
1397
-
1398
- const pushUpdate = async () => {
1399
- if (closed) return;
1400
- if (pushing) { pushPending = true; return; }
1401
- pushing = true;
1402
- try {
1403
- const thread = await appServerTasks.readTask(threadId, { includeTurns: true, turnLimit: 2 });
1404
- const timeline = normalizeAppServerTimeline(thread);
1405
- const update = {
1406
- thread: timeline.thread,
1407
- turns: timeline.turns.slice(-2),
1408
- turn: timeline.turns.at(-1) || null,
1409
- queue: publicQueue(threadId),
1410
- };
1411
- const serialized = JSON.stringify(update);
1412
- const hash = crypto.createHash('sha256').update(serialized).digest('base64url');
1413
- if (hash !== previousHash) {
1414
- sendEvent(response, 'update', update);
1415
- previousHash = hash;
1416
- }
1417
- } catch (error) {
1418
- sendEvent(response, 'bridge-error', { error: error instanceof Error ? error.message : String(error) });
1419
- } finally {
1420
- pushing = false;
1421
- if (pushPending && !closed) {
1422
- pushPending = false;
1423
- void pushUpdate();
1424
- }
1425
- }
1426
- };
1427
- const scheduleUpdate = event => {
1428
- if (closed || event.threadId !== threadId || updateTimer) return;
1429
- updateTimer = setTimeout(() => {
1430
- updateTimer = null;
1431
- void pushUpdate();
1432
- }, 60);
1433
- };
1434
- appServerTasks.on('thread-event', scheduleUpdate);
1435
- await pushUpdate();
1436
-
1437
- await closedPromise;
1438
- closed = true;
1439
- if (updateTimer) clearTimeout(updateTimer);
1440
- appServerTasks.off('thread-event', scheduleUpdate);
1441
- if (!response.writableEnded) response.end();
1442
- }
1443
-
1444
- async function streamThreadEvents(request, response, threadId, hostId) {
1445
- response.startEvents();
1446
-
1447
- let closed = false;
1448
- let previousHash = '';
1449
- response.once('close', () => { closed = true; });
1450
-
1451
- while (!closed) {
1452
- let intervalMs = 2_500;
1453
- try {
1454
- const argumentsValue = {
1455
- threadId,
1456
- turnLimit: 2,
1457
- includeOutputs: true,
1458
- maxOutputCharsPerItem: 4_000,
1459
- };
1460
- if (hostId) argumentsValue.hostId = hostId;
1461
- const result = await callTool('read_thread', argumentsValue);
1462
- if (!result.success) {
1463
- throw new BridgeError('Codex Desktop 工具返回失败。', result.data);
1464
- }
1465
-
1466
- const timeline = normalizeTimeline(result.data);
1467
- enrichTimelineWithLocalActivity(timeline, threadId);
1468
- const update = {
1469
- thread: timeline.thread,
1470
- turns: timeline.turns.slice(-2),
1471
- turn: timeline.turns.at(-1) || null,
1472
- queue: publicQueue(threadId),
1473
- };
1474
- const serialized = JSON.stringify(update);
1475
- const hash = crypto.createHash('sha256').update(serialized).digest('base64url');
1476
- if (hash !== previousHash) {
1477
- sendEvent(response, 'update', update);
1478
- previousHash = hash;
1479
- }
1480
- if (timeline.thread.status === 'active' || update.turn?.status === 'inProgress') {
1481
- intervalMs = 1_000;
1482
- }
1483
- } catch (error) {
1484
- sendEvent(response, 'bridge-error', {
1485
- error: error instanceof Error ? error.message : String(error),
1486
- });
1487
- intervalMs = 3_000;
1488
- }
1489
-
1490
- await delay(intervalMs);
1491
- }
1492
-
1493
- if (!response.writableEnded) response.end();
1494
- }
1495
-
1496
- function sendEvent(response, eventName, value) {
1497
- if (response.destroyed || response.writableEnded) return;
1498
- response.event(eventName, value);
1499
- }
1500
-
1501
- function normalizeTimeline(data, reverseTurns = true) {
1502
- const source = data && typeof data === 'object' ? data : {};
1503
- const sourceTurns = Array.isArray(source.turns) ? source.turns : source.thread?.turns;
1504
- const turns = Array.isArray(sourceTurns) ? sourceTurns.map(normalizeTurn) : [];
1505
- if (reverseTurns) turns.reverse();
1506
- if (reverseTurns) restoreEmptyTurnMessages(source.thread?.id, turns);
1507
- return {
701
+ function normalizeTimeline(data) {
702
+ const source = data && typeof data === 'object' ? data : {};
703
+ const sourceTurns = Array.isArray(source.turns) ? source.turns : source.thread?.turns;
704
+ const turns = Array.isArray(sourceTurns) ? sourceTurns.map(normalizeTurn) : [];
705
+ return {
1508
706
  schemaVersion: 1,
1509
707
  thread: normalizeThread(source.thread),
1510
708
  turns,
1511
709
  };
1512
710
  }
1513
711
 
1514
- function normalizeAppServerTimeline(thread) {
1515
- const record = appServerTasks.store.get(thread?.id);
1516
- return normalizeTimeline({
1517
- thread: {
1518
- ...thread,
1519
- title: thread?.name || thread?.title || record?.title || '',
1520
- projectId: record?.projectId || '',
1521
- hostId: 'app-server',
1522
- transport: 'app-server',
1523
- },
1524
- }, false);
1525
- }
1526
-
1527
712
  function normalizeSharedTimeline({ thread, nextCursor, sync }) {
1528
- return { ...normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } }, false),
713
+ return { ...normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } }),
1529
714
  historyView: 'grouped-lazy-skeleton-v11',
1530
715
  ...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
1531
716
  }
1532
717
 
1533
- function restoreEmptyTurnMessages(threadId, turns) {
1534
- const missing = new Map(turns.filter(turn => turn.entries.length === 0).map(turn => [turn.id, turn]));
1535
- if (!missing.size || !threadId) return;
1536
- const sessionPath = findSessionPath(threadId);
1537
- if (!sessionPath) return;
1538
- const handle = fs.openSync(sessionPath, 'r');
1539
- try {
1540
- const size = fs.fstatSync(handle).size;
1541
- const length = Math.min(size, MAX_SETTINGS_SCAN_BYTES);
1542
- const buffer = Buffer.allocUnsafe(length);
1543
- fs.readSync(handle, buffer, 0, length, size - length);
1544
- const lines = buffer.toString('utf8').split(/\r?\n/);
1545
- if (length < size) lines.shift();
1546
- let turnId;
1547
- for (const line of lines) {
1548
- if (!line.trim()) continue;
1549
- let event;
1550
- try { event = JSON.parse(line); } catch { continue; }
1551
- const item = event.payload;
1552
- if (event.type === 'event_msg' && item?.type === 'task_started') turnId = item.turn_id;
1553
- if (event.type === 'turn_context') turnId = item?.turn_id;
1554
- const turn = missing.get(turnId);
1555
- if (!turn || event.type !== 'response_item') continue;
1556
- let entry;
1557
- if (item.type === 'function_call_output' && item.namespace === 'codex_app' && ['create_thread', 'send_message_to_thread'].includes(item.name)) {
1558
- const text = extractDelegatedInput(item.output);
1559
- if (text) entry = { type: 'message', id: item.id, role: 'user', source: 'remote', ...splitUploadedImages(text) };
1560
- } else if (item.type === 'message' && ['user', 'assistant'].includes(item.role) && item.channel !== 'analysis') {
1561
- const text = (item.content || []).filter(part => ['input_text', 'output_text'].includes(part.type)).map(part => part.text || '').join('\n');
1562
- if (text) entry = item.role === 'user'
1563
- ? normalizeItem({ type: 'userMessage', id: item.id, content: item.content.map(part => ({ ...part, type: part.type === 'input_text' ? 'text' : part.type })) })
1564
- : normalizeItem({ type: 'agentMessage', id: item.id, phase: item.phase || item.channel, text });
1565
- }
1566
- if (entry) turn.entries.push(entry);
1567
- }
1568
- } finally { fs.closeSync(handle); }
1569
- }
1570
-
1571
718
  function normalizeThread(thread) {
1572
719
  const source = thread && typeof thread === 'object' ? thread : {};
1573
720
  return {
@@ -1579,7 +726,7 @@ function normalizeThread(thread) {
1579
726
  projectId: source.projectId || '',
1580
727
  cwd: source.cwd || '',
1581
728
  hostId: source.hostId || '',
1582
- transport: source.transport || (source.hostId === 'app-server' ? 'app-server' : 'desktop-tools'),
729
+ transport: source.transport || 'shared-app-server',
1583
730
  readOnly: source.readOnly === true,
1584
731
  readOnlyReason: source.readOnlyReason || '',
1585
732
  };
@@ -1831,10 +978,6 @@ function sharedHistoryPageOptions() {
1831
978
  };
1832
979
  }
1833
980
 
1834
- function threadRecency(thread) {
1835
- return Number(thread?.recencyAt ?? thread?.updatedAt) || 0;
1836
- }
1837
-
1838
981
  function normalizeAttachments(content) {
1839
982
  if (!Array.isArray(content)) return [];
1840
983
  const items = content.filter(item => item && !['text', 'input_text'].includes(item.type)).map(item => ({ ...item }));
@@ -2116,10 +1259,6 @@ function statusText(value) {
2116
1259
  return value == null ? 'unknown' : String(value);
2117
1260
  }
2118
1261
 
2119
- function delay(milliseconds) {
2120
- return new Promise(resolve => setTimeout(resolve, milliseconds));
2121
- }
2122
-
2123
1262
  function sendJson(response, statusCode, value) {
2124
1263
  if (response.json) { response.json(statusCode, value); return; }
2125
1264
  sendBuffer(response, statusCode, 'application/json; charset=utf-8', Buffer.from(JSON.stringify(value)));
@@ -2144,10 +1283,10 @@ function parseOptions(args) {
2144
1283
  let port = Number.parseInt(process.env.CODEX_REMOTE_BRIDGE_PORT || '45831', 10);
2145
1284
  let lan = process.env.CODEX_REMOTE_BRIDGE_LAN === 'true';
2146
1285
  let token = process.env.CODEX_REMOTE_BRIDGE_TOKEN?.trim() || '';
2147
- let appServer = process.env.CODEX_REMOTE_APP_SERVER === 'true';
1286
+ if (process.env.CODEX_REMOTE_APP_SERVER === 'true') throw new Error('此连接方式已停用。');
2148
1287
  for (let i = 0; i < args.length; i += 1) {
2149
1288
  if (args[i] === '--lan') lan = true;
2150
- if (args[i] === '--app-server') appServer = true;
1289
+ if (args[i] === '--app-server') throw new Error('此连接方式已停用。');
2151
1290
  if (args[i] === '--port' && args[i + 1]) port = Number.parseInt(args[++i], 10);
2152
1291
  if (args[i] === '--token' && args[i + 1]) token = args[++i];
2153
1292
  }
@@ -2155,32 +1294,10 @@ function parseOptions(args) {
2155
1294
  throw new Error('端口必须位于 1024 到 65535 之间。');
2156
1295
  }
2157
1296
  if (lan && !token) token = crypto.randomBytes(32).toString('base64url');
2158
- return { port, lan, token, appServer };
2159
- }
2160
-
2161
- function boundedInteger(value, fallback, minimum, maximum) {
2162
- const parsed = Number.parseInt(value || '', 10);
2163
- return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, parsed)) : fallback;
2164
- }
2165
-
2166
- function booleanValue(value, fallback) {
2167
- if (value === 'true') return true;
2168
- if (value === 'false') return false;
2169
- return fallback;
1297
+ return { port, lan, token };
2170
1298
  }
2171
1299
 
2172
- class BridgeError extends Error {
2173
- constructor(message, details, options) {
2174
- super(message, options ? { cause: options } : undefined);
2175
- this.details = details;
2176
- }
2177
- }
2178
-
2179
- class RequestError extends Error {
2180
- constructor(statusCode, message, details) {
2181
- super(message);
2182
- this.statusCode = statusCode;
2183
- this.details = details;
2184
- this.code = details?.code;
2185
- }
1300
+ function boundedInteger(value, fallback, minimum, maximum) {
1301
+ const parsed = Number.parseInt(value || '', 10);
1302
+ return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, parsed)) : fallback;
2186
1303
  }