@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,107 +1,107 @@
1
- import { EventEmitter } from 'node:events';
2
- import WebSocket from 'ws';
3
-
4
- // Shared Celsius framing and acknowledged topic lifecycle; callers own identity and HTTP.
5
- export class CelsiusClient extends EventEmitter {
6
- constructor({ getConnection, topics = [] }) {
7
- super(); this.getConnection = getConnection; this.initialTopics = topics;
8
- this.generation = 0; this.topics = new Map(); this.commands = new Map();
9
- this.nextId = 0; this.topicLoads = new Map();
10
- }
11
- async connect() {
12
- if (this.ready) return;
13
- if (this.connecting) return this.connecting;
14
- const generation = this.generation;
15
- const work = (async () => {
16
- const value = await this.getConnection();
17
- if (generation !== this.generation) throw new Error('消息连接已变化。');
18
- const url = new URL(value.websocket_url);
19
- if (url.protocol !== 'wss:' || url.username || url.password) throw new Error('消息推送地址无效。');
20
- await new Promise((resolve, reject) => {
21
- const socket = new WebSocket(url, { handshakeTimeout: 15000, maxPayload: 8 * 1024 * 1024 });
22
- this.socket = socket;
23
- const timer = setTimeout(() => { reject(new Error('消息推送订阅超时。')); socket.terminate(); }, 30000);
24
- const current = () => this.socket === socket && generation === this.generation;
25
- socket.on('error', () => {}); // Do not expose the signed URL in library errors.
26
- socket.on('open', async () => {
27
- try {
28
- await this.command({ type: 'connect', presence: { type: 'presence', state: 'background' } });
29
- for (const topic of this.initialTopics) await this.subscribe(topic);
30
- if (!current()) throw new Error('消息连接已变化。');
31
- this.ready = true; clearTimeout(timer); resolve(); this.emit('connected');
32
- } catch (error) { clearTimeout(timer); reject(error); socket.terminate(); }
33
- });
34
- socket.on('message', data => {
35
- if (!current()) return;
36
- let frames; try { frames = JSON.parse(data.toString()); } catch { return; }
37
- if (!Array.isArray(frames)) return;
38
- for (const frame of frames) {
39
- if (frame.type === 'message') this.receive(frame);
40
- else if (frame.id != null) this.commands.get(frame.id)?.resolve(frame.reply);
41
- }
42
- });
43
- socket.on('close', code => {
44
- clearTimeout(timer); reject(new Error('消息推送连接已断开。'));
45
- if (!current()) return;
46
- this.ready = false; this.socket = null;
47
- for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已断开。'));
48
- this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
49
- this.emit('disconnected', { source: 'celsius', code });
50
- });
51
- });
52
- })();
53
- this.connecting = work;
54
- try { await work; } finally { if (this.connecting === work) this.connecting = null; }
55
- }
56
-
57
- command(command) {
58
- if (this.socket?.readyState !== WebSocket.OPEN) return Promise.reject(new Error('消息推送尚未连接。'));
59
- const id = ++this.nextId;
60
- const socket = this.socket;
61
- return new Promise((resolve, reject) => {
62
- const finish = (error, value) => {
63
- if (!this.commands.has(id)) return;
64
- clearTimeout(timer); this.commands.delete(id);
65
- if (error) reject(error); else resolve(value);
66
- };
67
- const timer = setTimeout(() => { finish(new Error('消息推送请求超时。')); socket.terminate(); }, 30000);
68
- this.commands.set(id, { resolve: value => finish(null, value), reject: error => finish(error) });
69
- socket.send(JSON.stringify([{ id, command }]), error => { if (error) finish(new Error('消息推送连接已断开。')); });
70
- });
71
- }
72
-
73
- async subscribe(name, { offset, requireRecovery = false } = {}) {
74
- if (this.topics.has(name)) return;
75
- if (this.topicLoads.has(name)) return this.topicLoads.get(name);
76
- const generation = this.generation, socket = this.socket;
77
- // Every transport reconnection invalidates chat snapshots and reads a new
78
- // latest window. An old offset never certifies the new snapshot by itself.
79
- const work = this.command({ type: 'subscribe', topic_id: name, ...(offset == null ? {} : { offset }) }).then(reply => {
80
- if (socket !== this.socket || generation !== this.generation) throw new Error('消息连接已变化。');
81
- if (reply?.type !== 'subscribe' || reply.topic_id !== name) throw new Error('消息推送订阅未确认。');
82
- if (requireRecovery && !reply.recovered) throw new Error('ChatGPT 输出历史未完整接续,请核对原消息。');
83
- this.topics.set(name, reply.last_offset || null);
84
- if (reply.recovered) for (const message of reply.catchups || []) this.receive(message);
85
- });
86
- this.topicLoads.set(name, work);
87
- try { await work; } finally { if (this.topicLoads.get(name) === work) this.topicLoads.delete(name); }
88
- }
89
-
90
- async unsubscribe(name) {
91
- if (!this.topics.delete(name) || this.socket?.readyState !== WebSocket.OPEN) return;
92
- await this.command({ type: 'unsubscribe', topic_id: name });
93
- }
94
-
95
- receive(frame) {
96
- if (frame.offset) this.topics.set(frame.topic_id, frame.offset);
97
- this.emit('message', frame.topic_id, frame.payload);
98
- }
99
-
100
-
101
- close() {
102
- this.generation++; this.ready = false; this.connecting = null;
103
- const socket = this.socket; this.socket = null; socket?.terminate();
104
- for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已结束。'));
105
- this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
106
- }
107
- }
1
+ import { EventEmitter } from 'node:events';
2
+ import WebSocket from 'ws';
3
+
4
+ // Shared Celsius framing and acknowledged topic lifecycle; callers own identity and HTTP.
5
+ export class CelsiusClient extends EventEmitter {
6
+ constructor({ getConnection, topics = [] }) {
7
+ super(); this.getConnection = getConnection; this.initialTopics = topics;
8
+ this.generation = 0; this.topics = new Map(); this.commands = new Map();
9
+ this.nextId = 0; this.topicLoads = new Map();
10
+ }
11
+ async connect() {
12
+ if (this.ready) return;
13
+ if (this.connecting) return this.connecting;
14
+ const generation = this.generation;
15
+ const work = (async () => {
16
+ const value = await this.getConnection();
17
+ if (generation !== this.generation) throw new Error('消息连接已变化。');
18
+ const url = new URL(value.websocket_url);
19
+ if (url.protocol !== 'wss:' || url.username || url.password) throw new Error('消息推送地址无效。');
20
+ await new Promise((resolve, reject) => {
21
+ const socket = new WebSocket(url, { handshakeTimeout: 15000, maxPayload: 8 * 1024 * 1024 });
22
+ this.socket = socket;
23
+ const timer = setTimeout(() => { reject(new Error('消息推送订阅超时。')); socket.terminate(); }, 30000);
24
+ const current = () => this.socket === socket && generation === this.generation;
25
+ socket.on('error', () => {}); // Do not expose the signed URL in library errors.
26
+ socket.on('open', async () => {
27
+ try {
28
+ await this.command({ type: 'connect', presence: { type: 'presence', state: 'background' } });
29
+ for (const topic of this.initialTopics) await this.subscribe(topic);
30
+ if (!current()) throw new Error('消息连接已变化。');
31
+ this.ready = true; clearTimeout(timer); resolve(); this.emit('connected');
32
+ } catch (error) { clearTimeout(timer); reject(error); socket.terminate(); }
33
+ });
34
+ socket.on('message', data => {
35
+ if (!current()) return;
36
+ let frames; try { frames = JSON.parse(data.toString()); } catch { return; }
37
+ if (!Array.isArray(frames)) return;
38
+ for (const frame of frames) {
39
+ if (frame.type === 'message') this.receive(frame);
40
+ else if (frame.id != null) this.commands.get(frame.id)?.resolve(frame.reply);
41
+ }
42
+ });
43
+ socket.on('close', code => {
44
+ clearTimeout(timer); reject(new Error('消息推送连接已断开。'));
45
+ if (!current()) return;
46
+ this.ready = false; this.socket = null;
47
+ for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已断开。'));
48
+ this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
49
+ this.emit('disconnected', { source: 'celsius', code });
50
+ });
51
+ });
52
+ })();
53
+ this.connecting = work;
54
+ try { await work; } finally { if (this.connecting === work) this.connecting = null; }
55
+ }
56
+
57
+ command(command) {
58
+ if (this.socket?.readyState !== WebSocket.OPEN) return Promise.reject(new Error('消息推送尚未连接。'));
59
+ const id = ++this.nextId;
60
+ const socket = this.socket;
61
+ return new Promise((resolve, reject) => {
62
+ const finish = (error, value) => {
63
+ if (!this.commands.has(id)) return;
64
+ clearTimeout(timer); this.commands.delete(id);
65
+ if (error) reject(error); else resolve(value);
66
+ };
67
+ const timer = setTimeout(() => { finish(new Error('消息推送请求超时。')); socket.terminate(); }, 30000);
68
+ this.commands.set(id, { resolve: value => finish(null, value), reject: error => finish(error) });
69
+ socket.send(JSON.stringify([{ id, command }]), error => { if (error) finish(new Error('消息推送连接已断开。')); });
70
+ });
71
+ }
72
+
73
+ async subscribe(name, { offset, requireRecovery = false } = {}) {
74
+ if (this.topics.has(name)) return;
75
+ if (this.topicLoads.has(name)) return this.topicLoads.get(name);
76
+ const generation = this.generation, socket = this.socket;
77
+ // Every transport reconnection invalidates chat snapshots and reads a new
78
+ // latest window. An old offset never certifies the new snapshot by itself.
79
+ const work = this.command({ type: 'subscribe', topic_id: name, ...(offset == null ? {} : { offset }) }).then(reply => {
80
+ if (socket !== this.socket || generation !== this.generation) throw new Error('消息连接已变化。');
81
+ if (reply?.type !== 'subscribe' || reply.topic_id !== name) throw new Error('消息推送订阅未确认。');
82
+ if (requireRecovery && !reply.recovered) throw new Error('ChatGPT 输出历史未完整接续,请核对原消息。');
83
+ this.topics.set(name, reply.last_offset || null);
84
+ if (reply.recovered) for (const message of reply.catchups || []) this.receive(message);
85
+ });
86
+ this.topicLoads.set(name, work);
87
+ try { await work; } finally { if (this.topicLoads.get(name) === work) this.topicLoads.delete(name); }
88
+ }
89
+
90
+ async unsubscribe(name) {
91
+ if (!this.topics.delete(name) || this.socket?.readyState !== WebSocket.OPEN) return;
92
+ await this.command({ type: 'unsubscribe', topic_id: name });
93
+ }
94
+
95
+ receive(frame) {
96
+ if (frame.offset) this.topics.set(frame.topic_id, frame.offset);
97
+ this.emit('message', frame.topic_id, frame.payload);
98
+ }
99
+
100
+
101
+ close() {
102
+ this.generation++; this.ready = false; this.connecting = null;
103
+ const socket = this.socket; this.socket = null; socket?.terminate();
104
+ for (const entry of this.commands.values()) entry.reject(new Error('消息推送连接已结束。'));
105
+ this.commands.clear(); this.topics.clear(); this.topicLoads.clear();
106
+ }
107
+ }
@@ -1,100 +1,100 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import protocol from '../shared/code-browser.cjs';
5
- import { fileRange, fileRevision } from './file-transfer-range.mjs';
6
-
7
- async function checkedPath(root, relative) {
8
- const target = protocol.child(root, relative);
9
- return fs.realpath(target);
10
- }
11
-
12
- function git(cwd, args) {
13
- return new Promise((resolve, reject) => execFile('git', ['--no-optional-locks', '--literal-pathspecs', '-c', 'core.quotePath=false', ...args],
14
- { cwd, encoding: 'utf8', maxBuffer: protocol.maxBytes, timeout: 15000, windowsHide: true },
15
- (error, stdout, stderr) => {
16
- if (error) reject(Object.assign(new Error(error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
17
- ? 'Git 输出超过预览上限(1 MiB)。' : String(stderr || error.message).trim()), { statusCode: 422 }));
18
- else resolve(stdout);
19
- }));
20
- }
21
-
22
- export async function browseCode(rpc, operation, query) {
23
- if (operation === 'change') return protocol.change(rpc, query);
24
- if (operation === 'changes') return protocol.changes(rpc, query);
25
- if (operation === 'roots') return { ...await protocol.context(rpc, query), workspaceDiff: true, sizeCheckedBeforeRead: true };
26
- const root = query.get('root');
27
- if (typeof root !== 'string' || !path.isAbsolute(root)) protocol.fail('请输入电脑上的绝对目录路径。');
28
- if (operation === 'workspace' || operation === 'workspace-file') {
29
- const realRoot = await checkedPath(root, '');
30
- const repository = (await git(realRoot, ['rev-parse', '--show-toplevel'])).trim();
31
- const scope = path.relative(repository, realRoot).replaceAll('\\', '/') || '.';
32
- if (scope.startsWith('..') || path.isAbsolute(scope)) protocol.fail('无法确认 Git 仓库范围。', 403);
33
- const toRelative = name => protocol.relative(realRoot, path.resolve(repository, name));
34
- if (operation === 'workspace-file') {
35
- const group = query.get('group');
36
- if (!['staged', 'unstaged'].includes(group)) protocol.fail('未知的变更来源。');
37
- const names = [query.get('path'), query.get('oldPath')].filter(Boolean);
38
- if (!names.length) protocol.fail('请选择变更文件。');
39
- const paths = names.map(name => path.relative(repository, protocol.child(realRoot, name)).replaceAll('\\', '/'));
40
- const diff = await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--no-ext-diff', '--no-textconv', '--no-color', names.length > 1 ? '--find-renames' : '--no-renames', '--', ...paths]);
41
- return { diff, scope: group === 'staged' ? '当前工作区 · 已暂存' : '当前工作区 · 未暂存' };
42
- }
43
- const entries = [];
44
- for (const group of ['unstaged', 'staged']) {
45
- const fields = (await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--name-status', '-z', '--find-renames', '--no-ext-diff', '--no-textconv', '--', scope])).split('\0');
46
- for (let i = 0; i < fields.length && fields[i];) {
47
- const status = fields[i++], first = fields[i++];
48
- const renamed = /^[RC]/.test(status), last = renamed ? fields[i++] : first;
49
- const name = toRelative(last), oldPath = renamed ? toRelative(first) : null;
50
- if (name == null) { if (oldPath != null) entries.push({ path: oldPath, oldPath: null, status: 'D', group }); continue; }
51
- entries.push({ path: name, oldPath, status: renamed && oldPath == null ? 'A' : status, group });
52
- }
53
- }
54
- for (const name of (await git(repository, ['ls-files', '--others', '--exclude-standard', '-z', '--', scope])).split('\0').filter(Boolean)) {
55
- const relative = toRelative(name);
56
- if (relative != null) entries.push({ path: relative, status: '?', group: 'untracked' });
57
- }
58
- if (entries.length > protocol.maxEntries) protocol.fail('变更文件超过 4000 项,暂不支持展示。', 413);
59
- return { root, entries, scope: '当前工作区快照 · 非本次任务归属' };
60
- }
61
- const relative = query.get('path') || '';
62
- const target = await checkedPath(root, relative);
63
- if ((operation === 'image' || operation === 'binary')) {
64
- const handle = await fs.open(target, 'r');
65
- try {
66
- const before = await handle.stat();
67
- if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
68
- if (before.size > protocol.maxFileBytes) protocol.fail('文件超过预览上限(20 MiB)。', 413);
69
- if (!before.size) protocol.fail('文件为空。', 415);
70
- const revision = fileRevision(before);
71
- const { offset, length } = fileRange(query, before.size, revision);
72
- const header = Buffer.alloc(12);
73
- await handle.read(header, 0, header.length, 0);
74
- const mimeType = operation === 'binary' ? 'application/octet-stream' : protocol.imageMime(header);
75
- const data = Buffer.alloc(length);
76
- const { bytesRead } = await handle.read(data, 0, data.length, offset);
77
- const real = await checkedPath(root, relative), after = await handle.stat(), onDisk = await fs.stat(real);
78
- if (real !== target || bytesRead !== data.length || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.size !== before.size || onDisk.ino !== before.ino || onDisk.dev !== before.dev || onDisk.mtimeMs !== before.mtimeMs || onDisk.size !== before.size) protocol.fail('读取期间文件发生变化,请重新打开。', 409);
79
- return { name: path.basename(target), size: before.size, offset, revision, mimeType, data };
80
- } finally { await handle.close(); }
81
- }
82
- if (operation === 'directory') {
83
- const entries = await fs.readdir(target, { withFileTypes: true });
84
- const result = { entries: entries.map(entry => ({ fileName: entry.name, isDirectory: entry.isDirectory(), isFile: entry.isFile() })) };
85
- await checkedPath(root, relative);
86
- return protocol.directory(result);
87
- }
88
- if (operation === 'file') {
89
- const before = await fs.stat(target);
90
- if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
91
- if (before.size > protocol.maxBytes) protocol.fail('文件超过只读预览上限(1 MiB)。', 413);
92
- const result = { dataBase64: (await fs.readFile(target)).toString('base64') };
93
- const real = await checkedPath(root, relative), after = await fs.stat(real);
94
- if (real !== target || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
95
- protocol.fail('读取期间文件发生变化,请刷新。', 409);
96
- }
97
- return { ...protocol.fromBase64(result.dataBase64), modifiedAtMs: after.mtimeMs };
98
- }
99
- protocol.fail('未知的代码浏览操作。', 404);
100
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import protocol from '../shared/code-browser.cjs';
5
+ import { fileRange, fileRevision } from './file-transfer-range.mjs';
6
+
7
+ async function checkedPath(root, relative) {
8
+ const target = protocol.child(root, relative);
9
+ return fs.realpath(target);
10
+ }
11
+
12
+ function git(cwd, args) {
13
+ return new Promise((resolve, reject) => execFile('git', ['--no-optional-locks', '--literal-pathspecs', '-c', 'core.quotePath=false', ...args],
14
+ { cwd, encoding: 'utf8', maxBuffer: protocol.maxBytes, timeout: 15000, windowsHide: true },
15
+ (error, stdout, stderr) => {
16
+ if (error) reject(Object.assign(new Error(error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
17
+ ? 'Git 输出超过预览上限(1 MiB)。' : String(stderr || error.message).trim()), { statusCode: 422 }));
18
+ else resolve(stdout);
19
+ }));
20
+ }
21
+
22
+ export async function browseCode(rpc, operation, query) {
23
+ if (operation === 'change') return protocol.change(rpc, query);
24
+ if (operation === 'changes') return protocol.changes(rpc, query);
25
+ if (operation === 'roots') return { ...await protocol.context(rpc, query), workspaceDiff: true, sizeCheckedBeforeRead: true };
26
+ const root = query.get('root');
27
+ if (typeof root !== 'string' || !path.isAbsolute(root)) protocol.fail('请输入电脑上的绝对目录路径。');
28
+ if (operation === 'workspace' || operation === 'workspace-file') {
29
+ const realRoot = await checkedPath(root, '');
30
+ const repository = (await git(realRoot, ['rev-parse', '--show-toplevel'])).trim();
31
+ const scope = path.relative(repository, realRoot).replaceAll('\\', '/') || '.';
32
+ if (scope.startsWith('..') || path.isAbsolute(scope)) protocol.fail('无法确认 Git 仓库范围。', 403);
33
+ const toRelative = name => protocol.relative(realRoot, path.resolve(repository, name));
34
+ if (operation === 'workspace-file') {
35
+ const group = query.get('group');
36
+ if (!['staged', 'unstaged'].includes(group)) protocol.fail('未知的变更来源。');
37
+ const names = [query.get('path'), query.get('oldPath')].filter(Boolean);
38
+ if (!names.length) protocol.fail('请选择变更文件。');
39
+ const paths = names.map(name => path.relative(repository, protocol.child(realRoot, name)).replaceAll('\\', '/'));
40
+ const diff = await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--no-ext-diff', '--no-textconv', '--no-color', names.length > 1 ? '--find-renames' : '--no-renames', '--', ...paths]);
41
+ return { diff, scope: group === 'staged' ? '当前工作区 · 已暂存' : '当前工作区 · 未暂存' };
42
+ }
43
+ const entries = [];
44
+ for (const group of ['unstaged', 'staged']) {
45
+ const fields = (await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--name-status', '-z', '--find-renames', '--no-ext-diff', '--no-textconv', '--', scope])).split('\0');
46
+ for (let i = 0; i < fields.length && fields[i];) {
47
+ const status = fields[i++], first = fields[i++];
48
+ const renamed = /^[RC]/.test(status), last = renamed ? fields[i++] : first;
49
+ const name = toRelative(last), oldPath = renamed ? toRelative(first) : null;
50
+ if (name == null) { if (oldPath != null) entries.push({ path: oldPath, oldPath: null, status: 'D', group }); continue; }
51
+ entries.push({ path: name, oldPath, status: renamed && oldPath == null ? 'A' : status, group });
52
+ }
53
+ }
54
+ for (const name of (await git(repository, ['ls-files', '--others', '--exclude-standard', '-z', '--', scope])).split('\0').filter(Boolean)) {
55
+ const relative = toRelative(name);
56
+ if (relative != null) entries.push({ path: relative, status: '?', group: 'untracked' });
57
+ }
58
+ if (entries.length > protocol.maxEntries) protocol.fail('变更文件超过 4000 项,暂不支持展示。', 413);
59
+ return { root, entries, scope: '当前工作区快照 · 非本次任务归属' };
60
+ }
61
+ const relative = query.get('path') || '';
62
+ const target = await checkedPath(root, relative);
63
+ if ((operation === 'image' || operation === 'binary')) {
64
+ const handle = await fs.open(target, 'r');
65
+ try {
66
+ const before = await handle.stat();
67
+ if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
68
+ if (before.size > protocol.maxFileBytes) protocol.fail('文件超过预览上限(20 MiB)。', 413);
69
+ if (!before.size) protocol.fail('文件为空。', 415);
70
+ const revision = fileRevision(before);
71
+ const { offset, length } = fileRange(query, before.size, revision);
72
+ const header = Buffer.alloc(12);
73
+ await handle.read(header, 0, header.length, 0);
74
+ const mimeType = operation === 'binary' ? 'application/octet-stream' : protocol.imageMime(header);
75
+ const data = Buffer.alloc(length);
76
+ const { bytesRead } = await handle.read(data, 0, data.length, offset);
77
+ const real = await checkedPath(root, relative), after = await handle.stat(), onDisk = await fs.stat(real);
78
+ if (real !== target || bytesRead !== data.length || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.size !== before.size || onDisk.ino !== before.ino || onDisk.dev !== before.dev || onDisk.mtimeMs !== before.mtimeMs || onDisk.size !== before.size) protocol.fail('读取期间文件发生变化,请重新打开。', 409);
79
+ return { name: path.basename(target), size: before.size, offset, revision, mimeType, data };
80
+ } finally { await handle.close(); }
81
+ }
82
+ if (operation === 'directory') {
83
+ const entries = await fs.readdir(target, { withFileTypes: true });
84
+ const result = { entries: entries.map(entry => ({ fileName: entry.name, isDirectory: entry.isDirectory(), isFile: entry.isFile() })) };
85
+ await checkedPath(root, relative);
86
+ return protocol.directory(result);
87
+ }
88
+ if (operation === 'file') {
89
+ const before = await fs.stat(target);
90
+ if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
91
+ if (before.size > protocol.maxBytes) protocol.fail('文件超过只读预览上限(1 MiB)。', 413);
92
+ const result = { dataBase64: (await fs.readFile(target)).toString('base64') };
93
+ const real = await checkedPath(root, relative), after = await fs.stat(real);
94
+ if (real !== target || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
95
+ protocol.fail('读取期间文件发生变化,请刷新。', 409);
96
+ }
97
+ return { ...protocol.fromBase64(result.dataBase64), modifiedAtMs: after.mtimeMs };
98
+ }
99
+ protocol.fail('未知的代码浏览操作。', 404);
100
+ }
@@ -1,46 +1,46 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { setTimeout as delay } from 'node:timers/promises';
4
- import { readRecord } from './service-lifecycle.mjs';
5
- import { ManagedAppServer, managedAppServerUrl as sharedUrl, managedPortAvailable as portAvailable } from './managed-app-server.mjs';
6
- export { portAvailable };
7
- export async function launchDesktop({ stateRoot, host, environment = process.env, available = portAvailable, wait = delay, timeout = 30000, recoveryId, progress = () => {} }) {
8
- fs.mkdirSync(stateRoot, { recursive: true });
9
- const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
10
- if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请用 relax doctor 查看恢复状态。');
11
- const lock = path.join(stateRoot, 'desktop-launch.lock');
12
- let descriptor;
13
- let managed;
14
- try { descriptor = fs.openSync(lock, 'wx'); }
15
- catch (error) {
16
- if (error.code === 'EEXIST') throw new Error('另一个 Desktop 启动操作正在进行;若上次异常退出,请确认没有启动器运行后移除 desktop-launch.lock。');
17
- throw error;
18
- }
19
- try {
20
- const info = await host.inspect();
21
- if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
22
- const connected = await host.sharedServiceUrl();
23
- if (info.running) {
24
- if (connected) return { reused: true };
25
- throw new Error('请先保存任务并完全退出 Codex Desktop,再接入 Codex 内核;不会自动结束正在运行的任务。');
26
- }
27
- const childEnv = { ...environment };
28
- for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
29
- progress('starting-app-server');
30
- managed = new ManagedAppServer({ stateRoot, host, environment, available, wait, timeout });
31
- if (!await managed.start()) throw new Error(managed.snapshot().error || 'Codex 内核未就绪。');
32
- const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
33
- if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
34
- childEnv.CODEX_APP_SERVER_WS_URL = sharedUrl;
35
- progress('starting-desktop');
36
- await host.startDesktop(info.desktop, childEnv);
37
- const deadline = Date.now() + timeout;
38
- while (true) {
39
- const ready = await host.desktopReady();
40
- if (ready) break;
41
- if (Date.now() >= deadline) throw new Error('Desktop 尚未接入 Codex 内核。请查看 Desktop 窗口后重试。');
42
- await wait(500);
43
- }
44
- return { reused: false };
45
- } finally { managed?.close(); fs.closeSync(descriptor); fs.rmSync(lock, { force: true }); }
46
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { readRecord } from './service-lifecycle.mjs';
5
+ import { ManagedAppServer, managedAppServerUrl as sharedUrl, managedPortAvailable as portAvailable } from './managed-app-server.mjs';
6
+ export { portAvailable };
7
+ export async function launchDesktop({ stateRoot, host, environment = process.env, available = portAvailable, wait = delay, timeout = 30000, recoveryId, progress = () => {} }) {
8
+ fs.mkdirSync(stateRoot, { recursive: true });
9
+ const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
10
+ if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请用 relax doctor 查看恢复状态。');
11
+ const lock = path.join(stateRoot, 'desktop-launch.lock');
12
+ let descriptor;
13
+ let managed;
14
+ try { descriptor = fs.openSync(lock, 'wx'); }
15
+ catch (error) {
16
+ if (error.code === 'EEXIST') throw new Error('另一个 Desktop 启动操作正在进行;若上次异常退出,请确认没有启动器运行后移除 desktop-launch.lock。');
17
+ throw error;
18
+ }
19
+ try {
20
+ const info = await host.inspect();
21
+ if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
22
+ const connected = await host.sharedServiceUrl();
23
+ if (info.running) {
24
+ if (connected) return { reused: true };
25
+ throw new Error('请先保存任务并完全退出 Codex Desktop,再接入 Codex 内核;不会自动结束正在运行的任务。');
26
+ }
27
+ const childEnv = { ...environment };
28
+ for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
29
+ progress('starting-app-server');
30
+ managed = new ManagedAppServer({ stateRoot, host, environment, available, wait, timeout });
31
+ if (!await managed.start()) throw new Error(managed.snapshot().error || 'Codex 内核未就绪。');
32
+ const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
33
+ if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
34
+ childEnv.CODEX_APP_SERVER_WS_URL = sharedUrl;
35
+ progress('starting-desktop');
36
+ await host.startDesktop(info.desktop, childEnv);
37
+ const deadline = Date.now() + timeout;
38
+ while (true) {
39
+ const ready = await host.desktopReady();
40
+ if (ready) break;
41
+ if (Date.now() >= deadline) throw new Error('Desktop 尚未接入 Codex 内核。请查看 Desktop 窗口后重试。');
42
+ await wait(500);
43
+ }
44
+ return { reused: false };
45
+ } finally { managed?.close(); fs.closeSync(descriptor); fs.rmSync(lock, { force: true }); }
46
+ }