@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.
Files changed (68) hide show
  1. package/README.md +80 -67
  2. package/dist/bin/codex-remote.mjs +303 -239
  3. package/dist/bin/self-relay-server.mjs +2 -0
  4. package/dist/shared/app-server-events.cjs +102 -16
  5. package/dist/shared/code-browser.cjs +159 -0
  6. package/dist/shared/file-changes.cjs +31 -0
  7. package/dist/shared/file-citations.cjs +117 -82
  8. package/dist/shared/file-mentions.cjs +80 -35
  9. package/dist/shared/message-delivery.cjs +38 -0
  10. package/dist/shared/p2p-file-channel.cjs +74 -0
  11. package/dist/shared/server-requests.cjs +149 -41
  12. package/dist/shared/service-endpoints.cjs +11 -11
  13. package/dist/shared/timeline-sync.cjs +173 -156
  14. package/dist/skills/code-relax/SKILL.md +2 -1
  15. package/dist/src/bounded-process-log.mjs +63 -0
  16. package/dist/src/celsius-client.mjs +107 -0
  17. package/dist/src/code-browser.mjs +101 -0
  18. package/dist/src/desktop-launcher.mjs +30 -30
  19. package/dist/src/dots-approval-timeline.mjs +99 -0
  20. package/dist/src/dots-avatar.mjs +42 -0
  21. package/dist/src/dots-chat-projection.mjs +50 -0
  22. package/dist/src/dots-chat-server.mjs +412 -0
  23. package/dist/src/dots-message-images.mjs +319 -0
  24. package/dist/src/dots-profile.mjs +77 -0
  25. package/dist/src/dots-room-activity.mjs +94 -0
  26. package/dist/src/dots-room-client.mjs +135 -0
  27. package/dist/src/dots-room-projection.mjs +94 -0
  28. package/dist/src/dots-schedules.mjs +66 -0
  29. package/dist/src/dots-summary-tasks.mjs +32 -0
  30. package/dist/src/dots-task-catalog.mjs +411 -0
  31. package/dist/src/file-transfer-range.mjs +22 -0
  32. package/dist/src/file-upload-session.mjs +130 -124
  33. package/dist/src/hosted-history-app-server.mjs +103 -0
  34. package/dist/src/hosted-history-cache.mjs +113 -0
  35. package/dist/src/lan-socket-server.mjs +205 -160
  36. package/dist/src/managed-app-server.mjs +241 -238
  37. package/dist/src/message-upload-files.mjs +74 -0
  38. package/dist/src/platform/windows/activate-desktop.ps1 +48 -0
  39. package/dist/src/platform/windows/codex-runtime.mjs +253 -251
  40. package/dist/src/platform/windows/desktop-host.mjs +38 -32
  41. package/dist/src/platform/windows/desktop-info.ps1 +2 -2
  42. package/dist/src/platform/windows/desktop-project-registration.mjs +153 -0
  43. package/dist/src/platform/windows/dots-profile.mjs +49 -0
  44. package/dist/src/platform/windows/launch-worker.mjs +41 -42
  45. package/dist/src/platform/windows/project-locations.mjs +16 -0
  46. package/dist/src/project-create.mjs +119 -0
  47. package/dist/src/project-management.mjs +69 -0
  48. package/dist/src/self-relay/account-connections.mjs +52 -0
  49. package/dist/src/self-relay/admin-page.mjs +80 -80
  50. package/dist/src/self-relay/admin-style.mjs +14 -9
  51. package/dist/src/self-relay/connector.mjs +23 -5
  52. package/dist/src/self-relay/email-accounts.mjs +35 -10
  53. package/dist/src/self-relay/file-wire.mjs +13 -0
  54. package/dist/src/self-relay/lifecycle.mjs +22 -4
  55. package/dist/src/self-relay/long-pairing.mjs +40 -0
  56. package/dist/src/self-relay/p2p-probe.mjs +152 -123
  57. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -0
  58. package/dist/src/self-relay/server.mjs +922 -849
  59. package/dist/src/server.mjs +474 -199
  60. package/dist/src/service-control.mjs +13 -3
  61. package/dist/src/service-doctor.mjs +72 -61
  62. package/dist/src/service-lifecycle.mjs +51 -43
  63. package/dist/src/shared-app-server.mjs +166 -107
  64. package/dist/src/shared-catalog.mjs +132 -40
  65. package/dist/src/shared-thread-stream.mjs +103 -25
  66. package/dist/src/usage-reset.mjs +17 -0
  67. package/dist/src/workstation-downloads.mjs +120 -0
  68. package/package.json +1 -1
@@ -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 = 16 * 1024 * 1024;
34
- const MAX_FILE_DOWNLOAD_BYTES = 16 * 1024 * 1024;
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 downloadOffset(rawOffset, size) {
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 && sharedServer) sharedServer.requireWritable(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 (url.searchParams.get('variant') === 'thumbnail') {
247
- const asset = await attachmentThumbnail(image);
248
- sendJson(response, 200, { url: `data:${asset.mimeType};base64,${asset.buffer.toString('base64')}` });
249
- return;
250
- }
251
- const size = image.buffer.length;
252
- if (size > MAX_ATTACHMENT_BYTES) throw new RequestError(413, '图片超过下载上限(16 MiB),无法下载。');
253
- if (!size) throw new RequestError(400, '图片为空,无法下载。');
254
- const offset = downloadOffset(url.searchParams.get('offset'), size);
255
- const data = image.buffer.subarray(offset, Math.min(size, offset + FILE_DOWNLOAD_CHUNK_BYTES));
256
- sendJson(response, 200, { name: image.name || '图片', size, offset, mimeType: image.mimeType,
257
- dataBase64: data.toString('base64') });
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
- const offset = downloadOffset(url.searchParams.get('offset'), file.size);
266
- let handle;
267
- try { handle = fs.openSync(file.path, 'r'); }
268
- catch (error) {
269
- if (error.code === 'ENOENT') throw new RequestError(404, '电脑上的文件已不存在。');
270
- throw error;
271
- }
272
- try {
273
- const validate = () => {
274
- const stat = fs.fstatSync(handle);
275
- if (!stat.isFile() || stat.size !== file.size || stat.mtimeMs !== file.mtimeMs) {
276
- fileCache.delete(fileMatch[1]);
277
- throw new RequestError(409, '电脑上的文件已发生变化,请重新读取对话。');
278
- }
279
- if (stat.size > MAX_FILE_DOWNLOAD_BYTES) throw new RequestError(413, '文件超过下载上限(16 MiB),无法下载。');
280
- if (!stat.size) throw new RequestError(400, '文件为空,无法下载。');
281
- };
282
- validate();
283
- const data = Buffer.alloc(Math.min(FILE_DOWNLOAD_CHUNK_BYTES, file.size - offset));
284
- let bytesRead = 0;
285
- while (bytesRead < data.length) {
286
- const count = fs.readSync(handle, data, bytesRead, data.length - bytesRead, offset + bytesRead);
287
- if (!count) throw new RequestError(409, '电脑上的文件读取不完整,请重新读取对话。');
288
- bytesRead += count;
289
- }
290
- validate();
291
- sendJson(response, 200, { name: file.name, size: file.size, offset, dataBase64: data.toString('base64') });
292
- } finally { fs.closeSync(handle); }
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
- if (url.searchParams.has('hostId') && url.searchParams.get('hostId') !== 'local') {
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 sharedServer.models());
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
- sendJson(response, 200, await sharedCatalog.threads(limit, archived));
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/turn-events') {
346
- const execution = sharedServer;
347
- response.startEvents();
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
- execution.on('notification', listener);
357
- execution.on('disconnected', disconnected);
358
- response.once('close', () => {
359
- execution.off('notification', listener);
360
- execution.off('disconnected', disconnected);
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 sharedServer.connect();
369
- const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
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 sharedServer.request('thread/turns/list', {
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 nativeId = nativeProjects.find(item => item.id === project.projectId)?.id;
401
- if (!nativeId) throw new RequestError(404, 'Codex 内核中没有找到对应项目,请刷新项目列表。');
402
- target = { projectId:nativeId, cwd:project.path };
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 execution = sharedServer;
425
- const timeline = normalizeSharedTimeline(await execution.timeline(threadId, sharedHistoryPageOptions()));
426
- if (request.socketRequest) {
427
- response.on('progress', () => execution.keepSnapshotAlive(threadId, timeline.sync));
428
- response.once('close', () => execution.keepSnapshotAlive(threadId, timeline.sync));
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 sharedServer.respondApproval(threadId, body)); return;
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') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
447
- ? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
448
- await streamSharedThread(response, threadId, sharedServer,
449
- normalizeSharedTimeline, requestedSync);
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 sharedServer.readContext(threadId);
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
- sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'), sharedHistoryPageOptions())));
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
- await requireExecutionWritable(id);
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 sharedServer.settings(id, body.model, body.thinking, body.serviceTier));
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
- await requireExecutionWritable(id);
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 sharedServer.permissions(id, body.mode));
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 sharedServer.request(`thread/${archiveMatch[2]}`, { threadId });
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 sharedServer.interrupt(threadId, body.turnId);
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 !== 'local') throw new RequestError(410, '此对话的连接方式已停用。', { code: 'unsupported_control' });
542
- const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
543
- sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
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
- const status = error instanceof RequestError || error instanceof ThreadWriterConflictError ? (error.statusCode || 409) : 502;
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
- const model = body.model.trim();
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: 'local', transport: 'shared-app-server' } });
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
- historyView: 'item-pages-v1',
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
- if (item.type === 'functionCallOutput'
862
- && item.namespace === 'codex_app'
863
- && ['create_thread', 'send_message_to_thread'].includes(item.name)) {
864
- const text = extractDelegatedInput(delegatedOutputText(item.output));
865
- if (text) {
866
- return {
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: Array.isArray(item.changes) ? item.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
- : null;
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
- let localPath;
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 (/^\/?[a-z]:[\\/]/i.test(localPath)) {
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
- let localPath;
1130
- try { localPath = decodeURIComponent(value); } catch { localPath = ''; }
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, mtimeMs: stat.mtimeMs });
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 normalizeAttachment(item) {
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(/&lt;/g, '<')
1338
- .replace(/&gt;/g, '>')
1339
- .replace(/&quot;/g, '"')
1340
- .replace(/&#39;/g, "'")
1341
- .replace(/&amp;/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 delegatedOutputText(output) {
1346
- if (typeof output === 'string') return output;
1347
- return output && typeof output.text === 'string' ? output.text : '';
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
  }