@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.32

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 (187) hide show
  1. package/README.md +95 -89
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +374 -367
  4. package/dist/bin/manage-relay-admin.mjs +20 -20
  5. package/dist/bin/migrate-relay-db.mjs +109 -109
  6. package/dist/bin/publish-android-release.mjs +63 -58
  7. package/dist/bin/publish-harmony-release.mjs +46 -0
  8. package/dist/bin/release-upload-server.mjs +174 -0
  9. package/dist/bin/self-relay-admin.mjs +8 -8
  10. package/dist/bin/self-relay-demo.mjs +33 -33
  11. package/dist/bin/self-relay-server.mjs +66 -66
  12. package/dist/build-platform.json +1 -1
  13. package/dist/get-lan-networks.ps1 +40 -40
  14. package/dist/get-shared-service-url.ps1 +6 -6
  15. package/dist/shared/app-server-events.cjs +422 -412
  16. package/dist/shared/code-browser.cjs +159 -159
  17. package/dist/shared/file-changes.cjs +31 -31
  18. package/dist/shared/file-citations.cjs +117 -117
  19. package/dist/shared/file-mentions.cjs +92 -92
  20. package/dist/shared/message-delivery.cjs +54 -54
  21. package/dist/shared/p2p-data-channel.cjs +72 -72
  22. package/dist/shared/p2p-diagnostics.cjs +28 -28
  23. package/dist/shared/p2p-file-channel.cjs +78 -74
  24. package/dist/shared/plan-pricing.cjs +32 -32
  25. package/dist/shared/question-attention.cjs +69 -0
  26. package/dist/shared/server-requests.cjs +219 -219
  27. package/dist/shared/timeline-sync.cjs +173 -173
  28. package/dist/skills/code-relax/SKILL.md +22 -22
  29. package/dist/src/archived-threads.mjs +22 -0
  30. package/dist/src/background-process.mjs +18 -18
  31. package/dist/src/bounded-process-log.mjs +63 -63
  32. package/dist/src/celsius-client.mjs +107 -107
  33. package/dist/src/code-browser.mjs +100 -100
  34. package/dist/src/desktop-launcher.mjs +46 -46
  35. package/dist/src/dots-approval-timeline.mjs +99 -99
  36. package/dist/src/dots-avatar.mjs +42 -42
  37. package/dist/src/dots-chat-projection.mjs +50 -50
  38. package/dist/src/dots-chat-server.mjs +461 -461
  39. package/dist/src/dots-message-images.mjs +319 -319
  40. package/dist/src/dots-profile.mjs +77 -77
  41. package/dist/src/dots-room-activity.mjs +94 -94
  42. package/dist/src/dots-room-client.mjs +135 -135
  43. package/dist/src/dots-room-projection.mjs +94 -94
  44. package/dist/src/dots-schedules.mjs +66 -66
  45. package/dist/src/dots-summary-tasks.mjs +32 -32
  46. package/dist/src/dots-task-catalog.mjs +430 -430
  47. package/dist/src/dots-task-files.mjs +52 -52
  48. package/dist/src/execution-health.mjs +26 -26
  49. package/dist/src/file-transfer-range.mjs +22 -22
  50. package/dist/src/file-upload-session.mjs +130 -130
  51. package/dist/src/hosted-history-app-server.mjs +103 -103
  52. package/dist/src/hosted-history-cache.mjs +113 -113
  53. package/dist/src/lan-socket-server.mjs +265 -263
  54. package/dist/src/managed-app-server.mjs +244 -244
  55. package/dist/src/message-images.mjs +9 -9
  56. package/dist/src/message-upload-files.mjs +74 -74
  57. package/dist/src/onboarding.mjs +12 -12
  58. package/dist/src/plan-pricing.mjs +47 -47
  59. package/dist/src/platform/README.md +12 -12
  60. package/dist/src/platform/codex-protocol.mjs +46 -46
  61. package/dist/src/platform/codex-runtime.mjs +3 -3
  62. package/dist/src/platform/credential-store.mjs +3 -3
  63. package/dist/src/platform/desktop-host.mjs +3 -3
  64. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  65. package/dist/src/platform/desktop-project-store.mjs +208 -149
  66. package/dist/src/platform/file-opener.mjs +9 -0
  67. package/dist/src/platform/linux/README.md +3 -3
  68. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  69. package/dist/src/platform/linux/credential-store.mjs +28 -0
  70. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  71. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  72. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  73. package/dist/src/platform/linux/network-info.mjs +11 -0
  74. package/dist/src/platform/linux/process.mjs +42 -0
  75. package/dist/src/platform/linux/project-locations.mjs +17 -0
  76. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  77. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  78. package/dist/src/platform/linux/service-host.mjs +18 -0
  79. package/dist/src/platform/linux/system.mjs +49 -0
  80. package/dist/src/platform/linux/user-environment.mjs +7 -0
  81. package/dist/src/platform/macos/README.md +3 -3
  82. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  83. package/dist/src/platform/macos/credential-store.mjs +10 -10
  84. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  85. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  86. package/dist/src/platform/macos/network-info.mjs +8 -28
  87. package/dist/src/platform/macos/project-locations.mjs +8 -8
  88. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  89. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  90. package/dist/src/platform/macos/service-host.mjs +18 -18
  91. package/dist/src/platform/macos/system.mjs +29 -29
  92. package/dist/src/platform/macos/user-environment.mjs +7 -7
  93. package/dist/src/platform/native-package.mjs +58 -46
  94. package/dist/src/platform/network-info.mjs +3 -3
  95. package/dist/src/platform/path-identity.mjs +5 -5
  96. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  97. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  98. package/dist/src/platform/project-locations.mjs +3 -3
  99. package/dist/src/platform/runtime-paths.mjs +8 -8
  100. package/dist/src/platform/select.mjs +5 -5
  101. package/dist/src/platform/service-host.mjs +3 -3
  102. package/dist/src/platform/user-environment.mjs +3 -3
  103. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  104. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  105. package/dist/src/platform/windows/background-process.mjs +30 -30
  106. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  107. package/dist/src/platform/windows/credential-store.mjs +27 -27
  108. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  109. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  110. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  111. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  112. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  113. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  114. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  115. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  116. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  117. package/dist/src/platform/windows/network-info.mjs +34 -34
  118. package/dist/src/platform/windows/project-locations.mjs +16 -16
  119. package/dist/src/platform/windows/resident-start.js +20 -20
  120. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  121. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  122. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  123. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  124. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  125. package/dist/src/platform/windows/service-host.mjs +31 -31
  126. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  127. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  128. package/dist/src/platform/windows/user-environment.mjs +50 -50
  129. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  130. package/dist/src/project-archive.mjs +30 -0
  131. package/dist/src/project-create.mjs +121 -119
  132. package/dist/src/project-management.mjs +59 -69
  133. package/dist/src/project-remove.mjs +55 -0
  134. package/dist/src/project-update.mjs +56 -0
  135. package/dist/src/runtime-discovery.mjs +47 -47
  136. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  137. package/dist/src/self-relay/account-connections.mjs +52 -52
  138. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  139. package/dist/src/self-relay/admin-page.mjs +80 -80
  140. package/dist/src/self-relay/admin-server.mjs +74 -74
  141. package/dist/src/self-relay/admin-state.mjs +127 -127
  142. package/dist/src/self-relay/admin-style.mjs +14 -14
  143. package/dist/src/self-relay/android-releases.mjs +61 -61
  144. package/dist/src/self-relay/client.mjs +61 -61
  145. package/dist/src/self-relay/connector.mjs +439 -438
  146. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  147. package/dist/src/self-relay/demo.mjs +37 -37
  148. package/dist/src/self-relay/device-info.mjs +27 -0
  149. package/dist/src/self-relay/device-store.mjs +92 -84
  150. package/dist/src/self-relay/diagnostics.mjs +54 -54
  151. package/dist/src/self-relay/email-accounts.mjs +183 -183
  152. package/dist/src/self-relay/file-wire.mjs +13 -13
  153. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  154. package/dist/src/self-relay/lifecycle.mjs +264 -250
  155. package/dist/src/self-relay/long-pairing.mjs +40 -40
  156. package/dist/src/self-relay/mysql-state.mjs +118 -105
  157. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  158. package/dist/src/self-relay/privacy-page.mjs +26 -26
  159. package/dist/src/self-relay/public-releases.mjs +52 -0
  160. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  161. package/dist/src/self-relay/server.mjs +1006 -932
  162. package/dist/src/self-relay/usage-daily.mjs +55 -55
  163. package/dist/src/self-relay/usage-meter.mjs +141 -141
  164. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  165. package/dist/src/self-relay/wire.mjs +36 -36
  166. package/dist/src/server.mjs +1861 -1778
  167. package/dist/src/service-control.mjs +82 -82
  168. package/dist/src/service-doctor.mjs +110 -110
  169. package/dist/src/service-health.mjs +33 -33
  170. package/dist/src/service-install.mjs +91 -91
  171. package/dist/src/service-lifecycle.mjs +138 -138
  172. package/dist/src/service-restart.mjs +81 -81
  173. package/dist/src/shared-app-server.mjs +615 -611
  174. package/dist/src/shared-catalog.mjs +132 -132
  175. package/dist/src/shared-recovery.mjs +146 -146
  176. package/dist/src/shared-thread-stream.mjs +164 -164
  177. package/dist/src/skill-install.mjs +51 -51
  178. package/dist/src/thread-title-generation.mjs +150 -150
  179. package/dist/src/turn-model-history.mjs +47 -47
  180. package/dist/src/usage-reset.mjs +17 -17
  181. package/dist/src/workstation-downloads.mjs +120 -120
  182. package/dist/tools/rtc-route-helper.ps1 +99 -99
  183. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  184. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  185. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  186. package/package.json +7 -6
  187. package/tools/postinstall.mjs +127 -127
@@ -1,103 +1,103 @@
1
- import { SharedAppServer } from './shared-app-server.mjs';
2
- import { historyCacheScope, completedHistory, turnMetadata } from './hosted-history-cache.mjs';
3
-
4
- // Hosted history uses a disk range cache; local kernel history retains its own storage.
5
- export class HostedHistoryAppServer extends SharedAppServer {
6
- constructor(url, historyCache) {
7
- super(url);
8
- this.historyCache = historyCache;
9
- this.historyHeadKey = 'latest';
10
- this.diskHeads = new Map(); this.invalidatedTurns = new Set(); this.windowSaves = new Map();
11
- this.historyVersions = new Map();
12
- this.on('disconnected', () => { this.diskHeads.clear(); this.invalidatedTurns.clear(); this.historyVersions.clear(); });
13
- }
14
-
15
- historyScope(threadId) { return historyCacheScope(this.historyIdentity, this.historySource, threadId); }
16
- historyVersion(threadId) { return `${this.epoch}:${this.historyVersions.get(threadId) || 0}`; }
17
-
18
- async restoreHistoryHead(threadId) {
19
- const scope = this.historyScope(threadId);
20
- const result = await this.historyCache?.get(scope, this.historyHeadKey);
21
- if (scope !== this.historyScope(threadId)) throw new Error('云端账号连接已变化,请重新读取历史。');
22
- return result;
23
- }
24
-
25
- saveHistoryWindow(threadId, immediate = false) {
26
- const record = this.threads.get(threadId), scope = this.historyScope(threadId);
27
- if (!this.historyCache || !scope || !record || record.hydrating || record.needsReload
28
- || record.historyTrimmed || record.metadataOnly || record.readOnly || !completedHistory(record.thread.turns)) return;
29
- const value = structuredClone({ updatedAt: record.thread.updatedAt, turns: record.thread.turns,
30
- nextCursor: record.nextCursor, revision: record.thread.chatRevision,
31
- ...(record.approvalBeforeMs !== undefined ? { approvalBeforeMs: record.approvalBeforeMs } : {}) });
32
- const pending = this.windowSaves.get(scope);
33
- if (pending) { clearTimeout(pending); this.windowSaves.delete(scope); }
34
- const save = () => { this.windowSaves.delete(scope); return this.historyCache.put(scope, this.historyHeadKey, value); };
35
- if (immediate) return save();
36
- const timer = setTimeout(save, 200); timer.unref(); this.windowSaves.set(scope, timer);
37
- }
38
-
39
- invalidateHistory(method, params) {
40
- const id = params.threadId || params.thread?.id, scope = this.historyScope(id);
41
- if (!scope || !this.historyCache) return;
42
- const turnId = params.turnId || params.turn?.id;
43
- const whole = ['thread/deleted', 'thread/compacted'].includes(method);
44
- if (!whole && (!turnId || !/^(?:item\/|turn\/)/.test(method))) return;
45
- this.historyVersions.set(id, (this.historyVersions.get(id) || 0) + 1);
46
- clearTimeout(this.windowSaves.get(scope)); this.windowSaves.delete(scope);
47
- const key = `${scope}:${turnId}`;
48
- if (whole || !this.invalidatedTurns.has(key)) {
49
- this.invalidatedTurns.add(key); this.diskHeads.delete(id);
50
- void this.historyCache.invalidate(scope, whole ? undefined : turnId);
51
- }
52
- if (whole) {
53
- const record = this.threads.get(id);
54
- if (record) record.needsReload = true;
55
- this.emit('history-invalidated', id);
56
- }
57
- if (method === 'turn/completed') this.invalidatedTurns.delete(key);
58
- }
59
-
60
- async compactTurn(threadId, turn, cursor = null, apply, limit = 100) {
61
- const scope = this.historyScope(threadId), candidate = this.diskHeads.get(threadId);
62
- const version = this.historyVersion(threadId);
63
- if (cursor && !turn.status && this.historyCache) {
64
- const final = await this.historyCache.get(scope, `turn:${turn.id}`);
65
- if (scope !== this.historyScope(threadId)) throw new Error('云端账号连接已变化,请重新读取历史。');
66
- if (final) turn = { ...final.turns[0], ...turn };
67
- }
68
- const saved = !cursor && candidate?.head?.turns.find(value => value.id === turn.id);
69
- if (scope && this.historyCache && saved && candidate.scope === scope && candidate.updatedAt != null
70
- && candidate.head.updatedAt === candidate.updatedAt && completedHistory([saved])
71
- && turnMetadata(saved) === turnMetadata(turn)) {
72
- const result = { ...saved, ...turn, items: saved.items, itemPage: saved.itemPage };
73
- apply?.(result); return result;
74
- }
75
- const result = await super.compactTurn(threadId, turn, cursor, apply, limit);
76
- if (this.historyCache && scope === this.historyScope(threadId) && version === this.historyVersion(threadId)
77
- && completedHistory([{ ...turn, items: [] }])) {
78
- const { items, itemPage, ...metadata } = turn;
79
- await this.historyCache.put(scope, `turn:${turn.id}`, { turns: [metadata] });
80
- }
81
- return result;
82
- }
83
-
84
- async history(threadId, cursor, options = {}) {
85
- await this.connect();
86
- let position;
87
- try { position = JSON.parse(cursor); } catch { position = null; }
88
- if (position?.threadId !== threadId) return super.history(threadId, cursor, options);
89
- const scope = this.historyScope(threadId);
90
- const version = this.historyVersion(threadId);
91
- const key = JSON.stringify(['history', position.turnId, position.itemCursor, position.turnCursor, options.itemLimit ?? 100]);
92
- const cached = await this.historyCache?.get(scope, key);
93
- if (scope !== this.historyScope(threadId)) throw new Error('云端账号连接已变化,请重新读取历史。');
94
- if (cached && version === this.historyVersion(threadId)) return { thread: { id: threadId, turns: cached.turns },
95
- nextCursor: cached.position ? JSON.stringify(cached.position) : null };
96
- const result = await super.history(threadId, cursor, options);
97
- if (scope === this.historyScope(threadId) && version === this.historyVersion(threadId) && completedHistory(result.thread.turns)) {
98
- const next = result.nextCursor ? JSON.parse(result.nextCursor) : null;
99
- await this.historyCache?.put(scope, key, { turns: result.thread.turns, position: next });
100
- }
101
- return result;
102
- }
103
- }
1
+ import { SharedAppServer } from './shared-app-server.mjs';
2
+ import { historyCacheScope, completedHistory, turnMetadata } from './hosted-history-cache.mjs';
3
+
4
+ // Hosted history uses a disk range cache; local kernel history retains its own storage.
5
+ export class HostedHistoryAppServer extends SharedAppServer {
6
+ constructor(url, historyCache) {
7
+ super(url);
8
+ this.historyCache = historyCache;
9
+ this.historyHeadKey = 'latest';
10
+ this.diskHeads = new Map(); this.invalidatedTurns = new Set(); this.windowSaves = new Map();
11
+ this.historyVersions = new Map();
12
+ this.on('disconnected', () => { this.diskHeads.clear(); this.invalidatedTurns.clear(); this.historyVersions.clear(); });
13
+ }
14
+
15
+ historyScope(threadId) { return historyCacheScope(this.historyIdentity, this.historySource, threadId); }
16
+ historyVersion(threadId) { return `${this.epoch}:${this.historyVersions.get(threadId) || 0}`; }
17
+
18
+ async restoreHistoryHead(threadId) {
19
+ const scope = this.historyScope(threadId);
20
+ const result = await this.historyCache?.get(scope, this.historyHeadKey);
21
+ if (scope !== this.historyScope(threadId)) throw new Error('云端账号连接已变化,请重新读取历史。');
22
+ return result;
23
+ }
24
+
25
+ saveHistoryWindow(threadId, immediate = false) {
26
+ const record = this.threads.get(threadId), scope = this.historyScope(threadId);
27
+ if (!this.historyCache || !scope || !record || record.hydrating || record.needsReload
28
+ || record.historyTrimmed || record.metadataOnly || record.readOnly || !completedHistory(record.thread.turns)) return;
29
+ const value = structuredClone({ updatedAt: record.thread.updatedAt, turns: record.thread.turns,
30
+ nextCursor: record.nextCursor, revision: record.thread.chatRevision,
31
+ ...(record.approvalBeforeMs !== undefined ? { approvalBeforeMs: record.approvalBeforeMs } : {}) });
32
+ const pending = this.windowSaves.get(scope);
33
+ if (pending) { clearTimeout(pending); this.windowSaves.delete(scope); }
34
+ const save = () => { this.windowSaves.delete(scope); return this.historyCache.put(scope, this.historyHeadKey, value); };
35
+ if (immediate) return save();
36
+ const timer = setTimeout(save, 200); timer.unref(); this.windowSaves.set(scope, timer);
37
+ }
38
+
39
+ invalidateHistory(method, params) {
40
+ const id = params.threadId || params.thread?.id, scope = this.historyScope(id);
41
+ if (!scope || !this.historyCache) return;
42
+ const turnId = params.turnId || params.turn?.id;
43
+ const whole = ['thread/deleted', 'thread/compacted'].includes(method);
44
+ if (!whole && (!turnId || !/^(?:item\/|turn\/)/.test(method))) return;
45
+ this.historyVersions.set(id, (this.historyVersions.get(id) || 0) + 1);
46
+ clearTimeout(this.windowSaves.get(scope)); this.windowSaves.delete(scope);
47
+ const key = `${scope}:${turnId}`;
48
+ if (whole || !this.invalidatedTurns.has(key)) {
49
+ this.invalidatedTurns.add(key); this.diskHeads.delete(id);
50
+ void this.historyCache.invalidate(scope, whole ? undefined : turnId);
51
+ }
52
+ if (whole) {
53
+ const record = this.threads.get(id);
54
+ if (record) record.needsReload = true;
55
+ this.emit('history-invalidated', id);
56
+ }
57
+ if (method === 'turn/completed') this.invalidatedTurns.delete(key);
58
+ }
59
+
60
+ async compactTurn(threadId, turn, cursor = null, apply, limit = 100) {
61
+ const scope = this.historyScope(threadId), candidate = this.diskHeads.get(threadId);
62
+ const version = this.historyVersion(threadId);
63
+ if (cursor && !turn.status && this.historyCache) {
64
+ const final = await this.historyCache.get(scope, `turn:${turn.id}`);
65
+ if (scope !== this.historyScope(threadId)) throw new Error('云端账号连接已变化,请重新读取历史。');
66
+ if (final) turn = { ...final.turns[0], ...turn };
67
+ }
68
+ const saved = !cursor && candidate?.head?.turns.find(value => value.id === turn.id);
69
+ if (scope && this.historyCache && saved && candidate.scope === scope && candidate.updatedAt != null
70
+ && candidate.head.updatedAt === candidate.updatedAt && completedHistory([saved])
71
+ && turnMetadata(saved) === turnMetadata(turn)) {
72
+ const result = { ...saved, ...turn, items: saved.items, itemPage: saved.itemPage };
73
+ apply?.(result); return result;
74
+ }
75
+ const result = await super.compactTurn(threadId, turn, cursor, apply, limit);
76
+ if (this.historyCache && scope === this.historyScope(threadId) && version === this.historyVersion(threadId)
77
+ && completedHistory([{ ...turn, items: [] }])) {
78
+ const { items, itemPage, ...metadata } = turn;
79
+ await this.historyCache.put(scope, `turn:${turn.id}`, { turns: [metadata] });
80
+ }
81
+ return result;
82
+ }
83
+
84
+ async history(threadId, cursor, options = {}) {
85
+ await this.connect();
86
+ let position;
87
+ try { position = JSON.parse(cursor); } catch { position = null; }
88
+ if (position?.threadId !== threadId) return super.history(threadId, cursor, options);
89
+ const scope = this.historyScope(threadId);
90
+ const version = this.historyVersion(threadId);
91
+ const key = JSON.stringify(['history', position.turnId, position.itemCursor, position.turnCursor, options.itemLimit ?? 100]);
92
+ const cached = await this.historyCache?.get(scope, key);
93
+ if (scope !== this.historyScope(threadId)) throw new Error('云端账号连接已变化,请重新读取历史。');
94
+ if (cached && version === this.historyVersion(threadId)) return { thread: { id: threadId, turns: cached.turns },
95
+ nextCursor: cached.position ? JSON.stringify(cached.position) : null };
96
+ const result = await super.history(threadId, cursor, options);
97
+ if (scope === this.historyScope(threadId) && version === this.historyVersion(threadId) && completedHistory(result.thread.turns)) {
98
+ const next = result.nextCursor ? JSON.parse(result.nextCursor) : null;
99
+ await this.historyCache?.put(scope, key, { turns: result.thread.turns, position: next });
100
+ }
101
+ return result;
102
+ }
103
+ }
@@ -1,113 +1,113 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
-
5
- const threadBudget = 8 * 1024 * 1024;
6
- const totalBudget = 128 * 1024 * 1024;
7
- const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
8
-
9
- // Store confirmed ranges, not connection tokens, execution context or image bytes.
10
- export function historyCacheScope(identity, source, threadId) {
11
- return identity?.accountId && threadId ? digest([identity.accountId, identity.userId || '', source, threadId]) : null;
12
- }
13
-
14
- export function completedHistory(turns) {
15
- if (!turns?.length || turns.some(turn => !['completed', 'failed', 'interrupted'].includes(turn.status)
16
- || turn.cacheStub || turn.items?.some(item => item.dotsStreaming
17
- || item.type === 'imageGeneration' && !item.savedPath && item.result))) return false;
18
- // Signed media addresses and embedded bytes must be reread/refreshed, not replayed from disk.
19
- const media = value => value && typeof value === 'object' && Object.entries(value).some(([key, part]) =>
20
- typeof part === 'string' && (/^data:/.test(part) || /[?&](?:X-Amz-|X-Goog-|signature=|sig=|token=)/i.test(part))
21
- || key === 'imageBytes' || media(part));
22
- return !media(turns);
23
- }
24
-
25
- export function turnMetadata(turn) {
26
- return digest(Object.entries(turn || {}).filter(([key]) => !['items', 'itemPage', 'cacheStub'].includes(key))
27
- .sort(([a], [b]) => a.localeCompare(b)));
28
- }
29
-
30
- export class HostedHistoryCache {
31
- constructor(stateRoot, { report = error => console.error(`托管历史缓存失败:${error.message}`), maxThreadBytes = threadBudget, maxTotalBytes = totalBudget } = {}) {
32
- this.directory = path.join(stateRoot, 'hosted-history');
33
- this.report = report; this.maxThreadBytes = maxThreadBytes; this.maxTotalBytes = maxTotalBytes;
34
- this.tail = Promise.resolve();
35
- }
36
-
37
- async record(scope) {
38
- try {
39
- const record = JSON.parse(await fs.readFile(path.join(this.directory, `${scope}.json`), 'utf8'));
40
- if (record.version !== 1 || !record.entries || Array.isArray(record.entries) || typeof record.entries !== 'object') {
41
- throw new Error('历史缓存结构无效,需重新读取云端历史。');
42
- }
43
- if (Object.values(record.entries).some(entry => !Number.isFinite(entry.at) || !entry.value
44
- || !Array.isArray(entry.value.turns) && !Array.isArray(entry.value.data))) {
45
- throw new Error('历史缓存页结构无效,需重新读取云端历史。');
46
- }
47
- return record;
48
- } catch (error) { if (error.code !== 'ENOENT') this.report(error); return { version: 1, entries: {} }; }
49
- }
50
-
51
- run(operation, fallback) {
52
- const work = this.tail.then(operation).catch(error => { this.report(error); return fallback; });
53
- this.tail = work.then(() => {}); return work;
54
- }
55
-
56
- get(scope, key) {
57
- if (!scope) return Promise.resolve(null);
58
- return this.run(async () => (await this.record(scope)).entries?.[key]?.value ?? null, null);
59
- }
60
-
61
- put(scope, key, value) {
62
- if (!scope) return Promise.resolve();
63
- // Freeze before queuing; live events may mutate the source window during disk I/O.
64
- const snapshot = structuredClone(value);
65
- return this.run(async () => {
66
- const record = await this.record(scope);
67
- record.entries ||= {};
68
- record.entries[key] = { at: Date.now(), value: snapshot };
69
- let text = JSON.stringify(record);
70
- for (const [oldKey] of Object.entries(record.entries).sort((a, b) => a[1].at - b[1].at)) {
71
- if (Buffer.byteLength(text) <= this.maxThreadBytes) break;
72
- delete record.entries[oldKey]; text = JSON.stringify(record);
73
- }
74
- await this.write(scope, text);
75
- await this.prune(scope);
76
- });
77
- }
78
-
79
- invalidate(scope, turnId) {
80
- if (!scope) return Promise.resolve();
81
- return this.run(async () => {
82
- const record = await this.record(scope);
83
- for (const [key, entry] of Object.entries(record.entries || {})) {
84
- if (!turnId || entry.value.turnIds?.includes(turnId) || entry.value.turns?.some(turn => turn.id === turnId)
85
- || entry.value.thread?.turns?.some(turn => turn.id === turnId)) delete record.entries[key];
86
- }
87
- await this.write(scope, JSON.stringify(record));
88
- });
89
- }
90
-
91
- async write(scope, text) {
92
- if (!/^[a-f0-9]{64}$/.test(scope)) throw new Error('历史缓存身份无效');
93
- await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });
94
- const file = path.join(this.directory, `${scope}.json`), temporary = `${file}.tmp`;
95
- try {
96
- await fs.writeFile(temporary, text, { mode: 0o600 });
97
- await fs.rename(temporary, file);
98
- } finally {
99
- try { await fs.unlink(temporary); } catch (error) { if (error.code !== 'ENOENT') this.report(error); }
100
- }
101
- }
102
-
103
- async prune(current) {
104
- const files = await Promise.all((await fs.readdir(this.directory)).filter(name => /^[a-f0-9]{64}\.json$/.test(name))
105
- .map(async name => ({ name, ...await fs.stat(path.join(this.directory, name)) })));
106
- let bytes = files.reduce((sum, file) => sum + file.size, 0);
107
- for (const file of files.sort((a, b) => a.mtimeMs - b.mtimeMs)) {
108
- if (bytes <= this.maxTotalBytes) break;
109
- if (file.name === `${current}.json`) continue;
110
- await fs.unlink(path.join(this.directory, file.name)); bytes -= file.size;
111
- }
112
- }
113
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+
5
+ const threadBudget = 8 * 1024 * 1024;
6
+ const totalBudget = 128 * 1024 * 1024;
7
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
8
+
9
+ // Store confirmed ranges, not connection tokens, execution context or image bytes.
10
+ export function historyCacheScope(identity, source, threadId) {
11
+ return identity?.accountId && threadId ? digest([identity.accountId, identity.userId || '', source, threadId]) : null;
12
+ }
13
+
14
+ export function completedHistory(turns) {
15
+ if (!turns?.length || turns.some(turn => !['completed', 'failed', 'interrupted'].includes(turn.status)
16
+ || turn.cacheStub || turn.items?.some(item => item.dotsStreaming
17
+ || item.type === 'imageGeneration' && !item.savedPath && item.result))) return false;
18
+ // Signed media addresses and embedded bytes must be reread/refreshed, not replayed from disk.
19
+ const media = value => value && typeof value === 'object' && Object.entries(value).some(([key, part]) =>
20
+ typeof part === 'string' && (/^data:/.test(part) || /[?&](?:X-Amz-|X-Goog-|signature=|sig=|token=)/i.test(part))
21
+ || key === 'imageBytes' || media(part));
22
+ return !media(turns);
23
+ }
24
+
25
+ export function turnMetadata(turn) {
26
+ return digest(Object.entries(turn || {}).filter(([key]) => !['items', 'itemPage', 'cacheStub'].includes(key))
27
+ .sort(([a], [b]) => a.localeCompare(b)));
28
+ }
29
+
30
+ export class HostedHistoryCache {
31
+ constructor(stateRoot, { report = error => console.error(`托管历史缓存失败:${error.message}`), maxThreadBytes = threadBudget, maxTotalBytes = totalBudget } = {}) {
32
+ this.directory = path.join(stateRoot, 'hosted-history');
33
+ this.report = report; this.maxThreadBytes = maxThreadBytes; this.maxTotalBytes = maxTotalBytes;
34
+ this.tail = Promise.resolve();
35
+ }
36
+
37
+ async record(scope) {
38
+ try {
39
+ const record = JSON.parse(await fs.readFile(path.join(this.directory, `${scope}.json`), 'utf8'));
40
+ if (record.version !== 1 || !record.entries || Array.isArray(record.entries) || typeof record.entries !== 'object') {
41
+ throw new Error('历史缓存结构无效,需重新读取云端历史。');
42
+ }
43
+ if (Object.values(record.entries).some(entry => !Number.isFinite(entry.at) || !entry.value
44
+ || !Array.isArray(entry.value.turns) && !Array.isArray(entry.value.data))) {
45
+ throw new Error('历史缓存页结构无效,需重新读取云端历史。');
46
+ }
47
+ return record;
48
+ } catch (error) { if (error.code !== 'ENOENT') this.report(error); return { version: 1, entries: {} }; }
49
+ }
50
+
51
+ run(operation, fallback) {
52
+ const work = this.tail.then(operation).catch(error => { this.report(error); return fallback; });
53
+ this.tail = work.then(() => {}); return work;
54
+ }
55
+
56
+ get(scope, key) {
57
+ if (!scope) return Promise.resolve(null);
58
+ return this.run(async () => (await this.record(scope)).entries?.[key]?.value ?? null, null);
59
+ }
60
+
61
+ put(scope, key, value) {
62
+ if (!scope) return Promise.resolve();
63
+ // Freeze before queuing; live events may mutate the source window during disk I/O.
64
+ const snapshot = structuredClone(value);
65
+ return this.run(async () => {
66
+ const record = await this.record(scope);
67
+ record.entries ||= {};
68
+ record.entries[key] = { at: Date.now(), value: snapshot };
69
+ let text = JSON.stringify(record);
70
+ for (const [oldKey] of Object.entries(record.entries).sort((a, b) => a[1].at - b[1].at)) {
71
+ if (Buffer.byteLength(text) <= this.maxThreadBytes) break;
72
+ delete record.entries[oldKey]; text = JSON.stringify(record);
73
+ }
74
+ await this.write(scope, text);
75
+ await this.prune(scope);
76
+ });
77
+ }
78
+
79
+ invalidate(scope, turnId) {
80
+ if (!scope) return Promise.resolve();
81
+ return this.run(async () => {
82
+ const record = await this.record(scope);
83
+ for (const [key, entry] of Object.entries(record.entries || {})) {
84
+ if (!turnId || entry.value.turnIds?.includes(turnId) || entry.value.turns?.some(turn => turn.id === turnId)
85
+ || entry.value.thread?.turns?.some(turn => turn.id === turnId)) delete record.entries[key];
86
+ }
87
+ await this.write(scope, JSON.stringify(record));
88
+ });
89
+ }
90
+
91
+ async write(scope, text) {
92
+ if (!/^[a-f0-9]{64}$/.test(scope)) throw new Error('历史缓存身份无效');
93
+ await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });
94
+ const file = path.join(this.directory, `${scope}.json`), temporary = `${file}.tmp`;
95
+ try {
96
+ await fs.writeFile(temporary, text, { mode: 0o600 });
97
+ await fs.rename(temporary, file);
98
+ } finally {
99
+ try { await fs.unlink(temporary); } catch (error) { if (error.code !== 'ENOENT') this.report(error); }
100
+ }
101
+ }
102
+
103
+ async prune(current) {
104
+ const files = await Promise.all((await fs.readdir(this.directory)).filter(name => /^[a-f0-9]{64}\.json$/.test(name))
105
+ .map(async name => ({ name, ...await fs.stat(path.join(this.directory, name)) })));
106
+ let bytes = files.reduce((sum, file) => sum + file.size, 0);
107
+ for (const file of files.sort((a, b) => a.mtimeMs - b.mtimeMs)) {
108
+ if (bytes <= this.maxTotalBytes) break;
109
+ if (file.name === `${current}.json`) continue;
110
+ await fs.unlink(path.join(this.directory, file.name)); bytes -= file.size;
111
+ }
112
+ }
113
+ }