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