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