@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.51

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 (89) hide show
  1. package/README.md +105 -105
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +403 -403
  4. package/dist/bin/publish-android-release.mjs +63 -63
  5. package/dist/bin/release-upload-server.mjs +190 -190
  6. package/dist/build-platform.json +1 -1
  7. package/dist/get-shared-service-url.ps1 +14 -14
  8. package/dist/shared/code-browser.cjs +159 -159
  9. package/dist/shared/file-changes.cjs +31 -31
  10. package/dist/shared/file-citations.cjs +117 -117
  11. package/dist/shared/file-mentions.cjs +92 -92
  12. package/dist/shared/message-delivery.cjs +115 -115
  13. package/dist/shared/p2p-file-channel.cjs +87 -87
  14. package/dist/shared/server-requests.cjs +311 -311
  15. package/dist/shared/timeline-sync.cjs +173 -173
  16. package/dist/skills/code-relax/SKILL.md +22 -22
  17. package/dist/src/bounded-process-log.mjs +63 -63
  18. package/dist/src/celsius-client.mjs +107 -107
  19. package/dist/src/dots-approval-timeline.mjs +99 -99
  20. package/dist/src/dots-avatar.mjs +42 -42
  21. package/dist/src/dots-chat-projection.mjs +50 -50
  22. package/dist/src/dots-chat-server.mjs +483 -483
  23. package/dist/src/dots-message-images.mjs +319 -319
  24. package/dist/src/dots-profile.mjs +77 -77
  25. package/dist/src/dots-room-activity.mjs +172 -172
  26. package/dist/src/dots-room-client.mjs +135 -135
  27. package/dist/src/dots-room-projection.mjs +94 -94
  28. package/dist/src/dots-schedules.mjs +66 -66
  29. package/dist/src/dots-summary-tasks.mjs +32 -32
  30. package/dist/src/dots-task-catalog.mjs +430 -430
  31. package/dist/src/file-transfer-range.mjs +22 -22
  32. package/dist/src/file-upload-session.mjs +130 -130
  33. package/dist/src/hosted-history-app-server.mjs +103 -103
  34. package/dist/src/hosted-history-cache.mjs +113 -113
  35. package/dist/src/lan-socket-server.mjs +266 -266
  36. package/dist/src/managed-app-server.mjs +373 -373
  37. package/dist/src/managed-endpoint.mjs +25 -25
  38. package/dist/src/message-upload-files.mjs +109 -109
  39. package/dist/src/onboarding.mjs +12 -12
  40. package/dist/src/platform/README.md +1 -1
  41. package/dist/src/platform/codex-protocol.mjs +46 -46
  42. package/dist/src/platform/desktop-project-store.mjs +208 -208
  43. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  44. package/dist/src/platform/linux/process.mjs +53 -53
  45. package/dist/src/platform/linux/system.mjs +50 -50
  46. package/dist/src/platform/linux/user-environment.mjs +9 -9
  47. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  48. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  49. package/dist/src/platform/macos/user-environment.mjs +9 -9
  50. package/dist/src/platform/native-package.mjs +58 -58
  51. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  52. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  53. package/dist/src/platform/windows/credential-store.mjs +27 -27
  54. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  55. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  56. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  57. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  58. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  59. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  60. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  61. package/dist/src/platform/windows/project-locations.mjs +16 -16
  62. package/dist/src/platform/windows/service-host.mjs +35 -35
  63. package/dist/src/platform/windows/user-environment.mjs +52 -52
  64. package/dist/src/project-management.mjs +58 -58
  65. package/dist/src/runtime-discovery.mjs +47 -47
  66. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  67. package/dist/src/self-relay/admin-page.mjs +80 -80
  68. package/dist/src/self-relay/admin-style.mjs +14 -14
  69. package/dist/src/self-relay/connector.mjs +439 -439
  70. package/dist/src/self-relay/demo.mjs +31 -31
  71. package/dist/src/self-relay/diagnostics.mjs +54 -54
  72. package/dist/src/self-relay/email-accounts.mjs +183 -183
  73. package/dist/src/self-relay/file-wire.mjs +13 -13
  74. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  75. package/dist/src/self-relay/privacy-page.mjs +26 -26
  76. package/dist/src/self-relay/public-releases.mjs +67 -67
  77. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  78. package/dist/src/self-relay/usage-meter.mjs +141 -141
  79. package/dist/src/server.mjs +5 -4
  80. package/dist/src/service-doctor.mjs +148 -148
  81. package/dist/src/service-install.mjs +97 -97
  82. package/dist/src/service-lifecycle.mjs +139 -139
  83. package/dist/src/shared-app-server.mjs +666 -666
  84. package/dist/src/shared-catalog.mjs +132 -132
  85. package/dist/src/shared-thread-stream.mjs +165 -165
  86. package/dist/src/usage-reset.mjs +17 -17
  87. package/dist/src/workstation-downloads.mjs +120 -120
  88. package/package.json +4 -4
  89. package/tools/postinstall.mjs +186 -186
@@ -1,92 +1,92 @@
1
- (function (root, factory) {
2
- const api = factory();
3
- if (typeof module === 'object' && module.exports) module.exports = api;
4
- else root.CodexFileMentions = api;
5
- })(globalThis, function () {
6
- const heading = '# Files mentioned by the user:';
7
- const pastedHeading = '# Files pasted by the user:';
8
- const request = '## My request:';
9
-
10
- function stripBrowserContext(value) {
11
- const context = /^\s*<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>\s*/;
12
- if (!context.test(value)) return value;
13
- return value.replace(context, '').replace(/^## My request:[ \t]*(?:\r?\n|$)/, '').trim();
14
- }
15
-
16
- function parse(value) {
17
- const source = stripBrowserContext(typeof value === 'string' ? value : '');
18
- const firstHeading = [heading, pastedHeading].find(value => source.startsWith(value));
19
- if (!firstHeading) return { text: source, files: [] };
20
- const marker = source.indexOf(`\n${request}`, firstHeading.length);
21
- if (marker < 0) return { text: source, files: [] };
22
- const files = [];
23
- let pasted = firstHeading === pastedHeading;
24
- for (const line of source.slice(firstHeading.length, marker).split(/\r?\n/)) {
25
- if (line === heading || line === pastedHeading) { pasted = line === pastedHeading; continue; }
26
- if (line.startsWith('Uploaded file: ')) {
27
- try {
28
- const value = JSON.parse(line.slice('Uploaded file: '.length));
29
- const fileId = /^sediment:\/\/([A-Za-z0-9_-]{1,256})$/.exec(value.pointer)?.[1];
30
- if (fileId && typeof value.fileName === 'string' && value.fileName) files.push({ type: 'file', name: value.fileName, fileId });
31
- } catch { /* Leave unrecognized text untouched when no attachment was parsed. */ }
32
- continue;
33
- }
34
- const media = /^<!-- code-relax-media:(.+) -->$/.exec(line);
35
- if (media && files.length) {
36
- try { Object.assign(files.at(-1), mediaMetadata(JSON.parse(media[1]))); } catch {}
37
- continue;
38
- }
39
- if (!pasted && line.trim() === 'Image attachment: true' && files.length) {
40
- files.at(-1).isImageAttachment = true;
41
- continue;
42
- }
43
- const match = /^## (.+): ((?:[A-Za-z]:[\\/]|\/)[^\r\n]+)$/.exec(line);
44
- if (!match) continue;
45
- let name = match[1];
46
- if (name.startsWith('"')) {
47
- try { name = JSON.parse(name); } catch { continue; }
48
- }
49
- if (typeof name !== 'string' || !name) continue;
50
- files.push({ type: 'file', name: pasted ? match[2].split(/[\\/]/).at(-1) : name, path: match[2] });
51
- }
52
- if (!files.length) return { text: source, files: [] };
53
- return { text: stripBrowserContext(source.slice(marker + request.length + 1).trim()), files };
54
- }
55
-
56
- function wrap(prompt, files) {
57
- if (!files?.length) return prompt;
58
- return `${heading}\n${files.map(file => {
59
- const media = mediaMetadata(file);
60
- return `\n## ${JSON.stringify(file.name)}: ${file.path}\n${media.mediaKind ? `<!-- code-relax-media:${JSON.stringify(media).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')} -->\n` : ''}`;
61
- }).join('')}\n${request}\n${prompt || '请查看上述文件。'}`;
62
- }
63
-
64
- function mediaMetadata(value) {
65
- if (!value || !['image', 'video'].includes(value.mediaKind)) return {};
66
- const result = { mediaKind: value.mediaKind };
67
- for (const field of ['sha256', 'previewSha256']) if (/^[a-f0-9]{64}$/.test(value[field] || '')) result[field] = value[field];
68
- if (typeof value.previewPath === 'string' && /^(?:[A-Za-z]:[\\/]|\/(?!\/))[^\r\n]+$/.test(value.previewPath)) result.previewPath = value.previewPath;
69
- if (/^(?:image|video)\/[a-z0-9.+-]+$/i.test(value.mimeType || '')) result.mimeType = value.mimeType;
70
- for (const field of ['size', 'duration']) if (Number.isFinite(value[field]) && value[field] > 0) result[field] = value[field];
71
- return result;
72
- }
73
-
74
- // Formats accepted as native Codex image inputs; other files remain file references.
75
- function isNativeImage(file) {
76
- return file?.mediaKind === 'image' && /\.(png|jpe?g|webp|gif)$/i.test(file.name || file.path || '');
77
- }
78
- function attachment(file) {
79
- return isNativeImage(file) ? { type: 'image', name: file.name, remotePath: file.path,
80
- originalUpload: true, localImageId: file.sha256, localThumbnailId: file.previewSha256 }
81
- : { ...file, type: 'file' };
82
- }
83
- function originalInputs(body) {
84
- const paths = body.originalImages || [];
85
- if (!Array.isArray(paths) || paths.length + (body.images || []).length > 4
86
- || paths.some(value => typeof value !== 'string' || !/^(?:[A-Za-z]:[\\/]|\/(?!\/))[^\r\n]+\.(png|jpe?g|webp|gif)$/i.test(value))) {
87
- throw new Error('原图路径无效或每条消息图片超过 4 张');
88
- }
89
- return paths.map(path => ({ type: 'localImage', path }));
90
- }
91
- return Object.freeze({ parse, wrap, mediaMetadata, isNativeImage, attachment, originalInputs });
92
- });
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ else root.CodexFileMentions = api;
5
+ })(globalThis, function () {
6
+ const heading = '# Files mentioned by the user:';
7
+ const pastedHeading = '# Files pasted by the user:';
8
+ const request = '## My request:';
9
+
10
+ function stripBrowserContext(value) {
11
+ const context = /^\s*<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>\s*/;
12
+ if (!context.test(value)) return value;
13
+ return value.replace(context, '').replace(/^## My request:[ \t]*(?:\r?\n|$)/, '').trim();
14
+ }
15
+
16
+ function parse(value) {
17
+ const source = stripBrowserContext(typeof value === 'string' ? value : '');
18
+ const firstHeading = [heading, pastedHeading].find(value => source.startsWith(value));
19
+ if (!firstHeading) return { text: source, files: [] };
20
+ const marker = source.indexOf(`\n${request}`, firstHeading.length);
21
+ if (marker < 0) return { text: source, files: [] };
22
+ const files = [];
23
+ let pasted = firstHeading === pastedHeading;
24
+ for (const line of source.slice(firstHeading.length, marker).split(/\r?\n/)) {
25
+ if (line === heading || line === pastedHeading) { pasted = line === pastedHeading; continue; }
26
+ if (line.startsWith('Uploaded file: ')) {
27
+ try {
28
+ const value = JSON.parse(line.slice('Uploaded file: '.length));
29
+ const fileId = /^sediment:\/\/([A-Za-z0-9_-]{1,256})$/.exec(value.pointer)?.[1];
30
+ if (fileId && typeof value.fileName === 'string' && value.fileName) files.push({ type: 'file', name: value.fileName, fileId });
31
+ } catch { /* Leave unrecognized text untouched when no attachment was parsed. */ }
32
+ continue;
33
+ }
34
+ const media = /^<!-- code-relax-media:(.+) -->$/.exec(line);
35
+ if (media && files.length) {
36
+ try { Object.assign(files.at(-1), mediaMetadata(JSON.parse(media[1]))); } catch {}
37
+ continue;
38
+ }
39
+ if (!pasted && line.trim() === 'Image attachment: true' && files.length) {
40
+ files.at(-1).isImageAttachment = true;
41
+ continue;
42
+ }
43
+ const match = /^## (.+): ((?:[A-Za-z]:[\\/]|\/)[^\r\n]+)$/.exec(line);
44
+ if (!match) continue;
45
+ let name = match[1];
46
+ if (name.startsWith('"')) {
47
+ try { name = JSON.parse(name); } catch { continue; }
48
+ }
49
+ if (typeof name !== 'string' || !name) continue;
50
+ files.push({ type: 'file', name: pasted ? match[2].split(/[\\/]/).at(-1) : name, path: match[2] });
51
+ }
52
+ if (!files.length) return { text: source, files: [] };
53
+ return { text: stripBrowserContext(source.slice(marker + request.length + 1).trim()), files };
54
+ }
55
+
56
+ function wrap(prompt, files) {
57
+ if (!files?.length) return prompt;
58
+ return `${heading}\n${files.map(file => {
59
+ const media = mediaMetadata(file);
60
+ return `\n## ${JSON.stringify(file.name)}: ${file.path}\n${media.mediaKind ? `<!-- code-relax-media:${JSON.stringify(media).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')} -->\n` : ''}`;
61
+ }).join('')}\n${request}\n${prompt || '请查看上述文件。'}`;
62
+ }
63
+
64
+ function mediaMetadata(value) {
65
+ if (!value || !['image', 'video'].includes(value.mediaKind)) return {};
66
+ const result = { mediaKind: value.mediaKind };
67
+ for (const field of ['sha256', 'previewSha256']) if (/^[a-f0-9]{64}$/.test(value[field] || '')) result[field] = value[field];
68
+ if (typeof value.previewPath === 'string' && /^(?:[A-Za-z]:[\\/]|\/(?!\/))[^\r\n]+$/.test(value.previewPath)) result.previewPath = value.previewPath;
69
+ if (/^(?:image|video)\/[a-z0-9.+-]+$/i.test(value.mimeType || '')) result.mimeType = value.mimeType;
70
+ for (const field of ['size', 'duration']) if (Number.isFinite(value[field]) && value[field] > 0) result[field] = value[field];
71
+ return result;
72
+ }
73
+
74
+ // Formats accepted as native Codex image inputs; other files remain file references.
75
+ function isNativeImage(file) {
76
+ return file?.mediaKind === 'image' && /\.(png|jpe?g|webp|gif)$/i.test(file.name || file.path || '');
77
+ }
78
+ function attachment(file) {
79
+ return isNativeImage(file) ? { type: 'image', name: file.name, remotePath: file.path,
80
+ originalUpload: true, localImageId: file.sha256, localThumbnailId: file.previewSha256 }
81
+ : { ...file, type: 'file' };
82
+ }
83
+ function originalInputs(body) {
84
+ const paths = body.originalImages || [];
85
+ if (!Array.isArray(paths) || paths.length + (body.images || []).length > 4
86
+ || paths.some(value => typeof value !== 'string' || !/^(?:[A-Za-z]:[\\/]|\/(?!\/))[^\r\n]+\.(png|jpe?g|webp|gif)$/i.test(value))) {
87
+ throw new Error('原图路径无效或每条消息图片超过 4 张');
88
+ }
89
+ return paths.map(path => ({ type: 'localImage', path }));
90
+ }
91
+ return Object.freeze({ parse, wrap, mediaMetadata, isNativeImage, attachment, originalInputs });
92
+ });
@@ -1,115 +1,115 @@
1
- (function (root, factory) {
2
- const value = factory();
3
- if (typeof module === 'object' && module.exports) module.exports = value;
4
- else root.CodexMessageDelivery = value;
5
- })(globalThis, function () {
6
- function rejectedBeforeSubmission(error) {
7
- return error?.rpcCode === -32602 || error?.rpcCode === -32600
8
- || error?.rpcCode != null && /^(no active turn to steer|expected active turn id `)/i.test(error.message || '');
9
- }
10
-
11
- // A UI/request deadline must not discard a later authoritative write receipt.
12
- function retainLateResult(pending, error) {
13
- const reject = pending.reject;
14
- error.deliveryResult = new Promise((resolve, reject) => {
15
- pending.resolve = resolve; pending.reject = reject;
16
- });
17
- error.deliveryResult.catch(() => {});
18
- reject(error);
19
- }
20
-
21
- class Store {
22
- constructor() { this.entries = new Map(); }
23
- key(scope, clientId) { return JSON.stringify([scope, clientId]); }
24
- beginCreation(scope, clientId) {
25
- const entry = this.entries.get(this.key(scope, clientId));
26
- let finish;
27
- entry.creationReady = new Promise(resolve => { finish = resolve; });
28
- return threadId => {
29
- if (threadId) entry.createdThreadId = threadId;
30
- finish();
31
- };
32
- }
33
- async creationBindings() {
34
- await Promise.all([...this.entries.values()].map(entry => entry.creationReady));
35
- return new Map([...this.entries].filter(([, entry]) => entry.createdThreadId)
36
- .map(([key, entry]) => [entry.createdThreadId, JSON.parse(key)[1]]));
37
- }
38
- status(scope, clientId) {
39
- const entry = this.entries.get(this.key(scope, clientId));
40
- return entry ? { state: entry.state, ...(entry.result ? { result: entry.result } : {}),
41
- ...(entry.createdThreadId ? { createdThreadId: entry.createdThreadId } : {}),
42
- ...(entry.error ? { error: entry.error.message } : {}) } : { state: 'unknown' };
43
- }
44
- async reconcile(scope, clientId, inspect) {
45
- const key = this.key(scope, clientId);
46
- let entry = this.entries.get(key);
47
- if (entry?.state === 'accepted' || entry?.state === 'not_sent') return this.status(scope, clientId);
48
- if (entry?.checking) return entry.checking;
49
- if (!entry) {
50
- // Fence a request which never arrived before checking history. A delayed
51
- // transport frame with this admission must not execute after a replacement.
52
- entry = { state: 'unknown', fenced: true };
53
- this.entries.set(key, entry);
54
- }
55
- const target = entry;
56
- target.checking = (async () => {
57
- if (target.work) await target.work.catch(() => {});
58
- if (target.lateWork) await target.lateWork;
59
- if (target.state === 'accepted' || target.state === 'not_sent') return this.status(scope, clientId);
60
- const result = await inspect();
61
- target.state = result ? 'accepted' : 'not_sent';
62
- target.fenced = true;
63
- if (result) target.result = result;
64
- delete target.error;
65
- return this.status(scope, clientId);
66
- })().finally(() => { delete target.checking; });
67
- return target.checking;
68
- }
69
- submit(scope, clientId, operation) {
70
- if (!/^[a-f0-9]{32}$/i.test(clientId || '')) return Promise.reject(Object.assign(new Error('消息确认标识无效'), { deliveryUnknown: false }));
71
- const key = this.key(scope, clientId), previous = this.entries.get(key);
72
- if (previous?.state === 'accepted') return Promise.resolve(previous.result);
73
- if (previous?.fenced) return Promise.reject(Object.assign(new Error('原发送请求已结束,请使用新的发送标识'), { deliveryUnknown: false }));
74
- if (previous?.state === 'sending') return previous.work;
75
- if (previous?.state === 'unknown') return Promise.reject(Object.assign(new Error(previous.error?.message || '原发送结果仍未确认'), { deliveryUnknown: true }));
76
- const entry = { state: 'sending' };
77
- this.entries.set(key, entry);
78
- entry.work = Promise.resolve().then(operation).then(result => {
79
- entry.state = 'accepted'; entry.result = result; return result;
80
- }, error => {
81
- entry.state = error.deliveryUnknown === false || error.details?.deliveryUnknown === false ? 'not_sent' : 'unknown';
82
- error.deliveryUnknown = entry.state !== 'not_sent';
83
- if (error.deliveryResult) entry.lateWork = error.deliveryResult.then(result => {
84
- entry.state = 'accepted'; entry.result = result; delete entry.error;
85
- }, lateError => {
86
- entry.state = rejectedBeforeSubmission(lateError) || lateError.deliveryUnknown === false ? 'not_sent' : 'unknown';
87
- entry.error = lateError;
88
- });
89
- entry.error = error; throw error;
90
- }).finally(() => { delete entry.work; });
91
- return entry.work;
92
- }
93
- }
94
- const normalizeId = value => String(value || '').replace(/-/g, '').toLowerCase();
95
- async function findMessage(request, threadId, clientId, anchorItemId = '') {
96
- let cursor;
97
- const seen = new Set();
98
- do {
99
- const page = await request('thread/items/list', { threadId, limit: 100, sortDirection: 'desc', ...(cursor ? { cursor } : {}) });
100
- if (!Array.isArray(page?.data)) throw new Error('发送核对失败:未取得权威历史');
101
- for (const entry of page.data) {
102
- const item = entry.item;
103
- if (item?.type === 'userMessage' && normalizeId(item.clientId) === normalizeId(clientId)) {
104
- return { data: { messageId: item.id, turnId: entry.turnId } };
105
- }
106
- if (anchorItemId && item?.id === anchorItemId) return null;
107
- }
108
- cursor = page.nextCursor;
109
- if (cursor && seen.has(cursor)) throw new Error('发送核对失败:历史游标未推进');
110
- if (cursor) seen.add(cursor);
111
- } while (cursor);
112
- return null;
113
- }
114
- return Object.freeze({ Store, rejectedBeforeSubmission, retainLateResult, findMessage, normalizeId });
115
- });
1
+ (function (root, factory) {
2
+ const value = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = value;
4
+ else root.CodexMessageDelivery = value;
5
+ })(globalThis, function () {
6
+ function rejectedBeforeSubmission(error) {
7
+ return error?.rpcCode === -32602 || error?.rpcCode === -32600
8
+ || error?.rpcCode != null && /^(no active turn to steer|expected active turn id `)/i.test(error.message || '');
9
+ }
10
+
11
+ // A UI/request deadline must not discard a later authoritative write receipt.
12
+ function retainLateResult(pending, error) {
13
+ const reject = pending.reject;
14
+ error.deliveryResult = new Promise((resolve, reject) => {
15
+ pending.resolve = resolve; pending.reject = reject;
16
+ });
17
+ error.deliveryResult.catch(() => {});
18
+ reject(error);
19
+ }
20
+
21
+ class Store {
22
+ constructor() { this.entries = new Map(); }
23
+ key(scope, clientId) { return JSON.stringify([scope, clientId]); }
24
+ beginCreation(scope, clientId) {
25
+ const entry = this.entries.get(this.key(scope, clientId));
26
+ let finish;
27
+ entry.creationReady = new Promise(resolve => { finish = resolve; });
28
+ return threadId => {
29
+ if (threadId) entry.createdThreadId = threadId;
30
+ finish();
31
+ };
32
+ }
33
+ async creationBindings() {
34
+ await Promise.all([...this.entries.values()].map(entry => entry.creationReady));
35
+ return new Map([...this.entries].filter(([, entry]) => entry.createdThreadId)
36
+ .map(([key, entry]) => [entry.createdThreadId, JSON.parse(key)[1]]));
37
+ }
38
+ status(scope, clientId) {
39
+ const entry = this.entries.get(this.key(scope, clientId));
40
+ return entry ? { state: entry.state, ...(entry.result ? { result: entry.result } : {}),
41
+ ...(entry.createdThreadId ? { createdThreadId: entry.createdThreadId } : {}),
42
+ ...(entry.error ? { error: entry.error.message } : {}) } : { state: 'unknown' };
43
+ }
44
+ async reconcile(scope, clientId, inspect) {
45
+ const key = this.key(scope, clientId);
46
+ let entry = this.entries.get(key);
47
+ if (entry?.state === 'accepted' || entry?.state === 'not_sent') return this.status(scope, clientId);
48
+ if (entry?.checking) return entry.checking;
49
+ if (!entry) {
50
+ // Fence a request which never arrived before checking history. A delayed
51
+ // transport frame with this admission must not execute after a replacement.
52
+ entry = { state: 'unknown', fenced: true };
53
+ this.entries.set(key, entry);
54
+ }
55
+ const target = entry;
56
+ target.checking = (async () => {
57
+ if (target.work) await target.work.catch(() => {});
58
+ if (target.lateWork) await target.lateWork;
59
+ if (target.state === 'accepted' || target.state === 'not_sent') return this.status(scope, clientId);
60
+ const result = await inspect();
61
+ target.state = result ? 'accepted' : 'not_sent';
62
+ target.fenced = true;
63
+ if (result) target.result = result;
64
+ delete target.error;
65
+ return this.status(scope, clientId);
66
+ })().finally(() => { delete target.checking; });
67
+ return target.checking;
68
+ }
69
+ submit(scope, clientId, operation) {
70
+ if (!/^[a-f0-9]{32}$/i.test(clientId || '')) return Promise.reject(Object.assign(new Error('消息确认标识无效'), { deliveryUnknown: false }));
71
+ const key = this.key(scope, clientId), previous = this.entries.get(key);
72
+ if (previous?.state === 'accepted') return Promise.resolve(previous.result);
73
+ if (previous?.fenced) return Promise.reject(Object.assign(new Error('原发送请求已结束,请使用新的发送标识'), { deliveryUnknown: false }));
74
+ if (previous?.state === 'sending') return previous.work;
75
+ if (previous?.state === 'unknown') return Promise.reject(Object.assign(new Error(previous.error?.message || '原发送结果仍未确认'), { deliveryUnknown: true }));
76
+ const entry = { state: 'sending' };
77
+ this.entries.set(key, entry);
78
+ entry.work = Promise.resolve().then(operation).then(result => {
79
+ entry.state = 'accepted'; entry.result = result; return result;
80
+ }, error => {
81
+ entry.state = error.deliveryUnknown === false || error.details?.deliveryUnknown === false ? 'not_sent' : 'unknown';
82
+ error.deliveryUnknown = entry.state !== 'not_sent';
83
+ if (error.deliveryResult) entry.lateWork = error.deliveryResult.then(result => {
84
+ entry.state = 'accepted'; entry.result = result; delete entry.error;
85
+ }, lateError => {
86
+ entry.state = rejectedBeforeSubmission(lateError) || lateError.deliveryUnknown === false ? 'not_sent' : 'unknown';
87
+ entry.error = lateError;
88
+ });
89
+ entry.error = error; throw error;
90
+ }).finally(() => { delete entry.work; });
91
+ return entry.work;
92
+ }
93
+ }
94
+ const normalizeId = value => String(value || '').replace(/-/g, '').toLowerCase();
95
+ async function findMessage(request, threadId, clientId, anchorItemId = '') {
96
+ let cursor;
97
+ const seen = new Set();
98
+ do {
99
+ const page = await request('thread/items/list', { threadId, limit: 100, sortDirection: 'desc', ...(cursor ? { cursor } : {}) });
100
+ if (!Array.isArray(page?.data)) throw new Error('发送核对失败:未取得权威历史');
101
+ for (const entry of page.data) {
102
+ const item = entry.item;
103
+ if (item?.type === 'userMessage' && normalizeId(item.clientId) === normalizeId(clientId)) {
104
+ return { data: { messageId: item.id, turnId: entry.turnId } };
105
+ }
106
+ if (anchorItemId && item?.id === anchorItemId) return null;
107
+ }
108
+ cursor = page.nextCursor;
109
+ if (cursor && seen.has(cursor)) throw new Error('发送核对失败:历史游标未推进');
110
+ if (cursor) seen.add(cursor);
111
+ } while (cursor);
112
+ return null;
113
+ }
114
+ return Object.freeze({ Store, rejectedBeforeSubmission, retainLateResult, findMessage, normalizeId });
115
+ });
@@ -1,87 +1,87 @@
1
- (function (root, factory) {
2
- if (typeof module === 'object' && module.exports) module.exports = factory();
3
- else root.CodeRelaxFileChannel = factory();
4
- })(globalThis, function () {
5
- const PART = 16 * 1024, MAX = 256 * 1024, LIMIT = 1024 * 1024;
6
- // Socket-shaped adapter; one ordered RTC stream owns one file transaction.
7
- return class FileChannel {
8
- constructor(io) {
9
- this.io = io; this.readyState = 0; this.listeners = new Map();
10
- this.outbound = []; this.queued = 0; this.partial = null; this.timer = null;
11
- }
12
- get bufferedAmount() { return this.queued + this.io.buffered(); }
13
- addEventListener(type, callback, options = {}) {
14
- const list = this.listeners.get(type) || []; list.push({ callback, once: options.once }); this.listeners.set(type, list);
15
- }
16
- removeEventListener(type, callback) { this.listeners.set(type, (this.listeners.get(type) || []).filter(item => item.callback !== callback)); }
17
- emit(type, details = {}) {
18
- const event = { type, ...details };
19
- this['on' + type]?.(event);
20
- for (const item of [...(this.listeners.get(type) || [])]) {
21
- if (item.once) this.removeEventListener(type, item.callback);
22
- item.callback(event);
23
- }
24
- }
25
- opened() { if (this.readyState !== 0) return; this.readyState = 1; this.emit('open'); this.pump(); }
26
- send(value) {
27
- if (this.readyState !== 1) throw new Error('文件直连已断开');
28
- const binary = typeof value !== 'string';
29
- const data = binary ? (value instanceof Uint8Array ? value : new Uint8Array(value)) : value;
30
- const size = binary ? data.byteLength : new TextEncoder().encode(data).length;
31
- if (!size || size > (binary ? MAX : PART) || this.bufferedAmount + size > LIMIT) throw new Error('文件直连分片或队列超限');
32
- const negotiated = this.io.maxMessageSize?.();
33
- const frameBytes = Number.isFinite(negotiated) && negotiated > 0 ? Math.min(PART, Math.max(1, Math.floor(negotiated))) : PART;
34
- const frames = binary ? [JSON.stringify({ type: 'rtc-file-bytes', length: size })] : [];
35
- if (binary) for (let i = 0; i < size; i += frameBytes) frames.push(data.slice(i, i + frameBytes));
36
- else frames.push(data);
37
- const sizes = frames.map(frame => typeof frame === 'string' ? new TextEncoder().encode(frame).length : frame.byteLength);
38
- if (sizes.some(length => length > frameBytes)) throw new Error('文件传输消息超过协商上限');
39
- const framedSize = sizes.reduce((total, length) => total + length, 0);
40
- if (this.bufferedAmount + framedSize > LIMIT) throw new Error('文件直连队列超限');
41
- this.outbound.push({ frames, sizes, index: 0 }); this.queued += framedSize; this.pump();
42
- }
43
- pump() {
44
- if (this.readyState !== 1) return;
45
- try {
46
- while (this.outbound.length && this.io.buffered() < MAX) {
47
- const item = this.outbound[0];
48
- // libdatachannel returns false when it accepted the frame into its buffer.
49
- // Only an exception rejects a send; replaying a buffered frame duplicates bytes.
50
- this.io.send(item.frames[item.index]);
51
- this.queued -= item.sizes[item.index];
52
- if (++item.index === item.frames.length) this.outbound.shift();
53
- }
54
- } catch (error) { this.close(1011, `send-failed:${String(error?.name || 'Error').slice(0, 40)}`); }
55
- }
56
- waitForPartial() {
57
- clearTimeout(this.timer);
58
- this.timer = setTimeout(() => this.close(1008, 'partial-idle-timeout'), 30000);
59
- }
60
- receive(value) {
61
- if (this.readyState !== 1) return;
62
- try {
63
- if (typeof value === 'string') {
64
- if (this.partial || new TextEncoder().encode(value).length > PART) throw new Error();
65
- const frame = JSON.parse(value);
66
- if (frame.type !== 'rtc-file-bytes') { this.io.received?.(); this.emit('message', { data: value }); return; }
67
- if (!Number.isSafeInteger(frame.length) || frame.length < 1 || frame.length > MAX) throw new Error();
68
- this.io.received?.(); this.partial = { data: new Uint8Array(frame.length), offset: 0 };
69
- this.waitForPartial(); return;
70
- }
71
- const bytes = value instanceof Uint8Array ? value : new Uint8Array(value), part = this.partial;
72
- if (!part || bytes.length < 1 || bytes.length > Math.min(PART, part.data.length - part.offset)) throw new Error();
73
- this.waitForPartial(); this.io.received?.(); part.data.set(bytes, part.offset); part.offset += bytes.length;
74
- if (part.offset === part.data.length) {
75
- this.partial = null; clearTimeout(this.timer); this.timer = null;
76
- this.emit('message', { data: part.data.buffer });
77
- }
78
- } catch { this.close(1008, 'invalid-frame'); }
79
- }
80
- close(code = 1000, reason = '') {
81
- if (this.readyState === 3) return;
82
- this.readyState = 3; clearTimeout(this.timer); this.timer = null;
83
- this.partial = null; this.outbound = []; this.queued = 0;
84
- this.io.close(); this.emit('close', { code, reason });
85
- }
86
- };
87
- });
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodeRelaxFileChannel = factory();
4
+ })(globalThis, function () {
5
+ const PART = 16 * 1024, MAX = 256 * 1024, LIMIT = 1024 * 1024;
6
+ // Socket-shaped adapter; one ordered RTC stream owns one file transaction.
7
+ return class FileChannel {
8
+ constructor(io) {
9
+ this.io = io; this.readyState = 0; this.listeners = new Map();
10
+ this.outbound = []; this.queued = 0; this.partial = null; this.timer = null;
11
+ }
12
+ get bufferedAmount() { return this.queued + this.io.buffered(); }
13
+ addEventListener(type, callback, options = {}) {
14
+ const list = this.listeners.get(type) || []; list.push({ callback, once: options.once }); this.listeners.set(type, list);
15
+ }
16
+ removeEventListener(type, callback) { this.listeners.set(type, (this.listeners.get(type) || []).filter(item => item.callback !== callback)); }
17
+ emit(type, details = {}) {
18
+ const event = { type, ...details };
19
+ this['on' + type]?.(event);
20
+ for (const item of [...(this.listeners.get(type) || [])]) {
21
+ if (item.once) this.removeEventListener(type, item.callback);
22
+ item.callback(event);
23
+ }
24
+ }
25
+ opened() { if (this.readyState !== 0) return; this.readyState = 1; this.emit('open'); this.pump(); }
26
+ send(value) {
27
+ if (this.readyState !== 1) throw new Error('文件直连已断开');
28
+ const binary = typeof value !== 'string';
29
+ const data = binary ? (value instanceof Uint8Array ? value : new Uint8Array(value)) : value;
30
+ const size = binary ? data.byteLength : new TextEncoder().encode(data).length;
31
+ if (!size || size > (binary ? MAX : PART) || this.bufferedAmount + size > LIMIT) throw new Error('文件直连分片或队列超限');
32
+ const negotiated = this.io.maxMessageSize?.();
33
+ const frameBytes = Number.isFinite(negotiated) && negotiated > 0 ? Math.min(PART, Math.max(1, Math.floor(negotiated))) : PART;
34
+ const frames = binary ? [JSON.stringify({ type: 'rtc-file-bytes', length: size })] : [];
35
+ if (binary) for (let i = 0; i < size; i += frameBytes) frames.push(data.slice(i, i + frameBytes));
36
+ else frames.push(data);
37
+ const sizes = frames.map(frame => typeof frame === 'string' ? new TextEncoder().encode(frame).length : frame.byteLength);
38
+ if (sizes.some(length => length > frameBytes)) throw new Error('文件传输消息超过协商上限');
39
+ const framedSize = sizes.reduce((total, length) => total + length, 0);
40
+ if (this.bufferedAmount + framedSize > LIMIT) throw new Error('文件直连队列超限');
41
+ this.outbound.push({ frames, sizes, index: 0 }); this.queued += framedSize; this.pump();
42
+ }
43
+ pump() {
44
+ if (this.readyState !== 1) return;
45
+ try {
46
+ while (this.outbound.length && this.io.buffered() < MAX) {
47
+ const item = this.outbound[0];
48
+ // libdatachannel returns false when it accepted the frame into its buffer.
49
+ // Only an exception rejects a send; replaying a buffered frame duplicates bytes.
50
+ this.io.send(item.frames[item.index]);
51
+ this.queued -= item.sizes[item.index];
52
+ if (++item.index === item.frames.length) this.outbound.shift();
53
+ }
54
+ } catch (error) { this.close(1011, `send-failed:${String(error?.name || 'Error').slice(0, 40)}`); }
55
+ }
56
+ waitForPartial() {
57
+ clearTimeout(this.timer);
58
+ this.timer = setTimeout(() => this.close(1008, 'partial-idle-timeout'), 30000);
59
+ }
60
+ receive(value) {
61
+ if (this.readyState !== 1) return;
62
+ try {
63
+ if (typeof value === 'string') {
64
+ if (this.partial || new TextEncoder().encode(value).length > PART) throw new Error();
65
+ const frame = JSON.parse(value);
66
+ if (frame.type !== 'rtc-file-bytes') { this.io.received?.(); this.emit('message', { data: value }); return; }
67
+ if (!Number.isSafeInteger(frame.length) || frame.length < 1 || frame.length > MAX) throw new Error();
68
+ this.io.received?.(); this.partial = { data: new Uint8Array(frame.length), offset: 0 };
69
+ this.waitForPartial(); return;
70
+ }
71
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value), part = this.partial;
72
+ if (!part || bytes.length < 1 || bytes.length > Math.min(PART, part.data.length - part.offset)) throw new Error();
73
+ this.waitForPartial(); this.io.received?.(); part.data.set(bytes, part.offset); part.offset += bytes.length;
74
+ if (part.offset === part.data.length) {
75
+ this.partial = null; clearTimeout(this.timer); this.timer = null;
76
+ this.emit('message', { data: part.data.buffer });
77
+ }
78
+ } catch { this.close(1008, 'invalid-frame'); }
79
+ }
80
+ close(code = 1000, reason = '') {
81
+ if (this.readyState === 3) return;
82
+ this.readyState = 3; clearTimeout(this.timer); this.timer = null;
83
+ this.partial = null; this.outbound = []; this.queued = 0;
84
+ this.io.close(); this.emit('close', { code, reason });
85
+ }
86
+ };
87
+ });