@walkhi/code-relax 0.1.0-beta.27 → 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 -82
- 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 +471 -199
- 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
|
@@ -7,33 +7,48 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import vm from 'node:vm';
|
|
8
8
|
import sharp from 'sharp';
|
|
9
9
|
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
10
|
-
import fileMentions from '../shared/file-mentions.cjs';
|
|
11
|
-
import fileCitations from '../shared/file-citations.cjs';
|
|
10
|
+
import fileMentions from '../shared/file-mentions.cjs';
|
|
11
|
+
import fileCitations from '../shared/file-citations.cjs';
|
|
12
12
|
import { validateInlineImage } from './message-images.mjs';
|
|
13
13
|
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
14
14
|
import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
|
|
15
15
|
import { RuntimeDiscovery } from './runtime-discovery.mjs';
|
|
16
|
-
import { streamSharedThread } from './shared-thread-stream.mjs';
|
|
16
|
+
import { streamSharedThread, threadListNotificationMethods } from './shared-thread-stream.mjs';
|
|
17
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';
|
|
18
20
|
import { runtimePaths } from './platform/windows/runtime-paths.mjs';
|
|
19
21
|
import { openControl } from './service-control.mjs';
|
|
20
22
|
import { writeRecord, readRecord } from './service-lifecycle.mjs';
|
|
21
23
|
import { ExecutionHealth } from './execution-health.mjs';
|
|
22
|
-
import { authorizeLanDevice, onLanAuthorizationsChanged } from './self-relay/lan-authorizations.mjs';
|
|
24
|
+
import { authorizeLanDevice, onLanAuthorizationsChanged } from './self-relay/lan-authorizations.mjs';
|
|
23
25
|
import { createRelayManager } from './self-relay/lifecycle.mjs';
|
|
24
26
|
import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
|
|
25
27
|
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
26
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';
|
|
27
36
|
import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
|
|
28
37
|
import { requestServiceRestart } from './service-restart.mjs';
|
|
29
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';
|
|
30
46
|
|
|
31
47
|
const turnModelHistory = new TurnModelHistory();
|
|
48
|
+
const messageReceipts = new messageDelivery.Store();
|
|
32
49
|
|
|
33
|
-
const MAX_ATTACHMENT_BYTES =
|
|
34
|
-
const MAX_FILE_DOWNLOAD_BYTES =
|
|
35
|
-
// Each Base64 response fits the 8 MiB relay limit; full chunks have no padding.
|
|
36
|
-
const FILE_DOWNLOAD_CHUNK_BYTES = 3 * 1024 * 1024;
|
|
50
|
+
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
51
|
+
const MAX_FILE_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
37
52
|
const MAX_CACHED_ATTACHMENTS = 32;
|
|
38
53
|
const MAX_CACHED_FILES = 64;
|
|
39
54
|
const MAX_CONTEXT_TAIL_BYTES = 4 * 1024 * 1024;
|
|
@@ -41,36 +56,18 @@ const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
|
41
56
|
const SHARED_HISTORY_PAGE_BYTES = 500 * 1024;
|
|
42
57
|
const SHARED_HISTORY_PAGE_TURNS = 8;
|
|
43
58
|
const MAX_SETTINGS_SCAN_BYTES = 64 * 1024 * 1024;
|
|
44
|
-
const ALLOWED_MODELS = new Set([
|
|
45
|
-
'gpt-6-astra',
|
|
46
|
-
'gpt-6-sol',
|
|
47
|
-
'gpt-5.6-sol',
|
|
48
|
-
'gpt-5.6-terra',
|
|
49
|
-
'gpt-5.6-luna',
|
|
50
|
-
'gpt-5.5',
|
|
51
|
-
'gpt-5.3-codex-spark',
|
|
52
|
-
]);
|
|
53
59
|
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
54
60
|
|
|
55
|
-
class RequestError extends Error {
|
|
61
|
+
class RequestError extends Error {
|
|
56
62
|
constructor(statusCode, message, details) {
|
|
57
63
|
super(message);
|
|
58
64
|
this.statusCode = statusCode;
|
|
59
65
|
this.details = details;
|
|
60
66
|
this.code = details?.code;
|
|
61
67
|
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function
|
|
65
|
-
const offset = Number(rawOffset);
|
|
66
|
-
if (!/^(0|[1-9]\d*)$/.test(rawOffset || '') || !Number.isSafeInteger(offset) ||
|
|
67
|
-
offset > size || offset % FILE_DOWNLOAD_CHUNK_BYTES !== 0) {
|
|
68
|
-
throw new RequestError(400, '无效的文件下载位置。');
|
|
69
|
-
}
|
|
70
|
-
return offset;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function readRuntimeVersion() {
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readRuntimeVersion() {
|
|
74
71
|
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
75
72
|
while (true) {
|
|
76
73
|
const manifest = readRecord(path.join(directory, 'package.json'));
|
|
@@ -87,11 +84,12 @@ const runtimeVersion = readRuntimeVersion();
|
|
|
87
84
|
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
|
|
88
85
|
const projectRoot = os.homedir();
|
|
89
86
|
const { codexDataRoot, stateRoot, sessionsRoot, uploadsRoot } = runtimePaths();
|
|
87
|
+
const workstationDownloads = new WorkstationDownloads(stateRoot);
|
|
90
88
|
const markdownItScript = fs.readFileSync(path.join(webRoot, 'vendor', 'markdown-it.umd.min.js'));
|
|
91
89
|
const markdownSandbox = { atob };
|
|
92
90
|
vm.runInNewContext(markdownItScript.toString(), markdownSandbox);
|
|
93
|
-
const imageMarkdown = markdownSandbox.markdownit({ html: false });
|
|
94
|
-
fileCitations.install(imageMarkdown);
|
|
91
|
+
const imageMarkdown = markdownSandbox.markdownit({ html: false });
|
|
92
|
+
fileCitations.install(imageMarkdown);
|
|
95
93
|
const attachmentCache = new Map();
|
|
96
94
|
const fileCache = new Map();
|
|
97
95
|
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
@@ -102,6 +100,11 @@ const sessionSettingsCache = new Map();
|
|
|
102
100
|
const liveActivityCache = new Map();
|
|
103
101
|
let sharedServer = null;
|
|
104
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 });
|
|
105
108
|
const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
|
|
106
109
|
const relayManager = createRelayManager(stateRoot);
|
|
107
110
|
let sharedCatalog = null, executionMode = 'ordinary';
|
|
@@ -109,7 +112,7 @@ function selectSharedExecution(enabled) {
|
|
|
109
112
|
const next = enabled ? managedAppServer.client : null;
|
|
110
113
|
if (sharedServer === next) return;
|
|
111
114
|
sharedServer = next;
|
|
112
|
-
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer) : null;
|
|
115
|
+
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer, dotsServer, dotsChat) : null;
|
|
113
116
|
}
|
|
114
117
|
const executionHealth = new ExecutionHealth(async () => {
|
|
115
118
|
await managedAppServer.ensure();
|
|
@@ -128,18 +131,23 @@ async function refreshExecution() {
|
|
|
128
131
|
await executionHealth.refresh();
|
|
129
132
|
return { mode: executionMode };
|
|
130
133
|
}
|
|
131
|
-
async function requireExecutionWritable(threadId) {
|
|
134
|
+
async function requireExecutionWritable(threadId, client = sharedServer) {
|
|
132
135
|
const execution = await executionHealth.refresh();
|
|
133
136
|
if (!execution.canWrite) {
|
|
134
137
|
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
135
138
|
{ code: 'full_control_required', execution });
|
|
136
139
|
}
|
|
137
|
-
if (threadId &&
|
|
140
|
+
if (threadId && client) client.requireWritable(threadId);
|
|
138
141
|
return execution;
|
|
139
142
|
}
|
|
140
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));
|
|
141
147
|
|
|
148
|
+
dotsServer.on('profile-error', error => console.error(error.message));
|
|
142
149
|
async function handleRequest(request, response) {
|
|
150
|
+
let messageSubmissionStarted = false;
|
|
143
151
|
try {
|
|
144
152
|
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
145
153
|
|
|
@@ -229,9 +237,19 @@ async function handleRequest(request, response) {
|
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
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
|
+
|
|
232
249
|
const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
233
250
|
if (request.method === 'GET' && imageMatch) {
|
|
234
251
|
let image = attachmentCache.get(imageMatch[1])
|
|
252
|
+
|| dotsImages.restore(imageMatch[1], url.searchParams.get('recovery') || '')
|
|
235
253
|
|| recoverLocalImage(imageMatch[1], url.searchParams.get('recovery') || '');
|
|
236
254
|
const contextThreadId = url.searchParams.get('threadId') || '';
|
|
237
255
|
const contextTurnId = url.searchParams.get('turnId') || '';
|
|
@@ -243,53 +261,64 @@ async function handleRequest(request, response) {
|
|
|
243
261
|
} catch {}
|
|
244
262
|
}
|
|
245
263
|
if (!image) throw new RequestError(404, '这张图片已失效,无法从工作站重新读取。');
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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));
|
|
258
278
|
return;
|
|
259
279
|
}
|
|
260
280
|
|
|
261
281
|
const fileMatch = /^\/api\/files\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
262
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
|
+
}
|
|
263
291
|
const file = fileCache.get(fileMatch[1]);
|
|
264
292
|
if (!file) throw new RequestError(404, '文件授权已失效,请重新读取对话。');
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
throw
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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;
|
|
300
|
+
}
|
|
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); }
|
|
293
322
|
return;
|
|
294
323
|
}
|
|
295
324
|
|
|
@@ -300,23 +329,112 @@ async function handleRequest(request, response) {
|
|
|
300
329
|
if (taskPath && (url.pathname.endsWith('/takeover') || url.pathname === `/api/threads/${taskPath[1]}`)) {
|
|
301
330
|
throw new RequestError(410, '此连接方式已停用。', { code: 'unsupported_control' });
|
|
302
331
|
}
|
|
303
|
-
|
|
332
|
+
const hostId = url.searchParams.get('hostId') || 'local';
|
|
333
|
+
if (!['local', 'durable'].includes(hostId)) {
|
|
304
334
|
throw new RequestError(410, '此对话的连接方式已停用,请检查 Codex 内核状态。', { code: 'unsupported_control' });
|
|
305
335
|
}
|
|
306
336
|
if (!executionHealth.state.ready || sharedServer?.ready !== true) {
|
|
307
337
|
throw new RequestError(503, 'Codex 内核未就绪。请重启工作站服务;若仍失败,运行 relax doctor。',
|
|
308
338
|
{ code: 'full_control_required' });
|
|
309
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
|
+
}
|
|
310
384
|
|
|
311
385
|
if (request.method === 'GET' && url.pathname === '/api/models') {
|
|
312
|
-
sendJson(response, 200, await
|
|
386
|
+
sendJson(response, 200, await execution.models());
|
|
313
387
|
return;
|
|
314
388
|
}
|
|
315
389
|
|
|
316
390
|
if (request.method === 'GET' && url.pathname === '/api/threads') {
|
|
317
391
|
const limit = boundedInteger(url.searchParams.get('limit'), 20, 1, 200);
|
|
318
392
|
const archived = url.searchParams.get('archived') === 'true';
|
|
319
|
-
|
|
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); }
|
|
320
438
|
return;
|
|
321
439
|
}
|
|
322
440
|
|
|
@@ -325,11 +443,45 @@ async function handleRequest(request, response) {
|
|
|
325
443
|
return;
|
|
326
444
|
}
|
|
327
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
|
+
|
|
328
471
|
if (request.method === 'GET' && url.pathname === '/api/usage-limits') {
|
|
329
472
|
sendJson(response, 200, await sharedServer.usage());
|
|
330
473
|
return;
|
|
331
474
|
}
|
|
332
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
|
+
|
|
333
485
|
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
334
486
|
const execution = sharedServer;
|
|
335
487
|
response.startEvents();
|
|
@@ -342,9 +494,37 @@ async function handleRequest(request, response) {
|
|
|
342
494
|
return;
|
|
343
495
|
}
|
|
344
496
|
|
|
345
|
-
if (request.method === 'GET' && url.pathname === '/api/
|
|
346
|
-
|
|
347
|
-
|
|
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();
|
|
348
528
|
const listener = ({ method, params }) => {
|
|
349
529
|
if (!['turn/started', 'turn/completed'].includes(method)) return;
|
|
350
530
|
const threadId = params?.threadId, turnId = params?.turn?.id;
|
|
@@ -353,24 +533,29 @@ async function handleRequest(request, response) {
|
|
|
353
533
|
}
|
|
354
534
|
};
|
|
355
535
|
const disconnected = () => response.end();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
545
|
});
|
|
362
546
|
return;
|
|
363
547
|
}
|
|
364
548
|
|
|
365
549
|
const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
|
|
366
550
|
if (request.method === 'GET' && activeTurnMatch) {
|
|
551
|
+
if (rootChat) { sendJson(response, 200, { turnId: null }); return; }
|
|
367
552
|
const threadId = decodeURIComponent(activeTurnMatch[1]);
|
|
368
|
-
await
|
|
369
|
-
const { thread } = await
|
|
553
|
+
await execution.connect();
|
|
554
|
+
const { thread } = await execution.request('thread/read', { threadId, includeTurns: false });
|
|
370
555
|
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
371
556
|
let turnId = null;
|
|
372
557
|
if (status === 'active') {
|
|
373
|
-
const page = await
|
|
558
|
+
const page = await execution.request('thread/turns/list', {
|
|
374
559
|
threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
375
560
|
});
|
|
376
561
|
const latest = page.data?.[0];
|
|
@@ -397,11 +582,12 @@ async function handleRequest(request, response) {
|
|
|
397
582
|
let target = appServerEvents.projectlessThreadTarget(projectRoot);
|
|
398
583
|
if (project) {
|
|
399
584
|
const nativeProjects = await appServerEvents.readProjects(rpc);
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
402
|
-
target =
|
|
585
|
+
const nativeProject = nativeProjects.find(item => item.id === project.projectId);
|
|
586
|
+
if (!nativeProject) throw new RequestError(404, 'Codex 内核中没有找到对应项目,请刷新项目列表。');
|
|
587
|
+
target = appServerEvents.projectThreadTarget(nativeProject);
|
|
403
588
|
}
|
|
404
589
|
const { input } = appServerTurnArguments('', body);
|
|
590
|
+
messageSubmissionStarted = true;
|
|
405
591
|
const created = await appServerEvents.createThread(rpc, target, input, body);
|
|
406
592
|
const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
407
593
|
void generateAndSetThreadTitle({
|
|
@@ -419,13 +605,24 @@ async function handleRequest(request, response) {
|
|
|
419
605
|
}
|
|
420
606
|
|
|
421
607
|
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
422
|
-
if (request.method === 'GET' && timelineMatch) {
|
|
423
|
-
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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));
|
|
429
626
|
}
|
|
430
627
|
sendJson(response, 200, timeline);
|
|
431
628
|
return;
|
|
@@ -434,19 +631,20 @@ async function handleRequest(request, response) {
|
|
|
434
631
|
const approvalMatch = /^\/api\/threads\/([^/]+)\/approvals$/.exec(url.pathname);
|
|
435
632
|
if (request.method === 'POST' && approvalMatch) {
|
|
436
633
|
const threadId = decodeURIComponent(approvalMatch[1]);
|
|
437
|
-
await requireExecutionWritable(threadId);
|
|
634
|
+
await requireExecutionWritable(threadId, execution);
|
|
438
635
|
const body = await readJsonBody(request);
|
|
439
|
-
sendJson(response, 200, await
|
|
636
|
+
sendJson(response, 200, await execution.respondApproval(threadId, body)); return;
|
|
440
637
|
}
|
|
441
638
|
|
|
442
639
|
const eventsMatch = /^\/api\/threads\/([^/]+)\/events$/.exec(url.pathname);
|
|
443
640
|
if (request.method === 'GET' && eventsMatch) {
|
|
444
641
|
const threadId = decodeURIComponent(eventsMatch[1]);
|
|
445
642
|
const after = Number(url.searchParams.get('afterSeq'));
|
|
446
|
-
const requestedSync = url.searchParams.get('syncId')
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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);
|
|
450
648
|
return;
|
|
451
649
|
}
|
|
452
650
|
|
|
@@ -454,14 +652,14 @@ async function handleRequest(request, response) {
|
|
|
454
652
|
if (request.method === 'GET' && modelChangesMatch) {
|
|
455
653
|
const threadId = decodeURIComponent(modelChangesMatch[1]);
|
|
456
654
|
if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) throw new RequestError(400, '对话 ID 无效。');
|
|
457
|
-
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)));
|
|
458
656
|
return;
|
|
459
657
|
}
|
|
460
658
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
461
659
|
if (request.method === 'GET' && contextMatch) {
|
|
462
660
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
463
|
-
const current = readContextUsage(threadId);
|
|
464
|
-
const native = await
|
|
661
|
+
const current = hostId === 'durable' ? { available: false } : readContextUsage(threadId);
|
|
662
|
+
const native = await execution.readContext(threadId, url.searchParams.get('refreshSettings') === 'true');
|
|
465
663
|
sendJson(response, 200, { ...current, ...(native.model ? { model: native.model, thinking: native.thinking, serviceTier: native.serviceTier,
|
|
466
664
|
permissionMode: native.permissionMode, permissionModes: native.permissionModes } : {}), ...(native.available ? native : {}) });
|
|
467
665
|
return;
|
|
@@ -471,34 +669,45 @@ async function handleRequest(request, response) {
|
|
|
471
669
|
if (sharedAction) {
|
|
472
670
|
const id = decodeURIComponent(sharedAction[1]);
|
|
473
671
|
if (request.method === 'GET' && sharedAction[2] === 'history') {
|
|
474
|
-
|
|
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));
|
|
475
680
|
return;
|
|
476
681
|
}
|
|
477
682
|
if (request.method === 'POST' && sharedAction[2] === 'settings') {
|
|
478
|
-
|
|
683
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供 Codex 模型设置。');
|
|
684
|
+
await requireExecutionWritable(id, execution);
|
|
479
685
|
const body = await readJsonBody(request);
|
|
480
|
-
sendJson(response, 200, await
|
|
686
|
+
sendJson(response, 200, await execution.settings(id, body.model, body.thinking, body.serviceTier));
|
|
481
687
|
return;
|
|
482
688
|
}
|
|
483
689
|
if (request.method === 'POST' && sharedAction[2] === 'permissions') {
|
|
484
|
-
|
|
690
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供 Codex 权限设置。');
|
|
691
|
+
await requireExecutionWritable(id, execution);
|
|
485
692
|
const body = await readJsonBody(request);
|
|
486
|
-
sendJson(response, 200, await
|
|
693
|
+
sendJson(response, 200, await execution.permissions(id, body.mode));
|
|
487
694
|
return;
|
|
488
695
|
}
|
|
489
696
|
}
|
|
490
697
|
|
|
491
698
|
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
492
699
|
if (request.method === 'POST' && archiveMatch) {
|
|
700
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供任务归档。');
|
|
493
701
|
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
494
|
-
await requireExecutionWritable(threadId);
|
|
495
|
-
await
|
|
702
|
+
await requireExecutionWritable(threadId, execution);
|
|
703
|
+
await execution.request(`thread/${archiveMatch[2]}`, { threadId });
|
|
496
704
|
sendJson(response, 200, { threadId, archived: archiveMatch[2] === 'archive' });
|
|
497
705
|
return;
|
|
498
706
|
}
|
|
499
707
|
|
|
500
708
|
const queueMatch = /^\/api\/threads\/([^/]+)\/queue$/.exec(url.pathname);
|
|
501
709
|
if (queueMatch) {
|
|
710
|
+
if (hostId === 'durable') throw new RequestError(405, '当前托管连接不提供排队消息。');
|
|
502
711
|
const threadId = decodeURIComponent(queueMatch[1]);
|
|
503
712
|
await sharedServer.connect();
|
|
504
713
|
const rpc = (method, params) => sharedServer.request(method, params);
|
|
@@ -513,6 +722,7 @@ async function handleRequest(request, response) {
|
|
|
513
722
|
|
|
514
723
|
const queueItemMatch = /^\/api\/threads\/([^/]+)\/queue\/([a-f0-9-]+)(?:\/(steer))?$/.exec(url.pathname);
|
|
515
724
|
if (queueItemMatch) {
|
|
725
|
+
if (hostId === 'durable') throw new RequestError(405, '当前托管连接不提供排队消息。');
|
|
516
726
|
const threadId = decodeURIComponent(queueItemMatch[1]);
|
|
517
727
|
const itemId = queueItemMatch[2];
|
|
518
728
|
await sharedServer.connect();
|
|
@@ -524,23 +734,43 @@ async function handleRequest(request, response) {
|
|
|
524
734
|
|
|
525
735
|
const interruptMatch = /^\/api\/threads\/([^/]+)\/interrupt$/.exec(url.pathname);
|
|
526
736
|
if (request.method === 'POST' && interruptMatch) {
|
|
737
|
+
if (rootChat) throw new RequestError(405, 'Dots 主对话不提供停止 Codex 任务的操作。');
|
|
527
738
|
const threadId = decodeURIComponent(interruptMatch[1]);
|
|
528
|
-
await requireExecutionWritable(threadId);
|
|
739
|
+
await requireExecutionWritable(threadId, execution);
|
|
529
740
|
const body = await readJsonBody(request);
|
|
530
741
|
if (!body.turnId) throw new RequestError(400, '缺少活动轮次 ID。');
|
|
531
|
-
await
|
|
742
|
+
await execution.interrupt(threadId, body.turnId);
|
|
532
743
|
sendJson(response, 200, { requested: true });
|
|
533
744
|
return;
|
|
534
745
|
}
|
|
535
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
|
+
}
|
|
536
760
|
const messageMatch = /^\/api\/threads\/([^/]+)\/messages$/.exec(url.pathname);
|
|
537
761
|
if (request.method === 'POST' && messageMatch) {
|
|
538
762
|
const threadId = decodeURIComponent(messageMatch[1]);
|
|
539
|
-
await requireExecutionWritable(threadId);
|
|
540
763
|
const body = await readJsonBody(request);
|
|
541
|
-
if (body.hostId && body.hostId !==
|
|
542
|
-
const
|
|
543
|
-
|
|
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);
|
|
544
774
|
return;
|
|
545
775
|
}
|
|
546
776
|
|
|
@@ -550,13 +780,15 @@ async function handleRequest(request, response) {
|
|
|
550
780
|
if (!response.writableEnded) response.end();
|
|
551
781
|
return;
|
|
552
782
|
}
|
|
553
|
-
|
|
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;
|
|
554
786
|
sendJson(response, status, {
|
|
555
787
|
error: error instanceof Error ? error.message : String(error),
|
|
556
788
|
code: error?.code,
|
|
557
789
|
threadId: error?.threadId,
|
|
558
790
|
readOnly: error?.readOnly,
|
|
559
|
-
details: error?.details,
|
|
791
|
+
details: error?.deliveryUnknown === undefined ? error?.details : { ...error?.details, deliveryUnknown: error.deliveryUnknown },
|
|
560
792
|
});
|
|
561
793
|
}
|
|
562
794
|
}
|
|
@@ -564,8 +796,8 @@ async function handleRequest(request, response) {
|
|
|
564
796
|
const server = http.createServer(handleRequest);
|
|
565
797
|
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
566
798
|
uploadsRoot,
|
|
567
|
-
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
568
|
-
onDeviceAuthorizationsChanged: listener => onLanAuthorizationsChanged(stateRoot, listener),
|
|
799
|
+
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
800
|
+
onDeviceAuthorizationsChanged: listener => onLanAuthorizationsChanged(stateRoot, listener),
|
|
569
801
|
});
|
|
570
802
|
|
|
571
803
|
let control;
|
|
@@ -591,6 +823,7 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
591
823
|
const state = managedAppServer.snapshot();
|
|
592
824
|
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
593
825
|
},
|
|
826
|
+
'restart-managed-app-server-confirmed': () => managedAppServer.restart(crypto.randomUUID()),
|
|
594
827
|
} });
|
|
595
828
|
writeRecord(process.env.CODEX_REMOTE_READY_PATH, { instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
596
829
|
pid: process.pid, controlPort: control.port });
|
|
@@ -614,6 +847,8 @@ async function shutDown() {
|
|
|
614
847
|
executionHealth?.stop();
|
|
615
848
|
runtimeDiscovery.stop();
|
|
616
849
|
await relayManager.close();
|
|
850
|
+
dotsServer.close();
|
|
851
|
+
dotsChat.close();
|
|
617
852
|
managedAppServer.close();
|
|
618
853
|
await control?.close();
|
|
619
854
|
process.exit(0);
|
|
@@ -756,6 +991,14 @@ function messagePayload(body) {
|
|
|
756
991
|
if (!fs.existsSync(file)) throw new RequestError(400, '图片不存在,请重新添加。');
|
|
757
992
|
return file;
|
|
758
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
|
+
}
|
|
759
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。');
|
|
760
1003
|
if (!prompt && !images.length) throw new RequestError(400, 'prompt 不能为空。');
|
|
761
1004
|
if (prompt.length > 32_000) throw new RequestError(400, 'prompt 不能超过 32000 个字符。');
|
|
@@ -763,9 +1006,7 @@ function messagePayload(body) {
|
|
|
763
1006
|
const payload = { prompt, images };
|
|
764
1007
|
if (typeof body.hostId === 'string' && body.hostId.trim()) payload.hostId = body.hostId.trim();
|
|
765
1008
|
if (typeof body.model === 'string' && body.model.trim()) {
|
|
766
|
-
|
|
767
|
-
if (!ALLOWED_MODELS.has(model)) throw new RequestError(400, '不支持的模型。');
|
|
768
|
-
payload.model = model;
|
|
1009
|
+
payload.model = body.model.trim();
|
|
769
1010
|
}
|
|
770
1011
|
if (typeof body.thinking === 'string' && body.thinking.trim()) {
|
|
771
1012
|
const thinking = body.thinking.trim();
|
|
@@ -802,13 +1043,36 @@ function normalizeTimeline(data) {
|
|
|
802
1043
|
};
|
|
803
1044
|
}
|
|
804
1045
|
|
|
805
|
-
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
806
|
-
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' } });
|
|
807
1049
|
return { ...timeline,
|
|
808
|
-
|
|
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 } : {}),
|
|
809
1054
|
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
810
1055
|
}
|
|
811
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
|
+
|
|
812
1076
|
function normalizeThread(thread) {
|
|
813
1077
|
const source = thread && typeof thread === 'object' ? thread : {};
|
|
814
1078
|
return {
|
|
@@ -820,6 +1084,10 @@ function normalizeThread(thread) {
|
|
|
820
1084
|
projectId: source.projectId || '',
|
|
821
1085
|
cwd: source.cwd || '',
|
|
822
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 } : {}),
|
|
823
1091
|
transport: source.transport || 'shared-app-server',
|
|
824
1092
|
readOnly: source.readOnly === true,
|
|
825
1093
|
readOnlyReason: source.readOnlyReason || '',
|
|
@@ -841,7 +1109,12 @@ function normalizeTurn(turn) {
|
|
|
841
1109
|
completedAt: source.completedAt || null,
|
|
842
1110
|
durationMs: source.durationMs ?? null,
|
|
843
1111
|
error: source.error ?? null,
|
|
1112
|
+
runtimeNotice: source.runtimeNotice ?? null,
|
|
844
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 } : {}),
|
|
845
1118
|
entries,
|
|
846
1119
|
};
|
|
847
1120
|
}
|
|
@@ -849,6 +1122,10 @@ function normalizeTurn(turn) {
|
|
|
849
1122
|
function normalizeItem(item) {
|
|
850
1123
|
if (!item || typeof item !== 'object') return null;
|
|
851
1124
|
|
|
1125
|
+
if (item.type === 'approval') {
|
|
1126
|
+
return { ...item, type: 'activity', kind: 'approval', title: item.params?.message || '需要你的批准' };
|
|
1127
|
+
}
|
|
1128
|
+
|
|
852
1129
|
if (item.type === 'contextCompaction') {
|
|
853
1130
|
return {
|
|
854
1131
|
type: 'activity',
|
|
@@ -858,24 +1135,21 @@ function normalizeItem(item) {
|
|
|
858
1135
|
};
|
|
859
1136
|
}
|
|
860
1137
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
type: 'message',
|
|
868
|
-
id: item.id || '',
|
|
869
|
-
role: 'user',
|
|
870
|
-
source: 'remote',
|
|
871
|
-
...splitUploadedImages(text),
|
|
872
|
-
};
|
|
873
|
-
}
|
|
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
|
+
};
|
|
874
1144
|
}
|
|
875
1145
|
|
|
876
1146
|
if (item.type === 'userMessage') {
|
|
877
|
-
const { text, files } = extractUserText(item.content);
|
|
878
|
-
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
|
+
}))];
|
|
879
1153
|
if (!text && !attachments.length) return null;
|
|
880
1154
|
return {
|
|
881
1155
|
clientId: item.clientId,
|
|
@@ -884,6 +1158,7 @@ function normalizeItem(item) {
|
|
|
884
1158
|
role: 'user',
|
|
885
1159
|
text,
|
|
886
1160
|
attachments,
|
|
1161
|
+
...(delegation ? { delegation: delegationSource(delegation) } : {}),
|
|
887
1162
|
};
|
|
888
1163
|
}
|
|
889
1164
|
|
|
@@ -896,6 +1171,7 @@ function normalizeItem(item) {
|
|
|
896
1171
|
delivery: item.delivery,
|
|
897
1172
|
questions: item.questions,
|
|
898
1173
|
text: item.text || '',
|
|
1174
|
+
attachments: normalizeAttachments(item.attachments),
|
|
899
1175
|
imageAssets: markdownImageAssets(item.text || ''),
|
|
900
1176
|
fileAssets: markdownFileAssets(item.text || ''),
|
|
901
1177
|
};
|
|
@@ -969,7 +1245,7 @@ function normalizeItem(item) {
|
|
|
969
1245
|
title: '文件修改',
|
|
970
1246
|
titleOnly: true,
|
|
971
1247
|
status: statusText(item.status),
|
|
972
|
-
changes:
|
|
1248
|
+
changes: fileChanges.summarize(item.changes),
|
|
973
1249
|
};
|
|
974
1250
|
}
|
|
975
1251
|
|
|
@@ -989,16 +1265,6 @@ function normalizeItem(item) {
|
|
|
989
1265
|
const image = typeof item.path === 'string' && item.path
|
|
990
1266
|
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
991
1267
|
: null;
|
|
992
|
-
if (image?.available) {
|
|
993
|
-
return {
|
|
994
|
-
type: 'message',
|
|
995
|
-
id: item.id || '',
|
|
996
|
-
role: 'assistant',
|
|
997
|
-
phase: 'commentary',
|
|
998
|
-
text: '',
|
|
999
|
-
attachments: [image],
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
1268
|
return {
|
|
1003
1269
|
type: 'activity',
|
|
1004
1270
|
id: item.id || '',
|
|
@@ -1011,9 +1277,11 @@ function normalizeItem(item) {
|
|
|
1011
1277
|
}
|
|
1012
1278
|
|
|
1013
1279
|
if (item.type === 'imageGeneration') {
|
|
1280
|
+
const result = typeof item.result === 'string' ? item.result.trim() : '';
|
|
1014
1281
|
const image = typeof item.savedPath === 'string' && item.savedPath
|
|
1015
1282
|
? normalizeAttachment({ type: 'localImage', path: item.savedPath, name: '生成的图片' })
|
|
1016
|
-
:
|
|
1283
|
+
: result ? normalizeAttachment({ type: 'image', name: '生成的图片',
|
|
1284
|
+
url: result.startsWith('data:image/') ? result : `data:image/png;base64,${result}` }) : null;
|
|
1017
1285
|
return {
|
|
1018
1286
|
type: 'activity',
|
|
1019
1287
|
id: item.id || '',
|
|
@@ -1088,7 +1356,7 @@ function normalizeAttachments(content) {
|
|
|
1088
1356
|
else items.push({ type: 'localImage', path: imagePath });
|
|
1089
1357
|
}
|
|
1090
1358
|
const seen = new Set();
|
|
1091
|
-
return items.map(normalizeAttachment).filter(item => {
|
|
1359
|
+
return items.map(item => normalizeAttachment(item)).filter(item => {
|
|
1092
1360
|
if (!item.url) return true;
|
|
1093
1361
|
if (seen.has(item.url)) return false;
|
|
1094
1362
|
seen.add(item.url); return true;
|
|
@@ -1099,23 +1367,21 @@ function markdownImageAssets(text) {
|
|
|
1099
1367
|
const assets = Object.create(null);
|
|
1100
1368
|
function visit(tokens) {
|
|
1101
1369
|
for (const token of tokens) {
|
|
1102
|
-
const value = token.type === 'image' ? token.attrGet('src')
|
|
1103
|
-
: token.type === 'link_open' && !token.meta?.fileCitation ? token.attrGet('href') : null;
|
|
1370
|
+
const value = token.type === 'image' ? token.attrGet('src')
|
|
1371
|
+
: token.type === 'link_open' && !token.meta?.fileCitation ? token.attrGet('href') : null;
|
|
1104
1372
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1105
|
-
|
|
1106
|
-
try { localPath = decodeURIComponent(value); } catch { continue; }
|
|
1373
|
+
const localPath = fileCitations.localReference(value)?.path;
|
|
1107
1374
|
// Only paths actually referenced by Desktop messages become accessible.
|
|
1108
1375
|
// Do not expose a client-supplied filesystem path endpoint or UNC shares.
|
|
1109
|
-
if (
|
|
1110
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1376
|
+
if (localPath && /^[a-z]:[\\/]/i.test(localPath)) {
|
|
1111
1377
|
const asset = normalizeAttachment({ type: 'localImage', path: localPath });
|
|
1112
1378
|
if (asset.available) assets[value] = asset;
|
|
1113
1379
|
}
|
|
1114
1380
|
}
|
|
1115
|
-
if (token.children && token.type !== 'image') visit(token.children);
|
|
1381
|
+
if (token.children && token.type !== 'image') visit(token.children);
|
|
1116
1382
|
}
|
|
1117
1383
|
}
|
|
1118
|
-
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1384
|
+
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1119
1385
|
return assets;
|
|
1120
1386
|
}
|
|
1121
1387
|
|
|
@@ -1126,20 +1392,17 @@ function markdownFileAssets(text) {
|
|
|
1126
1392
|
if (token.type === 'link_open') {
|
|
1127
1393
|
const value = token.attrGet('href');
|
|
1128
1394
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1132
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1133
|
-
localPath = localPath.replace(/:(\d+)(?::\d+)?$/, '');
|
|
1395
|
+
const localPath = fileCitations.localReference(value)?.path;
|
|
1396
|
+
if (localPath && /^[a-z]:[\\/]/i.test(localPath)) {
|
|
1134
1397
|
const asset = rememberLocalFile(localPath);
|
|
1135
1398
|
if (asset) assets[value] = { remotePath: asset.id, name: asset.name, size: asset.size };
|
|
1136
1399
|
}
|
|
1137
1400
|
}
|
|
1138
1401
|
}
|
|
1139
|
-
if (token.children && token.type !== 'image') visit(token.children);
|
|
1402
|
+
if (token.children && token.type !== 'image') visit(token.children);
|
|
1140
1403
|
}
|
|
1141
1404
|
}
|
|
1142
|
-
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1405
|
+
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1143
1406
|
return assets;
|
|
1144
1407
|
}
|
|
1145
1408
|
|
|
@@ -1147,12 +1410,12 @@ function rememberLocalFile(filePath) {
|
|
|
1147
1410
|
try {
|
|
1148
1411
|
const resolved = path.resolve(filePath);
|
|
1149
1412
|
const stat = fs.statSync(resolved);
|
|
1150
|
-
if (!stat.isFile()) return null;
|
|
1413
|
+
if (!stat.isFile()) return null;
|
|
1151
1414
|
const id = crypto.createHmac('sha256', fileCapabilitySecret)
|
|
1152
1415
|
.update(resolved).update('\0').update(String(stat.size)).update('\0').update(String(stat.mtimeMs)).digest('hex');
|
|
1153
1416
|
if (!fileCache.has(id)) {
|
|
1154
1417
|
while (fileCache.size >= MAX_CACHED_FILES) fileCache.delete(fileCache.keys().next().value);
|
|
1155
|
-
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) });
|
|
1156
1419
|
}
|
|
1157
1420
|
return fileCache.get(id);
|
|
1158
1421
|
} catch {
|
|
@@ -1183,12 +1446,29 @@ function splitUploadedImages(text) {
|
|
|
1183
1446
|
return { text: visibleText, attachments, imageAssets: markdownImageAssets(visibleText) };
|
|
1184
1447
|
}
|
|
1185
1448
|
|
|
1186
|
-
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) {
|
|
1187
1466
|
const name = item.name || item.fileName || (typeof item.path === 'string' ? path.basename(item.path) : '');
|
|
1188
1467
|
let cached = null;
|
|
1189
1468
|
const isImage = ['image', 'input_image', 'localImage'].includes(item.type);
|
|
1469
|
+
if (isImage || item.type === 'file') cached = dotsImages.remember(item, identity);
|
|
1190
1470
|
if (isImage && typeof item.path === 'string') {
|
|
1191
|
-
cached = cacheLocalImage(item.path, name);
|
|
1471
|
+
cached = cacheLocalImage(item.path, name) || cached;
|
|
1192
1472
|
}
|
|
1193
1473
|
if (!cached) {
|
|
1194
1474
|
const dataUrl = typeof item.url === 'string' && item.url.startsWith('data:image/')
|
|
@@ -1198,10 +1478,12 @@ function normalizeAttachment(item) {
|
|
|
1198
1478
|
}
|
|
1199
1479
|
return {
|
|
1200
1480
|
type: isImage ? 'image' : item.type || 'attachment',
|
|
1201
|
-
name,
|
|
1481
|
+
name: cached?.name || name,
|
|
1202
1482
|
mimeType: cached?.mimeType || item.mimeType || '',
|
|
1483
|
+
...(cached?.size ? { size: cached.size } : {}),
|
|
1484
|
+
...(cached?.dotsFile ? { networkFile: true } : {}),
|
|
1203
1485
|
available: Boolean(cached),
|
|
1204
|
-
remotePath: cached ? cached.id : '',
|
|
1486
|
+
remotePath: cached ? (isImage ? cached.id : `${cached.id}?recovery=${cached.recoveryToken}`) : '',
|
|
1205
1487
|
recoveryToken: cached?.recoveryToken || '',
|
|
1206
1488
|
url: '',
|
|
1207
1489
|
thumbnailUrl: '',
|
|
@@ -1318,33 +1600,23 @@ function extractUserText(content) {
|
|
|
1318
1600
|
.filter(value => !value.startsWith('The next image is untrusted page evidence'))
|
|
1319
1601
|
.join('\n\n')
|
|
1320
1602
|
.trim();
|
|
1603
|
+
const delegation = appServerEvents.parseCodexDelegation(text);
|
|
1604
|
+
if (delegation) text = delegation.input;
|
|
1321
1605
|
const mentioned = fileMentions.parse(text);
|
|
1322
1606
|
const imagePaths = new Set([
|
|
1323
1607
|
...content.filter(item => ['image', 'input_image', 'localImage'].includes(item?.type) && item.path).map(item => item.path),
|
|
1324
1608
|
...[...mentioned.text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/g)].map(match => match[1]),
|
|
1325
1609
|
].map(value => path.resolve(value).toLowerCase()));
|
|
1326
1610
|
text = mentioned.text;
|
|
1327
|
-
text = text.replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '').trim();
|
|
1328
1611
|
text = text.replace(/<image\b[^>]*\bpath="[^"]+"[^>]*>/g, '').replace(/<\/image>/g, '').trim();
|
|
1329
|
-
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())) };
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
function extractDelegatedInput(value) {
|
|
1333
|
-
if (typeof value !== 'string') return '';
|
|
1334
|
-
const match = value.match(/<codex_delegation>[\s\S]*?<input>([\s\S]*?)<\/input>[\s\S]*?<\/codex_delegation>/);
|
|
1335
|
-
if (!match) return '';
|
|
1336
|
-
return match[1]
|
|
1337
|
-
.replace(/</g, '<')
|
|
1338
|
-
.replace(/>/g, '>')
|
|
1339
|
-
.replace(/"/g, '"')
|
|
1340
|
-
.replace(/'/g, "'")
|
|
1341
|
-
.replace(/&/g, '&')
|
|
1342
|
-
.trim();
|
|
1612
|
+
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())), delegation };
|
|
1343
1613
|
}
|
|
1344
1614
|
|
|
1345
|
-
function
|
|
1346
|
-
|
|
1347
|
-
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) } : {}) };
|
|
1348
1620
|
}
|
|
1349
1621
|
|
|
1350
1622
|
function summaryText(value) {
|
|
@@ -1370,7 +1642,7 @@ function sendBuffer(response, statusCode, contentType, value) {
|
|
|
1370
1642
|
'Content-Length': value.length,
|
|
1371
1643
|
'Cache-Control': 'no-store',
|
|
1372
1644
|
'X-Content-Type-Options': 'nosniff',
|
|
1373
|
-
'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:",
|
|
1374
1646
|
});
|
|
1375
1647
|
response.end(value);
|
|
1376
1648
|
}
|