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

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 (101) 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/bin/set-relay-admin-password.mjs +11 -0
  7. package/dist/build-platform.json +1 -1
  8. package/dist/get-shared-service-url.ps1 +14 -14
  9. package/dist/shared/code-browser.cjs +159 -159
  10. package/dist/shared/file-changes.cjs +31 -31
  11. package/dist/shared/file-citations.cjs +117 -117
  12. package/dist/shared/file-mentions.cjs +92 -92
  13. package/dist/shared/message-delivery.cjs +115 -115
  14. package/dist/shared/p2p-file-channel.cjs +87 -87
  15. package/dist/shared/release-version.cjs +31 -0
  16. package/dist/shared/server-requests.cjs +311 -311
  17. package/dist/shared/timeline-sync.cjs +173 -173
  18. package/dist/skills/code-relax/SKILL.md +22 -22
  19. package/dist/src/bounded-process-log.mjs +63 -63
  20. package/dist/src/celsius-client.mjs +107 -107
  21. package/dist/src/dots-approval-timeline.mjs +99 -99
  22. package/dist/src/dots-avatar.mjs +42 -42
  23. package/dist/src/dots-chat-projection.mjs +50 -50
  24. package/dist/src/dots-chat-server.mjs +483 -483
  25. package/dist/src/dots-message-images.mjs +319 -319
  26. package/dist/src/dots-profile.mjs +77 -77
  27. package/dist/src/dots-room-activity.mjs +172 -172
  28. package/dist/src/dots-room-client.mjs +137 -135
  29. package/dist/src/dots-room-projection.mjs +94 -94
  30. package/dist/src/dots-schedules.mjs +66 -66
  31. package/dist/src/dots-summary-tasks.mjs +32 -32
  32. package/dist/src/dots-task-catalog.mjs +430 -430
  33. package/dist/src/file-transfer-range.mjs +22 -22
  34. package/dist/src/file-upload-session.mjs +146 -130
  35. package/dist/src/hosted-history-app-server.mjs +103 -103
  36. package/dist/src/hosted-history-cache.mjs +113 -113
  37. package/dist/src/lan-socket-server.mjs +338 -266
  38. package/dist/src/managed-app-server.mjs +373 -373
  39. package/dist/src/managed-endpoint.mjs +25 -25
  40. package/dist/src/message-upload-files.mjs +109 -109
  41. package/dist/src/onboarding.mjs +12 -12
  42. package/dist/src/platform/README.md +1 -1
  43. package/dist/src/platform/codex-protocol.mjs +46 -46
  44. package/dist/src/platform/desktop-project-store.mjs +219 -208
  45. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  46. package/dist/src/platform/linux/process.mjs +53 -53
  47. package/dist/src/platform/linux/system.mjs +50 -50
  48. package/dist/src/platform/linux/user-environment.mjs +9 -9
  49. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  50. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  51. package/dist/src/platform/macos/user-environment.mjs +9 -9
  52. package/dist/src/platform/native-package.mjs +58 -58
  53. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  54. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  55. package/dist/src/platform/windows/credential-store.mjs +27 -27
  56. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  57. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  58. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  59. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  60. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  61. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  62. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  63. package/dist/src/platform/windows/project-locations.mjs +16 -16
  64. package/dist/src/platform/windows/service-host.mjs +35 -35
  65. package/dist/src/platform/windows/user-environment.mjs +52 -52
  66. package/dist/src/project-management.mjs +58 -58
  67. package/dist/src/runtime-discovery.mjs +47 -47
  68. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  69. package/dist/src/self-relay/admin-auth.mjs +59 -0
  70. package/dist/src/self-relay/admin-login-page.mjs +7 -0
  71. package/dist/src/self-relay/admin-page.mjs +87 -80
  72. package/dist/src/self-relay/admin-server.mjs +49 -18
  73. package/dist/src/self-relay/admin-style.mjs +58 -14
  74. package/dist/src/self-relay/admin-workstations-page.mjs +95 -0
  75. package/dist/src/self-relay/admin-workstations.mjs +143 -0
  76. package/dist/src/self-relay/connection-reporting.mjs +38 -0
  77. package/dist/src/self-relay/connector.mjs +444 -439
  78. package/dist/src/self-relay/demo.mjs +31 -31
  79. package/dist/src/self-relay/diagnostics.mjs +54 -54
  80. package/dist/src/self-relay/email-accounts.mjs +183 -183
  81. package/dist/src/self-relay/file-wire.mjs +13 -13
  82. package/dist/src/self-relay/lifecycle.mjs +11 -0
  83. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  84. package/dist/src/self-relay/phone-connections.mjs +55 -0
  85. package/dist/src/self-relay/privacy-page.mjs +26 -26
  86. package/dist/src/self-relay/public-releases.mjs +67 -67
  87. package/dist/src/self-relay/rtc-file-bridge.mjs +46 -41
  88. package/dist/src/self-relay/server.mjs +63 -3
  89. package/dist/src/self-relay/usage-meter.mjs +141 -141
  90. package/dist/src/server.mjs +23 -9
  91. package/dist/src/service-doctor.mjs +148 -148
  92. package/dist/src/service-install.mjs +97 -97
  93. package/dist/src/service-lifecycle.mjs +139 -139
  94. package/dist/src/service-update.mjs +6 -3
  95. package/dist/src/shared-app-server.mjs +683 -666
  96. package/dist/src/shared-catalog.mjs +132 -132
  97. package/dist/src/shared-thread-stream.mjs +165 -165
  98. package/dist/src/usage-reset.mjs +17 -17
  99. package/dist/src/workstation-downloads.mjs +120 -120
  100. package/package.json +4 -4
  101. 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
+ });
@@ -0,0 +1,31 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ else root.CodeRelaxReleaseVersion = api;
5
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
6
+ function parse(value) {
7
+ if (typeof value !== 'string') return null;
8
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
9
+ if (!match) return null;
10
+ const core = match.slice(1, 4).map(Number), pre = match[4]?.split('.') || [];
11
+ if (core.some(part => !Number.isSafeInteger(part)) || pre.some(part => /^\d+$/.test(part) && (part.length > 1 && part[0] === '0' || !Number.isSafeInteger(Number(part))))) return null;
12
+ return { core, pre };
13
+ }
14
+ function compare(a, b) {
15
+ const left = parse(a), right = parse(b);
16
+ if (!left || !right) return null;
17
+ for (let i = 0; i < 3; i++) if (left.core[i] !== right.core[i]) return left.core[i] > right.core[i] ? 1 : -1;
18
+ if (!left.pre.length || !right.pre.length) return left.pre.length === right.pre.length ? 0 : left.pre.length ? -1 : 1;
19
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i++) {
20
+ const x = left.pre[i], y = right.pre[i];
21
+ if (x === y) continue;
22
+ if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
23
+ const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
24
+ if (nx && ny) return Number(x) > Number(y) ? 1 : -1;
25
+ if (nx !== ny) return nx ? -1 : 1;
26
+ return x > y ? 1 : -1;
27
+ }
28
+ return 0;
29
+ }
30
+ return { compare };
31
+ });