@walkhi/code-relax 0.1.0-beta.27 → 0.1.0-beta.29
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 +303 -239
- package/dist/bin/self-relay-server.mjs +2 -0
- 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 +23 -5
- package/dist/src/self-relay/email-accounts.mjs +35 -10
- package/dist/src/self-relay/file-wire.mjs +13 -0
- package/dist/src/self-relay/lifecycle.mjs +22 -4
- package/dist/src/self-relay/long-pairing.mjs +40 -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 +922 -849
- package/dist/src/server.mjs +474 -199
- package/dist/src/service-control.mjs +13 -3
- 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;
|
|
@@ -587,10 +819,14 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
587
819
|
'relay-stop': () => relayManager.run('relay-stop'),
|
|
588
820
|
'relay-pair': () => relayManager.run('relay-pair'),
|
|
589
821
|
'relay-revoke': () => relayManager.run('relay-revoke'),
|
|
822
|
+
'invite-create': details => relayManager.run('invite-create', details),
|
|
823
|
+
'invite-list': () => relayManager.run('invite-list'),
|
|
824
|
+
'invite-revoke': details => relayManager.run('invite-revoke', details),
|
|
590
825
|
'restart-managed-app-server': () => {
|
|
591
826
|
const state = managedAppServer.snapshot();
|
|
592
827
|
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
593
828
|
},
|
|
829
|
+
'restart-managed-app-server-confirmed': () => managedAppServer.restart(crypto.randomUUID()),
|
|
594
830
|
} });
|
|
595
831
|
writeRecord(process.env.CODEX_REMOTE_READY_PATH, { instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
596
832
|
pid: process.pid, controlPort: control.port });
|
|
@@ -614,6 +850,8 @@ async function shutDown() {
|
|
|
614
850
|
executionHealth?.stop();
|
|
615
851
|
runtimeDiscovery.stop();
|
|
616
852
|
await relayManager.close();
|
|
853
|
+
dotsServer.close();
|
|
854
|
+
dotsChat.close();
|
|
617
855
|
managedAppServer.close();
|
|
618
856
|
await control?.close();
|
|
619
857
|
process.exit(0);
|
|
@@ -756,6 +994,14 @@ function messagePayload(body) {
|
|
|
756
994
|
if (!fs.existsSync(file)) throw new RequestError(400, '图片不存在,请重新添加。');
|
|
757
995
|
return file;
|
|
758
996
|
});
|
|
997
|
+
for (const input of fileMentions.originalInputs(body)) {
|
|
998
|
+
const incoming = fs.realpathSync(path.join(uploadsRoot, 'incoming'));
|
|
999
|
+
const imagePath = fs.realpathSync(input.path);
|
|
1000
|
+
if (path.dirname(imagePath).toLowerCase() !== incoming.toLowerCase()) throw new RequestError(400, '原图必须来自上传目录。');
|
|
1001
|
+
const stat = fs.statSync(imagePath);
|
|
1002
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_ATTACHMENT_BYTES) throw new RequestError(400, '原图为空或超过 20 MiB。');
|
|
1003
|
+
images.push(imagePath);
|
|
1004
|
+
}
|
|
759
1005
|
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
1006
|
if (!prompt && !images.length) throw new RequestError(400, 'prompt 不能为空。');
|
|
761
1007
|
if (prompt.length > 32_000) throw new RequestError(400, 'prompt 不能超过 32000 个字符。');
|
|
@@ -763,9 +1009,7 @@ function messagePayload(body) {
|
|
|
763
1009
|
const payload = { prompt, images };
|
|
764
1010
|
if (typeof body.hostId === 'string' && body.hostId.trim()) payload.hostId = body.hostId.trim();
|
|
765
1011
|
if (typeof body.model === 'string' && body.model.trim()) {
|
|
766
|
-
|
|
767
|
-
if (!ALLOWED_MODELS.has(model)) throw new RequestError(400, '不支持的模型。');
|
|
768
|
-
payload.model = model;
|
|
1012
|
+
payload.model = body.model.trim();
|
|
769
1013
|
}
|
|
770
1014
|
if (typeof body.thinking === 'string' && body.thinking.trim()) {
|
|
771
1015
|
const thinking = body.thinking.trim();
|
|
@@ -802,13 +1046,36 @@ function normalizeTimeline(data) {
|
|
|
802
1046
|
};
|
|
803
1047
|
}
|
|
804
1048
|
|
|
805
|
-
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
806
|
-
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId
|
|
1049
|
+
function normalizeSharedTimeline({ thread, nextCursor, sync, notModified, incremental }, hostId = thread?.hostId || 'local') {
|
|
1050
|
+
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId,
|
|
1051
|
+
transport: 'shared-app-server' } });
|
|
807
1052
|
return { ...timeline,
|
|
808
|
-
|
|
1053
|
+
historyItemCount: (thread.turns || []).reduce((count, turn) => count + (turn.roomMessage ? 1 : turn.items?.length || 0), 0),
|
|
1054
|
+
historyView: thread?.threadSource === 'aeon' ? 'chat-items-v1' : 'item-pages-v1',
|
|
1055
|
+
...(notModified ? { notModified: true } : {}),
|
|
1056
|
+
...(incremental ? { incremental: true } : {}),
|
|
809
1057
|
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
810
1058
|
}
|
|
811
1059
|
|
|
1060
|
+
async function prepareSharedTimeline(value, hostId) {
|
|
1061
|
+
if (value.thread?.threadSource !== 'aeon') return normalizeSharedTimeline(value, hostId);
|
|
1062
|
+
const epoch = dotsChat.epoch, threadId = value.thread.id;
|
|
1063
|
+
const current = () => {
|
|
1064
|
+
if (epoch !== dotsChat.epoch) throw new Error('点点连接已变化,请重新同步。');
|
|
1065
|
+
};
|
|
1066
|
+
if (value.notModified) {
|
|
1067
|
+
const record = dotsChat.threads.get(threadId);
|
|
1068
|
+
if (await dotsImages.prepareTimeline(record?.thread)) {
|
|
1069
|
+
current();
|
|
1070
|
+
value = dotsChat.snapshot(threadId, record);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const snapshot = structuredClone(value);
|
|
1074
|
+
await dotsImages.prepareTimeline(snapshot.thread);
|
|
1075
|
+
current();
|
|
1076
|
+
return normalizeSharedTimeline(snapshot, hostId);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
812
1079
|
function normalizeThread(thread) {
|
|
813
1080
|
const source = thread && typeof thread === 'object' ? thread : {};
|
|
814
1081
|
return {
|
|
@@ -820,6 +1087,10 @@ function normalizeThread(thread) {
|
|
|
820
1087
|
projectId: source.projectId || '',
|
|
821
1088
|
cwd: source.cwd || '',
|
|
822
1089
|
hostId: source.hostId || '',
|
|
1090
|
+
threadSource: source.threadSource || '',
|
|
1091
|
+
...(['aeon', 'aeon_child'].includes(source.threadSource) ? { chatRevision: source.chatRevision || '' } : {}),
|
|
1092
|
+
...(source.threadSource === 'aeon' ? { avatar: source.avatar || '', color: source.color || '',
|
|
1093
|
+
chatMessageRevision: source.chatMessageRevision, messagingRoomId: source.messagingRoomId, lastMessageAt: source.lastMessageAt } : {}),
|
|
823
1094
|
transport: source.transport || 'shared-app-server',
|
|
824
1095
|
readOnly: source.readOnly === true,
|
|
825
1096
|
readOnlyReason: source.readOnlyReason || '',
|
|
@@ -841,7 +1112,12 @@ function normalizeTurn(turn) {
|
|
|
841
1112
|
completedAt: source.completedAt || null,
|
|
842
1113
|
durationMs: source.durationMs ?? null,
|
|
843
1114
|
error: source.error ?? null,
|
|
1115
|
+
runtimeNotice: source.runtimeNotice ?? null,
|
|
844
1116
|
...(source.itemPage ? { itemPage: source.itemPage } : {}),
|
|
1117
|
+
...(source.roomMessage ? { roomMessage: true } : {}),
|
|
1118
|
+
...(source.deleted ? { deleted: true } : {}),
|
|
1119
|
+
...(source.historyOnly ? { historyOnly: true } : {}),
|
|
1120
|
+
...(source.cacheStub ? { cacheStub: true } : {}),
|
|
845
1121
|
entries,
|
|
846
1122
|
};
|
|
847
1123
|
}
|
|
@@ -849,6 +1125,10 @@ function normalizeTurn(turn) {
|
|
|
849
1125
|
function normalizeItem(item) {
|
|
850
1126
|
if (!item || typeof item !== 'object') return null;
|
|
851
1127
|
|
|
1128
|
+
if (item.type === 'approval') {
|
|
1129
|
+
return { ...item, type: 'activity', kind: 'approval', title: item.params?.message || '需要你的批准' };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
852
1132
|
if (item.type === 'contextCompaction') {
|
|
853
1133
|
return {
|
|
854
1134
|
type: 'activity',
|
|
@@ -858,24 +1138,21 @@ function normalizeItem(item) {
|
|
|
858
1138
|
};
|
|
859
1139
|
}
|
|
860
1140
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
type: 'message',
|
|
868
|
-
id: item.id || '',
|
|
869
|
-
role: 'user',
|
|
870
|
-
source: 'remote',
|
|
871
|
-
...splitUploadedImages(text),
|
|
872
|
-
};
|
|
873
|
-
}
|
|
1141
|
+
const delegated = appServerEvents.toolDelegation(item);
|
|
1142
|
+
if (delegated) {
|
|
1143
|
+
return {
|
|
1144
|
+
type: 'message', id: item.id || '', role: 'user', source: 'remote',
|
|
1145
|
+
delegation: delegationSource(delegated), ...splitUploadedImages(delegated.input),
|
|
1146
|
+
};
|
|
874
1147
|
}
|
|
875
1148
|
|
|
876
1149
|
if (item.type === 'userMessage') {
|
|
877
|
-
const { text, files } = extractUserText(item.content);
|
|
878
|
-
const attachments = [...normalizeAttachments(item.content), ...files
|
|
1150
|
+
const { text, files, delegation } = extractUserText(item.content);
|
|
1151
|
+
const attachments = [...normalizeAttachments(item.content), ...files.map(file => ({
|
|
1152
|
+
...file,
|
|
1153
|
+
remotePath: rememberLocalFile(file.path)?.id || '',
|
|
1154
|
+
previewRemotePath: file.previewPath ? rememberLocalFile(file.previewPath)?.id || '' : '',
|
|
1155
|
+
}))];
|
|
879
1156
|
if (!text && !attachments.length) return null;
|
|
880
1157
|
return {
|
|
881
1158
|
clientId: item.clientId,
|
|
@@ -884,6 +1161,7 @@ function normalizeItem(item) {
|
|
|
884
1161
|
role: 'user',
|
|
885
1162
|
text,
|
|
886
1163
|
attachments,
|
|
1164
|
+
...(delegation ? { delegation: delegationSource(delegation) } : {}),
|
|
887
1165
|
};
|
|
888
1166
|
}
|
|
889
1167
|
|
|
@@ -896,6 +1174,7 @@ function normalizeItem(item) {
|
|
|
896
1174
|
delivery: item.delivery,
|
|
897
1175
|
questions: item.questions,
|
|
898
1176
|
text: item.text || '',
|
|
1177
|
+
attachments: normalizeAttachments(item.attachments),
|
|
899
1178
|
imageAssets: markdownImageAssets(item.text || ''),
|
|
900
1179
|
fileAssets: markdownFileAssets(item.text || ''),
|
|
901
1180
|
};
|
|
@@ -969,7 +1248,7 @@ function normalizeItem(item) {
|
|
|
969
1248
|
title: '文件修改',
|
|
970
1249
|
titleOnly: true,
|
|
971
1250
|
status: statusText(item.status),
|
|
972
|
-
changes:
|
|
1251
|
+
changes: fileChanges.summarize(item.changes),
|
|
973
1252
|
};
|
|
974
1253
|
}
|
|
975
1254
|
|
|
@@ -989,16 +1268,6 @@ function normalizeItem(item) {
|
|
|
989
1268
|
const image = typeof item.path === 'string' && item.path
|
|
990
1269
|
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
991
1270
|
: 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
1271
|
return {
|
|
1003
1272
|
type: 'activity',
|
|
1004
1273
|
id: item.id || '',
|
|
@@ -1011,9 +1280,11 @@ function normalizeItem(item) {
|
|
|
1011
1280
|
}
|
|
1012
1281
|
|
|
1013
1282
|
if (item.type === 'imageGeneration') {
|
|
1283
|
+
const result = typeof item.result === 'string' ? item.result.trim() : '';
|
|
1014
1284
|
const image = typeof item.savedPath === 'string' && item.savedPath
|
|
1015
1285
|
? normalizeAttachment({ type: 'localImage', path: item.savedPath, name: '生成的图片' })
|
|
1016
|
-
:
|
|
1286
|
+
: result ? normalizeAttachment({ type: 'image', name: '生成的图片',
|
|
1287
|
+
url: result.startsWith('data:image/') ? result : `data:image/png;base64,${result}` }) : null;
|
|
1017
1288
|
return {
|
|
1018
1289
|
type: 'activity',
|
|
1019
1290
|
id: item.id || '',
|
|
@@ -1088,7 +1359,7 @@ function normalizeAttachments(content) {
|
|
|
1088
1359
|
else items.push({ type: 'localImage', path: imagePath });
|
|
1089
1360
|
}
|
|
1090
1361
|
const seen = new Set();
|
|
1091
|
-
return items.map(normalizeAttachment).filter(item => {
|
|
1362
|
+
return items.map(item => normalizeAttachment(item)).filter(item => {
|
|
1092
1363
|
if (!item.url) return true;
|
|
1093
1364
|
if (seen.has(item.url)) return false;
|
|
1094
1365
|
seen.add(item.url); return true;
|
|
@@ -1099,23 +1370,21 @@ function markdownImageAssets(text) {
|
|
|
1099
1370
|
const assets = Object.create(null);
|
|
1100
1371
|
function visit(tokens) {
|
|
1101
1372
|
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;
|
|
1373
|
+
const value = token.type === 'image' ? token.attrGet('src')
|
|
1374
|
+
: token.type === 'link_open' && !token.meta?.fileCitation ? token.attrGet('href') : null;
|
|
1104
1375
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1105
|
-
|
|
1106
|
-
try { localPath = decodeURIComponent(value); } catch { continue; }
|
|
1376
|
+
const localPath = fileCitations.localReference(value)?.path;
|
|
1107
1377
|
// Only paths actually referenced by Desktop messages become accessible.
|
|
1108
1378
|
// Do not expose a client-supplied filesystem path endpoint or UNC shares.
|
|
1109
|
-
if (
|
|
1110
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1379
|
+
if (localPath && /^[a-z]:[\\/]/i.test(localPath)) {
|
|
1111
1380
|
const asset = normalizeAttachment({ type: 'localImage', path: localPath });
|
|
1112
1381
|
if (asset.available) assets[value] = asset;
|
|
1113
1382
|
}
|
|
1114
1383
|
}
|
|
1115
|
-
if (token.children && token.type !== 'image') visit(token.children);
|
|
1384
|
+
if (token.children && token.type !== 'image') visit(token.children);
|
|
1116
1385
|
}
|
|
1117
1386
|
}
|
|
1118
|
-
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1387
|
+
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1119
1388
|
return assets;
|
|
1120
1389
|
}
|
|
1121
1390
|
|
|
@@ -1126,20 +1395,17 @@ function markdownFileAssets(text) {
|
|
|
1126
1395
|
if (token.type === 'link_open') {
|
|
1127
1396
|
const value = token.attrGet('href');
|
|
1128
1397
|
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+)?$/, '');
|
|
1398
|
+
const localPath = fileCitations.localReference(value)?.path;
|
|
1399
|
+
if (localPath && /^[a-z]:[\\/]/i.test(localPath)) {
|
|
1134
1400
|
const asset = rememberLocalFile(localPath);
|
|
1135
1401
|
if (asset) assets[value] = { remotePath: asset.id, name: asset.name, size: asset.size };
|
|
1136
1402
|
}
|
|
1137
1403
|
}
|
|
1138
1404
|
}
|
|
1139
|
-
if (token.children && token.type !== 'image') visit(token.children);
|
|
1405
|
+
if (token.children && token.type !== 'image') visit(token.children);
|
|
1140
1406
|
}
|
|
1141
1407
|
}
|
|
1142
|
-
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1408
|
+
visit(imageMarkdown.parse(text, { allowFileCitations: true }));
|
|
1143
1409
|
return assets;
|
|
1144
1410
|
}
|
|
1145
1411
|
|
|
@@ -1147,12 +1413,12 @@ function rememberLocalFile(filePath) {
|
|
|
1147
1413
|
try {
|
|
1148
1414
|
const resolved = path.resolve(filePath);
|
|
1149
1415
|
const stat = fs.statSync(resolved);
|
|
1150
|
-
if (!stat.isFile()) return null;
|
|
1416
|
+
if (!stat.isFile()) return null;
|
|
1151
1417
|
const id = crypto.createHmac('sha256', fileCapabilitySecret)
|
|
1152
1418
|
.update(resolved).update('\0').update(String(stat.size)).update('\0').update(String(stat.mtimeMs)).digest('hex');
|
|
1153
1419
|
if (!fileCache.has(id)) {
|
|
1154
1420
|
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,
|
|
1421
|
+
fileCache.set(id, { id, path: resolved, name: path.basename(resolved), size: stat.size, revision: fileRevision(stat) });
|
|
1156
1422
|
}
|
|
1157
1423
|
return fileCache.get(id);
|
|
1158
1424
|
} catch {
|
|
@@ -1183,12 +1449,29 @@ function splitUploadedImages(text) {
|
|
|
1183
1449
|
return { text: visibleText, attachments, imageAssets: markdownImageAssets(visibleText) };
|
|
1184
1450
|
}
|
|
1185
1451
|
|
|
1186
|
-
function
|
|
1452
|
+
async function externalDownloadAsset(value, recovery = '') {
|
|
1453
|
+
const match = /^([a-f0-9]{64})(?:\?recovery=([A-Za-z0-9_-]{1,4096}))?$/.exec(value || '');
|
|
1454
|
+
if (!match) throw new RequestError(400, '无效的外部文件标识。');
|
|
1455
|
+
const asset = attachmentCache.get(match[1]) || dotsImages.restore(match[1], match[2] || recovery || '');
|
|
1456
|
+
if (!asset?.dotsFile) throw new RequestError(404, '外部文件授权已失效,请重新读取对话。');
|
|
1457
|
+
await dotsImages.authorization(asset.identity);
|
|
1458
|
+
return asset;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function workstationCopyReference(copy, asset) {
|
|
1462
|
+
const id = crypto.createHmac('sha256', fileCapabilitySecret).update(JSON.stringify([asset.id, copy.path, copy.revision])).digest('hex');
|
|
1463
|
+
fileCache.set(id, { ...copy, id, externalIdentity: asset.identity });
|
|
1464
|
+
while (fileCache.size > MAX_CACHED_FILES) fileCache.delete(fileCache.keys().next().value);
|
|
1465
|
+
return { remotePath: id, path: copy.path, name: copy.name, size: copy.size, mimeType: copy.mimeType };
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function normalizeAttachment(item, identity) {
|
|
1187
1469
|
const name = item.name || item.fileName || (typeof item.path === 'string' ? path.basename(item.path) : '');
|
|
1188
1470
|
let cached = null;
|
|
1189
1471
|
const isImage = ['image', 'input_image', 'localImage'].includes(item.type);
|
|
1472
|
+
if (isImage || item.type === 'file') cached = dotsImages.remember(item, identity);
|
|
1190
1473
|
if (isImage && typeof item.path === 'string') {
|
|
1191
|
-
cached = cacheLocalImage(item.path, name);
|
|
1474
|
+
cached = cacheLocalImage(item.path, name) || cached;
|
|
1192
1475
|
}
|
|
1193
1476
|
if (!cached) {
|
|
1194
1477
|
const dataUrl = typeof item.url === 'string' && item.url.startsWith('data:image/')
|
|
@@ -1198,10 +1481,12 @@ function normalizeAttachment(item) {
|
|
|
1198
1481
|
}
|
|
1199
1482
|
return {
|
|
1200
1483
|
type: isImage ? 'image' : item.type || 'attachment',
|
|
1201
|
-
name,
|
|
1484
|
+
name: cached?.name || name,
|
|
1202
1485
|
mimeType: cached?.mimeType || item.mimeType || '',
|
|
1486
|
+
...(cached?.size ? { size: cached.size } : {}),
|
|
1487
|
+
...(cached?.dotsFile ? { networkFile: true } : {}),
|
|
1203
1488
|
available: Boolean(cached),
|
|
1204
|
-
remotePath: cached ? cached.id : '',
|
|
1489
|
+
remotePath: cached ? (isImage ? cached.id : `${cached.id}?recovery=${cached.recoveryToken}`) : '',
|
|
1205
1490
|
recoveryToken: cached?.recoveryToken || '',
|
|
1206
1491
|
url: '',
|
|
1207
1492
|
thumbnailUrl: '',
|
|
@@ -1318,33 +1603,23 @@ function extractUserText(content) {
|
|
|
1318
1603
|
.filter(value => !value.startsWith('The next image is untrusted page evidence'))
|
|
1319
1604
|
.join('\n\n')
|
|
1320
1605
|
.trim();
|
|
1606
|
+
const delegation = appServerEvents.parseCodexDelegation(text);
|
|
1607
|
+
if (delegation) text = delegation.input;
|
|
1321
1608
|
const mentioned = fileMentions.parse(text);
|
|
1322
1609
|
const imagePaths = new Set([
|
|
1323
1610
|
...content.filter(item => ['image', 'input_image', 'localImage'].includes(item?.type) && item.path).map(item => item.path),
|
|
1324
1611
|
...[...mentioned.text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/g)].map(match => match[1]),
|
|
1325
1612
|
].map(value => path.resolve(value).toLowerCase()));
|
|
1326
1613
|
text = mentioned.text;
|
|
1327
|
-
text = text.replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '').trim();
|
|
1328
1614
|
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();
|
|
1615
|
+
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())), delegation };
|
|
1343
1616
|
}
|
|
1344
1617
|
|
|
1345
|
-
function
|
|
1346
|
-
|
|
1347
|
-
return
|
|
1618
|
+
function delegationSource(delegation) {
|
|
1619
|
+
const dots = dotsServer.threadSources.get(delegation.sourceThreadId) === 'aeon';
|
|
1620
|
+
return { sourceThreadId: delegation.sourceThreadId, sourceKind: dots ? 'dots' : 'codex',
|
|
1621
|
+
...(dots ? { sourceName: dotsServer.profileName(delegation.sourceThreadId),
|
|
1622
|
+
sourceAvatar: dotsServer.profileAvatar(delegation.sourceThreadId) } : {}) };
|
|
1348
1623
|
}
|
|
1349
1624
|
|
|
1350
1625
|
function summaryText(value) {
|
|
@@ -1370,7 +1645,7 @@ function sendBuffer(response, statusCode, contentType, value) {
|
|
|
1370
1645
|
'Content-Length': value.length,
|
|
1371
1646
|
'Cache-Control': 'no-store',
|
|
1372
1647
|
'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:",
|
|
1648
|
+
'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
1649
|
});
|
|
1375
1650
|
response.end(value);
|
|
1376
1651
|
}
|