@walkhi/code-relax 0.1.0-beta.26 → 0.1.0-beta.28
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 +80 -67
- package/dist/bin/codex-remote.mjs +286 -239
- package/dist/shared/app-server-events.cjs +102 -16
- package/dist/shared/code-browser.cjs +159 -0
- package/dist/shared/file-changes.cjs +31 -0
- package/dist/shared/file-citations.cjs +117 -0
- package/dist/shared/file-mentions.cjs +80 -35
- package/dist/shared/message-delivery.cjs +38 -0
- package/dist/shared/p2p-file-channel.cjs +74 -0
- package/dist/shared/server-requests.cjs +149 -41
- package/dist/shared/service-endpoints.cjs +11 -11
- package/dist/shared/timeline-sync.cjs +173 -156
- package/dist/skills/code-relax/SKILL.md +2 -1
- package/dist/src/bounded-process-log.mjs +63 -0
- package/dist/src/celsius-client.mjs +107 -0
- package/dist/src/code-browser.mjs +101 -0
- package/dist/src/desktop-launcher.mjs +30 -30
- package/dist/src/dots-approval-timeline.mjs +99 -0
- package/dist/src/dots-avatar.mjs +42 -0
- package/dist/src/dots-chat-projection.mjs +50 -0
- package/dist/src/dots-chat-server.mjs +412 -0
- package/dist/src/dots-message-images.mjs +319 -0
- package/dist/src/dots-profile.mjs +77 -0
- package/dist/src/dots-room-activity.mjs +94 -0
- package/dist/src/dots-room-client.mjs +135 -0
- package/dist/src/dots-room-projection.mjs +94 -0
- package/dist/src/dots-schedules.mjs +66 -0
- package/dist/src/dots-summary-tasks.mjs +32 -0
- package/dist/src/dots-task-catalog.mjs +411 -0
- package/dist/src/file-transfer-range.mjs +22 -0
- package/dist/src/file-upload-session.mjs +130 -124
- package/dist/src/hosted-history-app-server.mjs +103 -0
- package/dist/src/hosted-history-cache.mjs +113 -0
- package/dist/src/lan-socket-server.mjs +205 -160
- package/dist/src/managed-app-server.mjs +241 -238
- package/dist/src/message-upload-files.mjs +74 -0
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -0
- package/dist/src/platform/windows/codex-runtime.mjs +253 -251
- package/dist/src/platform/windows/desktop-host.mjs +38 -32
- package/dist/src/platform/windows/desktop-info.ps1 +2 -2
- package/dist/src/platform/windows/desktop-project-registration.mjs +153 -0
- package/dist/src/platform/windows/dots-profile.mjs +49 -0
- package/dist/src/platform/windows/launch-worker.mjs +41 -42
- package/dist/src/platform/windows/project-locations.mjs +16 -0
- package/dist/src/project-create.mjs +119 -0
- package/dist/src/project-management.mjs +69 -0
- package/dist/src/self-relay/account-connections.mjs +52 -0
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-style.mjs +14 -9
- package/dist/src/self-relay/connector.mjs +22 -4
- package/dist/src/self-relay/file-wire.mjs +13 -0
- package/dist/src/self-relay/p2p-probe.mjs +152 -123
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -0
- package/dist/src/self-relay/server.mjs +894 -849
- package/dist/src/server.mjs +463 -148
- package/dist/src/service-doctor.mjs +72 -61
- package/dist/src/service-lifecycle.mjs +51 -43
- package/dist/src/shared-app-server.mjs +166 -107
- package/dist/src/shared-catalog.mjs +132 -40
- package/dist/src/shared-thread-stream.mjs +103 -25
- package/dist/src/usage-reset.mjs +17 -0
- package/dist/src/workstation-downloads.mjs +120 -0
- package/package.json +1 -1
package/dist/src/server.mjs
CHANGED
|
@@ -8,28 +8,47 @@ import vm from 'node:vm';
|
|
|
8
8
|
import sharp from 'sharp';
|
|
9
9
|
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
10
10
|
import fileMentions from '../shared/file-mentions.cjs';
|
|
11
|
+
import fileCitations from '../shared/file-citations.cjs';
|
|
11
12
|
import { validateInlineImage } from './message-images.mjs';
|
|
12
13
|
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
13
14
|
import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
|
|
14
15
|
import { RuntimeDiscovery } from './runtime-discovery.mjs';
|
|
15
|
-
import { streamSharedThread } from './shared-thread-stream.mjs';
|
|
16
|
+
import { streamSharedThread, threadListNotificationMethods } from './shared-thread-stream.mjs';
|
|
16
17
|
import appServerEvents from '../shared/app-server-events.cjs';
|
|
18
|
+
import messageDelivery from '../shared/message-delivery.cjs';
|
|
19
|
+
import fileChanges from '../shared/file-changes.cjs';
|
|
17
20
|
import { runtimePaths } from './platform/windows/runtime-paths.mjs';
|
|
18
21
|
import { openControl } from './service-control.mjs';
|
|
19
22
|
import { writeRecord, readRecord } from './service-lifecycle.mjs';
|
|
20
23
|
import { ExecutionHealth } from './execution-health.mjs';
|
|
21
|
-
import { authorizeLanDevice, onLanAuthorizationsChanged } from './self-relay/lan-authorizations.mjs';
|
|
24
|
+
import { authorizeLanDevice, onLanAuthorizationsChanged } from './self-relay/lan-authorizations.mjs';
|
|
22
25
|
import { createRelayManager } from './self-relay/lifecycle.mjs';
|
|
23
26
|
import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
|
|
24
27
|
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
25
28
|
import { SharedCatalog } from './shared-catalog.mjs';
|
|
29
|
+
import { DotsAppServer } from './dots-task-catalog.mjs';
|
|
30
|
+
import { DotsChatServer } from './dots-chat-server.mjs';
|
|
31
|
+
import { HostedHistoryCache } from './hosted-history-cache.mjs';
|
|
32
|
+
import { readDotsSummaryTasks } from './dots-summary-tasks.mjs';
|
|
33
|
+
import { readDotsSchedules } from './dots-schedules.mjs';
|
|
34
|
+
import { DotsMessageImages } from './dots-message-images.mjs';
|
|
35
|
+
import { desktopDotsProfiles } from './platform/windows/dots-profile.mjs';
|
|
26
36
|
import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
|
|
27
37
|
import { requestServiceRestart } from './service-restart.mjs';
|
|
28
38
|
import { TurnModelHistory } from './turn-model-history.mjs';
|
|
39
|
+
import { browseCode } from './code-browser.mjs';
|
|
40
|
+
import { fileRange, fileRevision, FileRangeError } from './file-transfer-range.mjs';
|
|
41
|
+
import { browseProjectFolders, createProjectFolder, saveProject } from './project-management.mjs';
|
|
42
|
+
import { projectLocations } from './platform/windows/project-locations.mjs';
|
|
43
|
+
import { createWorkstationProject } from './project-create.mjs';
|
|
44
|
+
import { WorkstationDownloads } from './workstation-downloads.mjs';
|
|
45
|
+
import { consumeUsageReset } from './usage-reset.mjs';
|
|
29
46
|
|
|
30
47
|
const turnModelHistory = new TurnModelHistory();
|
|
48
|
+
const messageReceipts = new messageDelivery.Store();
|
|
31
49
|
|
|
32
|
-
const MAX_ATTACHMENT_BYTES =
|
|
50
|
+
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
51
|
+
const MAX_FILE_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
33
52
|
const MAX_CACHED_ATTACHMENTS = 32;
|
|
34
53
|
const MAX_CACHED_FILES = 64;
|
|
35
54
|
const MAX_CONTEXT_TAIL_BYTES = 4 * 1024 * 1024;
|
|
@@ -37,15 +56,6 @@ const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
|
37
56
|
const SHARED_HISTORY_PAGE_BYTES = 500 * 1024;
|
|
38
57
|
const SHARED_HISTORY_PAGE_TURNS = 8;
|
|
39
58
|
const MAX_SETTINGS_SCAN_BYTES = 64 * 1024 * 1024;
|
|
40
|
-
const ALLOWED_MODELS = new Set([
|
|
41
|
-
'gpt-6-astra',
|
|
42
|
-
'gpt-6-sol',
|
|
43
|
-
'gpt-5.6-sol',
|
|
44
|
-
'gpt-5.6-terra',
|
|
45
|
-
'gpt-5.6-luna',
|
|
46
|
-
'gpt-5.5',
|
|
47
|
-
'gpt-5.3-codex-spark',
|
|
48
|
-
]);
|
|
49
59
|
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
50
60
|
|
|
51
61
|
class RequestError extends Error {
|
|
@@ -74,10 +84,12 @@ const runtimeVersion = readRuntimeVersion();
|
|
|
74
84
|
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
|
|
75
85
|
const projectRoot = os.homedir();
|
|
76
86
|
const { codexDataRoot, stateRoot, sessionsRoot, uploadsRoot } = runtimePaths();
|
|
87
|
+
const workstationDownloads = new WorkstationDownloads(stateRoot);
|
|
77
88
|
const markdownItScript = fs.readFileSync(path.join(webRoot, 'vendor', 'markdown-it.umd.min.js'));
|
|
78
89
|
const markdownSandbox = { atob };
|
|
79
90
|
vm.runInNewContext(markdownItScript.toString(), markdownSandbox);
|
|
80
91
|
const imageMarkdown = markdownSandbox.markdownit({ html: false });
|
|
92
|
+
fileCitations.install(imageMarkdown);
|
|
81
93
|
const attachmentCache = new Map();
|
|
82
94
|
const fileCache = new Map();
|
|
83
95
|
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
@@ -88,6 +100,11 @@ const sessionSettingsCache = new Map();
|
|
|
88
100
|
const liveActivityCache = new Map();
|
|
89
101
|
let sharedServer = null;
|
|
90
102
|
const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
|
|
103
|
+
const hostedHistoryCache = new HostedHistoryCache(stateRoot);
|
|
104
|
+
const dotsServer = new DotsAppServer(managedAppServer.client, desktopDotsProfiles(codexDataRoot, stateRoot), hostedHistoryCache);
|
|
105
|
+
const dotsImages = new DotsMessageImages({ localServer: managedAppServer.client, identity: () => dotsServer.profileIdentity,
|
|
106
|
+
cache: attachmentCache, capabilityKey: imageCapabilityKey, maxEntries: MAX_CACHED_ATTACHMENTS, maxBytes: MAX_ATTACHMENT_BYTES });
|
|
107
|
+
const dotsChat = new DotsChatServer(dotsServer, undefined, { uploadsRoot });
|
|
91
108
|
const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
|
|
92
109
|
const relayManager = createRelayManager(stateRoot);
|
|
93
110
|
let sharedCatalog = null, executionMode = 'ordinary';
|
|
@@ -95,7 +112,7 @@ function selectSharedExecution(enabled) {
|
|
|
95
112
|
const next = enabled ? managedAppServer.client : null;
|
|
96
113
|
if (sharedServer === next) return;
|
|
97
114
|
sharedServer = next;
|
|
98
|
-
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer) : null;
|
|
115
|
+
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer, dotsServer, dotsChat) : null;
|
|
99
116
|
}
|
|
100
117
|
const executionHealth = new ExecutionHealth(async () => {
|
|
101
118
|
await managedAppServer.ensure();
|
|
@@ -114,18 +131,23 @@ async function refreshExecution() {
|
|
|
114
131
|
await executionHealth.refresh();
|
|
115
132
|
return { mode: executionMode };
|
|
116
133
|
}
|
|
117
|
-
async function requireExecutionWritable(threadId) {
|
|
134
|
+
async function requireExecutionWritable(threadId, client = sharedServer) {
|
|
118
135
|
const execution = await executionHealth.refresh();
|
|
119
136
|
if (!execution.canWrite) {
|
|
120
137
|
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
121
138
|
{ code: 'full_control_required', execution });
|
|
122
139
|
}
|
|
123
|
-
if (threadId &&
|
|
140
|
+
if (threadId && client) client.requireWritable(threadId);
|
|
124
141
|
return execution;
|
|
125
142
|
}
|
|
126
143
|
managedAppServer.client.on('subscription-error', error => console.error('Codex 内核任务取消订阅失败:', error.message));
|
|
144
|
+
dotsServer.on('subscription-error', error => console.error('Dots 任务取消订阅失败:', error.message));
|
|
145
|
+
dotsChat.on('subscription-error', error => console.error('Dots 对话取消订阅失败:', error.message));
|
|
146
|
+
dotsChat.on('activity-error', error => console.error('点点状态通道不可用:', error.message));
|
|
127
147
|
|
|
148
|
+
dotsServer.on('profile-error', error => console.error(error.message));
|
|
128
149
|
async function handleRequest(request, response) {
|
|
150
|
+
let messageSubmissionStarted = false;
|
|
129
151
|
try {
|
|
130
152
|
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
131
153
|
|
|
@@ -215,9 +237,19 @@ async function handleRequest(request, response) {
|
|
|
215
237
|
return;
|
|
216
238
|
}
|
|
217
239
|
|
|
240
|
+
const fileLinkMatch = /^\/api\/files\/([a-f0-9]{64})\/link$/.exec(url.pathname);
|
|
241
|
+
if (request.method === 'GET' && fileLinkMatch) {
|
|
242
|
+
const attachment = attachmentCache.get(fileLinkMatch[1])
|
|
243
|
+
|| dotsImages.restore(fileLinkMatch[1], url.searchParams.get('recovery') || '');
|
|
244
|
+
if (!attachment?.dotsFile) throw new RequestError(404, '此附件没有网络链接,请重新读取对话。');
|
|
245
|
+
sendJson(response, 200, { url: await dotsImages.fileLink(attachment) });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
218
249
|
const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
219
250
|
if (request.method === 'GET' && imageMatch) {
|
|
220
251
|
let image = attachmentCache.get(imageMatch[1])
|
|
252
|
+
|| dotsImages.restore(imageMatch[1], url.searchParams.get('recovery') || '')
|
|
221
253
|
|| recoverLocalImage(imageMatch[1], url.searchParams.get('recovery') || '');
|
|
222
254
|
const contextThreadId = url.searchParams.get('threadId') || '';
|
|
223
255
|
const contextTurnId = url.searchParams.get('turnId') || '';
|
|
@@ -229,24 +261,64 @@ async function handleRequest(request, response) {
|
|
|
229
261
|
} catch {}
|
|
230
262
|
}
|
|
231
263
|
if (!image) throw new RequestError(404, '这张图片已失效,无法从工作站重新读取。');
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
264
|
+
if (image.dotsFile) await dotsImages.load(image);
|
|
265
|
+
if (url.searchParams.get('variant') === 'thumbnail') {
|
|
266
|
+
const asset = await attachmentThumbnail(image);
|
|
267
|
+
sendJson(response, 200, { url: `data:${asset.mimeType};base64,${asset.buffer.toString('base64')}` });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const size = image.buffer.length;
|
|
271
|
+
if (size > MAX_ATTACHMENT_BYTES) throw new RequestError(413, '图片超过下载上限(20 MiB),无法下载。');
|
|
272
|
+
if (!size) throw new RequestError(400, '图片为空,无法下载。');
|
|
273
|
+
if (!request.fileTransfer) throw new RequestError(400, '请通过文件通道读取原图。');
|
|
274
|
+
const revision = imageMatch[1];
|
|
275
|
+
const { offset, length } = fileRange(url.searchParams, size, revision);
|
|
276
|
+
response.filePart({ name: image.name || '图片', size, offset, revision, mimeType: image.mimeType },
|
|
277
|
+
image.buffer.subarray(offset, offset + length));
|
|
235
278
|
return;
|
|
236
279
|
}
|
|
237
280
|
|
|
238
281
|
const fileMatch = /^\/api\/files\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
239
282
|
if (request.method === 'GET' && fileMatch) {
|
|
283
|
+
if (!request.fileTransfer) throw new RequestError(400, '请通过文件通道下载附件。');
|
|
284
|
+
const hosted = attachmentCache.get(fileMatch[1])
|
|
285
|
+
|| dotsImages.restore(fileMatch[1], url.searchParams.get('recovery') || '');
|
|
286
|
+
if (hosted?.dotsFile) {
|
|
287
|
+
const { bytes, ...metadata } = await dotsImages.readPart(hosted, url.searchParams);
|
|
288
|
+
response.filePart(metadata, bytes);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
240
291
|
const file = fileCache.get(fileMatch[1]);
|
|
241
292
|
if (!file) throw new RequestError(404, '文件授权已失效,请重新读取对话。');
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
293
|
+
if (file.externalIdentity) await dotsImages.authorization(file.externalIdentity);
|
|
294
|
+
const { offset, length } = fileRange(url.searchParams, file.size, file.revision);
|
|
295
|
+
let handle;
|
|
296
|
+
try { handle = fs.openSync(file.path, 'r'); }
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (error.code === 'ENOENT') throw new RequestError(404, '电脑上的文件已不存在。');
|
|
299
|
+
throw error;
|
|
247
300
|
}
|
|
248
|
-
|
|
249
|
-
|
|
301
|
+
try {
|
|
302
|
+
const validate = () => {
|
|
303
|
+
const stat = fs.fstatSync(handle);
|
|
304
|
+
if (!stat.isFile() || fileRevision(stat) !== file.revision || fileRevision(fs.statSync(file.path)) !== file.revision) {
|
|
305
|
+
fileCache.delete(fileMatch[1]);
|
|
306
|
+
throw new RequestError(409, '电脑上的文件已发生变化,请重新读取对话。');
|
|
307
|
+
}
|
|
308
|
+
if (stat.size > MAX_FILE_DOWNLOAD_BYTES) throw new RequestError(413, '文件超过下载上限(20 MiB),无法下载。');
|
|
309
|
+
if (!stat.size) throw new RequestError(400, '文件为空,无法下载。');
|
|
310
|
+
};
|
|
311
|
+
validate();
|
|
312
|
+
const data = Buffer.alloc(length);
|
|
313
|
+
let bytesRead = 0;
|
|
314
|
+
while (bytesRead < data.length) {
|
|
315
|
+
const count = fs.readSync(handle, data, bytesRead, data.length - bytesRead, offset + bytesRead);
|
|
316
|
+
if (!count) throw new RequestError(409, '电脑上的文件读取不完整,请重新读取对话。');
|
|
317
|
+
bytesRead += count;
|
|
318
|
+
}
|
|
319
|
+
validate();
|
|
320
|
+
response.filePart({ name: file.name, size: file.size, offset, revision: file.revision }, data);
|
|
321
|
+
} finally { fs.closeSync(handle); }
|
|
250
322
|
return;
|
|
251
323
|
}
|
|
252
324
|
|
|
@@ -257,23 +329,112 @@ async function handleRequest(request, response) {
|
|
|
257
329
|
if (taskPath && (url.pathname.endsWith('/takeover') || url.pathname === `/api/threads/${taskPath[1]}`)) {
|
|
258
330
|
throw new RequestError(410, '此连接方式已停用。', { code: 'unsupported_control' });
|
|
259
331
|
}
|
|
260
|
-
|
|
332
|
+
const hostId = url.searchParams.get('hostId') || 'local';
|
|
333
|
+
if (!['local', 'durable'].includes(hostId)) {
|
|
261
334
|
throw new RequestError(410, '此对话的连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
262
335
|
}
|
|
263
336
|
if (!executionHealth.state.ready || sharedServer?.ready !== true) {
|
|
264
337
|
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
265
338
|
{ code: 'full_control_required' });
|
|
266
339
|
}
|
|
340
|
+
const rootChat = hostId === 'durable' && taskPath
|
|
341
|
+
&& await dotsServer.source(decodeURIComponent(taskPath[1])) === 'aeon';
|
|
342
|
+
const execution = rootChat ? dotsChat : hostId === 'durable' ? dotsServer : sharedServer;
|
|
343
|
+
|
|
344
|
+
const dotsActivityMatch = /^\/api\/threads\/([^/]+)\/dots-activity-events$/.exec(url.pathname);
|
|
345
|
+
if (request.method === 'GET' && dotsActivityMatch) {
|
|
346
|
+
if (!rootChat) throw new RequestError(405, '只有点点主聊天提供此活动面板。');
|
|
347
|
+
response.startEvents();
|
|
348
|
+
const disconnected = () => response.end();
|
|
349
|
+
dotsChat.on('disconnected', disconnected);
|
|
350
|
+
const release = dotsChat.acquirePanelActivity(decodeURIComponent(dotsActivityMatch[1]), value => {
|
|
351
|
+
if (!response.destroyed) response.event('dots-activity', { threadId: value.threadId, activity: value.activity });
|
|
352
|
+
});
|
|
353
|
+
response.once('close', () => { release(); dotsChat.off('disconnected', disconnected); });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const dotsTasksMatch = /^\/api\/threads\/([^/]+)\/dots-tasks$/.exec(url.pathname);
|
|
358
|
+
if (request.method === 'GET' && dotsTasksMatch) {
|
|
359
|
+
if (!rootChat) throw new RequestError(405, '只有点点主聊天提供此任务面板。');
|
|
360
|
+
sendJson(response, 200, await readDotsSummaryTasks(dotsServer, decodeURIComponent(dotsTasksMatch[1])));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const dotsSchedulesMatch = /^\/api\/threads\/([^/]+)\/dots-schedules$/.exec(url.pathname);
|
|
365
|
+
if (request.method === 'GET' && dotsSchedulesMatch) {
|
|
366
|
+
if (!rootChat) throw new RequestError(405, '只有点点主聊天提供此定时任务列表。');
|
|
367
|
+
sendJson(response, 200, await readDotsSchedules(dotsServer, decodeURIComponent(dotsSchedulesMatch[1])));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (request.method === 'GET' && url.pathname.startsWith('/api/code/')) {
|
|
372
|
+
const operation = url.pathname.slice('/api/code/'.length);
|
|
373
|
+
if (hostId === 'durable' && operation !== 'change') throw new RequestError(405, 'Dots 委派任务尚未接入代码浏览。');
|
|
374
|
+
try {
|
|
375
|
+
if ((operation === 'image' || operation === 'binary') && !request.fileTransfer) throw new RequestError(400, '请通过文件通道读取预览文件。');
|
|
376
|
+
const result = await browseCode((method, params) => execution.request(method, params), operation, url.searchParams);
|
|
377
|
+
if (operation === 'image' || operation === 'binary') {
|
|
378
|
+
const { data, ...metadata } = result;
|
|
379
|
+
response.filePart(metadata, data);
|
|
380
|
+
} else sendJson(response, 200, result);
|
|
381
|
+
} catch (error) { throw new RequestError(error.statusCode || 502, error.message); }
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
267
384
|
|
|
268
385
|
if (request.method === 'GET' && url.pathname === '/api/models') {
|
|
269
|
-
sendJson(response, 200, await
|
|
386
|
+
sendJson(response, 200, await execution.models());
|
|
270
387
|
return;
|
|
271
388
|
}
|
|
272
389
|
|
|
273
390
|
if (request.method === 'GET' && url.pathname === '/api/threads') {
|
|
274
391
|
const limit = boundedInteger(url.searchParams.get('limit'), 20, 1, 200);
|
|
275
392
|
const archived = url.searchParams.get('archived') === 'true';
|
|
276
|
-
|
|
393
|
+
const scope = url.searchParams.get('scope') || 'all';
|
|
394
|
+
if (!['all', 'local', 'durable'].includes(scope)) { sendJson(response, 400, { error: '无效的会话列表范围' }); return; }
|
|
395
|
+
sendJson(response, 200, await sharedCatalog.threads(limit, archived, scope, url.searchParams.get('refresh') === '1'));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (request.method === 'GET' && url.pathname === '/api/file-local-copy') {
|
|
400
|
+
const value = url.searchParams.get('source') || '';
|
|
401
|
+
const id = /^([a-f0-9]{64})(?:\?recovery=[A-Za-z0-9_-]{1,4096})?$/.exec(value)?.[1];
|
|
402
|
+
if (id && (fileCache.has(id) || attachmentCache.has(id) && !attachmentCache.get(id).dotsFile)) {
|
|
403
|
+
sendJson(response, 200, { external: false, localCopy: null }); return;
|
|
404
|
+
}
|
|
405
|
+
const asset = await externalDownloadAsset(value, url.searchParams.get('recovery'));
|
|
406
|
+
const copy = await workstationDownloads.copy(asset.id);
|
|
407
|
+
sendJson(response, 200, { external: true, localCopy: copy ? workstationCopyReference(copy, asset) : null,
|
|
408
|
+
cachedContent: asset.buffer ? { remotePath: asset.id, name: asset.name, size: asset.buffer.length, mimeType: asset.mimeType } : null });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (request.method === 'GET' && url.pathname === '/api/file-save-locations') {
|
|
413
|
+
await requireExecutionWritable();
|
|
414
|
+
const locations = await projectLocations();
|
|
415
|
+
const projects = (await sharedCatalog.projects()).projects.filter(project => project.hostId === 'local' && project.roots?.length);
|
|
416
|
+
sendJson(response, 200, { projects, downloads: locations.downloads });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (request.method === 'GET' && url.pathname === '/api/file-save-folders') {
|
|
421
|
+
await requireExecutionWritable();
|
|
422
|
+
sendJson(response, 200, await workstationDownloads.browse((method, params) => sharedServer.request(method, params),
|
|
423
|
+
await projectLocations(), Object.fromEntries(url.searchParams)));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (request.method === 'POST' && url.pathname === '/api/file-saves') {
|
|
428
|
+
await requireExecutionWritable();
|
|
429
|
+
const body = await readJsonBody(request);
|
|
430
|
+
const asset = await externalDownloadAsset(body.source, body.recovery);
|
|
431
|
+
const controller = new AbortController(), cancel = () => controller.abort();
|
|
432
|
+
response.once('close', cancel);
|
|
433
|
+
try {
|
|
434
|
+
const result = await workstationDownloads.save((method, params) => sharedServer.request(method, params),
|
|
435
|
+
await projectLocations(), body, asset, (file, query) => dotsImages.readPart(file, query), controller.signal);
|
|
436
|
+
sendJson(response, 201, { localCopy: workstationCopyReference(result, asset), warning: result.warning || '' });
|
|
437
|
+
} finally { response.off('close', cancel); }
|
|
277
438
|
return;
|
|
278
439
|
}
|
|
279
440
|
|
|
@@ -282,11 +443,45 @@ async function handleRequest(request, response) {
|
|
|
282
443
|
return;
|
|
283
444
|
}
|
|
284
445
|
|
|
446
|
+
if (url.pathname === '/api/project-folders' && ['GET', 'POST'].includes(request.method)) {
|
|
447
|
+
await requireExecutionWritable();
|
|
448
|
+
try {
|
|
449
|
+
const rpc = (method, params) => sharedServer.request(method, params);
|
|
450
|
+
const result = request.method === 'GET'
|
|
451
|
+
? await browseProjectFolders(rpc, await projectLocations(), url.searchParams.get('path'))
|
|
452
|
+
: await createProjectFolder(rpc, await readJsonBody(request));
|
|
453
|
+
sendJson(response, request.method === 'GET' ? 200 : 201, result);
|
|
454
|
+
} catch (error) { throw new RequestError(error.statusCode || 502, error.message); }
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const updateProjectMatch = /^\/api\/projects\/([^/]+)$/.exec(url.pathname);
|
|
459
|
+
if (request.method === 'POST' && (url.pathname === '/api/projects' || updateProjectMatch)) {
|
|
460
|
+
await requireExecutionWritable();
|
|
461
|
+
const body = await readJsonBody(request);
|
|
462
|
+
try {
|
|
463
|
+
const result = !updateProjectMatch ? await createWorkstationProject(sharedServer, stateRoot, codexDataRoot, body)
|
|
464
|
+
: await saveProject((method, params) => sharedServer.request(method, params),
|
|
465
|
+
await projectLocations(), body, updateProjectMatch ? decodeURIComponent(updateProjectMatch[1]) : null);
|
|
466
|
+
sendJson(response, updateProjectMatch ? 200 : 201, result);
|
|
467
|
+
} catch (error) { throw new RequestError(error.statusCode || 502, error.message, error.details); }
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
285
471
|
if (request.method === 'GET' && url.pathname === '/api/usage-limits') {
|
|
286
472
|
sendJson(response, 200, await sharedServer.usage());
|
|
287
473
|
return;
|
|
288
474
|
}
|
|
289
475
|
|
|
476
|
+
if (request.method === 'POST' && url.pathname === '/api/usage-reset') {
|
|
477
|
+
await requireExecutionWritable();
|
|
478
|
+
const body = await readJsonBody(request);
|
|
479
|
+
try { sendJson(response, 200, await consumeUsageReset(sharedServer, body)); }
|
|
480
|
+
catch (error) { throw new RequestError(error.statusCode || 502, error.message,
|
|
481
|
+
error.code === 'usage_reset_not_submitted' ? { code: error.code, deliveryUnknown: false } : undefined); }
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
290
485
|
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
291
486
|
const execution = sharedServer;
|
|
292
487
|
response.startEvents();
|
|
@@ -299,9 +494,37 @@ async function handleRequest(request, response) {
|
|
|
299
494
|
return;
|
|
300
495
|
}
|
|
301
496
|
|
|
302
|
-
if (request.method === 'GET' && url.pathname === '/api/
|
|
303
|
-
|
|
304
|
-
|
|
497
|
+
if (request.method === 'GET' && url.pathname === '/api/thread-events') {
|
|
498
|
+
await sharedServer.connect();
|
|
499
|
+
const clients = [sharedServer, dotsServer, dotsChat];
|
|
500
|
+
response.startEvents();
|
|
501
|
+
const listener = ({ method, params }) => {
|
|
502
|
+
if (!threadListNotificationMethods.has(method) || response.destroyed) return;
|
|
503
|
+
response.event('thread-list-changed', { method, threadId: params.threadId || params.thread?.id || '',
|
|
504
|
+
...(params.hostId ? { hostId: params.hostId } : {}),
|
|
505
|
+
...(params.chatRevision !== undefined ? { chatRevision: params.chatRevision } : {}),
|
|
506
|
+
...(params.updatedAt !== undefined ? { updatedAt: params.updatedAt } : {}),
|
|
507
|
+
...(params.chatMessageRevision !== undefined ? { chatMessageRevision: params.chatMessageRevision } : {}),
|
|
508
|
+
...(params.status ? { status: typeof params.status === 'string' ? params.status : params.status.type } : {}) });
|
|
509
|
+
};
|
|
510
|
+
const disconnected = () => response.end();
|
|
511
|
+
for (const client of clients) {
|
|
512
|
+
client.on('notification', listener);
|
|
513
|
+
client.on('disconnected', disconnected);
|
|
514
|
+
}
|
|
515
|
+
response.once('close', () => {
|
|
516
|
+
for (const client of clients) {
|
|
517
|
+
client.off('notification', listener);
|
|
518
|
+
client.off('disconnected', disconnected);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
response.event('ready', {});
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (request.method === 'GET' && url.pathname === '/api/turn-events') {
|
|
526
|
+
const clients = [sharedServer, dotsServer, dotsChat];
|
|
527
|
+
response.startEvents();
|
|
305
528
|
const listener = ({ method, params }) => {
|
|
306
529
|
if (!['turn/started', 'turn/completed'].includes(method)) return;
|
|
307
530
|
const threadId = params?.threadId, turnId = params?.turn?.id;
|
|
@@ -310,24 +533,29 @@ async function handleRequest(request, response) {
|
|
|
310
533
|
}
|
|
311
534
|
};
|
|
312
535
|
const disconnected = () => response.end();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
536
|
+
for (const client of clients) {
|
|
537
|
+
client.on('notification', listener);
|
|
538
|
+
client.on('disconnected', disconnected);
|
|
539
|
+
}
|
|
540
|
+
response.once('close', () => {
|
|
541
|
+
for (const client of clients) {
|
|
542
|
+
client.off('notification', listener);
|
|
543
|
+
client.off('disconnected', disconnected);
|
|
544
|
+
}
|
|
318
545
|
});
|
|
319
546
|
return;
|
|
320
547
|
}
|
|
321
548
|
|
|
322
549
|
const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
|
|
323
550
|
if (request.method === 'GET' && activeTurnMatch) {
|
|
551
|
+
if (rootChat) { sendJson(response, 200, { turnId: null }); return; }
|
|
324
552
|
const threadId = decodeURIComponent(activeTurnMatch[1]);
|
|
325
|
-
await
|
|
326
|
-
const { thread } = await
|
|
553
|
+
await execution.connect();
|
|
554
|
+
const { thread } = await execution.request('thread/read', { threadId, includeTurns: false });
|
|
327
555
|
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
328
556
|
let turnId = null;
|
|
329
557
|
if (status === 'active') {
|
|
330
|
-
const page = await
|
|
558
|
+
const page = await execution.request('thread/turns/list', {
|
|
331
559
|
threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
332
560
|
});
|
|
333
561
|
const latest = page.data?.[0];
|
|
@@ -354,11 +582,12 @@ async function handleRequest(request, response) {
|
|
|
354
582
|
let target = appServerEvents.projectlessThreadTarget(projectRoot);
|
|
355
583
|
if (project) {
|
|
356
584
|
const nativeProjects = await appServerEvents.readProjects(rpc);
|
|
357
|
-
const
|
|
358
|
-
if (!
|
|
359
|
-
target =
|
|
585
|
+
const nativeProject = nativeProjects.find(item => item.id === project.projectId);
|
|
586
|
+
if (!nativeProject) throw new RequestError(404, 'Codex 内核中没有找到对应项目,请刷新项目列表。');
|
|
587
|
+
target = appServerEvents.projectThreadTarget(nativeProject);
|
|
360
588
|
}
|
|
361
589
|
const { input } = appServerTurnArguments('', body);
|
|
590
|
+
messageSubmissionStarted = true;
|
|
362
591
|
const created = await appServerEvents.createThread(rpc, target, input, body);
|
|
363
592
|
const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
364
593
|
void generateAndSetThreadTitle({
|
|
@@ -376,13 +605,24 @@ async function handleRequest(request, response) {
|
|
|
376
605
|
}
|
|
377
606
|
|
|
378
607
|
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
379
|
-
if (request.method === 'GET' && timelineMatch) {
|
|
380
|
-
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
608
|
+
if (request.method === 'GET' && timelineMatch) {
|
|
609
|
+
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
610
|
+
const knownSeq = url.searchParams.get('knownSeq');
|
|
611
|
+
const knownSync = { id: url.searchParams.get('knownSyncId'),
|
|
612
|
+
epoch: url.searchParams.get('knownEpoch'), seq: knownSeq === null ? undefined : Number(knownSeq),
|
|
613
|
+
updatedAt: Number(url.searchParams.get('knownUpdatedAt')) };
|
|
614
|
+
const pageOptions = sharedHistoryPageOptions();
|
|
615
|
+
pageOptions.reconcileObserved = url.searchParams.get('refresh') === '1';
|
|
616
|
+
if (request.socketRequest) {
|
|
617
|
+
pageOptions.onWindow = async window => {
|
|
618
|
+
if (response.writableEnded) return;
|
|
619
|
+
await response.historyPage(await prepareSharedTimeline(window));
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const timeline = await prepareSharedTimeline(await execution.timeline(threadId, pageOptions, knownSync));
|
|
623
|
+
if (request.socketRequest) {
|
|
624
|
+
response.on('progress', () => execution.keepSnapshotAlive(threadId, timeline.sync));
|
|
625
|
+
response.once('close', () => execution.keepSnapshotAlive(threadId, timeline.sync));
|
|
386
626
|
}
|
|
387
627
|
sendJson(response, 200, timeline);
|
|
388
628
|
return;
|
|
@@ -391,19 +631,20 @@ async function handleRequest(request, response) {
|
|
|
391
631
|
const approvalMatch = /^\/api\/threads\/([^/]+)\/approvals$/.exec(url.pathname);
|
|
392
632
|
if (request.method === 'POST' && approvalMatch) {
|
|
393
633
|
const threadId = decodeURIComponent(approvalMatch[1]);
|
|
394
|
-
await requireExecutionWritable(threadId);
|
|
634
|
+
await requireExecutionWritable(threadId, execution);
|
|
395
635
|
const body = await readJsonBody(request);
|
|
396
|
-
sendJson(response, 200, await
|
|
636
|
+
sendJson(response, 200, await execution.respondApproval(threadId, body)); return;
|
|
397
637
|
}
|
|
398
638
|
|
|
399
639
|
const eventsMatch = /^\/api\/threads\/([^/]+)\/events$/.exec(url.pathname);
|
|
400
640
|
if (request.method === 'GET' && eventsMatch) {
|
|
401
641
|
const threadId = decodeURIComponent(eventsMatch[1]);
|
|
402
642
|
const after = Number(url.searchParams.get('afterSeq'));
|
|
403
|
-
const requestedSync = url.searchParams.get('syncId')
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
643
|
+
const requestedSync = { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'),
|
|
644
|
+
after: Number.isSafeInteger(after) && after >= 0 ? after : undefined,
|
|
645
|
+
updatedAt: Number(url.searchParams.get('knownUpdatedAt')) };
|
|
646
|
+
await streamSharedThread(response, threadId, execution,
|
|
647
|
+
prepareSharedTimeline, requestedSync);
|
|
407
648
|
return;
|
|
408
649
|
}
|
|
409
650
|
|
|
@@ -411,14 +652,14 @@ async function handleRequest(request, response) {
|
|
|
411
652
|
if (request.method === 'GET' && modelChangesMatch) {
|
|
412
653
|
const threadId = decodeURIComponent(modelChangesMatch[1]);
|
|
413
654
|
if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) throw new RequestError(400, '对话 ID 无效。');
|
|
414
|
-
sendJson(response, 200, await turnModelHistory.read(findSessionPath(threadId), (url.searchParams.get('turnIds') || '').split(',').filter(Boolean)));
|
|
655
|
+
sendJson(response, 200, hostId === 'durable' ? [] : await turnModelHistory.read(findSessionPath(threadId), (url.searchParams.get('turnIds') || '').split(',').filter(Boolean)));
|
|
415
656
|
return;
|
|
416
657
|
}
|
|
417
658
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
418
659
|
if (request.method === 'GET' && contextMatch) {
|
|
419
660
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
420
|
-
const current = readContextUsage(threadId);
|
|
421
|
-
const native = await
|
|
661
|
+
const current = hostId === 'durable' ? { available: false } : readContextUsage(threadId);
|
|
662
|
+
const native = await execution.readContext(threadId, url.searchParams.get('refreshSettings') === 'true');
|
|
422
663
|
sendJson(response, 200, { ...current, ...(native.model ? { model: native.model, thinking: native.thinking, serviceTier: native.serviceTier,
|
|
423
664
|
permissionMode: native.permissionMode, permissionModes: native.permissionModes } : {}), ...(native.available ? native : {}) });
|
|
424
665
|
return;
|
|
@@ -428,34 +669,45 @@ async function handleRequest(request, response) {
|
|
|
428
669
|
if (sharedAction) {
|
|
429
670
|
const id = decodeURIComponent(sharedAction[1]);
|
|
430
671
|
if (request.method === 'GET' && sharedAction[2] === 'history') {
|
|
431
|
-
|
|
672
|
+
const pageOptions = sharedHistoryPageOptions();
|
|
673
|
+
if (url.searchParams.has('itemLimit')) {
|
|
674
|
+
const itemLimit = Number(url.searchParams.get('itemLimit'));
|
|
675
|
+
if (!Number.isInteger(itemLimit) || itemLimit < 1 || itemLimit > 100) throw new RequestError(400, '历史条目数量无效。');
|
|
676
|
+
pageOptions.itemLimit = itemLimit;
|
|
677
|
+
}
|
|
678
|
+
const history = await execution.history(id, url.searchParams.get('cursor'), pageOptions);
|
|
679
|
+
sendJson(response, 200, await prepareSharedTimeline(history, hostId));
|
|
432
680
|
return;
|
|
433
681
|
}
|
|
434
682
|
if (request.method === 'POST' && sharedAction[2] === 'settings') {
|
|
435
|
-
|
|
683
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供 Codex 模型设置。');
|
|
684
|
+
await requireExecutionWritable(id, execution);
|
|
436
685
|
const body = await readJsonBody(request);
|
|
437
|
-
sendJson(response, 200, await
|
|
686
|
+
sendJson(response, 200, await execution.settings(id, body.model, body.thinking, body.serviceTier));
|
|
438
687
|
return;
|
|
439
688
|
}
|
|
440
689
|
if (request.method === 'POST' && sharedAction[2] === 'permissions') {
|
|
441
|
-
|
|
690
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供 Codex 权限设置。');
|
|
691
|
+
await requireExecutionWritable(id, execution);
|
|
442
692
|
const body = await readJsonBody(request);
|
|
443
|
-
sendJson(response, 200, await
|
|
693
|
+
sendJson(response, 200, await execution.permissions(id, body.mode));
|
|
444
694
|
return;
|
|
445
695
|
}
|
|
446
696
|
}
|
|
447
697
|
|
|
448
698
|
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
449
699
|
if (request.method === 'POST' && archiveMatch) {
|
|
700
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供任务归档。');
|
|
450
701
|
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
451
|
-
await requireExecutionWritable(threadId);
|
|
452
|
-
await
|
|
702
|
+
await requireExecutionWritable(threadId, execution);
|
|
703
|
+
await execution.request(`thread/${archiveMatch[2]}`, { threadId });
|
|
453
704
|
sendJson(response, 200, { threadId, archived: archiveMatch[2] === 'archive' });
|
|
454
705
|
return;
|
|
455
706
|
}
|
|
456
707
|
|
|
457
708
|
const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
|
|
458
709
|
if (queueMatch) {
|
|
710
|
+
if (hostId === 'durable') throw new RequestError(405, '当前托管连接不提供排队消息。');
|
|
459
711
|
const threadId = decodeURIComponent(queueMatch[1]);
|
|
460
712
|
await sharedServer.connect();
|
|
461
713
|
const rpc = (method, params) => sharedServer.request(method, params);
|
|
@@ -470,6 +722,7 @@ async function handleRequest(request, response) {
|
|
|
470
722
|
|
|
471
723
|
const queueItemMatch = /^\/api\/threads\/([^/]+)\/queue\/([a-f0-9-]+)(?:\/(steer))?$/.exec(url.pathname);
|
|
472
724
|
if (queueItemMatch) {
|
|
725
|
+
if (hostId === 'durable') throw new RequestError(405, '当前托管连接不提供排队消息。');
|
|
473
726
|
const threadId = decodeURIComponent(queueItemMatch[1]);
|
|
474
727
|
const itemId = queueItemMatch[2];
|
|
475
728
|
await sharedServer.connect();
|
|
@@ -481,23 +734,43 @@ async function handleRequest(request, response) {
|
|
|
481
734
|
|
|
482
735
|
const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
|
|
483
736
|
if (request.method === 'POST' && interruptMatch) {
|
|
737
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供停止 Codex 任务的操作。');
|
|
484
738
|
const threadId = decodeURIComponent(interruptMatch[1]);
|
|
485
|
-
await requireExecutionWritable(threadId);
|
|
739
|
+
await requireExecutionWritable(threadId, execution);
|
|
486
740
|
const body = await readJsonBody(request);
|
|
487
741
|
if (!body.turnId) throw new RequestError(400, '缺少活动轮次 ID。');
|
|
488
|
-
await
|
|
742
|
+
await execution.interrupt(threadId, body.turnId);
|
|
489
743
|
sendJson(response, 200, { requested: true });
|
|
490
744
|
return;
|
|
491
745
|
}
|
|
492
746
|
|
|
747
|
+
const roomReadMatch = /^\/api\/threads\/([^/]+)\/read$/.exec(url.pathname);
|
|
748
|
+
if (request.method === 'POST' && roomReadMatch) {
|
|
749
|
+
if (!rootChat) throw new RequestError(405, '此对话不提供消息房间已读操作。');
|
|
750
|
+
await dotsChat.markRead(decodeURIComponent(roomReadMatch[1]), await readJsonBody(request));
|
|
751
|
+
sendJson(response, 200, { success: true });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const messageStatusMatch = /^\/api\/threads\/([^/]+)\/messages\/([a-f0-9]{32})\/status$/.exec(url.pathname);
|
|
756
|
+
if (request.method === 'GET' && messageStatusMatch) {
|
|
757
|
+
sendJson(response, 200, messageReceipts.status([hostId, decodeURIComponent(messageStatusMatch[1])], messageStatusMatch[2]));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
493
760
|
const messageMatch = /^\/api\/threads\/([^/]+)\/messages$/.exec(url.pathname);
|
|
494
761
|
if (request.method === 'POST' && messageMatch) {
|
|
495
762
|
const threadId = decodeURIComponent(messageMatch[1]);
|
|
496
|
-
await requireExecutionWritable(threadId);
|
|
497
763
|
const body = await readJsonBody(request);
|
|
498
|
-
if (body.hostId && body.hostId !==
|
|
499
|
-
const
|
|
500
|
-
|
|
764
|
+
if (body.hostId && body.hostId !== hostId) throw new RequestError(400, '请求中的对话主机不一致。');
|
|
765
|
+
const result = await messageReceipts.submit([hostId, threadId], body.clientUserMessageId, async () => {
|
|
766
|
+
let args;
|
|
767
|
+
try { await requireExecutionWritable(threadId, execution); args = execution === dotsChat ? { ...body, threadId } : appServerTurnArguments(threadId, body); }
|
|
768
|
+
catch (error) { error.deliveryUnknown = false; throw error; }
|
|
769
|
+
messageSubmissionStarted = true;
|
|
770
|
+
const submitted = await execution.send(args);
|
|
771
|
+
return { success: true, data: submitted.result, usage: submitted.usage };
|
|
772
|
+
});
|
|
773
|
+
sendJson(response, 200, result);
|
|
501
774
|
return;
|
|
502
775
|
}
|
|
503
776
|
|
|
@@ -507,13 +780,15 @@ async function handleRequest(request, response) {
|
|
|
507
780
|
if (!response.writableEnded) response.end();
|
|
508
781
|
return;
|
|
509
782
|
}
|
|
510
|
-
|
|
783
|
+
if (!messageSubmissionStarted && error.deliveryUnknown === undefined && request.method === 'POST'
|
|
784
|
+
&& /^\/api\/threads\/[^/]+\/messages(?:\?|$)/.test(request.url || '')) error.deliveryUnknown = false;
|
|
785
|
+
const status = error instanceof RequestError || error instanceof ThreadWriterConflictError || error instanceof FileRangeError ? (error.statusCode || 409) : 502;
|
|
511
786
|
sendJson(response, status, {
|
|
512
787
|
error: error instanceof Error ? error.message : String(error),
|
|
513
788
|
code: error?.code,
|
|
514
789
|
threadId: error?.threadId,
|
|
515
790
|
readOnly: error?.readOnly,
|
|
516
|
-
details: error?.details,
|
|
791
|
+
details: error?.deliveryUnknown === undefined ? error?.details : { ...error?.details, deliveryUnknown: error.deliveryUnknown },
|
|
517
792
|
});
|
|
518
793
|
}
|
|
519
794
|
}
|
|
@@ -521,8 +796,8 @@ async function handleRequest(request, response) {
|
|
|
521
796
|
const server = http.createServer(handleRequest);
|
|
522
797
|
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
523
798
|
uploadsRoot,
|
|
524
|
-
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
525
|
-
onDeviceAuthorizationsChanged: listener => onLanAuthorizationsChanged(stateRoot, listener),
|
|
799
|
+
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
800
|
+
onDeviceAuthorizationsChanged: listener => onLanAuthorizationsChanged(stateRoot, listener),
|
|
526
801
|
});
|
|
527
802
|
|
|
528
803
|
let control;
|
|
@@ -548,6 +823,7 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
548
823
|
const state = managedAppServer.snapshot();
|
|
549
824
|
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
550
825
|
},
|
|
826
|
+
'restart-managed-app-server-confirmed': () => managedAppServer.restart(crypto.randomUUID()),
|
|
551
827
|
} });
|
|
552
828
|
writeRecord(process.env.CODEX_REMOTE_READY_PATH, { instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
553
829
|
pid: process.pid, controlPort: control.port });
|
|
@@ -571,6 +847,8 @@ async function shutDown() {
|
|
|
571
847
|
executionHealth?.stop();
|
|
572
848
|
runtimeDiscovery.stop();
|
|
573
849
|
await relayManager.close();
|
|
850
|
+
dotsServer.close();
|
|
851
|
+
dotsChat.close();
|
|
574
852
|
managedAppServer.close();
|
|
575
853
|
await control?.close();
|
|
576
854
|
process.exit(0);
|
|
@@ -713,6 +991,14 @@ function messagePayload(body) {
|
|
|
713
991
|
if (!fs.existsSync(file)) throw new RequestError(400, '图片不存在,请重新添加。');
|
|
714
992
|
return file;
|
|
715
993
|
});
|
|
994
|
+
for (const input of fileMentions.originalInputs(body)) {
|
|
995
|
+
const incoming = fs.realpathSync(path.join(uploadsRoot, 'incoming'));
|
|
996
|
+
const imagePath = fs.realpathSync(input.path);
|
|
997
|
+
if (path.dirname(imagePath).toLowerCase() !== incoming.toLowerCase()) throw new RequestError(400, '原图必须来自上传目录。');
|
|
998
|
+
const stat = fs.statSync(imagePath);
|
|
999
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_ATTACHMENT_BYTES) throw new RequestError(400, '原图为空或超过 20 MiB。');
|
|
1000
|
+
images.push(imagePath);
|
|
1001
|
+
}
|
|
716
1002
|
if (images.filter(image => image.startsWith('data:')).reduce((size, image) => size + Buffer.byteLength(image.slice(image.indexOf(',') + 1), 'base64'), 0) > 8 * 1024 * 1024) throw new RequestError(413, '本条消息图片合计不能超过 8 MiB。');
|
|
717
1003
|
if (!prompt && !images.length) throw new RequestError(400, 'prompt 不能为空。');
|
|
718
1004
|
if (prompt.length > 32_000) throw new RequestError(400, 'prompt 不能超过 32000 个字符。');
|
|
@@ -720,9 +1006,7 @@ function messagePayload(body) {
|
|
|
720
1006
|
const payload = { prompt, images };
|
|
721
1007
|
if (typeof body.hostId === 'string' && body.hostId.trim()) payload.hostId = body.hostId.trim();
|
|
722
1008
|
if (typeof body.model === 'string' && body.model.trim()) {
|
|
723
|
-
|
|
724
|
-
if (!ALLOWED_MODELS.has(model)) throw new RequestError(400, '不支持的模型。');
|
|
725
|
-
payload.model = model;
|
|
1009
|
+
payload.model = body.model.trim();
|
|
726
1010
|
}
|
|
727
1011
|
if (typeof body.thinking === 'string' && body.thinking.trim()) {
|
|
728
1012
|
const thinking = body.thinking.trim();
|
|
@@ -759,13 +1043,36 @@ function normalizeTimeline(data) {
|
|
|
759
1043
|
};
|
|
760
1044
|
}
|
|
761
1045
|
|
|
762
|
-
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
763
|
-
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId
|
|
1046
|
+
function normalizeSharedTimeline({ thread, nextCursor, sync, notModified, incremental }, hostId = thread?.hostId || 'local') {
|
|
1047
|
+
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId,
|
|
1048
|
+
transport: 'shared-app-server' } });
|
|
764
1049
|
return { ...timeline,
|
|
765
|
-
|
|
1050
|
+
historyItemCount: (thread.turns || []).reduce((count, turn) => count + (turn.roomMessage ? 1 : turn.items?.length || 0), 0),
|
|
1051
|
+
historyView: thread?.threadSource === 'aeon' ? 'chat-items-v1' : 'item-pages-v1',
|
|
1052
|
+
...(notModified ? { notModified: true } : {}),
|
|
1053
|
+
...(incremental ? { incremental: true } : {}),
|
|
766
1054
|
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
767
1055
|
}
|
|
768
1056
|
|
|
1057
|
+
async function prepareSharedTimeline(value, hostId) {
|
|
1058
|
+
if (value.thread?.threadSource !== 'aeon') return normalizeSharedTimeline(value, hostId);
|
|
1059
|
+
const epoch = dotsChat.epoch, threadId = value.thread.id;
|
|
1060
|
+
const current = () => {
|
|
1061
|
+
if (epoch !== dotsChat.epoch) throw new Error('点点连接已变化,请重新同步。');
|
|
1062
|
+
};
|
|
1063
|
+
if (value.notModified) {
|
|
1064
|
+
const record = dotsChat.threads.get(threadId);
|
|
1065
|
+
if (await dotsImages.prepareTimeline(record?.thread)) {
|
|
1066
|
+
current();
|
|
1067
|
+
value = dotsChat.snapshot(threadId, record);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const snapshot = structuredClone(value);
|
|
1071
|
+
await dotsImages.prepareTimeline(snapshot.thread);
|
|
1072
|
+
current();
|
|
1073
|
+
return normalizeSharedTimeline(snapshot, hostId);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
769
1076
|
function normalizeThread(thread) {
|
|
770
1077
|
const source = thread && typeof thread === 'object' ? thread : {};
|
|
771
1078
|
return {
|
|
@@ -777,6 +1084,10 @@ function normalizeThread(thread) {
|
|
|
777
1084
|
projectId: source.projectId || '',
|
|
778
1085
|
cwd: source.cwd || '',
|
|
779
1086
|
hostId: source.hostId || '',
|
|
1087
|
+
threadSource: source.threadSource || '',
|
|
1088
|
+
...(['aeon', 'aeon_child'].includes(source.threadSource) ? { chatRevision: source.chatRevision || '' } : {}),
|
|
1089
|
+
...(source.threadSource === 'aeon' ? { avatar: source.avatar || '', color: source.color || '',
|
|
1090
|
+
chatMessageRevision: source.chatMessageRevision, messagingRoomId: source.messagingRoomId, lastMessageAt: source.lastMessageAt } : {}),
|
|
780
1091
|
transport: source.transport || 'shared-app-server',
|
|
781
1092
|
readOnly: source.readOnly === true,
|
|
782
1093
|
readOnlyReason: source.readOnlyReason || '',
|
|
@@ -798,7 +1109,12 @@ function normalizeTurn(turn) {
|
|
|
798
1109
|
completedAt: source.completedAt || null,
|
|
799
1110
|
durationMs: source.durationMs ?? null,
|
|
800
1111
|
error: source.error ?? null,
|
|
1112
|
+
runtimeNotice: source.runtimeNotice ?? null,
|
|
801
1113
|
...(source.itemPage ? { itemPage: source.itemPage } : {}),
|
|
1114
|
+
...(source.roomMessage ? { roomMessage: true } : {}),
|
|
1115
|
+
...(source.deleted ? { deleted: true } : {}),
|
|
1116
|
+
...(source.historyOnly ? { historyOnly: true } : {}),
|
|
1117
|
+
...(source.cacheStub ? { cacheStub: true } : {}),
|
|
802
1118
|
entries,
|
|
803
1119
|
};
|
|
804
1120
|
}
|
|
@@ -806,6 +1122,10 @@ function normalizeTurn(turn) {
|
|
|
806
1122
|
function normalizeItem(item) {
|
|
807
1123
|
if (!item || typeof item !== 'object') return null;
|
|
808
1124
|
|
|
1125
|
+
if (item.type === 'approval') {
|
|
1126
|
+
return { ...item, type: 'activity', kind: 'approval', title: item.params?.message || '需要你的批准' };
|
|
1127
|
+
}
|
|
1128
|
+
|
|
809
1129
|
if (item.type === 'contextCompaction') {
|
|
810
1130
|
return {
|
|
811
1131
|
type: 'activity',
|
|
@@ -815,24 +1135,21 @@ function normalizeItem(item) {
|
|
|
815
1135
|
};
|
|
816
1136
|
}
|
|
817
1137
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
type: 'message',
|
|
825
|
-
id: item.id || '',
|
|
826
|
-
role: 'user',
|
|
827
|
-
source: 'remote',
|
|
828
|
-
...splitUploadedImages(text),
|
|
829
|
-
};
|
|
830
|
-
}
|
|
1138
|
+
const delegated = appServerEvents.toolDelegation(item);
|
|
1139
|
+
if (delegated) {
|
|
1140
|
+
return {
|
|
1141
|
+
type: 'message', id: item.id || '', role: 'user', source: 'remote',
|
|
1142
|
+
delegation: delegationSource(delegated), ...splitUploadedImages(delegated.input),
|
|
1143
|
+
};
|
|
831
1144
|
}
|
|
832
1145
|
|
|
833
1146
|
if (item.type === 'userMessage') {
|
|
834
|
-
const { text, files } = extractUserText(item.content);
|
|
835
|
-
const attachments = [...normalizeAttachments(item.content), ...files
|
|
1147
|
+
const { text, files, delegation } = extractUserText(item.content);
|
|
1148
|
+
const attachments = [...normalizeAttachments(item.content), ...files.map(file => ({
|
|
1149
|
+
...file,
|
|
1150
|
+
remotePath: rememberLocalFile(file.path)?.id || '',
|
|
1151
|
+
previewRemotePath: file.previewPath ? rememberLocalFile(file.previewPath)?.id || '' : '',
|
|
1152
|
+
}))];
|
|
836
1153
|
if (!text && !attachments.length) return null;
|
|
837
1154
|
return {
|
|
838
1155
|
clientId: item.clientId,
|
|
@@ -841,6 +1158,7 @@ function normalizeItem(item) {
|
|
|
841
1158
|
role: 'user',
|
|
842
1159
|
text,
|
|
843
1160
|
attachments,
|
|
1161
|
+
...(delegation ? { delegation: delegationSource(delegation) } : {}),
|
|
844
1162
|
};
|
|
845
1163
|
}
|
|
846
1164
|
|
|
@@ -853,6 +1171,7 @@ function normalizeItem(item) {
|
|
|
853
1171
|
delivery: item.delivery,
|
|
854
1172
|
questions: item.questions,
|
|
855
1173
|
text: item.text || '',
|
|
1174
|
+
attachments: normalizeAttachments(item.attachments),
|
|
856
1175
|
imageAssets: markdownImageAssets(item.text || ''),
|
|
857
1176
|
fileAssets: markdownFileAssets(item.text || ''),
|
|
858
1177
|
};
|
|
@@ -926,7 +1245,7 @@ function normalizeItem(item) {
|
|
|
926
1245
|
title: '文件修改',
|
|
927
1246
|
titleOnly: true,
|
|
928
1247
|
status: statusText(item.status),
|
|
929
|
-
changes:
|
|
1248
|
+
changes: fileChanges.summarize(item.changes),
|
|
930
1249
|
};
|
|
931
1250
|
}
|
|
932
1251
|
|
|
@@ -946,16 +1265,6 @@ function normalizeItem(item) {
|
|
|
946
1265
|
const image = typeof item.path === 'string' && item.path
|
|
947
1266
|
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
948
1267
|
: null;
|
|
949
|
-
if (image?.available) {
|
|
950
|
-
return {
|
|
951
|
-
type: 'message',
|
|
952
|
-
id: item.id || '',
|
|
953
|
-
role: 'assistant',
|
|
954
|
-
phase: 'commentary',
|
|
955
|
-
text: '',
|
|
956
|
-
attachments: [image],
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
1268
|
return {
|
|
960
1269
|
type: 'activity',
|
|
961
1270
|
id: item.id || '',
|
|
@@ -968,9 +1277,11 @@ function normalizeItem(item) {
|
|
|
968
1277
|
}
|
|
969
1278
|
|
|
970
1279
|
if (item.type === 'imageGeneration') {
|
|
1280
|
+
const result = typeof item.result === 'string' ? item.result.trim() : '';
|
|
971
1281
|
const image = typeof item.savedPath === 'string' && item.savedPath
|
|
972
1282
|
? normalizeAttachment({ type: 'localImage', path: item.savedPath, name: '生成的图片' })
|
|
973
|
-
:
|
|
1283
|
+
: result ? normalizeAttachment({ type: 'image', name: '生成的图片',
|
|
1284
|
+
url: result.startsWith('data:image/') ? result : `data:image/png;base64,${result}` }) : null;
|
|
974
1285
|
return {
|
|
975
1286
|
type: 'activity',
|
|
976
1287
|
id: item.id || '',
|
|
@@ -1045,7 +1356,7 @@ function normalizeAttachments(content) {
|
|
|
1045
1356
|
else items.push({ type: 'localImage', path: imagePath });
|
|
1046
1357
|
}
|
|
1047
1358
|
const seen = new Set();
|
|
1048
|
-
return items.map(normalizeAttachment).filter(item => {
|
|
1359
|
+
return items.map(item => normalizeAttachment(item)).filter(item => {
|
|
1049
1360
|
if (!item.url) return true;
|
|
1050
1361
|
if (seen.has(item.url)) return false;
|
|
1051
1362
|
seen.add(item.url); return true;
|
|
@@ -1057,22 +1368,20 @@ function markdownImageAssets(text) {
|
|
|
1057
1368
|
function visit(tokens) {
|
|
1058
1369
|
for (const token of tokens) {
|
|
1059
1370
|
const value = token.type === 'image' ? token.attrGet('src')
|
|
1060
|
-
: token.type === 'link_open' ? token.attrGet('href') : null;
|
|
1371
|
+
: token.type === 'link_open' && !token.meta?.fileCitation ? token.attrGet('href') : null;
|
|
1061
1372
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1062
|
-
|
|
1063
|
-
try { localPath = decodeURIComponent(value); } catch { continue; }
|
|
1373
|
+
const localPath = fileCitations.localReference(value)?.path;
|
|
1064
1374
|
// Only paths actually referenced by Desktop messages become accessible.
|
|
1065
1375
|
// Do not expose a client-supplied filesystem path endpoint or UNC shares.
|
|
1066
|
-
if (
|
|
1067
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1376
|
+
if (localPath && /^[a-z]:[\\/]/i.test(localPath)) {
|
|
1068
1377
|
const asset = normalizeAttachment({ type: 'localImage', path: localPath });
|
|
1069
1378
|
if (asset.available) assets[value] = asset;
|
|
1070
1379
|
}
|
|
1071
1380
|
}
|
|
1072
|
-
if (token.children) visit(token.children);
|
|
1381
|
+
if (token.children && token.type !== 'image') visit(token.children);
|
|
1073
1382
|
}
|
|
1074
1383
|
}
|
|
1075
|
-
visit(imageMarkdown.parse(text, {}));
|
|
1384
|
+
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1076
1385
|
return assets;
|
|
1077
1386
|
}
|
|
1078
1387
|
|
|
@@ -1083,20 +1392,17 @@ function markdownFileAssets(text) {
|
|
|
1083
1392
|
if (token.type === 'link_open') {
|
|
1084
1393
|
const value = token.attrGet('href');
|
|
1085
1394
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1089
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1090
|
-
localPath = localPath.replace(/:(\d+)(?::\d+)?$/, '');
|
|
1395
|
+
const localPath = fileCitations.localReference(value)?.path;
|
|
1396
|
+
if (localPath && /^[a-z]:[\\/]/i.test(localPath)) {
|
|
1091
1397
|
const asset = rememberLocalFile(localPath);
|
|
1092
1398
|
if (asset) assets[value] = { remotePath: asset.id, name: asset.name, size: asset.size };
|
|
1093
1399
|
}
|
|
1094
1400
|
}
|
|
1095
1401
|
}
|
|
1096
|
-
if (token.children) visit(token.children);
|
|
1402
|
+
if (token.children && token.type !== 'image') visit(token.children);
|
|
1097
1403
|
}
|
|
1098
1404
|
}
|
|
1099
|
-
visit(imageMarkdown.parse(text, {}));
|
|
1405
|
+
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1100
1406
|
return assets;
|
|
1101
1407
|
}
|
|
1102
1408
|
|
|
@@ -1104,12 +1410,12 @@ function rememberLocalFile(filePath) {
|
|
|
1104
1410
|
try {
|
|
1105
1411
|
const resolved = path.resolve(filePath);
|
|
1106
1412
|
const stat = fs.statSync(resolved);
|
|
1107
|
-
if (!stat.isFile()
|
|
1413
|
+
if (!stat.isFile()) return null;
|
|
1108
1414
|
const id = crypto.createHmac('sha256', fileCapabilitySecret)
|
|
1109
1415
|
.update(resolved).update('\0').update(String(stat.size)).update('\0').update(String(stat.mtimeMs)).digest('hex');
|
|
1110
1416
|
if (!fileCache.has(id)) {
|
|
1111
1417
|
while (fileCache.size >= MAX_CACHED_FILES) fileCache.delete(fileCache.keys().next().value);
|
|
1112
|
-
fileCache.set(id, { id, path: resolved, name: path.basename(resolved), size: stat.size,
|
|
1418
|
+
fileCache.set(id, { id, path: resolved, name: path.basename(resolved), size: stat.size, revision: fileRevision(stat) });
|
|
1113
1419
|
}
|
|
1114
1420
|
return fileCache.get(id);
|
|
1115
1421
|
} catch {
|
|
@@ -1140,12 +1446,29 @@ function splitUploadedImages(text) {
|
|
|
1140
1446
|
return { text: visibleText, attachments, imageAssets: markdownImageAssets(visibleText) };
|
|
1141
1447
|
}
|
|
1142
1448
|
|
|
1143
|
-
function
|
|
1449
|
+
async function externalDownloadAsset(value, recovery = '') {
|
|
1450
|
+
const match = /^([a-f0-9]{64})(?:\?recovery=([A-Za-z0-9_-]{1,4096}))?$/.exec(value || '');
|
|
1451
|
+
if (!match) throw new RequestError(400, '无效的外部文件标识。');
|
|
1452
|
+
const asset = attachmentCache.get(match[1]) || dotsImages.restore(match[1], match[2] || recovery || '');
|
|
1453
|
+
if (!asset?.dotsFile) throw new RequestError(404, '外部文件授权已失效,请重新读取对话。');
|
|
1454
|
+
await dotsImages.authorization(asset.identity);
|
|
1455
|
+
return asset;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function workstationCopyReference(copy, asset) {
|
|
1459
|
+
const id = crypto.createHmac('sha256', fileCapabilitySecret).update(JSON.stringify([asset.id, copy.path, copy.revision])).digest('hex');
|
|
1460
|
+
fileCache.set(id, { ...copy, id, externalIdentity: asset.identity });
|
|
1461
|
+
while (fileCache.size > MAX_CACHED_FILES) fileCache.delete(fileCache.keys().next().value);
|
|
1462
|
+
return { remotePath: id, path: copy.path, name: copy.name, size: copy.size, mimeType: copy.mimeType };
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function normalizeAttachment(item, identity) {
|
|
1144
1466
|
const name = item.name || item.fileName || (typeof item.path === 'string' ? path.basename(item.path) : '');
|
|
1145
1467
|
let cached = null;
|
|
1146
1468
|
const isImage = ['image', 'input_image', 'localImage'].includes(item.type);
|
|
1469
|
+
if (isImage || item.type === 'file') cached = dotsImages.remember(item, identity);
|
|
1147
1470
|
if (isImage && typeof item.path === 'string') {
|
|
1148
|
-
cached = cacheLocalImage(item.path, name);
|
|
1471
|
+
cached = cacheLocalImage(item.path, name) || cached;
|
|
1149
1472
|
}
|
|
1150
1473
|
if (!cached) {
|
|
1151
1474
|
const dataUrl = typeof item.url === 'string' && item.url.startsWith('data:image/')
|
|
@@ -1155,10 +1478,12 @@ function normalizeAttachment(item) {
|
|
|
1155
1478
|
}
|
|
1156
1479
|
return {
|
|
1157
1480
|
type: isImage ? 'image' : item.type || 'attachment',
|
|
1158
|
-
name,
|
|
1481
|
+
name: cached?.name || name,
|
|
1159
1482
|
mimeType: cached?.mimeType || item.mimeType || '',
|
|
1483
|
+
...(cached?.size ? { size: cached.size } : {}),
|
|
1484
|
+
...(cached?.dotsFile ? { networkFile: true } : {}),
|
|
1160
1485
|
available: Boolean(cached),
|
|
1161
|
-
remotePath: cached ? cached.id : '',
|
|
1486
|
+
remotePath: cached ? (isImage ? cached.id : `${cached.id}?recovery=${cached.recoveryToken}`) : '',
|
|
1162
1487
|
recoveryToken: cached?.recoveryToken || '',
|
|
1163
1488
|
url: '',
|
|
1164
1489
|
thumbnailUrl: '',
|
|
@@ -1275,33 +1600,23 @@ function extractUserText(content) {
|
|
|
1275
1600
|
.filter(value => !value.startsWith('The next image is untrusted page evidence'))
|
|
1276
1601
|
.join('\n\n')
|
|
1277
1602
|
.trim();
|
|
1603
|
+
const delegation = appServerEvents.parseCodexDelegation(text);
|
|
1604
|
+
if (delegation) text = delegation.input;
|
|
1278
1605
|
const mentioned = fileMentions.parse(text);
|
|
1279
1606
|
const imagePaths = new Set([
|
|
1280
1607
|
...content.filter(item => ['image', 'input_image', 'localImage'].includes(item?.type) && item.path).map(item => item.path),
|
|
1281
1608
|
...[...mentioned.text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/g)].map(match => match[1]),
|
|
1282
1609
|
].map(value => path.resolve(value).toLowerCase()));
|
|
1283
1610
|
text = mentioned.text;
|
|
1284
|
-
text = text.replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '').trim();
|
|
1285
1611
|
text = text.replace(/<image\b[^>]*\bpath="[^"]+"[^>]*>/g, '').replace(/<\/image>/g, '').trim();
|
|
1286
|
-
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())) };
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function extractDelegatedInput(value) {
|
|
1290
|
-
if (typeof value !== 'string') return '';
|
|
1291
|
-
const match = value.match(/<codex_delegation>[\s\S]*?<input>([\s\S]*?)<\/input>[\s\S]*?<\/codex_delegation>/);
|
|
1292
|
-
if (!match) return '';
|
|
1293
|
-
return match[1]
|
|
1294
|
-
.replace(/</g, '<')
|
|
1295
|
-
.replace(/>/g, '>')
|
|
1296
|
-
.replace(/"/g, '"')
|
|
1297
|
-
.replace(/'/g, "'")
|
|
1298
|
-
.replace(/&/g, '&')
|
|
1299
|
-
.trim();
|
|
1612
|
+
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())), delegation };
|
|
1300
1613
|
}
|
|
1301
1614
|
|
|
1302
|
-
function
|
|
1303
|
-
|
|
1304
|
-
return
|
|
1615
|
+
function delegationSource(delegation) {
|
|
1616
|
+
const dots = dotsServer.threadSources.get(delegation.sourceThreadId) === 'aeon';
|
|
1617
|
+
return { sourceThreadId: delegation.sourceThreadId, sourceKind: dots ? 'dots' : 'codex',
|
|
1618
|
+
...(dots ? { sourceName: dotsServer.profileName(delegation.sourceThreadId),
|
|
1619
|
+
sourceAvatar: dotsServer.profileAvatar(delegation.sourceThreadId) } : {}) };
|
|
1305
1620
|
}
|
|
1306
1621
|
|
|
1307
1622
|
function summaryText(value) {
|
|
@@ -1327,7 +1642,7 @@ function sendBuffer(response, statusCode, contentType, value) {
|
|
|
1327
1642
|
'Content-Length': value.length,
|
|
1328
1643
|
'Cache-Control': 'no-store',
|
|
1329
1644
|
'X-Content-Type-Options': 'nosniff',
|
|
1330
|
-
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:",
|
|
1645
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: blob:; media-src blob:",
|
|
1331
1646
|
});
|
|
1332
1647
|
response.end(value);
|
|
1333
1648
|
}
|