@walkhi/code-relax 0.1.0-beta.15 → 0.1.0-beta.17

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Code Relax Bridge(Windows 私测包)
1
+ # Code Relax 桌面服务(Windows 私测包)
2
2
 
3
- Bridge 让 HarmonyOS 客户端控制 Windows 上的受管 Codex 内核。当前要求 Windows x64、Node >=22.12,以及已登录的 Codex Desktop 配套内核或独立 Codex CLI;npm 包不包含 Node,也不修改官方程序文件。
3
+ 桌面服务让 HarmonyOS 客户端控制 Windows 上的Codex 内核。当前要求 Windows x64、Node >=22.12,以及已登录的 Codex Desktop 配套内核或独立 Codex CLI;npm 包不包含 Node,也不修改官方程序文件。
4
4
 
5
5
  ## 安装
6
6
 
@@ -10,38 +10,38 @@ Bridge 让 HarmonyOS 客户端控制 Windows 上的受管 Codex 内核。当前
10
10
  npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
11
11
  ```
12
12
 
13
- 首次安装前先保存工作,并从 Codex 菜单或 Windows 托盘完全退出 Codex。更新无需先停止正在运行的 Bridge;安装器会短暂停止、替换并恢复服务。npm 全局安装会自动执行 `setup` 和 Skill 安装。`setup` 将完整服务部署到 `%LOCALAPPDATA%\Programs\CodexRemote\service`,记录安装所用 Node 的绝对路径,不复制 Node。配置、日志和二维码以 `installation.json.stateRoot` 为准,通常位于 `%LOCALAPPDATA%\CodexRemote`,MSIX 环境可能解析到包缓存目录;附件位于 `%ProgramData%\CodexRemote\attachments`。
13
+ 首次安装前先保存工作,并从 Codex 菜单或 Windows 托盘完全退出 Codex。更新无需先停止正在运行的桌面服务;安装器会短暂停止、替换并恢复服务。npm 全局安装会自动执行 `setup` 和 Skill 安装。`setup` 将完整服务部署到 `%LOCALAPPDATA%\Programs\CodexRemote\service`,记录安装所用 Node 的绝对路径,不复制 Node。配置、日志和二维码以 `installation.json.stateRoot` 为准,通常位于 `%LOCALAPPDATA%\CodexRemote`,MSIX 环境可能解析到包缓存目录;附件位于 `%ProgramData%\CodexRemote\attachments`。
14
14
 
15
- `setup` 立即启动 Bridge 和固定 45839 受管 app-server,并启用当前用户登录自启;自启不打开 Codex。受管实例健康后配置当前用户 `CODEX_APP_SERVER_WS_URL`,之后正常启动的 Desktop 自动接入。若安装时 Desktop 仍未接入,安装器会关闭并重新打开 Desktop;安装前应保存工作。普通更新无接入或启动链迁移需求时复用现有内核。
15
+ `setup` 立即启动桌面服务和固定 45839 Codex 内核,并启用当前用户登录自启;自启不打开 Codex。Codex 内核健康后配置当前用户 `CODEX_APP_SERVER_WS_URL`,之后正常启动的 Desktop 自动接入。若安装时 Desktop 仍未接入,安装器会关闭并重新打开 Desktop;安装前应保存工作。普通更新无接入或启动链迁移需求时复用现有内核。
16
16
 
17
17
  ## Codex 内核
18
18
 
19
19
  ```powershell
20
- relax start # 启动/复用 Bridge、受管 Codex 内核和已配置中继
20
+ relax start # 启动/复用桌面服务、Codex 内核和已配置的 Code Relax 中继
21
21
  relax pair # 启动/复用服务并打开配对二维码
22
22
  relax doctor # 集中查看服务、登录自启和最近恢复结果
23
23
  ```
24
24
 
25
- Codex 内核由官方 `codex app-server --listen ws://127.0.0.1:45839` 提供执行服务,Desktop 和 Bridge 均可连接;它只监听回环地址。Bridge 独立报告内核健康、Desktop 接入和手机可写状态;Desktop 退出后,手机仍可操作内核中的任务。同一 thread 已被另一个实例持有时,手机只读且可重新检测。
25
+ Codex 内核由官方 `codex app-server --listen ws://127.0.0.1:45839` 提供执行服务,Desktop 和桌面服务均可连接;它只监听回环地址。桌面服务独立报告内核健康、Desktop 接入和手机可写状态;Desktop 退出后,手机仍可操作内核中的任务。同一 thread 已被另一个实例持有时,手机只读且可重新检测。
26
26
 
27
- 手机工作站详情可确认让 Desktop 接入 Codex 内核,独立进程逐步执行并保留 Bridge/中继连接,见[启用流程](../docs/shared-recovery.md)。本机恢复命令为 `relax desktop-recover --confirm`,查看恢复结果用 `relax doctor`。Bridge 状态匹配且健康时直接复用;未知进程占用 45831/45839 时拒绝接管。
27
+ 手机工作站详情可确认让 Desktop 接入Codex 内核,独立进程逐步执行并保留桌面服务/中继连接,见[启用流程](../docs/shared-recovery.md)。本机恢复命令为 `relax desktop-recover --confirm`,查看恢复结果用 `relax doctor`。桌面服务状态匹配且健康时直接复用;未知进程占用 45831/45839 时拒绝接管。
28
28
 
29
- 受管 app-server 通过隐藏控制台启动,使后续控制台子进程继承隐藏窗口。手机的项目、任务列表和新建任务均使用受管内核,不依赖 Desktop 工具管道;项目和任务 ID 保持原生值。
29
+ Codex 内核通过隐藏控制台启动,使后续控制台子进程继承隐藏窗口。手机的项目、任务列表和新建任务均使用Codex 内核,不依赖 Desktop 工具管道;项目和任务 ID 保持原生值。
30
30
 
31
- 受管 app-server 未就绪时,手机停留在连接界面并显示“Codex 内核未就绪”;Bridge 保留健康和恢复接口,聊天请求返回错误。
31
+ Codex 内核未就绪时,手机停留在连接界面并显示“Codex 内核未就绪”;桌面服务保留健康和恢复接口,聊天请求返回错误。
32
32
 
33
33
  ## Code Relax 中继
34
34
 
35
- 私有中继仍需要本机 Bridge:
35
+ 私有中继仍需要本机桌面服务:
36
36
 
37
37
  ```powershell
38
38
  relax start --json
39
39
  relax pair --json
40
40
  ```
41
41
 
42
- `start` 启动内核和 Bridge,并在 Bridge 内恢复已配置中继。每台移动设备通过 `pair` 配对一次;新增设备不替换已有设备。Bridge 把 Windows hostname 和当前 Wi-Fi/有线网络候选经已认证 WebSocket 临时发送给手机,手机本地保存电脑名,只在设备身份验证通过后保存可达地址;服务器不持久化这些 Code Relax 中继 hostname、IP 或网络名。OpenAI 官方认证和设备配对仍直接访问 OpenAI;完成后,手机使用统一配对产生的设备凭据,通过独立元数据 WebSocket 把官方环境 ID、名称和 hostname 写入设备记录,不上传 OpenAI access token、配对码或登录态。
42
+ `start` 启动内核和桌面服务,并在桌面服务内恢复已配置中继。每台移动设备通过 `pair` 配对一次;新增设备不替换已有设备。桌面服务把 Windows hostname 和当前 Wi-Fi/有线网络候选经已认证 WebSocket 临时发送给手机,手机本地保存电脑名,只在设备身份验证通过后保存可达地址;服务器不持久化这些 Code Relax 中继 hostname、IP 或网络名。OpenAI 官方认证和设备配对仍直接访问 OpenAI;完成后,手机使用统一配对产生的设备凭据,通过独立元数据 WebSocket 把官方环境 ID、名称和 hostname 写入设备记录,不上传 OpenAI access token、配对码或登录态。
43
43
 
44
- Bridge 的局域网端口仅提供鉴权 WebSocket,普通 HTTP 请求返回 404;手机聊天页面随 HAP 安装。
44
+ 桌面服务的局域网端口仅提供鉴权 WebSocket,普通 HTTP 请求返回 404;手机聊天页面随 HAP 安装。
45
45
 
46
46
  ## 更新与卸载
47
47
 
@@ -51,7 +51,7 @@ Bridge 的局域网端口仅提供鉴权 WebSocket,普通 HTTP 请求返回 40
51
51
  npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
52
52
  ```
53
53
 
54
- `setup` 先暂存并校验新版本,再短暂停止 Bridge/中继完成切换;原服务此前正在运行时会自动恢复,新版本启动失败则回滚并启动旧版本。无需提前停止服务。Node 路径变化或原 Node 被删除后需重新 setup。升级可能因 Node 可执行文件路径变化而再次触发 Windows 防火墙授权。
54
+ `setup` 先暂存并校验新版本,再短暂停止桌面服务/中继完成切换;原服务此前正在运行时会自动恢复,新版本启动失败则回滚并启动旧版本。无需提前停止服务。Node 路径变化或原 Node 被删除后需重新 setup。升级可能因 Node 可执行文件路径变化而再次触发 Windows 防火墙授权。
55
55
 
56
56
  卸载:
57
57
 
@@ -60,7 +60,7 @@ relax uninstall
60
60
  npm uninstall -g @walkhi/code-relax
61
61
  ```
62
62
 
63
- `uninstall` 停止 Bridge 后先核对并清理 Code Relax 所有的当前用户 `CODEX_APP_SERVER_WS_URL`;用户后来修改或不归属 Code Relax 的值会保留。随后删除固定程序和本包管理且未修改的 Skill;保留其他配置、Token、日志、长期设备凭据、附件和自定义 Skill。它不卸载 Node、Codex Desktop,也不强制结束完整控制的执行进程。
63
+ `uninstall` 停止桌面服务后先核对并清理 Code Relax 所有的当前用户 `CODEX_APP_SERVER_WS_URL`;用户后来修改或不归属 Code Relax 的值会保留。随后删除固定程序和本包管理且未修改的 Skill;保留其他配置、Token、日志、长期设备凭据、附件和自定义 Skill。它不卸载 Node、Codex Desktop,也不强制结束完整控制的执行进程。
64
64
 
65
65
  ## 发布边界
66
66
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { phonePairingSteps, desktopSetupSteps, pairingDisplayText } from '../src/onboarding.mjs';
3
- import { lifecycle, readRecord } from '../src/service-lifecycle.mjs';
3
+ import { lifecycle, readRecord, writeRecord } from '../src/service-lifecycle.mjs';
4
4
  import { runtimePaths } from '../src/platform/windows/runtime-paths.mjs';
5
5
  import * as host from '../src/platform/windows/service-host.mjs';
6
6
  import { installService, uninstallService, packageRoot } from '../src/service-install.mjs';
@@ -23,17 +23,27 @@ import { processAlive } from '../src/background-process.mjs';
23
23
  const help = `Code Relax(Windows)
24
24
  用法:relax <命令> [--json];下列命令均可用
25
25
  常用指令:
26
- start 启动或复用 Bridge 和 Codex 内核,并恢复已配置的私有中继
27
- pair 启动或复用工作站服务,并打开一次性配对二维码
26
+ start 启动或复用桌面服务和 Codex 内核,并恢复已配置的 Code Relax 中继
27
+ pair 启动或复用桌面服务,并打开一次性配对二维码
28
28
  维护指令:
29
- setup 部署并启动固定目录服务、受管 app-server 和登录自启动;安全配置 Desktop 后续接入
29
+ setup 部署并启动桌面服务、Codex 内核和登录自启动;安全配置 Desktop 后续接入
30
30
  doctor 检查服务、登录自启、最近恢复结果、网络和 Skill
31
31
  desktop-recover --confirm 重新打开 Desktop 并接入 Codex 内核;不强制重启已有内核
32
32
  uninstall 停止并移除固定目录服务和本包 Skill,清理自有 Desktop 环境变量,保留其他配置与附件
33
33
  默认监听 0.0.0.0,移动端与电脑需在同一受信任局域网。
34
- --json 输出机器可读结果;统一二维码不含 LAN 地址、Bridge Token 或中继地址。`;
34
+ --json 输出机器可读结果;统一二维码不含 LAN 地址、桌面服务凭据或中继地址。`;
35
+
36
+ function currentNodeExecutable() {
37
+ const link = process.env.NVM_SYMLINK && path.join(process.env.NVM_SYMLINK, 'node.exe');
38
+ if (link && fs.existsSync(link)) {
39
+ try {
40
+ if (fs.realpathSync(link).toLowerCase() === fs.realpathSync(process.execPath).toLowerCase()) return link;
41
+ } catch {}
42
+ }
43
+ return process.execPath;
44
+ }
35
45
 
36
- async function main() {
46
+ async function main() {
37
47
  const [command, ...flags] = process.argv.slice(2);
38
48
  if (!command || command === '--help' || command === '-h') {
39
49
  console.log(help);
@@ -46,11 +56,20 @@ async function main() {
46
56
  if (process.platform !== 'win32') throw new Error('当前被控端仅支持 Windows。');
47
57
  const json = flags.includes('--json');
48
58
  const paths = runtimePaths();
49
- const { stateRoot, serviceRoot } = paths;
50
- const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
51
- if (['desktop-recover', 'start', 'pair'].includes(command) && path.resolve(fileURLToPath(import.meta.url)).toLowerCase() !== installedEntry.toLowerCase()) {
59
+ const { stateRoot, serviceRoot } = paths;
60
+ const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
61
+ const installationPath = path.join(serviceRoot, 'installation.json');
62
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
63
+ const currentNodeSupported = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 12);
64
+ const installed = path.resolve(fileURLToPath(import.meta.url)).toLowerCase() === installedEntry.toLowerCase();
65
+ const installation = readRecord(installationPath);
66
+ if (installation?.nodeExecutable && !fs.existsSync(installation.nodeExecutable) && currentNodeSupported
67
+ && ['start', 'pair', 'doctor'].includes(command)) {
68
+ writeRecord(installationPath, { ...installation, nodeExecutable: currentNodeExecutable() });
69
+ }
70
+ if (['desktop-recover', 'start', 'pair'].includes(command) && !installed) {
52
71
  if (!fs.existsSync(installedEntry)) throw new Error('尚未部署服务,请先执行 relax setup。');
53
- const executable = readRecord(path.join(serviceRoot, 'installation.json'))?.nodeExecutable;
72
+ const executable = readRecord(installationPath)?.nodeExecutable;
54
73
  if (!executable || !fs.existsSync(executable)) throw new Error('安装时选用的 Node 不存在,请重新执行 relax setup。');
55
74
  const child = spawn(executable, [installedEntry, command, ...flags], { stdio: 'inherit', windowsHide: true });
56
75
  const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', resolve); });
@@ -59,21 +78,12 @@ async function main() {
59
78
  }
60
79
  const service = lifecycle({ stateRoot, host });
61
80
  const ensureAutostart = async () => {
62
- const installation = readRecord(path.join(serviceRoot, 'installation.json'));
63
- if (!installation?.nodeExecutable) throw new Error('请先执行 setup。');
64
- const current = await residentStartup({ serviceRoot, nodeExecutable: installation.nodeExecutable }, 'status');
81
+ const currentInstallation = readRecord(installationPath);
82
+ if (!currentInstallation?.nodeExecutable) throw new Error('请先执行 setup。');
83
+ const startup = { serviceRoot, nodeExecutable: currentInstallation.nodeExecutable };
84
+ const current = await residentStartup(startup, 'status');
65
85
  if (current.enabled) return { ...current, repaired: false };
66
- const running = await service.status();
67
- if (!running.running) await service.start();
68
- const deadline = Date.now() + 35000;
69
- while (true) {
70
- const ready = await service.status();
71
- if (ready.running && ready.managedAppServer?.ready) break;
72
- if (Date.now() >= deadline) throw new Error('Codex 内核未就绪,未启用 Desktop 自动接入。');
73
- await delay(250);
74
- }
75
- const result = await residentStartup({ serviceRoot, nodeExecutable: installation.nodeExecutable }, 'enable');
76
- if ((await desktopHost.inspect()).desktop) await configureManagedEnvironment(stateRoot);
86
+ const result = await residentStartup(startup, 'enable');
77
87
  return { ...result, repaired: true };
78
88
  };
79
89
  if (command === 'desktop-recover') {
@@ -83,13 +93,13 @@ async function main() {
83
93
  return;
84
94
  }
85
95
  if (command === 'pair') {
96
+ await ensureAutostart();
86
97
  let bridge = await service.status();
87
98
  if (!bridge.running) {
88
99
  await service.start();
89
100
  bridge = await service.status();
90
101
  }
91
102
  if (!bridge.running) throw new Error('Code Relax 工作站服务未能启动。');
92
- await ensureAutostart();
93
103
  const result = await selfRelay('relay-pair', stateRoot);
94
104
  if (!json && result.qrPath) {
95
105
  const viewer = spawn('explorer.exe', [result.qrPath], { detached: true, stdio: 'ignore', windowsHide: true });
@@ -142,7 +152,7 @@ async function main() {
142
152
  const legacyRelaySession = readRecord(path.join(stateRoot, 'self-relay-session.json'));
143
153
  const previousRelay = legacyRelaySession ? await controlRequest(legacyRelaySession) : await selfRelay('relay-status', stateRoot);
144
154
  const resumeAfterUpdate = previous.processRunning || previousRelay.running;
145
- const executable = fs.realpathSync(process.execPath);
155
+ const executable = currentNodeExecutable();
146
156
  const startInstalled = async () => {
147
157
  const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
148
158
  const child = spawn(executable, [entry, 'start', '--json'], { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] });
@@ -152,7 +162,7 @@ async function main() {
152
162
  if (code) {
153
163
  try { await selfRelay('relay-stop', stateRoot); } catch {}
154
164
  try { await service.stop(); } catch {}
155
- throw new Error(errors.trim().replace(/^(?:Code Relax:)+/u, '') || 'Bridge 启动失败');
165
+ throw new Error(errors.trim().replace(/^(?:Code Relax:)+/u, '') || '桌面服务启动失败');
156
166
  }
157
167
  };
158
168
  let stoppedForUpdate = false;
@@ -173,6 +183,7 @@ async function main() {
173
183
  verifyActivation: async () => { await startInstalled(); },
174
184
  restoreActivation: async () => { if (stoppedForUpdate) await startInstalled(); },
175
185
  });
186
+ const autostart = await residentStartup(installed, 'enable');
176
187
  const deadline = Date.now() + 35_000;
177
188
  let bridge = await service.status();
178
189
  while (bridge.running && !['ready', 'failed'].includes(bridge.managedAppServer?.state) && Date.now() < deadline) {
@@ -180,7 +191,7 @@ async function main() {
180
191
  bridge = await service.status();
181
192
  }
182
193
  if (!bridge.running || bridge.managedAppServer?.state !== 'ready') {
183
- throw new Error(bridge.managedAppServer?.error || 'Bridge 已安装,但受管 app-server 未就绪;未写入 Desktop 环境变量。');
194
+ throw new Error(bridge.managedAppServer?.error || '桌面服务已安装,但 Codex 内核未就绪;未写入 Desktop 环境变量。');
184
195
  }
185
196
  const environment = desktopBeforeSetup.desktop
186
197
  ? await configureManagedEnvironment(physicalStateRoot) : { configured: false, reason: 'desktop-not-installed' };
@@ -203,11 +214,11 @@ async function main() {
203
214
  await delay(250);
204
215
  bridge = await service.status();
205
216
  if (bridge.managedAppServer?.restart?.status === 'failed') {
206
- throw new Error(bridge.managedAppServer.restart.error || '受管 app-server 启动链迁移失败。');
217
+ throw new Error(bridge.managedAppServer.restart.error || 'Codex 内核启动链迁移失败。');
207
218
  }
208
219
  } while (Date.now() < migrationDeadline && (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired));
209
220
  if (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired) {
210
- throw new Error('受管 app-server 未在 40 秒内完成启动链迁移。');
221
+ throw new Error('Codex 内核未在 40 秒内完成启动链迁移。');
211
222
  }
212
223
  managedAppServerMigrated = true;
213
224
  managedAppServerMigration.restarted = true;
@@ -218,7 +229,6 @@ async function main() {
218
229
  bridge = await service.status();
219
230
  }
220
231
  }
221
- const autostart = await ensureAutostart();
222
232
  const skill = installSkill(path.join(serviceRoot, 'dist/skills/code-relax/SKILL.md'));
223
233
  if (desktopNeedsAttach && !desktopRestarted) {
224
234
  const currentDesktop = await desktopHost.inspect();
@@ -242,15 +252,15 @@ async function main() {
242
252
  installed.desktopRestartReason = desktopRestartReason;
243
253
  const migrationText = managedAppServerMigrated ? '已自动重建旧启动链的 app-server;安装前尚未完成的任务可能已中断。\n' : '';
244
254
  const restartText = desktopRestarted
245
- ? 'Codex Desktop 已干净重启并重新接入受管 app-server。'
246
- : desktopBeforeSetup.desktop ? 'Codex Desktop 使用同一受管 app-server;移动端控制不依赖 Desktop 是否打开。'
255
+ ? 'Codex Desktop 已干净重启并重新接入 Codex 内核。'
256
+ : desktopBeforeSetup.desktop ? 'Codex Desktop 使用同一 Codex 内核;移动端控制不依赖 Desktop 是否打开。'
247
257
  : '使用独立 Codex CLI;无需安装 Codex Desktop。';
248
- console.log(json ? JSON.stringify({ ...installed, nextSteps: desktopSetupSteps, displayText: `${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}` }) : `服务已安装:${installed.serviceRoot}\n使用已安装的 Node:${installed.nodeExecutable}\n受管 app-server 已就绪;首次联网可能需要 Windows 防火墙授权。\n${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}`);
258
+ console.log(json ? JSON.stringify({ ...installed, nextSteps: desktopSetupSteps, displayText: `${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}` }) : `桌面服务已安装:${installed.serviceRoot}\n使用已安装的 Node:${installed.nodeExecutable}\nCodex 内核已就绪;首次联网可能需要 Windows 防火墙授权。\n${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}`);
249
259
  return;
250
260
  }
251
- if (!json && command === 'start') console.log('正在检查电脑连接并准备 Bridge…');
252
- const result = await service[command]();
261
+ if (!json && command === 'start') console.log('正在检查电脑连接并准备桌面服务…');
253
262
  await ensureAutostart();
263
+ const result = await service[command]();
254
264
  const { accessUrl, qrPath: _legacyQrPath, ...metadata } = result;
255
265
  const notice = '本机服务检查通过,移动端连通性尚未验证。若电脑弹出 Node 网络访问提示,请先处理 Windows 防火墙授权,再用移动端连接。';
256
266
  let relay;
@@ -263,7 +273,7 @@ async function main() {
263
273
  lanAccess: 'unverified', notice };
264
274
  if (json) console.log(JSON.stringify(output));
265
275
  else {
266
- console.log('电脑连接检查通过 · Bridge 已就绪');
276
+ console.log('电脑连接检查通过 · 桌面服务已就绪');
267
277
  console.log(`局域网模式:监听 0.0.0.0:${result.port},连接地址 ${output.address},PID ${result.pid}`);
268
278
  if (relay?.qrPath) console.log(`配对二维码:${relay.qrPath}`);
269
279
  console.log(displayText);
@@ -25,7 +25,7 @@ try {
25
25
  const health = await client.request('/api/health');
26
26
  if (health.status !== 200 || health.body.transportMode !== mode) throw new Error('Relay validation failed');
27
27
  console.log(JSON.stringify({ ok: true, simulated: true, mode, relay: demo.relay.url,
28
- bridge: demo.bridgeUrl, message: '中继 → 连接器 → 模拟 Bridge 已连通;未连接 Codex,未执行任务。' }, null, 2));
28
+ bridge: demo.bridgeUrl, message: '中继 → 连接器 → 模拟桌面服务已连通;未连接 Codex,未执行任务。' }, null, 2));
29
29
  if (!process.argv.includes('--check')) {
30
30
  console.log('验证连接保持运行。Ctrl+C 停止全部模拟进程内服务。');
31
31
  await new Promise(resolve => { process.once('SIGINT', resolve); process.once('SIGTERM', resolve); });
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: code-relax
3
- description: 启动、检查或停止 Code Relax 工作站服务,生成并展示配对二维码。
3
+ description: 启动或检查 Code Relax 桌面服务,生成并展示配对二维码。
4
4
  ---
5
5
 
6
6
  # Code Relax
7
7
 
8
- - `relax start --json` 启动 Bridge 和受管 Codex 内核,无需打开 Desktop;支持纯 CLI 安装。Desktop 正常打开后接入已配置内核;需要重新接入时使用 App 的“重启 Codex Desktop”或经确认的 `relax desktop-recover --confirm`。已配置 Code Relax 中继时,启动命令同时恢复中继连接器和已有设备授权。
8
+ - `relax start --json` 启动桌面服务和 Codex 内核,无需打开 Desktop;支持纯 CLI 安装。Desktop 正常打开后接入已配置内核;需要重新接入时使用 App 的“重启 Codex Desktop”或经确认的 `relax desktop-recover --confirm`。已配置 Code Relax 中继时,启动命令同时恢复中继连接器和已有设备授权。
9
9
  - npm 全局安装会自动执行服务部署和 Skill 安装;修复安装时运行 `relax setup --json`。
10
- - `setup` 默认启用当前用户登录启动工作站服务,不自动打开 Codex;`start`、`pair`、`setup` 和 `doctor` 在自启缺失时自动恢复。恢复完整控制会保留 Bridge 和中继,但仍须明确确认可能中断全部完整控制任务。
10
+ - `setup` 默认启用当前用户登录启动桌面服务,不自动打开 Codex;`start`、`pair` 和 `doctor` 在自启缺失时自动恢复,`setup` 每次更新登录任务。恢复 Codex Desktop 接入会保留桌面服务和中继,但仍须明确确认可能中断正在运行的任务。
11
11
  - 用户要求启动 Code Relax 时,执行一次 `relax start --json`。用户要求二维码或当前启动结果包含二维码时,展示 CLI 返回的 `qrPath` 和 `displayText`;没有设备授权时可用 `relax pair --json` 生成新的配对二维码。
12
- - 配对二维码是短期 HTTPS 邀请,不属于工作站或某一台移动设备,也不含 LAN IP、端口、Bridge Token 或中继地址。新增设备不撤销已有设备。
12
+ - 配对二维码是短期 HTTPS 邀请,不属于工作站或某一台移动设备,也不含 LAN IP、端口、桌面服务凭据或中继地址。新增设备不撤销已有设备。
13
13
  - 已授权设备可直接使用 Code Relax 中继。App 以工作站名显示已配对电脑,进入后选择局域网、Code Relax 中继或 OpenAI 中继。启动命令会经已认证中继临时刷新当前 LAN 候选;App 只保存设备凭据验证成功的网络记录。
14
- - `status --json` 查询工作站服务;`stop --json` 停止 Bridge,不停止受管 Codex 内核。中继连接由 Bridge 管理,不提供单独的中继 CLI 命令。
14
+ - `relax doctor --json` 检查桌面服务、Codex 内核和登录自启动状态。中继连接由桌面服务管理,不提供单独的中继 CLI 命令。
15
15
  - 启动或配对成功后,将 `qrPath` 以 Markdown 图片嵌入回复,并逐字展示 `displayText`。不要把二维码内容、Token 或 Base64 写进消息。
16
16
  - 不自动修改防火墙或发送测试消息。`lanAccess: unverified` 或 `mobileAccess: unverified` 只表示设备链路尚未现场验证。
17
17
  - 服务位于用户 `Programs/CodexRemote/service`;配置在 `LocalAppData/CodexRemote`;附件在 `ProgramData/CodexRemote/attachments`。不要删除配置、附件或凭据来排障。
@@ -28,7 +28,7 @@ export async function launchDesktop({ stateRoot, host, environment = process.env
28
28
  for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
29
29
  progress('starting-app-server');
30
30
  managed = new ManagedAppServer({ stateRoot, host, environment, available, wait, timeout });
31
- if (!await managed.start()) throw new Error(managed.snapshot().error || '受管 app-server 未就绪。');
31
+ if (!await managed.start()) throw new Error(managed.snapshot().error || 'Codex 内核未就绪。');
32
32
  const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
33
33
  if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
34
34
  childEnv.CODEX_APP_SERVER_WS_URL = sharedUrl;
@@ -1,29 +1,29 @@
1
1
  export const connectionModesText = [
2
2
  '可选连接模式:',
3
3
  '• 局域网:启动时会把当前 Wi-Fi/有线网络候选临时发给已配对设备;App 验证后保存,也可手动输入 IP。',
4
- '• 私有中继:同一设备授权可跨网络长期重连;Codex 内核就绪后才能进入聊天。',
5
- '• OpenAI 官方中继:在已配对电脑内使用官方账号和设备授权;认证仍直连 OpenAI。',
4
+ '• Code Relax 中继:同一设备授权可跨网络长期重连;Codex 内核就绪后才能进入聊天。',
5
+ '• OpenAI 中继:在已配对电脑内使用官方账号和设备授权;认证仍直连 OpenAI。',
6
6
  ].join('\n');
7
7
 
8
8
  export function phonePairingSteps(transportMode, paired = false) {
9
9
  const mode = transportMode === 'lan-shared' ? '局域网 · Codex 内核已就绪' : '局域网(Codex 内核未就绪)';
10
10
  return paired ? [
11
- `已有移动设备授权。移动端可继续使用私有中继;${mode} 会显示验证成功的当前网络和历史记录。`,
11
+ `已有移动设备授权。移动端可继续使用 Code Relax 中继;${mode} 会显示验证成功的当前网络和历史记录。`,
12
12
  '添加另一台设备时执行 relax pair;每台设备只需扫码配对一次。',
13
13
  ] : [
14
14
  '请先在移动端安装 Code Relax。当前为鸿蒙 HarmonyOS 7 私测版,调试 HAP 需通过 DevEco/调试设备安装,尚无商店下载入口。',
15
15
  '使用 App 扫描配对二维码;每台移动设备只需配对一次。',
16
- `配对后可直接使用私有中继;${mode} 会接收并验证当前网络候选,也可手动输入 IP。`,
16
+ `配对后可直接使用 Code Relax 中继;${mode} 会接收并验证当前网络候选,也可手动输入 IP。`,
17
17
  ];
18
18
  }
19
19
  export const desktopSetupSteps = [
20
20
  'Code Relax Skill 已随 npm 包自动安装。',
21
- '安装完成后 Bridge 和受管 app-server 已启动,并默认在当前用户登录时启动。',
21
+ '安装完成后桌面服务和 Codex 内核已启动,并默认在当前用户登录时启动。',
22
22
  'CLI 用户无需安装 Desktop;已安装 Desktop 时,安装器负责将运行中的 Desktop 接入同一内核。',
23
23
  '尚无设备授权时执行 relax pair,打开并扫描配对二维码。',
24
24
  '统一配对先建立电脑记录;进入该电脑后可另行完成 OpenAI 官方 Remote 配对。',
25
25
  ];
26
26
 
27
27
  export function pairingDisplayText(nextSteps, notice) {
28
- return ['Bridge 已就绪,请在移动端完成连接。', connectionModesText, ...nextSteps.map((step, index) => `${index + 1}. ${step}`), notice].join('\n\n');
28
+ return ['桌面服务已就绪,请在移动端完成连接。', connectionModesText, ...nextSteps.map((step, index) => `${index + 1}. ${step}`), notice].join('\n\n');
29
29
  }
@@ -2,7 +2,19 @@
2
2
  // short-lived CLI has handed the resident service to the background broker.
3
3
  if (WScript.Arguments.length !== 2) WScript.Quit(2);
4
4
  var shell = new ActiveXObject('WScript.Shell');
5
+ var files = new ActiveXObject('Scripting.FileSystemObject');
5
6
  var node = WScript.Arguments.Item(0);
6
7
  var entry = WScript.Arguments.Item(1);
8
+ if (!files.FileExists(node)) {
9
+ var directories = shell.Environment('PROCESS').Item('PATH').split(';');
10
+ node = '';
11
+ for (var i = 0; i < directories.length; i++) {
12
+ var directory = directories[i].replace(/^"|"$/g, '');
13
+ if (!directory) continue;
14
+ var candidate = files.BuildPath(directory, 'node.exe');
15
+ if (files.FileExists(candidate)) { node = candidate; break; }
16
+ }
17
+ if (!node) WScript.Quit(2);
18
+ }
7
19
  var command = '"' + node + '" "' + entry + '" start --json';
8
20
  WScript.Quit(shell.Run(command, 0, true));
@@ -1,8 +1,9 @@
1
- param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
1
+ param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
2
2
  $ErrorActionPreference = 'Stop'
3
3
  [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
4
 
5
- $taskName = 'Code Relax Bridge'
5
+ $taskName = 'Code Relax 桌面服务'
6
+ $legacyTaskName = 'Code Relax Bridge'
6
7
  $taskSource = '@walkhi/code-relax'
7
8
  $taskPath = '\' + $taskName
8
9
  $launcher = Join-Path $PSScriptRoot 'resident-start.js'
@@ -14,9 +15,13 @@ $scheduler = New-Object -ComObject 'Schedule.Service'
14
15
  $scheduler.Connect()
15
16
  $root = $scheduler.GetFolder('\')
16
17
  $registered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
18
+ $legacyRegistered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $legacyTaskName } | Select-Object -First 1)
17
19
  if ($registered.Count -gt 0 -and $registered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
18
20
  throw 'Scheduled task is not owned by this installation.'
19
21
  }
22
+ if ($legacyRegistered.Count -gt 0 -and $legacyRegistered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
23
+ throw 'Legacy scheduled task is not owned by this installation.'
24
+ }
20
25
 
21
26
  # Remove only the legacy shortcut created by this package. A Startup LNK that
22
27
  # launches hidden PowerShell with ExecutionPolicy Bypass triggers AV heuristics.
@@ -35,7 +40,7 @@ if ($Action -eq 'enable') {
35
40
  if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry) -or
36
41
  -not (Test-Path -LiteralPath $launcher) -or -not (Test-Path -LiteralPath $wscript)) { throw 'Installed runtime is missing.' }
37
42
  $definition = $scheduler.NewTask(0)
38
- $definition.RegistrationInfo.Description = 'Start the Code Relax Bridge when this user signs in.'
43
+ $definition.RegistrationInfo.Description = 'Start the Code Relax desktop service when this user signs in.'
39
44
  $definition.RegistrationInfo.Source = $taskSource
40
45
  $definition.Principal.UserId = $currentUser
41
46
  $definition.Principal.LogonType = 3 # TASK_LOGON_INTERACTIVE_TOKEN
@@ -55,16 +60,24 @@ if ($Action -eq 'enable') {
55
60
  $exec.Arguments = $arguments
56
61
  $exec.WorkingDirectory = $env:USERPROFILE
57
62
  [void]$root.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, 3, $null) # TASK_CREATE_OR_UPDATE
58
- } elseif ($Action -eq 'disable' -and $registered.Count -gt 0) {
59
- $root.DeleteTask($taskName, 0)
63
+ if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
64
+ } elseif ($Action -eq 'disable') {
65
+ if ($registered.Count -gt 0) { $root.DeleteTask($taskName, 0) }
66
+ if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
60
67
  }
61
68
  if ($Action -ne 'status' -and $legacyExists) { Remove-Item -LiteralPath $legacyPath }
62
69
 
63
70
  $active = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
64
71
  $taskEnabled = $active.Count -gt 0 -and $active[0].Enabled
72
+ $taskCurrent = $false
73
+ if ($taskEnabled) {
74
+ $taskAction = $active[0].Definition.Actions.Item(1)
75
+ $taskCurrent = $taskAction.Path -ieq $wscript -and $taskAction.Arguments -ieq $arguments
76
+ }
65
77
  @{
66
- enabled=($taskEnabled -or ($Action -eq 'status' -and $legacyExists))
67
- path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
78
+ enabled=($taskCurrent -and $legacyRegistered.Count -eq 0)
79
+ taskPresent=$taskEnabled
80
+ path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyRegistered.Count -gt 0) { 'Task Scheduler:\' + $legacyTaskName } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
68
81
  scope='current-user-logon'
69
- mechanism=$(if ($taskEnabled) { 'task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
82
+ mechanism=$(if ($taskCurrent -and $legacyRegistered.Count -eq 0) { 'task-scheduler' } elseif ($taskEnabled -or $legacyRegistered.Count -gt 0) { 'stale-task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
70
83
  } | ConvertTo-Json -Compress
@@ -15,7 +15,7 @@ export async function configureManagedEnvironment(stateRoot) {
15
15
  const ownershipPath = path.join(stateRoot, ownershipFile);
16
16
  const ownership = readRecord(ownershipPath);
17
17
  if (current.forceCliUser === '1' || current.forceCliMachine === '1') {
18
- throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略受管 app-server;未修改该配置。');
18
+ throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略当前内核地址;未修改该配置。');
19
19
  }
20
20
  if (current.userValue && current.userValue !== managedAppServerUrl) {
21
21
  throw new Error(`用户已有 CODEX_APP_SERVER_WS_URL=${current.userValue};未覆盖该配置。`);
@@ -9,7 +9,7 @@ import { createWindowsRouteHelper } from './windows-route-helper.mjs';
9
9
  // The LAN token never travels to the relay. No requests are replayed after a close.
10
10
  export function connectBridgeRelay({ relayUrl, hostKey, bridgeUrl, bridgeToken, identity, bridgeConnection, hostName = 'Codex-PC', lanCandidates = [], p2pProbe = false, iceServers = [], p2pWindowsRouteHelper = false, p2pWindowsRouteHelperToken = '' }) {
11
11
  relayAddress(relayUrl); loopbackUrl(bridgeUrl, '/api/socket');
12
- if (!bridgeToken) throw new Error('Bridge token required');
12
+ if (!bridgeToken) throw new Error('Desktop service credential required');
13
13
  const events = new EventEmitter();
14
14
  const remote = new WebSocket(relayUrl, { maxPayload: 64 * 1024 * 1024, perMessageDeflate: {
15
15
  serverNoContextTakeover: true, clientNoContextTakeover: true,
@@ -114,18 +114,18 @@ export function connectBridgeRelay({ relayUrl, hostKey, bridgeUrl, bridgeToken,
114
114
  loopbackUrl(connection.url, '/api/socket');
115
115
  } catch {
116
116
  send(remote, { type: 'detach', session: message.session }); dropLocal(message.session); events.emit('detached', { session: message.session });
117
- events.emit('diagnostic', 'Local Bridge connection not ready'); return;
117
+ events.emit('diagnostic', 'Local desktop service connection not ready'); return;
118
118
  }
119
119
  const socket = current.socket = new WebSocket(connection.url, { maxPayload: 64 * 1024 * 1024, perMessageDeflate: false });
120
120
  current.forwardToBridge = payload => {
121
- if (locals.get(message.session) !== current || !current.ready) throw new Error('Local Bridge connection not ready');
121
+ if (locals.get(message.session) !== current || !current.ready) throw new Error('Local desktop service connection not ready');
122
122
  const request = decode(payload, false);
123
123
  if (!['request', 'subscribe', 'unsubscribe'].includes(request.type)) throw new Error('Unsupported LAN message');
124
124
  send(socket, request);
125
125
  };
126
126
  current.timer = setTimeout(() => socket.terminate(), 5000);
127
127
  socket.on('open', () => send(socket, { type: 'auth', token: connection.token }));
128
- socket.on('error', error => events.emit('diagnostic', `Local Bridge: ${error.message}`));
128
+ socket.on('error', error => events.emit('diagnostic', `Local desktop service: ${error.message}`));
129
129
  socket.on('close', () => {
130
130
  if (locals.get(message.session)?.socket !== socket) return;
131
131
  send(remote, { type: 'detach', session: message.session }); dropLocal(message.session); events.emit('detached', { session: message.session });
@@ -135,7 +135,7 @@ export function connectBridgeRelay({ relayUrl, hostKey, bridgeUrl, bridgeToken,
135
135
  try {
136
136
  const response = decode(data, isBinary);
137
137
  if (!current.ready) {
138
- if (response.type !== 'ready') throw new Error('Local Bridge authentication failed');
138
+ if (response.type !== 'ready') throw new Error('Local desktop service authentication failed');
139
139
  clearTimeout(current.timer); current.ready = true; send(remote, { type: 'ready', session: message.session });
140
140
  } else {
141
141
  const payload = data.toString();
@@ -18,7 +18,7 @@ export async function startRelayDemo({ mode = 'lan-shared', pairingTtlMs, dispat
18
18
  reply.startEvents(); reply.event('update', { thread: { id: 'demo' }, simulated: true }); return;
19
19
  }
20
20
  if (request.url === '/api/demo/stop') return reply.json(200, { stopped: true, simulated: true });
21
- reply.json(404, { error: '模拟 Bridge 不执行真实任务' });
21
+ reply.json(404, { error: '模拟桌面服务不执行真实任务' });
22
22
  } });
23
23
  await new Promise(resolve => bridge.listen(0, '127.0.0.1', resolve));
24
24
  const relay = remoteRelay ? { ...remoteRelay, async close() {} } : await startSelfRelay({ pairingTtlMs, ...relayOptions });
@@ -56,9 +56,9 @@ async function registrationKey(config) {
56
56
  export async function selfRelay(command, stateRoot) {
57
57
  const session = readRecord(path.join(stateRoot, 'bridge-session.json'));
58
58
  if (!session) {
59
- if (command === 'relay-status') return { running: false, displayText: 'Bridge 未运行。' };
59
+ if (command === 'relay-status') return { running: false, displayText: '桌面服务未运行。' };
60
60
  if (command === 'relay-stop') return { stopped: true, displayText: '中继连接已停止。' };
61
- throw new Error('Bridge 尚未运行,请先执行 relax start。');
61
+ throw new Error('桌面服务尚未运行,请先执行 relax start。');
62
62
  }
63
63
  return controlRequest(session, 'POST', command);
64
64
  }
@@ -67,7 +67,7 @@ async function createRelayRuntime(stateRoot) {
67
67
  const config = await loadConfig(stateRoot);
68
68
  const session = () => readRecord(path.join(stateRoot, 'bridge-session.json'));
69
69
  let health = await serviceHealth(session());
70
- if (health?.status !== 'ok') throw new Error('Bridge 尚未运行,请先执行 relax start --json。');
70
+ if (health?.status !== 'ok') throw new Error('桌面服务尚未运行,请先执行 relax start --json。');
71
71
  const stored = await credentialStore('load');
72
72
  const identities = stored.record || { version: 1, servers: {} };
73
73
  const validate = value => {
@@ -218,7 +218,7 @@ export function createRelayManager(stateRoot) {
218
218
  if (command === 'relay-status') return current ? current.inspect() : { running: false, displayText: '中继连接未运行。' };
219
219
  if (command === 'relay-stop') {
220
220
  await current?.shutdown(); current = undefined;
221
- return { stopped: true, displayText: '中继连接已停止;Bridge 和 Codex 保持运行。' };
221
+ return { stopped: true, displayText: '中继连接已停止;桌面服务和 Codex 内核保持运行。' };
222
222
  }
223
223
  if (!current) {
224
224
  if (command === 'relay-revoke') throw new Error('请先启动中继连接,再撤销移动端授权。');
@@ -505,7 +505,7 @@ void managedAppServer.start();
505
505
  executionHealth.start();
506
506
  runtimeDiscovery.start();
507
507
  server.on('error', error => {
508
- console.error('Bridge 监听失败:', error.message);
508
+ console.error('桌面服务监听失败:', error.message);
509
509
  process.exit(1);
510
510
  });
511
511
  server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
@@ -528,10 +528,10 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
528
528
  pid: process.pid, controlPort: control.port });
529
529
  }
530
530
  } catch (error) {
531
- console.error('Bridge 管理通道启动失败:', error.message);
531
+ console.error('桌面服务管理通道启动失败:', error.message);
532
532
  process.exit(1);
533
533
  }
534
- console.log(`Code Relax Bridge 已启动:http://127.0.0.1:${options.port}/`);
534
+ console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
535
535
  if (options.lan) {
536
536
  console.log('LAN 模式使用鉴权 WebSocket,仅适合受信任局域网测试,不要暴露到公网。');
537
537
  }
@@ -29,7 +29,7 @@ export async function openControl({ instanceId, secret, shutdown, status = () =>
29
29
  }
30
30
  if (request.method === 'POST') response.once('finish', () => {
31
31
  Promise.resolve(shutdown()).catch(error => {
32
- console.error('Bridge 关闭失败:', error.message);
32
+ console.error('桌面服务关闭失败:', error.message);
33
33
  process.exitCode = 1;
34
34
  });
35
35
  });
@@ -56,16 +56,16 @@ export function controlRequest(session, method = 'GET', action = '') {
56
56
  response.on('end', () => {
57
57
  try {
58
58
  if (response.statusCode === 409) throw new Error(JSON.parse(body).error);
59
- if (response.statusCode !== 200) throw new Error('Bridge 管理通道身份验证失败。');
59
+ if (response.statusCode !== 200) throw new Error('桌面服务管理通道身份验证失败。');
60
60
  const identity = JSON.parse(body);
61
61
  if (identity.instanceId !== session.instanceId || identity.pid !== session.pid) {
62
- throw new Error('Bridge 实例身份不匹配。');
62
+ throw new Error('桌面服务实例身份不匹配。');
63
63
  }
64
64
  resolve(identity);
65
65
  } catch (error) { reject(error); }
66
66
  });
67
67
  });
68
- request.on('timeout', () => request.destroy(new Error('Bridge 管理通道响应超时。')));
68
+ request.on('timeout', () => request.destroy(new Error('桌面服务管理通道响应超时。')));
69
69
  request.on('error', reject);
70
70
  request.end();
71
71
  });
@@ -20,7 +20,7 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
20
20
  });
21
21
  await check('bridge-runtime', async () => {
22
22
  await host.bridgeEnvironment();
23
- add('bridge-runtime', 'ok', 'Bridge 启动环境已就绪;实际任务操作尚未验证。');
23
+ add('bridge-runtime', 'ok', '桌面服务启动环境已就绪;实际任务操作尚未验证。');
24
24
  });
25
25
  await check('network', async () => {
26
26
  const address = await host.lanAddress();
@@ -37,10 +37,10 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
37
37
  ? `本机服务正常:${state.mode},端口 ${state.port},运行版本:${state.version || '未知'}` : '服务未就绪;安装后执行 relax start。');
38
38
  if (state.running && installedVersion && state.version !== installedVersion) add('service-version', 'warning', state.version
39
39
  ? `已安装版本 ${installedVersion},但当前运行版本为 ${state.version};请重新执行安装或重启工作站服务。`
40
- : `已安装版本 ${installedVersion},但当前运行 Bridge 未报告版本,可能仍是旧进程;请重新执行安装或重启工作站服务。`);
40
+ : `已安装版本 ${installedVersion},但当前运行的桌面服务未报告版本,可能仍是旧进程;请重新执行安装或重启桌面服务。`);
41
41
  if (state.running) add('managed-app-server', state.managedAppServer?.state === 'ready' ? 'ok' : 'error',
42
- state.managedAppServer?.state === 'ready' ? '受管 app-server 已就绪。'
43
- : state.managedAppServer?.error || '受管 app-server 尚未就绪。');
42
+ state.managedAppServer?.state === 'ready' ? 'Codex 内核已就绪。'
43
+ : state.managedAppServer?.error || 'Codex 内核尚未就绪。');
44
44
  if (state.managedAppServer?.updatePending) add('managed-app-server-update', 'warning',
45
45
  '已选择另一版本 CLI;当前 app-server 继续完成任务,下次内核启动时采用选定版本。');
46
46
  if (state.managedAppServer?.launcherRestartRequired) add('managed-app-server-launcher', 'warning',
@@ -14,7 +14,7 @@ export function serviceHealth(session) {
14
14
  socket.terminate();
15
15
  if (error) reject(error); else resolve(value);
16
16
  };
17
- const timer = setTimeout(() => finish(new Error('Bridge 健康检查超时。')), 6000);
17
+ const timer = setTimeout(() => finish(new Error('桌面服务健康检查超时。')), 6000);
18
18
  socket.on('open', () => socket.send(JSON.stringify({ type: 'auth', token })));
19
19
  socket.on('message', data => {
20
20
  try {
@@ -28,6 +28,6 @@ export function serviceHealth(session) {
28
28
  });
29
29
  socket.on('error', error => finish(error.code === 'ECONNREFUSED' ? null : error));
30
30
  socket.on('close', code => finish(new Error(code === 1008
31
- ? 'Bridge 健康检查鉴权失败。' : 'Bridge 健康检查连接已断开。')));
31
+ ? '桌面服务健康检查鉴权失败。' : '桌面服务健康检查连接已断开。')));
32
32
  });
33
33
  }
@@ -27,7 +27,7 @@ export function lifecycle({ stateRoot, host }) {
27
27
 
28
28
  async function alive(session) {
29
29
  if (!session?.controlPort || !session.instanceId || !session.controlSecret) {
30
- if (await health(session)) throw new Error('当前为旧版 Bridge,请先使用旧版停止入口结束服务,再启动新版。');
30
+ if (await health(session)) throw new Error('当前为旧版桌面服务,请先使用旧版停止入口结束服务,再启动新版。');
31
31
  return false;
32
32
  }
33
33
  try { await controlRequest(session); return true; }
@@ -35,7 +35,7 @@ export function lifecycle({ stateRoot, host }) {
35
35
  }
36
36
  async function stop() {
37
37
  const session = readRecord(sessionPath);
38
- if (!await alive(session)) return { stopped: true, message: 'Bridge 已停止。' };
38
+ if (!await alive(session)) return { stopped: true, message: '桌面服务已停止。' };
39
39
  await controlRequest(session, 'POST');
40
40
  const deadline = Date.now() + 10000;
41
41
  while (true) {
@@ -45,10 +45,10 @@ export function lifecycle({ stateRoot, host }) {
45
45
  // Verify refusal on the next connection; never resend the shutdown request.
46
46
  if (error.code !== 'ECONNRESET') throw error;
47
47
  }
48
- if (Date.now() > deadline) throw new Error('Bridge 未在 10 秒内退出;未强杀进程。');
48
+ if (Date.now() > deadline) throw new Error('桌面服务未在 10 秒内退出;未强杀进程。');
49
49
  await delay(100);
50
50
  }
51
- return { stopped: true, message: 'Bridge 已正常关闭。' };
51
+ return { stopped: true, message: '桌面服务已正常关闭。' };
52
52
  }
53
53
  async function status() {
54
54
  const session = readRecord(sessionPath);
@@ -78,7 +78,7 @@ export function lifecycle({ stateRoot, host }) {
78
78
  }
79
79
  async function restartManagedAppServer() {
80
80
  const session = readRecord(sessionPath);
81
- if (!await alive(session)) throw new Error('Bridge 尚未运行,无法迁移受管 app-server。');
81
+ if (!await alive(session)) throw new Error('桌面服务尚未运行,无法迁移 Codex 内核。');
82
82
  return controlRequest(session, 'POST', 'restart-managed-app-server');
83
83
  }
84
84
  async function start() {
@@ -110,11 +110,11 @@ export function lifecycle({ stateRoot, host }) {
110
110
  const deadline = Date.now() + 10000;
111
111
  let ready;
112
112
  while (!(ready = readRecord(readyPath))) {
113
- if (!processAlive(pid)) throw new Error('Bridge 启动失败,请查看 bridge.stderr.log。');
114
- if (Date.now() > deadline) throw new Error('Bridge 未在 10 秒内就绪。');
113
+ if (!processAlive(pid)) throw new Error('桌面服务启动失败,请查看 bridge.stderr.log。');
114
+ if (Date.now() > deadline) throw new Error('桌面服务未在 10 秒内就绪。');
115
115
  await delay(100);
116
116
  }
117
- if (ready.instanceId !== instanceId || ready.pid !== pid) throw new Error('Bridge 启动实例不匹配。');
117
+ if (ready.instanceId !== instanceId || ready.pid !== pid) throw new Error('桌面服务启动实例不匹配。');
118
118
  session = { ...ready, controlSecret, executablePath: executable, scriptPath, port: config.port,
119
119
  lan: true, appServer: false, accessUrl: `http://${address}:${config.port}/?token=${config.token}`,
120
120
  startedAt: new Date().toISOString() };
@@ -111,7 +111,7 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
111
111
  await waitFor(async () => {
112
112
  const health = await service.status();
113
113
  return health.running && health.mode === 'lan-shared' && health.execution?.ready === true;
114
- }, 'Codex 内核未就绪;Bridge 和中继管理连接仍保留。');
114
+ }, 'Codex 内核未就绪;桌面服务和中继连接仍保留。');
115
115
  record = { ...record, status: 'completed', stage: 'ready', steps: record.steps.map(step => ({ ...step, status: 'completed' })),
116
116
  message: 'Codex Desktop 已接入 Codex 内核;移动端控制保持就绪,原任务未自动重发。' };
117
117
  } catch (error) { record = { ...record, status: 'failed', error: error.message,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.15",
3
+ "version": "0.1.0-beta.17",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -59,9 +59,9 @@ export function formatSetupOutput(output, previous = {}) {
59
59
  ? `未重启(PID ${pid},启动代次 ${migration.launcherRevision ?? '未知'} 已是最新)`
60
60
  : `已启动(PID ${pid},启动代次 ${migration.launcherRevision ?? '未知'})`;
61
61
  return `Code Relax ${result.version || '未知版本'} 安装成功。\n`
62
- + `受管 app-server:${appServer}\n`
63
- + `Bridge:${result.restarted ? '已恢复并健康' : '已启动并健康'}\n`
64
- + `${result.desktopRestarted ? `Codex Desktop:已干净重启并重新接入(${result.desktopRestartReason === 'attach-managed-server' ? '接入受管内核' : 'app-server 迁移'})\n` : ''}`
62
+ + `Codex 内核:${appServer}\n`
63
+ + `桌面服务:${result.restarted ? '已恢复并健康' : '已启动并健康'}\n`
64
+ + `${result.desktopRestarted ? `Codex Desktop:已干净重启并重新接入(${result.desktopRestartReason === 'attach-managed-server' ? '接入 Codex 内核' : '内核迁移'})\n` : ''}`
65
65
  + `${result.desktopRestartRecommended ? 'Codex Desktop:建议完全退出后重新打开。\n' : ''}`;
66
66
  } catch { return output; }
67
67
  }