@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,91 +1,91 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
7
- import { assertPlainPath } from './skill-install.mjs';
8
-
9
- const exec = promisify(execFile);
10
- export function packageRoot(from) {
11
- let directory = path.resolve(from);
12
- while (true) {
13
- if (readRecord(path.join(directory, 'package.json'))?.name === '@walkhi/code-relax') return directory;
14
- const parent = path.dirname(directory);
15
- if (parent === directory) throw new Error('找不到 Code Relax 安装包。');
16
- directory = parent;
17
- }
18
- }
19
-
20
- export async function installService(source, target, executable, stateRoot, hooks = {}) {
21
- assertPlainPath(target);
22
- if (!fs.existsSync(path.join(source, 'dist/bin/codex-remote.mjs'))) throw new Error('缺少发布构建;源码目录请先执行 npm run build。');
23
- const build = readRecord(path.join(source, 'dist/build-platform.json'));
24
- if (build?.platform !== process.platform || build?.arch !== process.arch)
25
- throw new Error('构建产物与本机系统或 CPU 不匹配,请在目标工作站安装依赖并重新构建。');
26
- if (!fs.existsSync(path.join(source, 'node_modules'))) throw new Error('缺少包依赖,请重新安装完整 npm 包。');
27
- const parent = path.dirname(target);
28
- fs.mkdirSync(parent, { recursive: true });
29
- const stage = path.join(parent, `service-stage-${randomUUID()}`);
30
- const backup = path.join(parent, `service-backup-${randomUUID()}`);
31
- fs.mkdirSync(stage);
32
- try {
33
- for (const name of ['dist', 'node_modules', 'package.json', 'README.md']) {
34
- fs.cpSync(path.join(source, name), path.join(stage, name), { recursive: true });
35
- }
36
- await exec(executable, [path.join(stage, 'dist/bin/codex-remote.mjs'), '--help'], { windowsHide: true });
37
- await exec(executable, ['-e', 'const r=require("node:module").createRequire(process.argv[1]); r("sharp"); r("qrcode"); r("ws"); r("node-datachannel");', path.join(stage, 'package.json')], { windowsHide: true });
38
- writeRecord(path.join(stage, 'installation.json'), { installedAt: new Date().toISOString(), source, nodeExecutable: executable, stateRoot,
39
- version: readRecord(path.join(source, 'package.json')).version });
40
- const previous = fs.existsSync(target);
41
- let previousMoved = false, activated = false;
42
- try {
43
- await hooks.beforeActivate?.();
44
- if (previous) { fs.renameSync(target, backup); previousMoved = true; }
45
- fs.renameSync(stage, target); activated = true;
46
- await hooks.verifyActivation?.({ serviceRoot: target, nodeExecutable: executable,
47
- version: readRecord(path.join(target, 'package.json')).version });
48
- } catch (error) {
49
- try {
50
- if (activated && fs.existsSync(target)) fs.rmSync(target, { recursive: true });
51
- if (previousMoved) fs.renameSync(backup, target);
52
- await hooks.restoreActivation?.();
53
- } catch (restoreError) {
54
- throw new AggregateError([error, restoreError], `服务更新失败,旧版本恢复也未完成:${restoreError.message}`);
55
- }
56
- throw error;
57
- }
58
- if (previous) {
59
- if (path.dirname(backup) !== parent || !path.basename(backup).startsWith('service-backup-')) throw new Error('Invalid backup path');
60
- fs.rmSync(backup, { recursive: true });
61
- }
62
- return { serviceRoot: target, nodeExecutable: executable, version: readRecord(path.join(target, 'package.json')).version };
63
- } finally {
64
- // Only this invocation's generated sibling stage may be cleaned up.
65
- if (path.dirname(stage) !== parent || !path.basename(stage).startsWith('service-stage-')) throw new Error('Invalid stage path');
66
- fs.rmSync(stage, { recursive: true, force: true });
67
- }
68
- }
69
-
70
- export async function uninstallService({ serviceRoot, stateRoot, uploadsRoot, credentialsRoot }, service, beforeRemove = async () => {}) {
71
- const target = path.resolve(serviceRoot);
72
- assertPlainPath(target);
73
- if (path.basename(target) !== 'service' || path.basename(path.dirname(target)) !== 'CodexRemote') throw new Error('拒绝删除未识别的服务目录。');
74
- const preserved = [stateRoot, uploadsRoot, credentialsRoot].filter(Boolean);
75
- for (const data of preserved) {
76
- const relative = path.relative(target, path.resolve(data));
77
- if (!relative || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) throw new Error('服务目录包含用户数据,拒绝删除。');
78
- }
79
- if (!fs.existsSync(target)) {
80
- await service.stop();
81
- await beforeRemove();
82
- return { removed: false, serviceRoot: target, preserved };
83
- }
84
- const pkg = readRecord(path.join(target, 'package.json'));
85
- const record = readRecord(path.join(target, 'installation.json'));
86
- if (pkg?.name !== '@walkhi/code-relax' || !record?.installedAt || !record?.nodeExecutable) throw new Error('缺少有效安装记录,未删除服务目录。');
87
- await service.stop();
88
- await beforeRemove();
89
- fs.rmSync(target, { recursive: true });
90
- return { removed: true, serviceRoot: target, preserved };
91
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
7
+ import { assertPlainPath } from './skill-install.mjs';
8
+
9
+ const exec = promisify(execFile);
10
+ export function packageRoot(from) {
11
+ let directory = path.resolve(from);
12
+ while (true) {
13
+ if (readRecord(path.join(directory, 'package.json'))?.name === '@walkhi/code-relax') return directory;
14
+ const parent = path.dirname(directory);
15
+ if (parent === directory) throw new Error('找不到 Code Relax 安装包。');
16
+ directory = parent;
17
+ }
18
+ }
19
+
20
+ export async function installService(source, target, executable, stateRoot, hooks = {}) {
21
+ assertPlainPath(target);
22
+ if (!fs.existsSync(path.join(source, 'dist/bin/codex-remote.mjs'))) throw new Error('缺少发布构建;源码目录请先执行 npm run build。');
23
+ const build = readRecord(path.join(source, 'dist/build-platform.json'));
24
+ if (build?.platform !== process.platform || build?.arch !== process.arch)
25
+ throw new Error('构建产物与本机系统或 CPU 不匹配,请在目标工作站安装依赖并重新构建。');
26
+ if (!fs.existsSync(path.join(source, 'node_modules'))) throw new Error('缺少包依赖,请重新安装完整 npm 包。');
27
+ const parent = path.dirname(target);
28
+ fs.mkdirSync(parent, { recursive: true });
29
+ const stage = path.join(parent, `service-stage-${randomUUID()}`);
30
+ const backup = path.join(parent, `service-backup-${randomUUID()}`);
31
+ fs.mkdirSync(stage);
32
+ try {
33
+ for (const name of ['dist', 'node_modules', 'package.json', 'README.md']) {
34
+ fs.cpSync(path.join(source, name), path.join(stage, name), { recursive: true });
35
+ }
36
+ await exec(executable, [path.join(stage, 'dist/bin/codex-remote.mjs'), '--help'], { windowsHide: true });
37
+ await exec(executable, ['-e', 'const r=require("node:module").createRequire(process.argv[1]); r("sharp"); r("qrcode"); r("ws"); r("node-datachannel");', path.join(stage, 'package.json')], { windowsHide: true });
38
+ writeRecord(path.join(stage, 'installation.json'), { installedAt: new Date().toISOString(), source, nodeExecutable: executable, stateRoot,
39
+ version: readRecord(path.join(source, 'package.json')).version });
40
+ const previous = fs.existsSync(target);
41
+ let previousMoved = false, activated = false;
42
+ try {
43
+ await hooks.beforeActivate?.();
44
+ if (previous) { fs.renameSync(target, backup); previousMoved = true; }
45
+ fs.renameSync(stage, target); activated = true;
46
+ await hooks.verifyActivation?.({ serviceRoot: target, nodeExecutable: executable,
47
+ version: readRecord(path.join(target, 'package.json')).version });
48
+ } catch (error) {
49
+ try {
50
+ if (activated && fs.existsSync(target)) fs.rmSync(target, { recursive: true });
51
+ if (previousMoved) fs.renameSync(backup, target);
52
+ await hooks.restoreActivation?.();
53
+ } catch (restoreError) {
54
+ throw new AggregateError([error, restoreError], `服务更新失败,旧版本恢复也未完成:${restoreError.message}`);
55
+ }
56
+ throw error;
57
+ }
58
+ if (previous) {
59
+ if (path.dirname(backup) !== parent || !path.basename(backup).startsWith('service-backup-')) throw new Error('Invalid backup path');
60
+ fs.rmSync(backup, { recursive: true });
61
+ }
62
+ return { serviceRoot: target, nodeExecutable: executable, version: readRecord(path.join(target, 'package.json')).version };
63
+ } finally {
64
+ // Only this invocation's generated sibling stage may be cleaned up.
65
+ if (path.dirname(stage) !== parent || !path.basename(stage).startsWith('service-stage-')) throw new Error('Invalid stage path');
66
+ fs.rmSync(stage, { recursive: true, force: true });
67
+ }
68
+ }
69
+
70
+ export async function uninstallService({ serviceRoot, stateRoot, uploadsRoot, credentialsRoot }, service, beforeRemove = async () => {}) {
71
+ const target = path.resolve(serviceRoot);
72
+ assertPlainPath(target);
73
+ if (path.basename(target) !== 'service' || path.basename(path.dirname(target)) !== 'CodexRemote') throw new Error('拒绝删除未识别的服务目录。');
74
+ const preserved = [stateRoot, uploadsRoot, credentialsRoot].filter(Boolean);
75
+ for (const data of preserved) {
76
+ const relative = path.relative(target, path.resolve(data));
77
+ if (!relative || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) throw new Error('服务目录包含用户数据,拒绝删除。');
78
+ }
79
+ if (!fs.existsSync(target)) {
80
+ await service.stop();
81
+ await beforeRemove();
82
+ return { removed: false, serviceRoot: target, preserved };
83
+ }
84
+ const pkg = readRecord(path.join(target, 'package.json'));
85
+ const record = readRecord(path.join(target, 'installation.json'));
86
+ if (pkg?.name !== '@walkhi/code-relax' || !record?.installedAt || !record?.nodeExecutable) throw new Error('缺少有效安装记录,未删除服务目录。');
87
+ await service.stop();
88
+ await beforeRemove();
89
+ fs.rmSync(target, { recursive: true });
90
+ return { removed: true, serviceRoot: target, preserved };
91
+ }
@@ -1,138 +1,138 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { startBackground, processAlive } from './background-process.mjs';
4
- import { randomBytes, randomUUID } from 'node:crypto';
5
- import { setTimeout as delay } from 'node:timers/promises';
6
- import { fileURLToPath } from 'node:url';
7
- import { controlRequest } from './service-control.mjs';
8
- import { serviceHealth as health } from './service-health.mjs';
9
-
10
- export function readRecord(file) {
11
- try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); }
12
- catch (error) { if (error.code === 'ENOENT') return null; throw error; }
13
- }
14
- export function writeRecord(file, value) {
15
- const temp = `${file}.${randomUUID()}.tmp`;
16
- try {
17
- fs.writeFileSync(temp, JSON.stringify(value, null, 2), { mode: 0o600 });
18
- fs.renameSync(temp, file);
19
- } finally { fs.rmSync(temp, { force: true }); }
20
- }
21
-
22
- export function lifecycle({ stateRoot, host }) {
23
- fs.mkdirSync(stateRoot, { recursive: true });
24
- const sessionPath = path.join(stateRoot, 'bridge-session.json');
25
- const configPath = path.join(stateRoot, 'bridge-config.json');
26
- const scriptPath = fileURLToPath(new URL('./server.mjs', import.meta.url));
27
-
28
- async function alive(session) {
29
- if (!session?.controlPort || !session.instanceId || !session.controlSecret) {
30
- if (await health(session)) throw new Error('当前为旧版桌面服务,请先使用旧版停止入口结束服务,再启动新版。');
31
- return false;
32
- }
33
- try { await controlRequest(session); return true; }
34
- catch (error) { if (error.code === 'ECONNREFUSED') return false; throw error; }
35
- }
36
- async function stop() {
37
- const session = readRecord(sessionPath);
38
- if (!await alive(session)) return { stopped: true, message: '桌面服务已停止。' };
39
- await controlRequest(session, 'POST');
40
- const deadline = Date.now() + 10000;
41
- while (true) {
42
- try { if (!await alive(session)) break; }
43
- catch (error) {
44
- // A connection accepted just before the listener closes may be reset.
45
- // Verify refusal on the next connection; never resend the shutdown request.
46
- if (error.code !== 'ECONNRESET') throw error;
47
- }
48
- if (Date.now() > deadline) throw new Error('桌面服务未在 10 秒内退出;未强杀进程。');
49
- await delay(100);
50
- }
51
- return { stopped: true, message: '桌面服务已正常关闭。' };
52
- }
53
- async function status() {
54
- const session = readRecord(sessionPath);
55
- const running = await alive(session);
56
- const result = running ? await health(session) : null;
57
- return { running: running && result?.status === 'ok', processRunning: running, pid: session?.pid,
58
- port: session?.port, mode: result?.transportMode, version: result?.version, execution: result?.execution,
59
- managedAppServer: result?.managedAppServer, desktop: result?.desktop,
60
- desktopRestartRecommended: result?.desktopRestartRecommended === true,
61
- resident: result?.resident === true };
62
- }
63
- async function pair() {
64
- const session = readRecord(sessionPath);
65
- const connection = await alive(session) ? await health(session) : null;
66
- if (connection?.status !== 'ok' || !session.lan) {
67
- throw new Error('没有健康的局域网服务,请先执行 relax start。');
68
- }
69
- const address = await host.lanAddress();
70
- const url = new URL(session.accessUrl);
71
- url.hostname = address || '127.0.0.1';
72
- session.accessUrl = url.href;
73
- writeRecord(sessionPath, session);
74
- return { pid: session.pid, port: session.port, version: connection.version, transportMode: connection.transportMode,
75
- execution: connection.execution, managedAppServer: connection.managedAppServer, desktop: connection.desktop,
76
- desktopRestartRecommended: connection.desktopRestartRecommended === true, accessUrl: session.accessUrl,
77
- qrPath: path.join(stateRoot, 'connection-qr.png') };
78
- }
79
- async function restartManagedAppServer(confirmed = false) {
80
- const session = readRecord(sessionPath);
81
- if (!await alive(session)) throw new Error('桌面服务尚未运行,无法迁移 Codex 内核。');
82
- const action = confirmed ? 'restart-managed-app-server-confirmed' : 'restart-managed-app-server';
83
- return controlRequest(session, 'POST', action);
84
- }
85
- async function startBridge() {
86
- let session = readRecord(sessionPath);
87
- if (await alive(session)) {
88
- const result = await health(session);
89
- if (session.scriptPath === scriptPath && session.lan && result?.status === 'ok' && result.resident) {
90
- await controlRequest(session, 'POST', 'refresh-execution');
91
- return pair();
92
- }
93
- await stop();
94
- }
95
- let config = readRecord(configPath);
96
- if (!config && session?.lan) config = { port: session.port, token: new URL(session.accessUrl).searchParams.get('token') };
97
- config ||= { port: 45831, token: randomBytes(32).toString('base64url') };
98
- if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535 || !/^[A-Za-z0-9_-]{20,}$/.test(config.token)) {
99
- throw new Error('bridge-config.json 中端口或 Token 无效;不会自动重置凭据。');
100
- }
101
- const address = await host.lanAddress() || '127.0.0.1';
102
- const { executable, environment } = await host.bridgeEnvironment();
103
- writeRecord(configPath, config);
104
- const instanceId = randomUUID();
105
- const controlSecret = randomBytes(32).toString('base64url');
106
- const readyPath = path.join(stateRoot, `bridge-ready-${instanceId}.json`);
107
- const pid = await startBackground(executable, [scriptPath, '--lan', '--port', String(config.port), '--token', config.token],
108
- { ...environment, CODEX_REMOTE_INSTANCE: instanceId, CODEX_REMOTE_CONTROL_SECRET: controlSecret,
109
- CODEX_REMOTE_READY_PATH: readyPath }, [path.join(stateRoot, 'bridge.stdout.log'), path.join(stateRoot, 'bridge.stderr.log')]);
110
- try {
111
- const deadline = Date.now() + 10000;
112
- let ready;
113
- while (!(ready = readRecord(readyPath))) {
114
- if (!processAlive(pid)) throw new Error('桌面服务启动失败,请查看 bridge.stderr.log。');
115
- if (Date.now() > deadline) throw new Error('桌面服务未在 10 秒内就绪。');
116
- await delay(100);
117
- }
118
- if (ready.instanceId !== instanceId || ready.pid !== pid) throw new Error('桌面服务启动实例不匹配。');
119
- session = { ...ready, controlSecret, executablePath: executable, scriptPath, port: config.port,
120
- lan: true, appServer: false, accessUrl: `http://${address}:${config.port}/?token=${config.token}`,
121
- startedAt: new Date().toISOString() };
122
- await controlRequest(session);
123
- writeRecord(sessionPath, session);
124
- return await pair();
125
- } catch (error) {
126
- if (processAlive(pid)) process.kill(pid);
127
- throw error;
128
- } finally { fs.rmSync(readyPath, { force: true }); }
129
- }
130
- async function start() {
131
- const result = await startBridge();
132
- if (fs.existsSync(path.join(stateRoot, 'self-relay-config.json'))) {
133
- await controlRequest(readRecord(sessionPath), 'POST', 'relay-start');
134
- }
135
- return result;
136
- }
137
- return { start, pair, status, stop, restartManagedAppServer };
138
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { startBackground, processAlive } from './background-process.mjs';
4
+ import { randomBytes, randomUUID } from 'node:crypto';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { controlRequest } from './service-control.mjs';
8
+ import { serviceHealth as health } from './service-health.mjs';
9
+
10
+ export function readRecord(file) {
11
+ try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); }
12
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
13
+ }
14
+ export function writeRecord(file, value) {
15
+ const temp = `${file}.${randomUUID()}.tmp`;
16
+ try {
17
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2), { mode: 0o600 });
18
+ fs.renameSync(temp, file);
19
+ } finally { fs.rmSync(temp, { force: true }); }
20
+ }
21
+
22
+ export function lifecycle({ stateRoot, host }) {
23
+ fs.mkdirSync(stateRoot, { recursive: true });
24
+ const sessionPath = path.join(stateRoot, 'bridge-session.json');
25
+ const configPath = path.join(stateRoot, 'bridge-config.json');
26
+ const scriptPath = fileURLToPath(new URL('./server.mjs', import.meta.url));
27
+
28
+ async function alive(session) {
29
+ if (!session?.controlPort || !session.instanceId || !session.controlSecret) {
30
+ if (await health(session)) throw new Error('当前为旧版桌面服务,请先使用旧版停止入口结束服务,再启动新版。');
31
+ return false;
32
+ }
33
+ try { await controlRequest(session); return true; }
34
+ catch (error) { if (error.code === 'ECONNREFUSED') return false; throw error; }
35
+ }
36
+ async function stop() {
37
+ const session = readRecord(sessionPath);
38
+ if (!await alive(session)) return { stopped: true, message: '桌面服务已停止。' };
39
+ await controlRequest(session, 'POST');
40
+ const deadline = Date.now() + 10000;
41
+ while (true) {
42
+ try { if (!await alive(session)) break; }
43
+ catch (error) {
44
+ // A connection accepted just before the listener closes may be reset.
45
+ // Verify refusal on the next connection; never resend the shutdown request.
46
+ if (error.code !== 'ECONNRESET') throw error;
47
+ }
48
+ if (Date.now() > deadline) throw new Error('桌面服务未在 10 秒内退出;未强杀进程。');
49
+ await delay(100);
50
+ }
51
+ return { stopped: true, message: '桌面服务已正常关闭。' };
52
+ }
53
+ async function status() {
54
+ const session = readRecord(sessionPath);
55
+ const running = await alive(session);
56
+ const result = running ? await health(session) : null;
57
+ return { running: running && result?.status === 'ok', processRunning: running, pid: session?.pid,
58
+ port: session?.port, mode: result?.transportMode, version: result?.version, execution: result?.execution,
59
+ managedAppServer: result?.managedAppServer, desktop: result?.desktop,
60
+ desktopRestartRecommended: result?.desktopRestartRecommended === true,
61
+ resident: result?.resident === true };
62
+ }
63
+ async function pair() {
64
+ const session = readRecord(sessionPath);
65
+ const connection = await alive(session) ? await health(session) : null;
66
+ if (connection?.status !== 'ok' || !session.lan) {
67
+ throw new Error('没有健康的局域网服务,请先执行 relax start。');
68
+ }
69
+ const address = await host.lanAddress();
70
+ const url = new URL(session.accessUrl);
71
+ url.hostname = address || '127.0.0.1';
72
+ session.accessUrl = url.href;
73
+ writeRecord(sessionPath, session);
74
+ return { pid: session.pid, port: session.port, version: connection.version, transportMode: connection.transportMode,
75
+ execution: connection.execution, managedAppServer: connection.managedAppServer, desktop: connection.desktop,
76
+ desktopRestartRecommended: connection.desktopRestartRecommended === true, accessUrl: session.accessUrl,
77
+ qrPath: path.join(stateRoot, 'connection-qr.png') };
78
+ }
79
+ async function restartManagedAppServer(confirmed = false) {
80
+ const session = readRecord(sessionPath);
81
+ if (!await alive(session)) throw new Error('桌面服务尚未运行,无法迁移 Codex 内核。');
82
+ const action = confirmed ? 'restart-managed-app-server-confirmed' : 'restart-managed-app-server';
83
+ return controlRequest(session, 'POST', action);
84
+ }
85
+ async function startBridge() {
86
+ let session = readRecord(sessionPath);
87
+ if (await alive(session)) {
88
+ const result = await health(session);
89
+ if (session.scriptPath === scriptPath && session.lan && result?.status === 'ok' && result.resident) {
90
+ await controlRequest(session, 'POST', 'refresh-execution');
91
+ return pair();
92
+ }
93
+ await stop();
94
+ }
95
+ let config = readRecord(configPath);
96
+ if (!config && session?.lan) config = { port: session.port, token: new URL(session.accessUrl).searchParams.get('token') };
97
+ config ||= { port: 45831, token: randomBytes(32).toString('base64url') };
98
+ if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535 || !/^[A-Za-z0-9_-]{20,}$/.test(config.token)) {
99
+ throw new Error('bridge-config.json 中端口或 Token 无效;不会自动重置凭据。');
100
+ }
101
+ const address = await host.lanAddress() || '127.0.0.1';
102
+ const { executable, environment } = await host.bridgeEnvironment();
103
+ writeRecord(configPath, config);
104
+ const instanceId = randomUUID();
105
+ const controlSecret = randomBytes(32).toString('base64url');
106
+ const readyPath = path.join(stateRoot, `bridge-ready-${instanceId}.json`);
107
+ const pid = await startBackground(executable, [scriptPath, '--lan', '--port', String(config.port), '--token', config.token],
108
+ { ...environment, CODEX_REMOTE_INSTANCE: instanceId, CODEX_REMOTE_CONTROL_SECRET: controlSecret,
109
+ CODEX_REMOTE_READY_PATH: readyPath }, [path.join(stateRoot, 'bridge.stdout.log'), path.join(stateRoot, 'bridge.stderr.log')]);
110
+ try {
111
+ const deadline = Date.now() + 10000;
112
+ let ready;
113
+ while (!(ready = readRecord(readyPath))) {
114
+ if (!processAlive(pid)) throw new Error('桌面服务启动失败,请查看 bridge.stderr.log。');
115
+ if (Date.now() > deadline) throw new Error('桌面服务未在 10 秒内就绪。');
116
+ await delay(100);
117
+ }
118
+ if (ready.instanceId !== instanceId || ready.pid !== pid) throw new Error('桌面服务启动实例不匹配。');
119
+ session = { ...ready, controlSecret, executablePath: executable, scriptPath, port: config.port,
120
+ lan: true, appServer: false, accessUrl: `http://${address}:${config.port}/?token=${config.token}`,
121
+ startedAt: new Date().toISOString() };
122
+ await controlRequest(session);
123
+ writeRecord(sessionPath, session);
124
+ return await pair();
125
+ } catch (error) {
126
+ if (processAlive(pid)) process.kill(pid);
127
+ throw error;
128
+ } finally { fs.rmSync(readyPath, { force: true }); }
129
+ }
130
+ async function start() {
131
+ const result = await startBridge();
132
+ if (fs.existsSync(path.join(stateRoot, 'self-relay-config.json'))) {
133
+ await controlRequest(readRecord(sessionPath), 'POST', 'relay-start');
134
+ }
135
+ return result;
136
+ }
137
+ return { start, pair, status, stop, restartManagedAppServer };
138
+ }
@@ -1,81 +1,81 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { fileURLToPath } from 'node:url';
5
- import { setTimeout as delay } from 'node:timers/promises';
6
- import { startBackground } from './background-process.mjs';
7
- import { lifecycle, readRecord, writeRecord } from './service-lifecycle.mjs';
8
- import * as host from './platform/service-host.mjs';
9
- import { desktopHost } from './platform/desktop-host.mjs';
10
-
11
- const script = fileURLToPath(import.meta.url);
12
- const sameProcess = (expected, actual) => expected && actual && expected.pid === actual.pid &&
13
- expected.created === actual.created && expected.executable === actual.executable;
14
- async function processIdentity(pid) {
15
- const current = await desktopHost.process(pid);
16
- return current ? { pid: current.pid, created: current.created, executable: current.executable } : null;
17
- }
18
-
19
- export async function requestServiceRestart(stateRoot) {
20
- const lock = path.join(stateRoot, 'service-restart.lock');
21
- const operationId = randomUUID();
22
- const owner = await processIdentity(process.pid);
23
- if (!owner) throw new Error('无法确认服务重启请求进程。');
24
- const previous = readRecord(lock);
25
- if (previous) {
26
- const actual = previous.owner?.pid ? await processIdentity(previous.owner.pid) : null;
27
- if (sameProcess(previous.owner, actual) || readRecord(lock)?.operationId !== previous.operationId) {
28
- throw new Error('工作站服务已在重启中。');
29
- }
30
- fs.rmSync(lock);
31
- }
32
- const temporary = `${lock}.${operationId}.tmp`;
33
- try {
34
- fs.writeFileSync(temporary, JSON.stringify({ operationId, owner, requestedAt: new Date().toISOString() }),
35
- { flag: 'wx', mode: 0o600 });
36
- // Publish a complete record exclusively; a crash cannot leave a half-written lock.
37
- fs.linkSync(temporary, lock);
38
- } catch (error) {
39
- if (error.code === 'EEXIST') throw new Error('工作站服务已在重启中。');
40
- throw error;
41
- } finally { fs.rmSync(temporary, { force: true }); }
42
- try {
43
- const log = path.join(stateRoot, 'service-restart.log');
44
- const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
45
- const worker = await processIdentity(pid);
46
- if (!worker) throw new Error('服务重启进程未启动。');
47
- const current = readRecord(lock);
48
- if (current?.operationId !== operationId) throw new Error('服务重启实例已失效。');
49
- writeRecord(lock, { ...current, owner: worker });
50
- return { operationId, status: 'scheduled', message: '工作站服务重启已安排。' };
51
- } catch (error) {
52
- const current = readRecord(lock);
53
- if (current?.operationId === operationId && sameProcess(current.owner, owner)) fs.rmSync(lock, { force: true });
54
- throw error;
55
- }
56
- }
57
-
58
- async function run(stateRoot, operationId) {
59
- const lock = path.join(stateRoot, 'service-restart.lock');
60
- try {
61
- const deadline = Date.now() + 30000;
62
- while (true) {
63
- const record = readRecord(lock);
64
- if (record?.operationId !== operationId) throw new Error('服务重启实例已失效。');
65
- if (record.owner?.pid === process.pid) break;
66
- if (Date.now() >= deadline) throw new Error('服务重启进程交接超时。');
67
- await delay(100);
68
- }
69
- const service = lifecycle({ stateRoot, host });
70
- await delay(1000);
71
- await service.stop();
72
- await service.start();
73
- } finally {
74
- if (readRecord(lock)?.operationId === operationId) fs.rmSync(lock, { force: true });
75
- }
76
- }
77
-
78
- if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
79
- const [stateRoot, operationId] = process.argv.slice(3);
80
- run(stateRoot, operationId).catch(error => { console.error(error.message); process.exitCode = 1; });
81
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { startBackground } from './background-process.mjs';
7
+ import { lifecycle, readRecord, writeRecord } from './service-lifecycle.mjs';
8
+ import * as host from './platform/service-host.mjs';
9
+ import { desktopHost } from './platform/desktop-host.mjs';
10
+
11
+ const script = fileURLToPath(import.meta.url);
12
+ const sameProcess = (expected, actual) => expected && actual && expected.pid === actual.pid &&
13
+ expected.created === actual.created && expected.executable === actual.executable;
14
+ async function processIdentity(pid) {
15
+ const current = await desktopHost.process(pid);
16
+ return current ? { pid: current.pid, created: current.created, executable: current.executable } : null;
17
+ }
18
+
19
+ export async function requestServiceRestart(stateRoot) {
20
+ const lock = path.join(stateRoot, 'service-restart.lock');
21
+ const operationId = randomUUID();
22
+ const owner = await processIdentity(process.pid);
23
+ if (!owner) throw new Error('无法确认服务重启请求进程。');
24
+ const previous = readRecord(lock);
25
+ if (previous) {
26
+ const actual = previous.owner?.pid ? await processIdentity(previous.owner.pid) : null;
27
+ if (sameProcess(previous.owner, actual) || readRecord(lock)?.operationId !== previous.operationId) {
28
+ throw new Error('工作站服务已在重启中。');
29
+ }
30
+ fs.rmSync(lock);
31
+ }
32
+ const temporary = `${lock}.${operationId}.tmp`;
33
+ try {
34
+ fs.writeFileSync(temporary, JSON.stringify({ operationId, owner, requestedAt: new Date().toISOString() }),
35
+ { flag: 'wx', mode: 0o600 });
36
+ // Publish a complete record exclusively; a crash cannot leave a half-written lock.
37
+ fs.linkSync(temporary, lock);
38
+ } catch (error) {
39
+ if (error.code === 'EEXIST') throw new Error('工作站服务已在重启中。');
40
+ throw error;
41
+ } finally { fs.rmSync(temporary, { force: true }); }
42
+ try {
43
+ const log = path.join(stateRoot, 'service-restart.log');
44
+ const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
45
+ const worker = await processIdentity(pid);
46
+ if (!worker) throw new Error('服务重启进程未启动。');
47
+ const current = readRecord(lock);
48
+ if (current?.operationId !== operationId) throw new Error('服务重启实例已失效。');
49
+ writeRecord(lock, { ...current, owner: worker });
50
+ return { operationId, status: 'scheduled', message: '工作站服务重启已安排。' };
51
+ } catch (error) {
52
+ const current = readRecord(lock);
53
+ if (current?.operationId === operationId && sameProcess(current.owner, owner)) fs.rmSync(lock, { force: true });
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async function run(stateRoot, operationId) {
59
+ const lock = path.join(stateRoot, 'service-restart.lock');
60
+ try {
61
+ const deadline = Date.now() + 30000;
62
+ while (true) {
63
+ const record = readRecord(lock);
64
+ if (record?.operationId !== operationId) throw new Error('服务重启实例已失效。');
65
+ if (record.owner?.pid === process.pid) break;
66
+ if (Date.now() >= deadline) throw new Error('服务重启进程交接超时。');
67
+ await delay(100);
68
+ }
69
+ const service = lifecycle({ stateRoot, host });
70
+ await delay(1000);
71
+ await service.stop();
72
+ await service.start();
73
+ } finally {
74
+ if (readRecord(lock)?.operationId === operationId) fs.rmSync(lock, { force: true });
75
+ }
76
+ }
77
+
78
+ if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
79
+ const [stateRoot, operationId] = process.argv.slice(3);
80
+ run(stateRoot, operationId).catch(error => { console.error(error.message); process.exitCode = 1; });
81
+ }