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