@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,179 +1,179 @@
1
- import net from 'node:net';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import { createHash } from 'node:crypto';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const exec = promisify(execFile);
10
- const binary = fileURLToPath(new URL('../../../native/desktop-helper/CodeRelaxDesktop.exe', import.meta.url));
11
- const powershell = path.join(process.env.SystemRoot || 'C:/Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
12
- const quote = value => `'${String(value).replaceAll("'", "''")}'`;
13
- const encoded = script => Buffer.from(script, 'utf16le').toString('base64');
14
- const maxFrameBytes = 6 * 1024 * 1024;
15
-
16
- export async function desktopHelperStatus() {
17
- if (process.platform !== 'win32' || !fs.existsSync(binary)) return { installed: false, running: false, state: 'unavailable' };
18
- const { stdout } = await exec(binary, ['--status'], { windowsHide: true, encoding: 'utf8', timeout: 5000 });
19
- const result = JSON.parse(stdout.trim());
20
- if (result.error) throw new Error(result.error);
21
- return result;
22
- }
23
-
24
- async function elevateHelper(argument) {
25
- const script = `$ErrorActionPreference = 'Stop'; try { $child = Start-Process -FilePath ${quote(binary)} -ArgumentList ${quote(argument)} -Verb RunAs -PassThru -Wait; exit $child.ExitCode } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }`;
26
- try { await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded(script)], { windowsHide: true, timeout: 180000 }); }
27
- catch (error) { throw new Error(`桌面操作辅助服务配置未完成,需要在工作站确认 UAC:${error.stderr?.trim() || error.message}`); }
28
- }
29
-
30
- export async function removeDesktopHelper() {
31
- const current = await desktopHelperStatus();
32
- if (!current.installed) return { installed: false, removed: false };
33
- await elevateHelper('--remove');
34
- const result = await desktopHelperStatus();
35
- if (result.installed) throw new Error('桌面操作辅助服务尚未移除。');
36
- return { ...result, removed: true };
37
- }
38
-
39
- export async function installDesktopHelper() {
40
- if (!fs.existsSync(binary)) throw new Error('缺少原生 桌面操作辅助组件,请先构建或安装完整 npm 包。');
41
- const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded('[Console]::OutputEncoding = New-Object Text.UTF8Encoding; [Security.Principal.WindowsIdentity]::GetCurrent().User.Value')], { windowsHide: true, encoding: 'utf8' });
42
- const sid = stdout.trim();
43
- if (!/^S-1-5-21-(?:\d+-){3}\d+$/.test(sid)) throw new Error('无法获取当前 Windows 用户 SID。');
44
- const current = await desktopHelperStatus();
45
- const hash = target => createHash('sha256').update(fs.readFileSync(target)).digest('hex');
46
- if (current.running && current.userSid === sid && hash(binary) === hash(current.path)) return { ...current, changed: false };
47
- // Only this installer is elevated. Node and the workstation remain user-level.
48
- await elevateHelper(`--install ${sid}`);
49
- const installed = await desktopHelperStatus();
50
- if (!installed.running || installed.userSid !== sid) throw new Error('桌面操作辅助服务尚未就绪。');
51
- return { ...installed, changed: true };
52
- }
53
-
54
- class PipeSession {
55
- constructor() {
56
- this.socket = net.createConnection('\\\\.\\pipe\\CodeRelax.Desktop.v1');
57
- this.socket.setEncoding('utf8');
58
- this.buffer = '';
59
- this.waiter = null;
60
- this.closed = false;
61
- this.socket.on('data', data => {
62
- this.buffer += data;
63
- if (Buffer.byteLength(this.buffer) > maxFrameBytes) { this.fail(new Error('桌面画面超过大小限制')); return; }
64
- const end = this.buffer.indexOf('\n');
65
- if (end < 0) return;
66
- try {
67
- const result = JSON.parse(this.buffer.slice(0, end));
68
- this.buffer = this.buffer.slice(end + 1);
69
- if (!this.waiter) throw new Error('收到未请求的 桌面数据');
70
- const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer);
71
- const complete = () => result.error
72
- ? waiter.reject(Object.assign(new Error(result.error), { code: 'desktop_native_error' })) : waiter.resolve(result);
73
- if (result.state === 'pending' || result.continuing === true) complete();
74
- else this.socket.write('ACK\n', error => {
75
- if (error) console.warn(JSON.stringify({ event: 'desktop-response-ack', code: error.code || 'write_failed' }));
76
- // The parsed result remains authoritative even if cleanup disconnects.
77
- complete();
78
- });
79
- } catch (error) { this.fail(error); }
80
- });
81
- this.socket.on('error', error => this.fail(error));
82
- this.socket.on('close', () => { this.closed = true; this.fail(new Error('桌面操作辅助连接已关闭,请重新获取画面')); });
83
- }
84
- fail(error) {
85
- if (this.waiter) { const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer); waiter.reject(error); }
86
- this.socket.destroy();
87
- }
88
- request(command) {
89
- if (this.closed || this.socket.destroyed || this.waiter) return Promise.reject(new Error('桌面操作辅助连接不可用'));
90
- return new Promise((resolve, reject) => {
91
- this.waiter = { resolve, reject, timer: setTimeout(() => this.fail(new Error('桌面操作辅助请求超时;点击不会自动重发,请刷新画面核对')), 10000) };
92
- this.socket.write(`${command}\n`);
93
- });
94
- }
95
- close() { this.fail(new Error('桌面画面已释放')); }
96
- }
97
-
98
- // Frame and input share one worker pipe. Serialize them without replaying input.
99
- export class DesktopHelper {
100
- constructor() { this.active = null; this.reading = false; this.queue = Promise.resolve(); this.version = 0; }
101
- serial(operation) {
102
- const version = this.version;
103
- const result = this.queue.then(() => {
104
- if (version !== this.version) throw Object.assign(new Error('桌面操作已取消,请刷新'), { code: 'desktop_session_invalid' });
105
- return operation();
106
- });
107
- this.queue = result.catch(() => {});
108
- return result;
109
- }
110
- clear(current = this.active) {
111
- if (!current) return;
112
- clearTimeout(current.timer); current.session.close();
113
- if (this.active === current) this.active = null;
114
- }
115
- async frame({ preview = false } = {}) {
116
- if (this.reading) throw new Error('正在获取桌面画面,请稍候');
117
- this.reading = true;
118
- try { return await this.serial(async () => {
119
- if (!this.active || this.active.preview !== preview) {
120
- this.clear();
121
- const version = this.version;
122
- const status = await desktopHelperStatus();
123
- if (version !== this.version) throw new Error('桌面操作已取消');
124
- if (!status.running) return { state: 'unavailable', message: '工作站桌面服务尚未就绪' };
125
- this.active = { session: new PipeSession(), preview };
126
- }
127
- const current = this.active;
128
- try {
129
- clearTimeout(current.timer);
130
- const frame = await current.session.request(current.frame ? 'FRAME' : preview ? 'PREVIEW' : 'FRAME');
131
- if (this.active !== current) throw new Error('桌面画面已释放');
132
- if (frame.state !== 'pending') { this.clear(current); return frame; }
133
- if (!/^[a-f0-9]{32}$/.test(frame.frameId) || !/^[a-f0-9]{32}$/.test(frame.sessionId) || !Number.isSafeInteger(frame.width) || !Number.isSafeInteger(frame.height) ||
134
- !['image/png', 'image/jpeg'].includes(frame.mimeType) || !frame.image?.startsWith('data:' + frame.mimeType + ';base64,') ||
135
- frame.width < 1 || frame.height < 1 || frame.preview !== preview) throw new Error('桌面画面格式无效');
136
- const image = Buffer.from(frame.image.slice(('data:' + frame.mimeType + ';base64,').length), 'base64');
137
- delete frame.image;
138
- current.sessionId = frame.sessionId;
139
- frame.imagePath = '/api/desktop/frames/' + frame.frameId;
140
- frame.size = image.length;
141
- current.frame = frame; current.image = image; current.expires = Date.now() + 30000;
142
- current.timer = setTimeout(() => this.release(undefined, current.sessionId), 30000);
143
- return frame;
144
- } catch (error) { this.clear(current); throw error; }
145
- }); } finally { this.reading = false; }
146
- }
147
- input(sessionId, command) {
148
- return this.serial(async () => {
149
- const current = this.active;
150
- if (!current?.frame || sessionId !== current.sessionId || Date.now() >= current.expires)
151
- throw Object.assign(new Error('请刷新桌面后操作'), { code: 'desktop_session_invalid' });
152
- try {
153
- const result = await current.session.request(command);
154
- if (result.continuing !== true) this.clear(current);
155
- return result;
156
- } catch (error) { this.clear(current); throw error; }
157
- });
158
- }
159
- move({ sessionId, dx, dy }) {
160
- if (!Number.isSafeInteger(dx) || !Number.isSafeInteger(dy) || Math.abs(dx) > 512 || Math.abs(dy) > 512 || (!dx && !dy))
161
- throw new Error('无效的相对鼠标位移');
162
- return this.input(sessionId, 'MOVE ' + sessionId + ' ' + dx + ' ' + dy);
163
- }
164
- click({ sessionId, button }) {
165
- if (!['left', 'right'].includes(button)) throw new Error('无效的鼠标按键');
166
- return this.input(sessionId, 'BUTTON ' + sessionId + ' ' + button);
167
- }
168
- image(frameId) {
169
- const current = this.active;
170
- if (!current?.frame || current.frame.frameId !== frameId || Date.now() >= current.expires)
171
- throw new Error('桌面画面已释放或过期,请重新获取');
172
- return current.image;
173
- }
174
- release(frameId, sessionId) {
175
- if (frameId && this.active?.frame?.frameId !== frameId) return;
176
- if (sessionId && this.active?.sessionId !== sessionId) return;
177
- this.version++; this.clear();
178
- }
179
- }
1
+ import net from 'node:net';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { createHash } from 'node:crypto';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const exec = promisify(execFile);
10
+ const binary = fileURLToPath(new URL('../../../native/desktop-helper/CodeRelaxDesktop.exe', import.meta.url));
11
+ const powershell = path.join(process.env.SystemRoot || 'C:/Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
12
+ const quote = value => `'${String(value).replaceAll("'", "''")}'`;
13
+ const encoded = script => Buffer.from(script, 'utf16le').toString('base64');
14
+ const maxFrameBytes = 6 * 1024 * 1024;
15
+
16
+ export async function desktopHelperStatus() {
17
+ if (process.platform !== 'win32' || !fs.existsSync(binary)) return { installed: false, running: false, state: 'unavailable' };
18
+ const { stdout } = await exec(binary, ['--status'], { windowsHide: true, encoding: 'utf8', timeout: 5000 });
19
+ const result = JSON.parse(stdout.trim());
20
+ if (result.error) throw new Error(result.error);
21
+ return result;
22
+ }
23
+
24
+ async function elevateHelper(argument) {
25
+ const script = `$ErrorActionPreference = 'Stop'; try { $child = Start-Process -FilePath ${quote(binary)} -ArgumentList ${quote(argument)} -Verb RunAs -PassThru -Wait; exit $child.ExitCode } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }`;
26
+ try { await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded(script)], { windowsHide: true, timeout: 180000 }); }
27
+ catch (error) { throw new Error(`桌面操作辅助服务配置未完成,需要在工作站确认 UAC:${error.stderr?.trim() || error.message}`); }
28
+ }
29
+
30
+ export async function removeDesktopHelper() {
31
+ const current = await desktopHelperStatus();
32
+ if (!current.installed) return { installed: false, removed: false };
33
+ await elevateHelper('--remove');
34
+ const result = await desktopHelperStatus();
35
+ if (result.installed) throw new Error('桌面操作辅助服务尚未移除。');
36
+ return { ...result, removed: true };
37
+ }
38
+
39
+ export async function installDesktopHelper() {
40
+ if (!fs.existsSync(binary)) throw new Error('缺少原生 桌面操作辅助组件,请先构建或安装完整 npm 包。');
41
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded('[Console]::OutputEncoding = New-Object Text.UTF8Encoding; [Security.Principal.WindowsIdentity]::GetCurrent().User.Value')], { windowsHide: true, encoding: 'utf8' });
42
+ const sid = stdout.trim();
43
+ if (!/^S-1-5-21-(?:\d+-){3}\d+$/.test(sid)) throw new Error('无法获取当前 Windows 用户 SID。');
44
+ const current = await desktopHelperStatus();
45
+ const hash = target => createHash('sha256').update(fs.readFileSync(target)).digest('hex');
46
+ if (current.running && current.userSid === sid && hash(binary) === hash(current.path)) return { ...current, changed: false };
47
+ // Only this installer is elevated. Node and the workstation remain user-level.
48
+ await elevateHelper(`--install ${sid}`);
49
+ const installed = await desktopHelperStatus();
50
+ if (!installed.running || installed.userSid !== sid) throw new Error('桌面操作辅助服务尚未就绪。');
51
+ return { ...installed, changed: true };
52
+ }
53
+
54
+ class PipeSession {
55
+ constructor() {
56
+ this.socket = net.createConnection('\\\\.\\pipe\\CodeRelax.Desktop.v1');
57
+ this.socket.setEncoding('utf8');
58
+ this.buffer = '';
59
+ this.waiter = null;
60
+ this.closed = false;
61
+ this.socket.on('data', data => {
62
+ this.buffer += data;
63
+ if (Buffer.byteLength(this.buffer) > maxFrameBytes) { this.fail(new Error('桌面画面超过大小限制')); return; }
64
+ const end = this.buffer.indexOf('\n');
65
+ if (end < 0) return;
66
+ try {
67
+ const result = JSON.parse(this.buffer.slice(0, end));
68
+ this.buffer = this.buffer.slice(end + 1);
69
+ if (!this.waiter) throw new Error('收到未请求的 桌面数据');
70
+ const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer);
71
+ const complete = () => result.error
72
+ ? waiter.reject(Object.assign(new Error(result.error), { code: 'desktop_native_error' })) : waiter.resolve(result);
73
+ if (result.state === 'pending' || result.continuing === true) complete();
74
+ else this.socket.write('ACK\n', error => {
75
+ if (error) console.warn(JSON.stringify({ event: 'desktop-response-ack', code: error.code || 'write_failed' }));
76
+ // The parsed result remains authoritative even if cleanup disconnects.
77
+ complete();
78
+ });
79
+ } catch (error) { this.fail(error); }
80
+ });
81
+ this.socket.on('error', error => this.fail(error));
82
+ this.socket.on('close', () => { this.closed = true; this.fail(new Error('桌面操作辅助连接已关闭,请重新获取画面')); });
83
+ }
84
+ fail(error) {
85
+ if (this.waiter) { const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer); waiter.reject(error); }
86
+ this.socket.destroy();
87
+ }
88
+ request(command) {
89
+ if (this.closed || this.socket.destroyed || this.waiter) return Promise.reject(new Error('桌面操作辅助连接不可用'));
90
+ return new Promise((resolve, reject) => {
91
+ this.waiter = { resolve, reject, timer: setTimeout(() => this.fail(new Error('桌面操作辅助请求超时;点击不会自动重发,请刷新画面核对')), 10000) };
92
+ this.socket.write(`${command}\n`);
93
+ });
94
+ }
95
+ close() { this.fail(new Error('桌面画面已释放')); }
96
+ }
97
+
98
+ // Frame and input share one worker pipe. Serialize them without replaying input.
99
+ export class DesktopHelper {
100
+ constructor() { this.active = null; this.reading = false; this.queue = Promise.resolve(); this.version = 0; }
101
+ serial(operation) {
102
+ const version = this.version;
103
+ const result = this.queue.then(() => {
104
+ if (version !== this.version) throw Object.assign(new Error('桌面操作已取消,请刷新'), { code: 'desktop_session_invalid' });
105
+ return operation();
106
+ });
107
+ this.queue = result.catch(() => {});
108
+ return result;
109
+ }
110
+ clear(current = this.active) {
111
+ if (!current) return;
112
+ clearTimeout(current.timer); current.session.close();
113
+ if (this.active === current) this.active = null;
114
+ }
115
+ async frame({ preview = false } = {}) {
116
+ if (this.reading) throw new Error('正在获取桌面画面,请稍候');
117
+ this.reading = true;
118
+ try { return await this.serial(async () => {
119
+ if (!this.active || this.active.preview !== preview) {
120
+ this.clear();
121
+ const version = this.version;
122
+ const status = await desktopHelperStatus();
123
+ if (version !== this.version) throw new Error('桌面操作已取消');
124
+ if (!status.running) return { state: 'unavailable', message: '工作站桌面服务尚未就绪' };
125
+ this.active = { session: new PipeSession(), preview };
126
+ }
127
+ const current = this.active;
128
+ try {
129
+ clearTimeout(current.timer);
130
+ const frame = await current.session.request(current.frame ? 'FRAME' : preview ? 'PREVIEW' : 'FRAME');
131
+ if (this.active !== current) throw new Error('桌面画面已释放');
132
+ if (frame.state !== 'pending') { this.clear(current); return frame; }
133
+ if (!/^[a-f0-9]{32}$/.test(frame.frameId) || !/^[a-f0-9]{32}$/.test(frame.sessionId) || !Number.isSafeInteger(frame.width) || !Number.isSafeInteger(frame.height) ||
134
+ !['image/png', 'image/jpeg'].includes(frame.mimeType) || !frame.image?.startsWith('data:' + frame.mimeType + ';base64,') ||
135
+ frame.width < 1 || frame.height < 1 || frame.preview !== preview) throw new Error('桌面画面格式无效');
136
+ const image = Buffer.from(frame.image.slice(('data:' + frame.mimeType + ';base64,').length), 'base64');
137
+ delete frame.image;
138
+ current.sessionId = frame.sessionId;
139
+ frame.imagePath = '/api/desktop/frames/' + frame.frameId;
140
+ frame.size = image.length;
141
+ current.frame = frame; current.image = image; current.expires = Date.now() + 30000;
142
+ current.timer = setTimeout(() => this.release(undefined, current.sessionId), 30000);
143
+ return frame;
144
+ } catch (error) { this.clear(current); throw error; }
145
+ }); } finally { this.reading = false; }
146
+ }
147
+ input(sessionId, command) {
148
+ return this.serial(async () => {
149
+ const current = this.active;
150
+ if (!current?.frame || sessionId !== current.sessionId || Date.now() >= current.expires)
151
+ throw Object.assign(new Error('请刷新桌面后操作'), { code: 'desktop_session_invalid' });
152
+ try {
153
+ const result = await current.session.request(command);
154
+ if (result.continuing !== true) this.clear(current);
155
+ return result;
156
+ } catch (error) { this.clear(current); throw error; }
157
+ });
158
+ }
159
+ move({ sessionId, dx, dy }) {
160
+ if (!Number.isSafeInteger(dx) || !Number.isSafeInteger(dy) || Math.abs(dx) > 512 || Math.abs(dy) > 512 || (!dx && !dy))
161
+ throw new Error('无效的相对鼠标位移');
162
+ return this.input(sessionId, 'MOVE ' + sessionId + ' ' + dx + ' ' + dy);
163
+ }
164
+ click({ sessionId, button }) {
165
+ if (!['left', 'right'].includes(button)) throw new Error('无效的鼠标按键');
166
+ return this.input(sessionId, 'BUTTON ' + sessionId + ' ' + button);
167
+ }
168
+ image(frameId) {
169
+ const current = this.active;
170
+ if (!current?.frame || current.frame.frameId !== frameId || Date.now() >= current.expires)
171
+ throw new Error('桌面画面已释放或过期,请重新获取');
172
+ return current.image;
173
+ }
174
+ release(frameId, sessionId) {
175
+ if (frameId && this.active?.frame?.frameId !== frameId) return;
176
+ if (sessionId && this.active?.sessionId !== sessionId) return;
177
+ this.version++; this.clear();
178
+ }
179
+ }
@@ -1,58 +1,58 @@
1
- import { startWindowsProcess } from './background-process.mjs';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import { fileURLToPath } from 'node:url';
5
- import path from 'node:path';
6
- import { sharedServiceUrl } from './service-host.mjs';
7
- const exec = promisify(execFile);
8
- const directory = path.dirname(fileURLToPath(import.meta.url));
9
- export const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
10
- export async function runScript(name, args = []) {
11
- const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', path.join(directory, name), ...args], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
12
- return stdout.trim();
13
- }
14
- export const desktopHost = {
15
- inspect: async () => JSON.parse(await runScript('desktop-info.ps1')),
16
- process: async pid => JSON.parse(await runScript('desktop-info.ps1', ['-ProcessId', String(pid)])),
17
- sharedServiceUrl,
18
- start: startWindowsProcess,
19
- startDesktop: (executable, environment) => runScript('activate-desktop.ps1', [
20
- '-Executable', executable, '-AppServerUrl', environment.CODEX_APP_SERVER_WS_URL,
21
- ]),
22
- async desktopReady() {
23
- return (await this.inspect()).hasWindow === true && Boolean(await this.sharedServiceUrl());
24
- },
25
- closeDesktop: executable => runScript('stop-desktop.ps1', ['-Executable', executable]),
26
- stop: pid => process.kill(pid),
27
- async prepareKernelRestart() {
28
- const info = await this.inspect();
29
- if (!info.running || !await this.sharedServiceUrl()) return null;
30
- await this.closeDesktop(info.desktop);
31
- return info.desktop;
32
- },
33
- async resumeAfterKernelRestart(executable, environment) {
34
- if (!executable) return;
35
- await this.startDesktop(executable, environment);
36
- const deadline = Date.now() + 30000;
37
- while (!await this.desktopReady()) {
38
- if (Date.now() >= deadline) throw new Error('Codex 内核已就绪,但 Desktop 未完成重新接入。');
39
- await new Promise(resolve => setTimeout(resolve, 500));
40
- }
41
- },
42
- ready: async url => { try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } },
43
- };
44
- export const resolveStateRoot = stateRoot => runScript('resolve-state-root.ps1', ['-StateRoot', stateRoot]);
45
-
46
- export async function removeDesktopShortcut({ serviceRoot, nodeExecutable }, destination) {
47
- const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
48
- const args = ['-NodePath', nodeExecutable, '-Entry', entry];
49
- if (destination) args.push('-Destination', destination);
50
- return JSON.parse(await runScript('desktop-shortcuts.ps1', args));
51
- }
52
-
53
- export async function residentStartup({ serviceRoot, nodeExecutable }, action = 'status') {
54
- const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
55
- path.join(serviceRoot, 'dist/src/platform/windows/resident-startup.ps1'), '-NodePath', nodeExecutable,
56
- '-Entry', path.join(serviceRoot, 'dist/bin/codex-remote.mjs'), '-Action', action], { windowsHide: true, encoding: 'utf8' });
57
- return JSON.parse(stdout.trim());
58
- }
1
+ import { startWindowsProcess } from './background-process.mjs';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { fileURLToPath } from 'node:url';
5
+ import path from 'node:path';
6
+ import { sharedServiceUrl } from './service-host.mjs';
7
+ const exec = promisify(execFile);
8
+ const directory = path.dirname(fileURLToPath(import.meta.url));
9
+ export const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
10
+ export async function runScript(name, args = []) {
11
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', path.join(directory, name), ...args], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
12
+ return stdout.trim();
13
+ }
14
+ export const desktopHost = {
15
+ inspect: async () => JSON.parse(await runScript('desktop-info.ps1')),
16
+ process: async pid => JSON.parse(await runScript('desktop-info.ps1', ['-ProcessId', String(pid)])),
17
+ sharedServiceUrl,
18
+ start: startWindowsProcess,
19
+ startDesktop: (executable, environment) => runScript('activate-desktop.ps1', [
20
+ '-Executable', executable, '-AppServerUrl', environment.CODEX_APP_SERVER_WS_URL,
21
+ ]),
22
+ async desktopReady() {
23
+ return (await this.inspect()).hasWindow === true && Boolean(await this.sharedServiceUrl());
24
+ },
25
+ closeDesktop: executable => runScript('stop-desktop.ps1', ['-Executable', executable]),
26
+ stop: pid => process.kill(pid),
27
+ async prepareKernelRestart() {
28
+ const info = await this.inspect();
29
+ if (!info.running || !await this.sharedServiceUrl()) return null;
30
+ await this.closeDesktop(info.desktop);
31
+ return info.desktop;
32
+ },
33
+ async resumeAfterKernelRestart(executable, environment) {
34
+ if (!executable) return;
35
+ await this.startDesktop(executable, environment);
36
+ const deadline = Date.now() + 30000;
37
+ while (!await this.desktopReady()) {
38
+ if (Date.now() >= deadline) throw new Error('Codex 内核已就绪,但 Desktop 未完成重新接入。');
39
+ await new Promise(resolve => setTimeout(resolve, 500));
40
+ }
41
+ },
42
+ ready: async url => { try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } },
43
+ };
44
+ export const resolveStateRoot = stateRoot => runScript('resolve-state-root.ps1', ['-StateRoot', stateRoot]);
45
+
46
+ export async function removeDesktopShortcut({ serviceRoot, nodeExecutable }, destination) {
47
+ const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
48
+ const args = ['-NodePath', nodeExecutable, '-Entry', entry];
49
+ if (destination) args.push('-Destination', destination);
50
+ return JSON.parse(await runScript('desktop-shortcuts.ps1', args));
51
+ }
52
+
53
+ export async function residentStartup({ serviceRoot, nodeExecutable }, action = 'status') {
54
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
55
+ path.join(serviceRoot, 'dist/src/platform/windows/resident-startup.ps1'), '-NodePath', nodeExecutable,
56
+ '-Entry', path.join(serviceRoot, 'dist/bin/codex-remote.mjs'), '-Action', action], { windowsHide: true, encoding: 'utf8' });
57
+ return JSON.parse(stdout.trim());
58
+ }
@@ -1,14 +1,14 @@
1
- param([int]$ProcessId = 0)
2
- $ErrorActionPreference = 'Stop'
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
- if ($ProcessId -gt 0) {
5
- $item = Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId"
6
- if ($item) { @{ pid=$item.ProcessId; executable=$item.ExecutablePath; command=$item.CommandLine; created=$item.CreationDate.ToUniversalTime().ToString('o') } | ConvertTo-Json -Compress }
7
- else { 'null' }
8
- exit
9
- }
10
- $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
11
- if (-not $package) { @{ desktop=$null; running=$false } | ConvertTo-Json -Compress; exit }
12
- $desktop = Join-Path $package.InstallLocation 'app/ChatGPT.exe'
13
- $running = @(Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $desktop })
14
- @{ desktop=$desktop; running=($running.Count -gt 0); hasWindow=(@($running | Where-Object { $_.MainWindowHandle -ne 0 }).Count -gt 0) } | ConvertTo-Json -Compress
1
+ param([int]$ProcessId = 0)
2
+ $ErrorActionPreference = 'Stop'
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+ if ($ProcessId -gt 0) {
5
+ $item = Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId"
6
+ if ($item) { @{ pid=$item.ProcessId; executable=$item.ExecutablePath; command=$item.CommandLine; created=$item.CreationDate.ToUniversalTime().ToString('o') } | ConvertTo-Json -Compress }
7
+ else { 'null' }
8
+ exit
9
+ }
10
+ $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
11
+ if (-not $package) { @{ desktop=$null; running=$false } | ConvertTo-Json -Compress; exit }
12
+ $desktop = Join-Path $package.InstallLocation 'app/ChatGPT.exe'
13
+ $running = @(Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $desktop })
14
+ @{ desktop=$desktop; running=($running.Count -gt 0); hasWindow=(@($running | Where-Object { $_.MainWindowHandle -ne 0 }).Count -gt 0) } | ConvertTo-Json -Compress
@@ -1,4 +1,4 @@
1
- import { desktopHost } from './desktop-host.mjs';
2
- import { withDesktopProjectRegistration as register } from '../desktop-project-store.mjs';
3
- export { registerProjectInState, desktopProjectRestartWarning } from '../desktop-project-store.mjs';
4
- export const withDesktopProjectRegistration = (options, action) => register({ host: desktopHost, ...options }, action);
1
+ import { desktopHost } from './desktop-host.mjs';
2
+ import { withDesktopProjectRegistration as register } from '../desktop-project-store.mjs';
3
+ export { registerProjectInState, desktopProjectRestartWarning } from '../desktop-project-store.mjs';
4
+ export const withDesktopProjectRegistration = (options, action) => register({ host: desktopHost, ...options }, action);
@@ -1,10 +1,10 @@
1
- $ErrorActionPreference = 'Stop'
2
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
3
- $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
4
- if (-not $package) { 'null'; exit }
5
- $resources = Join-Path $package.InstallLocation 'app\resources'
6
- @{
7
- package = $package.PackageFullName
8
- executable = Join-Path $resources 'codex.exe'
9
- codeModeHost = Join-Path $resources 'codex-code-mode-host.exe'
10
- } | ConvertTo-Json -Compress
1
+ $ErrorActionPreference = 'Stop'
2
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
3
+ $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
4
+ if (-not $package) { 'null'; exit }
5
+ $resources = Join-Path $package.InstallLocation 'app\resources'
6
+ @{
7
+ package = $package.PackageFullName
8
+ executable = Join-Path $resources 'codex.exe'
9
+ codeModeHost = Join-Path $resources 'codex-code-mode-host.exe'
10
+ } | ConvertTo-Json -Compress
@@ -1,21 +1,21 @@
1
- param([string]$NodePath, [string]$Entry, [string]$Destination)
2
- $ErrorActionPreference = 'Stop'
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
- $shell = New-Object -ComObject WScript.Shell
5
- if (-not $Destination) { $Destination = $shell.SpecialFolders.Item('Desktop') }
6
- # Only identify and remove the retired shortcut; never create a new launcher.
7
- $launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
8
- $items = @(@{Name='Code Relax - Codex 内核'; Command='shared-start'})
9
- $results = @()
10
- foreach ($item in $items) {
11
- $target = Join-Path $Destination ($item.Name + '.lnk')
12
- $arguments = '-NoProfile -NoExit -ExecutionPolicy Bypass -File "' + $launcher + '" -NodePath "' + $NodePath + '" -Entry "' + $Entry + '" -Action ' + $item.Command
13
- if (Test-Path -LiteralPath $target) {
14
- $existing = $shell.CreateShortcut($target)
15
- $powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
16
- if ($existing.Arguments -ne $arguments -or $existing.TargetPath -ne $powershell) { continue }
17
- }
18
- if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target }
19
- $results += $target
20
- }
21
- ConvertTo-Json -InputObject $results -Compress
1
+ param([string]$NodePath, [string]$Entry, [string]$Destination)
2
+ $ErrorActionPreference = 'Stop'
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+ $shell = New-Object -ComObject WScript.Shell
5
+ if (-not $Destination) { $Destination = $shell.SpecialFolders.Item('Desktop') }
6
+ # Only identify and remove the retired shortcut; never create a new launcher.
7
+ $launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
8
+ $items = @(@{Name='Code Relax - Codex 内核'; Command='shared-start'})
9
+ $results = @()
10
+ foreach ($item in $items) {
11
+ $target = Join-Path $Destination ($item.Name + '.lnk')
12
+ $arguments = '-NoProfile -NoExit -ExecutionPolicy Bypass -File "' + $launcher + '" -NodePath "' + $NodePath + '" -Entry "' + $Entry + '" -Action ' + $item.Command
13
+ if (Test-Path -LiteralPath $target) {
14
+ $existing = $shell.CreateShortcut($target)
15
+ $powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
16
+ if ($existing.Arguments -ne $arguments -or $existing.TargetPath -ne $powershell) { continue }
17
+ }
18
+ if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target }
19
+ $results += $target
20
+ }
21
+ ConvertTo-Json -InputObject $results -Compress