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