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

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 (185) 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 +62 -58
  7. package/dist/bin/self-relay-admin.mjs +8 -8
  8. package/dist/bin/self-relay-demo.mjs +33 -33
  9. package/dist/bin/self-relay-server.mjs +66 -66
  10. package/dist/build-platform.json +1 -1
  11. package/dist/get-lan-networks.ps1 +40 -40
  12. package/dist/get-shared-service-url.ps1 +6 -6
  13. package/dist/shared/app-server-events.cjs +422 -412
  14. package/dist/shared/code-browser.cjs +159 -159
  15. package/dist/shared/file-changes.cjs +31 -31
  16. package/dist/shared/file-citations.cjs +117 -117
  17. package/dist/shared/file-mentions.cjs +92 -92
  18. package/dist/shared/message-delivery.cjs +54 -54
  19. package/dist/shared/p2p-data-channel.cjs +72 -72
  20. package/dist/shared/p2p-diagnostics.cjs +28 -28
  21. package/dist/shared/p2p-file-channel.cjs +74 -74
  22. package/dist/shared/plan-pricing.cjs +32 -32
  23. package/dist/shared/question-attention.cjs +40 -0
  24. package/dist/shared/server-requests.cjs +219 -219
  25. package/dist/shared/timeline-sync.cjs +173 -173
  26. package/dist/skills/code-relax/SKILL.md +22 -22
  27. package/dist/src/archived-threads.mjs +22 -0
  28. package/dist/src/background-process.mjs +18 -18
  29. package/dist/src/bounded-process-log.mjs +63 -63
  30. package/dist/src/celsius-client.mjs +107 -107
  31. package/dist/src/code-browser.mjs +100 -100
  32. package/dist/src/desktop-launcher.mjs +46 -46
  33. package/dist/src/dots-approval-timeline.mjs +99 -99
  34. package/dist/src/dots-avatar.mjs +42 -42
  35. package/dist/src/dots-chat-projection.mjs +50 -50
  36. package/dist/src/dots-chat-server.mjs +461 -461
  37. package/dist/src/dots-message-images.mjs +319 -319
  38. package/dist/src/dots-profile.mjs +77 -77
  39. package/dist/src/dots-room-activity.mjs +94 -94
  40. package/dist/src/dots-room-client.mjs +135 -135
  41. package/dist/src/dots-room-projection.mjs +94 -94
  42. package/dist/src/dots-schedules.mjs +66 -66
  43. package/dist/src/dots-summary-tasks.mjs +32 -32
  44. package/dist/src/dots-task-catalog.mjs +430 -430
  45. package/dist/src/dots-task-files.mjs +52 -52
  46. package/dist/src/execution-health.mjs +26 -26
  47. package/dist/src/file-transfer-range.mjs +22 -22
  48. package/dist/src/file-upload-session.mjs +130 -130
  49. package/dist/src/hosted-history-app-server.mjs +103 -103
  50. package/dist/src/hosted-history-cache.mjs +113 -113
  51. package/dist/src/lan-socket-server.mjs +265 -263
  52. package/dist/src/managed-app-server.mjs +244 -244
  53. package/dist/src/message-images.mjs +9 -9
  54. package/dist/src/message-upload-files.mjs +74 -74
  55. package/dist/src/onboarding.mjs +12 -12
  56. package/dist/src/plan-pricing.mjs +47 -47
  57. package/dist/src/platform/README.md +12 -12
  58. package/dist/src/platform/codex-protocol.mjs +46 -46
  59. package/dist/src/platform/codex-runtime.mjs +3 -3
  60. package/dist/src/platform/credential-store.mjs +3 -3
  61. package/dist/src/platform/desktop-host.mjs +3 -3
  62. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  63. package/dist/src/platform/desktop-project-store.mjs +208 -149
  64. package/dist/src/platform/file-opener.mjs +9 -0
  65. package/dist/src/platform/linux/README.md +3 -3
  66. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  67. package/dist/src/platform/linux/credential-store.mjs +28 -0
  68. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  69. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  70. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  71. package/dist/src/platform/linux/network-info.mjs +11 -0
  72. package/dist/src/platform/linux/process.mjs +42 -0
  73. package/dist/src/platform/linux/project-locations.mjs +17 -0
  74. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  75. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  76. package/dist/src/platform/linux/service-host.mjs +18 -0
  77. package/dist/src/platform/linux/system.mjs +49 -0
  78. package/dist/src/platform/linux/user-environment.mjs +7 -0
  79. package/dist/src/platform/macos/README.md +3 -3
  80. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  81. package/dist/src/platform/macos/credential-store.mjs +10 -10
  82. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  83. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  84. package/dist/src/platform/macos/network-info.mjs +8 -28
  85. package/dist/src/platform/macos/project-locations.mjs +8 -8
  86. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  87. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  88. package/dist/src/platform/macos/service-host.mjs +18 -18
  89. package/dist/src/platform/macos/system.mjs +29 -29
  90. package/dist/src/platform/macos/user-environment.mjs +7 -7
  91. package/dist/src/platform/native-package.mjs +58 -46
  92. package/dist/src/platform/network-info.mjs +3 -3
  93. package/dist/src/platform/path-identity.mjs +5 -5
  94. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  95. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  96. package/dist/src/platform/project-locations.mjs +3 -3
  97. package/dist/src/platform/runtime-paths.mjs +8 -8
  98. package/dist/src/platform/select.mjs +5 -5
  99. package/dist/src/platform/service-host.mjs +3 -3
  100. package/dist/src/platform/user-environment.mjs +3 -3
  101. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  102. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  103. package/dist/src/platform/windows/background-process.mjs +30 -30
  104. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  105. package/dist/src/platform/windows/credential-store.mjs +27 -27
  106. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  107. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  108. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  109. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  110. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  111. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  112. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  113. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  114. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  115. package/dist/src/platform/windows/network-info.mjs +34 -34
  116. package/dist/src/platform/windows/project-locations.mjs +16 -16
  117. package/dist/src/platform/windows/resident-start.js +20 -20
  118. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  119. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  120. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  121. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  122. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  123. package/dist/src/platform/windows/service-host.mjs +31 -31
  124. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  125. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  126. package/dist/src/platform/windows/user-environment.mjs +50 -50
  127. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  128. package/dist/src/project-archive.mjs +30 -0
  129. package/dist/src/project-create.mjs +121 -119
  130. package/dist/src/project-management.mjs +59 -69
  131. package/dist/src/project-remove.mjs +55 -0
  132. package/dist/src/project-update.mjs +56 -0
  133. package/dist/src/runtime-discovery.mjs +47 -47
  134. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  135. package/dist/src/self-relay/account-connections.mjs +52 -52
  136. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  137. package/dist/src/self-relay/admin-page.mjs +80 -80
  138. package/dist/src/self-relay/admin-server.mjs +74 -74
  139. package/dist/src/self-relay/admin-state.mjs +127 -127
  140. package/dist/src/self-relay/admin-style.mjs +14 -14
  141. package/dist/src/self-relay/android-releases.mjs +61 -61
  142. package/dist/src/self-relay/client.mjs +61 -61
  143. package/dist/src/self-relay/connector.mjs +439 -438
  144. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  145. package/dist/src/self-relay/demo.mjs +37 -37
  146. package/dist/src/self-relay/device-info.mjs +27 -0
  147. package/dist/src/self-relay/device-store.mjs +92 -84
  148. package/dist/src/self-relay/diagnostics.mjs +54 -54
  149. package/dist/src/self-relay/email-accounts.mjs +183 -183
  150. package/dist/src/self-relay/file-wire.mjs +13 -13
  151. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  152. package/dist/src/self-relay/lifecycle.mjs +264 -250
  153. package/dist/src/self-relay/long-pairing.mjs +40 -40
  154. package/dist/src/self-relay/mysql-state.mjs +118 -105
  155. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  156. package/dist/src/self-relay/privacy-page.mjs +26 -26
  157. package/dist/src/self-relay/public-releases.mjs +34 -0
  158. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  159. package/dist/src/self-relay/server.mjs +1006 -932
  160. package/dist/src/self-relay/usage-daily.mjs +55 -55
  161. package/dist/src/self-relay/usage-meter.mjs +141 -141
  162. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  163. package/dist/src/self-relay/wire.mjs +36 -36
  164. package/dist/src/server.mjs +1861 -1778
  165. package/dist/src/service-control.mjs +82 -82
  166. package/dist/src/service-doctor.mjs +110 -110
  167. package/dist/src/service-health.mjs +33 -33
  168. package/dist/src/service-install.mjs +91 -91
  169. package/dist/src/service-lifecycle.mjs +138 -138
  170. package/dist/src/service-restart.mjs +81 -81
  171. package/dist/src/shared-app-server.mjs +615 -611
  172. package/dist/src/shared-catalog.mjs +132 -132
  173. package/dist/src/shared-recovery.mjs +146 -146
  174. package/dist/src/shared-thread-stream.mjs +164 -164
  175. package/dist/src/skill-install.mjs +51 -51
  176. package/dist/src/thread-title-generation.mjs +150 -150
  177. package/dist/src/turn-model-history.mjs +47 -47
  178. package/dist/src/usage-reset.mjs +17 -17
  179. package/dist/src/workstation-downloads.mjs +120 -120
  180. package/dist/tools/rtc-route-helper.ps1 +99 -99
  181. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  182. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  183. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  184. package/package.json +7 -6
  185. 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