@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.10
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 +14 -15
- package/dist/bin/codex-remote.mjs +81 -26
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +50 -22
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +11 -11
- package/dist/src/lan-socket-server.mjs +8 -2
- package/dist/src/managed-app-server.mjs +27 -7
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +3 -5
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/codex-runtime.mjs +117 -0
- package/dist/src/platform/windows/desktop-host.mjs +16 -1
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -7
- package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +71 -0
- package/dist/src/self-relay/admin-server.mjs +61 -0
- package/dist/src/self-relay/admin-state.mjs +122 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +23 -8
- package/dist/src/self-relay/demo.mjs +4 -5
- package/dist/src/self-relay/lifecycle.mjs +53 -49
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +157 -17
- package/dist/src/server.mjs +484 -1274
- package/dist/src/service-doctor.mjs +13 -25
- package/dist/src/service-lifecycle.mjs +12 -12
- package/dist/src/shared-app-server.mjs +192 -34
- package/dist/src/shared-catalog.mjs +4 -14
- package/dist/src/shared-recovery.mjs +8 -6
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/web/activity-view.js +283 -0
- package/dist/web/capabilities.js +2 -9
- package/dist/web/chat-transport.js +21 -21
- package/dist/web/chat.css +148 -106
- package/dist/web/chat.js +1454 -2788
- package/dist/web/community.css +82 -0
- package/dist/web/community.html +36 -0
- package/dist/web/composer-controller.js +97 -0
- package/dist/web/conversation-controller.js +99 -0
- package/dist/web/disclosure-state-controller.js +95 -0
- package/dist/web/draft-controller.js +103 -0
- package/dist/web/harmony-platform.js +3 -2
- package/dist/web/history-cache.js +112 -18
- package/dist/web/history-controller.js +167 -0
- package/dist/web/index.html +113 -47
- package/dist/web/link-action-controller.js +212 -0
- package/dist/web/message-send-controller.js +177 -0
- package/dist/web/message-view.js +98 -0
- package/dist/web/p2p-data-channel.js +50 -0
- package/dist/web/p2p-probe.js +67 -27
- package/dist/web/page-resume.js +28 -0
- package/dist/web/pending-message-store.js +108 -0
- package/dist/web/queue-controller.js +82 -0
- package/dist/web/resources.json +1 -1
- package/dist/web/self-relay-session.js +28 -18
- package/dist/web/station-connection-controller.js +75 -0
- package/dist/web/task-list-view.js +289 -0
- package/dist/web/thread-attention-controller.js +124 -0
- package/dist/web/thread-context-controller.js +30 -0
- package/dist/web/thread-list-controller.js +61 -0
- package/dist/web/thread-list-sync.js +86 -0
- package/dist/web/thread-title-controller.js +57 -0
- package/dist/web/timeline-formatters.js +249 -0
- package/dist/web/timeline-reducer.js +77 -0
- package/dist/web/timeline-renderer.js +161 -0
- package/dist/web/timeline-scroll-controller.js +50 -0
- package/dist/web/usage-controller.js +258 -0
- package/dist/web/vendor/lucide.LICENSE.txt +17 -0
- package/package.json +16 -18
- package/tools/postinstall.mjs +140 -3
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -355
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -13
- package/dist/src/thread-catalog.mjs +0 -42
- package/dist/tools/find-desktop-pipe.mjs +0 -10
package/README.md
CHANGED
|
@@ -4,35 +4,34 @@ Bridge 让 HarmonyOS 客户端控制 Windows 上的 Codex Desktop。当前要求
|
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
公开 beta 已发布到 npm,安装或更新使用:
|
|
8
8
|
|
|
9
9
|
```powershell
|
|
10
|
-
npm
|
|
10
|
+
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
首次安装前先保存工作,并从 Codex 菜单或 Windows 托盘完全退出 Codex。更新无需先停止正在运行的 Bridge;安装器会短暂停止、替换并恢复服务。npm 全局安装会自动执行 `setup` 和 Skill 安装。`setup` 将完整服务部署到 `%LOCALAPPDATA%\Programs\CodexRemote\service`,记录安装所用 Node 的绝对路径,不复制 Node。配置、日志和二维码以 `installation.json.stateRoot` 为准,通常位于 `%LOCALAPPDATA%\CodexRemote`,MSIX 环境可能解析到包缓存目录;附件位于 `%ProgramData%\CodexRemote\attachments`。
|
|
14
14
|
|
|
15
15
|
`setup` 立即启动 Bridge 和固定 45839 受管 app-server,并启用当前用户登录自启;自启不打开 Codex。受管实例健康后配置当前用户 `CODEX_APP_SERVER_WS_URL`,之后正常启动的 Desktop 自动接入。若安装时 Desktop 仍在运行,不中断现有任务,允许两个 app-server 暂时共存,并在手机提示立即或稍后重启。
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Codex 内核
|
|
18
18
|
|
|
19
19
|
```powershell
|
|
20
|
-
relax shortcuts #
|
|
21
|
-
relax shared-start #
|
|
22
|
-
relax desktop-start # 基础控制、Desktop、Bridge
|
|
20
|
+
relax shortcuts # 创建 Codex 内核桌面快捷方式
|
|
21
|
+
relax shared-start # Codex 内核、Desktop、Bridge
|
|
23
22
|
relax start # 启动/复用常驻 Bridge 和已配置中继
|
|
24
23
|
relax pair # 启动/复用服务并打开配对二维码
|
|
25
24
|
relax status
|
|
26
25
|
relax stop
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
Codex 内核由官方 `codex app-server --listen ws://127.0.0.1:45839` 提供执行服务,Desktop 和 Bridge 均可连接;它只监听回环地址。Bridge 独立报告内核健康、Desktop 接入和手机可写状态;Desktop 退出后,手机仍可操作内核中的任务。同一 thread 已被另一个实例持有时,手机只读且可重新检测。
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
手机工作站详情可确认让 Desktop 接入 Codex 内核,独立进程逐步执行并保留 Bridge/中继连接,见[启用流程](../docs/shared-recovery.md)。本地 `shared-start` 在 Desktop 未接入受管服务时要求先自行退出 Desktop,不强制结束任务。Bridge 状态匹配且健康时直接复用;未知进程占用 45831/45839 时拒绝接管。`stop` 只停止 Bridge,不停止 Desktop 或受管 app-server。
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
受管 app-server 通过隐藏控制台启动,使后续控制台子进程继承隐藏窗口。手机的项目、任务列表和新建任务均使用受管内核,不依赖 Desktop 工具管道;项目和任务 ID 保持原生值。
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
受管 app-server 未就绪时,手机停留在连接界面并显示“Codex 内核未就绪”;Bridge 保留健康和恢复接口,聊天请求返回错误。
|
|
36
35
|
|
|
37
36
|
## Code Relax 中继
|
|
38
37
|
|
|
@@ -47,7 +46,7 @@ relax relay-stop --json
|
|
|
47
46
|
relax relay-revoke --json
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
`relay-shared-start`
|
|
49
|
+
`relay-shared-start` 先准备 Codex 内核和 Bridge,再连接公网私有中继。每台移动设备通过 `pair` 配对一次;新增设备不替换已有设备。连接器把 Windows hostname 和当前 Wi-Fi/有线网络候选经已认证 WebSocket 临时发送给手机,手机本地保存电脑名,只在设备身份验证通过后保存可达地址;服务器不持久化这些 Code Relax 中继 hostname、IP 或网络名。OpenAI 官方认证和设备配对仍直接访问 OpenAI;完成后,手机使用统一配对产生的设备凭据,通过独立元数据 WebSocket 把官方环境 ID、名称和 hostname 写入设备记录,不上传 OpenAI access token、配对码或登录态。停止连接器不撤销授权。
|
|
51
50
|
|
|
52
51
|
## Bridge 调试页面
|
|
53
52
|
|
|
@@ -57,14 +56,14 @@ relax dev-status --json
|
|
|
57
56
|
relax dev-stop --json
|
|
58
57
|
```
|
|
59
58
|
|
|
60
|
-
调试页面与 Bridge 共用进程和端口(默认 45831
|
|
59
|
+
调试页面与 Bridge 共用进程和端口(默认 45831),只额外提供白名单静态资源,用于局域网页面调试。`dev-stop` 只关闭静态页面,不影响 Bridge WebSocket。Code Relax 中继和 OpenAI 中继均使用 HAP 内置页面。
|
|
61
60
|
|
|
62
61
|
## 更新与卸载
|
|
63
62
|
|
|
64
63
|
更新固定服务:
|
|
65
64
|
|
|
66
65
|
```powershell
|
|
67
|
-
npm
|
|
66
|
+
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
|
|
68
67
|
```
|
|
69
68
|
|
|
70
69
|
`setup` 先暂存并校验新版本,再短暂停止 Bridge/中继完成切换;原服务此前正在运行时会自动恢复,新版本启动失败则回滚并启动旧版本。无需提前执行 `stop`。Node 路径变化或原 Node 被删除后需重新 setup。升级可能因 Node 可执行文件路径变化而再次触发 Windows 防火墙授权。
|
|
@@ -80,4 +79,4 @@ npm uninstall -g @walkhi/code-relax
|
|
|
80
79
|
|
|
81
80
|
## 发布边界
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
registry 测试包使用 `@walkhi/code-relax@beta`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。安装摘要通过 `--foreground-scripts` 显示,首次安装或启动链迁移时干净重启 Desktop,分别报告已安装与实际运行版本;登录启动使用当前用户任务计划。通信协议仍为 7。详细规则见 [npm 分发](../docs/npm-distribution.md)。
|
|
@@ -17,17 +17,17 @@ import { spawn } from 'node:child_process';
|
|
|
17
17
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
18
18
|
import { requestRecovery, recoveryStatus, recoveryWarning } from '../src/shared-recovery.mjs';
|
|
19
19
|
import { configureManagedEnvironment, clearManagedEnvironment } from '../src/platform/windows/user-environment.mjs';
|
|
20
|
+
import { resolveCodexRuntime } from '../src/platform/windows/codex-runtime.mjs';
|
|
20
21
|
|
|
21
22
|
const help = `Code Relax(Windows)
|
|
22
|
-
用法:relax <setup|doctor|skill-install|skill-remove|uninstall|start|shared-start|
|
|
23
|
+
用法:relax <setup|doctor|skill-install|skill-remove|uninstall|start|shared-start|shortcuts|pair|status|stop> [--json]
|
|
23
24
|
setup 部署并启动固定目录服务、受管 app-server 和登录自启动;安全配置 Desktop 后续接入
|
|
24
25
|
start 启动或复用 Bridge,并恢复已配置的私有中继
|
|
25
|
-
shared-start
|
|
26
|
-
shared-recover --confirm
|
|
26
|
+
shared-start 启动或复用 Codex 内核,再启动 Desktop、Bridge 和已配置的私有中继
|
|
27
|
+
shared-recover --confirm 重新打开 Desktop 并接入 Codex 内核;不强制重启已有内核
|
|
27
28
|
shared-recovery-status 查看最近一次恢复的实际阶段和结果
|
|
28
29
|
autostart-enable / autostart-disable / autostart-status 管理当前用户登录时启动 Bridge
|
|
29
|
-
|
|
30
|
-
shortcuts 创建完整控制/基础控制桌面快捷方式
|
|
30
|
+
shortcuts 创建 Codex 内核桌面快捷方式
|
|
31
31
|
pair 启动或复用工作站服务,并打开一次性配对二维码
|
|
32
32
|
status 查看服务健康状态
|
|
33
33
|
stop 停止服务
|
|
@@ -35,11 +35,11 @@ const help = `Code Relax(Windows)
|
|
|
35
35
|
dev-status 查看 Bridge 调试页面状态
|
|
36
36
|
dev-stop 关闭调试页面,不停止 Bridge
|
|
37
37
|
relay-start 启动或复用私有中继,已有授权直接恢复,首次授权生成二维码
|
|
38
|
-
relay-shared-start
|
|
38
|
+
relay-shared-start 启动 Codex 内核和 Bridge 后连接私有中继(需先退出当前 Codex)
|
|
39
39
|
relay-status 查看私有中继连接器及手机连接状态
|
|
40
40
|
relay-stop 停止私有中继连接器,不停止 Bridge 或 Codex
|
|
41
41
|
relay-revoke 撤销这台电脑的全部移动设备授权(停止连接器不会撤销授权)
|
|
42
|
-
doctor 检查安装、Node、
|
|
42
|
+
doctor 检查安装、Node、Codex 内核、网络和 Skill,提示防火墙处理方式
|
|
43
43
|
skill-install 安装/更新当前用户的 Codex Skill,不覆盖自定义文件
|
|
44
44
|
skill-remove 移除本包安装且未修改的 Skill
|
|
45
45
|
uninstall 停止并移除固定目录服务和本包 Skill,清理自有 Desktop 环境变量,保留其他配置与附件
|
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
console.log(help);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
if (!['autostart-enable', 'autostart-disable', 'autostart-status', 'shared-recover', 'shared-recovery-status', 'relay-shared-start', 'relay-revoke', 'relay-start', 'relay-status', 'relay-stop', 'dev', 'dev-status', 'dev-stop', 'setup', 'doctor', 'skill-install', 'skill-remove', 'uninstall', 'shared-start', '
|
|
55
|
+
if (!['autostart-enable', 'autostart-disable', 'autostart-status', 'shared-recover', 'shared-recovery-status', 'relay-shared-start', 'relay-revoke', 'relay-start', 'relay-status', 'relay-stop', 'dev', 'dev-status', 'dev-stop', 'setup', 'doctor', 'skill-install', 'skill-remove', 'uninstall', 'shared-start', 'shortcuts', 'start', 'pair', 'status', 'stop'].includes(command)
|
|
56
56
|
|| flags.some(flag => flag !== '--json' && !(command === 'shared-recover' && flag === '--confirm'))) {
|
|
57
57
|
throw new Error('无效命令或参数。使用 --help 查看用法。');
|
|
58
58
|
}
|
|
@@ -63,13 +63,13 @@ async function main() {
|
|
|
63
63
|
if (['relay-shared-start', 'relay-revoke', 'relay-start', 'relay-status', 'relay-stop'].includes(command)) {
|
|
64
64
|
if (command === 'relay-shared-start') {
|
|
65
65
|
const existing = await selfRelay('relay-status', stateRoot);
|
|
66
|
-
if (existing.running && existing.transportMode !== 'lan-shared') throw new Error('
|
|
66
|
+
if (existing.running && existing.transportMode !== 'lan-shared') throw new Error('旧连接器仍在运行,请先执行 relay-stop;不会自动中断手机连接。');
|
|
67
67
|
const result = await new Promise((resolve, reject) => {
|
|
68
68
|
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'shared-start', '--json'], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
69
69
|
let output = '', errors = '';
|
|
70
70
|
child.stdout.on('data', chunk => { output += chunk; }); child.stderr.on('data', chunk => { errors += chunk; });
|
|
71
71
|
child.once('error', reject);
|
|
72
|
-
child.once('exit', code => { if (code) reject(new Error(errors.trim().replace(/^(?:Code Relax:)+/u, '') || '
|
|
72
|
+
child.once('exit', code => { if (code) reject(new Error(errors.trim().replace(/^(?:Code Relax:)+/u, '') || 'Codex 内核启动失败')); else { try { resolve(JSON.parse(output)); } catch { reject(new Error('Codex 内核启动结果无法确认,未启动中继')); } } });
|
|
73
73
|
});
|
|
74
74
|
if (!result.bridgeReady) { console.log(json ? JSON.stringify(result) : result.displayText); return; }
|
|
75
75
|
}
|
|
@@ -83,7 +83,7 @@ async function main() {
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
|
|
86
|
-
if (['autostart-enable', 'autostart-disable', 'autostart-status', 'shared-recover', 'shared-recovery-status', 'shared-start', '
|
|
86
|
+
if (['autostart-enable', 'autostart-disable', 'autostart-status', 'shared-recover', 'shared-recovery-status', 'shared-start', 'shortcuts', 'start', 'pair', 'status', 'stop'].includes(command) && path.resolve(fileURLToPath(import.meta.url)).toLowerCase() !== installedEntry.toLowerCase()) {
|
|
87
87
|
if (!fs.existsSync(installedEntry)) throw new Error('尚未部署服务,请先执行 relax setup。');
|
|
88
88
|
const executable = readRecord(path.join(serviceRoot, 'installation.json'))?.nodeExecutable;
|
|
89
89
|
if (!executable || !fs.existsSync(executable)) throw new Error('安装时选用的 Node 不存在,请重新执行 relax setup。');
|
|
@@ -154,8 +154,8 @@ async function main() {
|
|
|
154
154
|
result.environment = environment;
|
|
155
155
|
result.skill = skill.managed ? removeSkill() : { removed: false, message: '未移除自定义或未安装的 Skill。' };
|
|
156
156
|
result.message = environment?.preserved
|
|
157
|
-
? '固定目录服务已卸载;检测到用户自定义的 CODEX_APP_SERVER_WS_URL
|
|
158
|
-
: '固定目录服务已卸载;Code Relax 设置的 app-server
|
|
157
|
+
? '固定目录服务已卸载;检测到用户自定义的 CODEX_APP_SERVER_WS_URL,已保留。配置、附件和可能仍在执行任务的 app-server 也已保留;重启 Windows可结束该进程。npm 命令入口可再用 npm uninstall -g @walkhi/code-relax 移除。'
|
|
158
|
+
: '固定目录服务已卸载;Code Relax 设置的 app-server 环境变量已清理,其他配置、附件和可能仍在执行任务的 app-server 已保留;重启 Windows可结束该进程。npm 命令入口可再用 npm uninstall -g @walkhi/code-relax 移除。';
|
|
159
159
|
console.log(json ? JSON.stringify(result) : `${result.message}\n${result.skill.message}`);
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
@@ -170,7 +170,11 @@ async function main() {
|
|
|
170
170
|
const source = path.join(stateRoot, name), target = path.join(physicalStateRoot, name);
|
|
171
171
|
if (fs.existsSync(source) && !fs.existsSync(target)) fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
|
|
172
172
|
}
|
|
173
|
-
const
|
|
173
|
+
const hadInstalledService = fs.existsSync(path.join(serviceRoot, 'installation.json'));
|
|
174
|
+
const desktopBeforeSetup = await desktopHost.inspect();
|
|
175
|
+
const runtime = await resolveCodexRuntime(physicalStateRoot, { refresh: true });
|
|
176
|
+
const desktopNeedsAttach = desktopBeforeSetup.running && !await host.sharedServiceUrl();
|
|
177
|
+
const previous = hadInstalledService ? await service.status() : { processRunning: false };
|
|
174
178
|
const previousRelay = await selfRelay('relay-status', stateRoot);
|
|
175
179
|
const resumeAfterUpdate = previous.processRunning || previousRelay.running;
|
|
176
180
|
const executable = fs.realpathSync(process.execPath);
|
|
@@ -189,6 +193,7 @@ async function main() {
|
|
|
189
193
|
let stoppedForUpdate = false;
|
|
190
194
|
const installed = await installService(packageRoot(path.dirname(fileURLToPath(import.meta.url))), serviceRoot, executable, physicalStateRoot, {
|
|
191
195
|
beforeActivate: async () => {
|
|
196
|
+
if (desktopNeedsAttach) await desktopHost.closeDesktop(desktopBeforeSetup.desktop);
|
|
192
197
|
stoppedForUpdate = resumeAfterUpdate;
|
|
193
198
|
if (previousRelay.running) await selfRelay('relay-stop', stateRoot);
|
|
194
199
|
if (previous.processRunning) await service.stop();
|
|
@@ -205,18 +210,68 @@ async function main() {
|
|
|
205
210
|
if (!bridge.running || bridge.managedAppServer?.state !== 'ready') {
|
|
206
211
|
throw new Error(bridge.managedAppServer?.error || 'Bridge 已安装,但受管 app-server 未就绪;未写入 Desktop 环境变量。');
|
|
207
212
|
}
|
|
213
|
+
const environment = desktopBeforeSetup.desktop
|
|
214
|
+
? await configureManagedEnvironment(physicalStateRoot) : { configured: false, reason: 'desktop-not-installed' };
|
|
215
|
+
let managedAppServerMigrated = false;
|
|
216
|
+
const managedAppServerMigration = {
|
|
217
|
+
required: bridge.managedAppServer.launcherRestartRequired === true,
|
|
218
|
+
restarted: false,
|
|
219
|
+
previousPid: bridge.managedAppServer.pid || null,
|
|
220
|
+
};
|
|
221
|
+
let desktopRestarted = false;
|
|
222
|
+
let desktopRestartReason = null;
|
|
223
|
+
if (managedAppServerMigration.required) {
|
|
224
|
+
const desktopInfo = await desktopHost.inspect();
|
|
225
|
+
const desktopAttached = desktopInfo.running
|
|
226
|
+
&& (bridge.desktop?.attachedToManagedServer === true || Boolean(await host.sharedServiceUrl()));
|
|
227
|
+
if (desktopAttached) await desktopHost.closeDesktop(desktopInfo.desktop);
|
|
228
|
+
await service.restartManagedAppServer();
|
|
229
|
+
const migrationDeadline = Date.now() + 40_000;
|
|
230
|
+
do {
|
|
231
|
+
await delay(250);
|
|
232
|
+
bridge = await service.status();
|
|
233
|
+
if (bridge.managedAppServer?.restart?.status === 'failed') {
|
|
234
|
+
throw new Error(bridge.managedAppServer.restart.error || '受管 app-server 启动链迁移失败。');
|
|
235
|
+
}
|
|
236
|
+
} while (Date.now() < migrationDeadline && (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired));
|
|
237
|
+
if (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired) {
|
|
238
|
+
throw new Error('受管 app-server 未在 40 秒内完成启动链迁移。');
|
|
239
|
+
}
|
|
240
|
+
managedAppServerMigrated = true;
|
|
241
|
+
managedAppServerMigration.restarted = true;
|
|
242
|
+
if (desktopAttached) {
|
|
243
|
+
await launchDesktop('shared', { stateRoot: physicalStateRoot, host: desktopHost });
|
|
244
|
+
desktopRestarted = true;
|
|
245
|
+
desktopRestartReason = 'app-server-migration';
|
|
246
|
+
bridge = await service.status();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
208
249
|
const autostart = await residentStartup(installed, 'enable');
|
|
209
|
-
|
|
250
|
+
if (desktopNeedsAttach && !desktopRestarted) {
|
|
251
|
+
const currentDesktop = await desktopHost.inspect();
|
|
252
|
+
if (currentDesktop.running) await desktopHost.closeDesktop(currentDesktop.desktop);
|
|
253
|
+
await launchDesktop('shared', { stateRoot: physicalStateRoot, host: desktopHost });
|
|
254
|
+
desktopRestarted = true;
|
|
255
|
+
desktopRestartReason = 'attach-managed-server';
|
|
256
|
+
bridge = await service.status();
|
|
257
|
+
}
|
|
210
258
|
installed.autostart = autostart;
|
|
211
259
|
installed.restarted = resumeAfterUpdate;
|
|
212
260
|
installed.environment = environment;
|
|
213
261
|
installed.managedAppServer = bridge.managedAppServer;
|
|
214
|
-
installed.
|
|
215
|
-
installed.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
:
|
|
219
|
-
|
|
262
|
+
installed.codexRuntime = runtime;
|
|
263
|
+
installed.managedAppServerMigrated = managedAppServerMigrated;
|
|
264
|
+
installed.managedAppServerMigration = { ...managedAppServerMigration,
|
|
265
|
+
currentPid: bridge.managedAppServer.pid || null,
|
|
266
|
+
launcherRevision: bridge.managedAppServer.launcherRevision ?? null };
|
|
267
|
+
installed.desktopRestarted = desktopRestarted;
|
|
268
|
+
installed.desktopRestartReason = desktopRestartReason;
|
|
269
|
+
const migrationText = managedAppServerMigrated ? '已自动重建旧启动链的 app-server;安装前尚未完成的任务可能已中断。\n' : '';
|
|
270
|
+
const restartText = desktopRestarted
|
|
271
|
+
? 'Codex Desktop 已干净重启并重新接入受管 app-server。'
|
|
272
|
+
: desktopBeforeSetup.desktop ? 'Codex Desktop 使用同一受管 app-server;手机控制不依赖 Desktop 是否打开。'
|
|
273
|
+
: '使用独立 Codex CLI;无需安装 Codex Desktop。';
|
|
274
|
+
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}`);
|
|
220
275
|
return;
|
|
221
276
|
}
|
|
222
277
|
if (command === 'stop') {
|
|
@@ -225,16 +280,16 @@ async function main() {
|
|
|
225
280
|
return;
|
|
226
281
|
}
|
|
227
282
|
let desktop;
|
|
228
|
-
if (command === 'shared-start'
|
|
283
|
+
if (command === 'shared-start') {
|
|
229
284
|
if (!json) console.log('正在检查 Codex 的启动模式…');
|
|
230
|
-
desktop = await launchDesktop(
|
|
285
|
+
desktop = await launchDesktop('shared', { stateRoot, host: desktopHost });
|
|
231
286
|
delete process.env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
232
287
|
}
|
|
233
288
|
if (!json && (desktop || command === 'start')) console.log('正在检查电脑连接并准备 Bridge…');
|
|
234
289
|
const result = await service[desktop ? 'start' : command]();
|
|
235
|
-
if (command === 'status') {
|
|
236
|
-
console.log(json ? JSON.stringify(result) : result.running
|
|
237
|
-
? `服务正常:${result.mode},端口 ${result.port},PID ${result.pid}` : '服务未就绪。');
|
|
290
|
+
if (command === 'status') {
|
|
291
|
+
console.log(json ? JSON.stringify(result) : result.running
|
|
292
|
+
? `服务正常:${result.mode},版本 ${result.version || '未知'},端口 ${result.port},PID ${result.pid}` : '服务未就绪。');
|
|
238
293
|
return;
|
|
239
294
|
}
|
|
240
295
|
const { accessUrl, qrPath: _legacyQrPath, ...metadata } = result;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { startAdminServer } from '../src/self-relay/admin-server.mjs';
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.RELAY_ADMIN_PORT || 45851);
|
|
4
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error('Invalid RELAY_ADMIN_PORT');
|
|
5
|
+
const server = await startAdminServer({ port });
|
|
6
|
+
console.log(JSON.stringify({ service: 'codex-relay-admin', port: server.port }));
|
|
7
|
+
await new Promise(resolve => { process.once('SIGTERM', resolve); process.once('SIGINT', resolve); });
|
|
8
|
+
await server.close();
|
|
@@ -3,7 +3,7 @@ import { startRelayDemo } from '../src/self-relay/demo.mjs';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
|
|
6
|
-
const mode =
|
|
6
|
+
const mode = 'lan-shared';
|
|
7
7
|
let remoteRelay;
|
|
8
8
|
if (process.env.RELAY_URL) {
|
|
9
9
|
let hostKey;
|
|
@@ -12,6 +12,9 @@ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('Inv
|
|
|
12
12
|
const stateFile = process.env.RELAY_STATE_FILE;
|
|
13
13
|
if (!stateFile) throw new Error('RELAY_STATE_FILE is required');
|
|
14
14
|
const debugAllowlistFile = process.env.RELAY_DEBUG_ALLOWLIST_FILE || '/etc/codex-relay/debug-allowlist.json';
|
|
15
|
+
const adminStateFile = process.env.RELAY_ADMIN_STATE_FILE || '/var/lib/codex-relay/admin.json';
|
|
16
|
+
const maxRelaySessions = Number(process.env.RELAY_MAX_RELAY_SESSIONS || 8);
|
|
17
|
+
if (!Number.isSafeInteger(maxRelaySessions) || maxRelaySessions < 1 || maxRelaySessions > 256) throw new Error('Invalid RELAY_MAX_RELAY_SESSIONS');
|
|
15
18
|
const debugCommand = process.argv.find(value => ['--debug-list', '--debug-allow', '--debug-deny'].includes(value));
|
|
16
19
|
if (debugCommand) {
|
|
17
20
|
const records = deviceStore(stateFile).entries();
|
|
@@ -41,10 +44,10 @@ if (revokeIndex >= 0) {
|
|
|
41
44
|
store.save({ ...record, status: 'revoked', clientHash: undefined, clientId: undefined, revision: record.revision + 1, updatedAt: new Date().toISOString() });
|
|
42
45
|
console.log('Computer and phone authorization revoked'); process.exit(0);
|
|
43
46
|
}
|
|
44
|
-
const relay = await startSelfRelay({ port, hostKey, stateFile, debugAllowlistFile,
|
|
47
|
+
const relay = await startSelfRelay({ port, hostKey, stateFile, debugAllowlistFile, adminStateFile, maxRelaySessions,
|
|
45
48
|
publicRelayUrl: process.env.RELAY_PUBLIC_URL || '', appDownloadUrl: process.env.RELAY_APP_DOWNLOAD_URL || '',
|
|
46
49
|
log: entry => console.log(JSON.stringify({ time: new Date().toISOString(), ...entry })) });
|
|
47
|
-
console.log(JSON.stringify({ service: 'codex-self-relay', listen: relay.url, maxConnections: 32,
|
|
50
|
+
console.log(JSON.stringify({ service: 'codex-self-relay', listen: relay.url, maxConnections: 32, maxRelaySessions,
|
|
48
51
|
maxFrameMiB: 8, encryption: 'TLS must be supplied by reverse proxy; no end-to-end encryption' }));
|
|
49
52
|
await new Promise(resolve => { process.once('SIGTERM', resolve); process.once('SIGINT', resolve); });
|
|
50
53
|
await relay.close();
|
|
@@ -2,31 +2,58 @@
|
|
|
2
2
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
3
|
else root.CodexAppServerEvents = factory();
|
|
4
4
|
})(globalThis, function () {
|
|
5
|
-
function mergeItems(existing, incoming = []) {
|
|
6
|
-
for (const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
function mergeItems(existing, incoming = []) {
|
|
6
|
+
for (const source of incoming) {
|
|
7
|
+
const item = compactItem(source);
|
|
8
|
+
const index = existing.findIndex(current => current.id === item.id);
|
|
9
|
+
if (index < 0) existing.push(item);
|
|
10
|
+
else existing[index] = { ...existing[index], ...item };
|
|
10
11
|
}
|
|
11
|
-
return existing;
|
|
12
|
-
}
|
|
12
|
+
return existing;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function compactItem(item) {
|
|
16
|
+
if (!item || typeof item !== 'object') return item;
|
|
17
|
+
if (item.type === 'commandExecution') {
|
|
18
|
+
const { aggregatedOutput, output, ...metadata } = item;
|
|
19
|
+
return metadata;
|
|
20
|
+
}
|
|
21
|
+
if (item.type === 'mcpToolCall') {
|
|
22
|
+
const { result, ...metadata } = item;
|
|
23
|
+
return metadata;
|
|
24
|
+
}
|
|
25
|
+
if (item.type === 'dynamicToolCall') {
|
|
26
|
+
const { contentItems, ...metadata } = item;
|
|
27
|
+
return metadata;
|
|
28
|
+
}
|
|
29
|
+
if (item.type === 'imageGeneration') {
|
|
30
|
+
const { result, ...metadata } = item;
|
|
31
|
+
return metadata;
|
|
32
|
+
}
|
|
33
|
+
return item;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compactTurn(turn) {
|
|
37
|
+
return { ...turn, items: Array.isArray(turn?.items) ? turn.items.map(compactItem) : [] };
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
// Both relay and local WebSocket carry these same app-server notifications.
|
|
15
41
|
// Mutate one thread and return only the affected turn for incremental rendering.
|
|
16
42
|
function applyNotification(thread, method, p) {
|
|
17
43
|
if (method === 'thread/status/changed') thread.status = p.status;
|
|
18
44
|
else if (method === 'thread/name/updated') thread.name = p.threadName;
|
|
19
|
-
else if (!['turn/started', 'turn/completed', 'item/started', 'item/completed', 'item/agentMessage/delta',
|
|
20
|
-
'item/reasoning/summaryTextDelta', 'item/reasoning/summaryPartAdded'
|
|
45
|
+
else if (!['turn/started', 'turn/completed', 'item/started', 'item/completed', 'item/agentMessage/delta',
|
|
46
|
+
'item/reasoning/summaryTextDelta', 'item/reasoning/summaryPartAdded'].includes(method)) return null;
|
|
21
47
|
thread.turns ||= [];
|
|
22
48
|
let turn = thread.turns.find(t => t.id === (p.turnId || p.turn?.id));
|
|
23
|
-
if (!turn && p.turnId && p.item) { turn = { id: p.turnId, status: 'inProgress', items: [] }; thread.turns.push(turn); }
|
|
24
|
-
if (p.turn) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
if (!turn && p.turnId && p.item) { turn = { id: p.turnId, status: 'inProgress', items: [] }; thread.turns.push(turn); }
|
|
50
|
+
if (p.turn) {
|
|
51
|
+
const incomingTurn = compactTurn(p.turn);
|
|
52
|
+
if (!turn) { turn = incomingTurn; thread.turns.push(turn); }
|
|
53
|
+
else Object.assign(turn, { ...incomingTurn, items: mergeItems(turn.items || [], incomingTurn.items) });
|
|
54
|
+
thread.status = method === 'turn/started' ? 'active' : method === 'turn/completed' ? 'idle' : thread.status;
|
|
55
|
+
}
|
|
56
|
+
if (p.item && turn) mergeItems(turn.items, [compactItem(p.item)]);
|
|
30
57
|
const item = turn?.items.find(item => item.id === p.itemId);
|
|
31
58
|
if (item) {
|
|
32
59
|
if (method === 'item/agentMessage/delta') item.text = (item.text || '') + p.delta;
|
|
@@ -34,8 +61,7 @@
|
|
|
34
61
|
item.summary ||= []; item.summary[p.summaryIndex] = (item.summary[p.summaryIndex] || '') + p.delta;
|
|
35
62
|
}
|
|
36
63
|
if (method === 'item/reasoning/summaryPartAdded') { item.summary ||= []; item.summary[p.summaryIndex] ??= ''; }
|
|
37
|
-
|
|
38
|
-
}
|
|
64
|
+
}
|
|
39
65
|
return turn || (['thread/status/changed', 'thread/name/updated'].includes(method) ? thread.turns.at(-1) : null);
|
|
40
66
|
}
|
|
41
67
|
function mergeAvailable(previous, incoming) {
|
|
@@ -246,9 +272,11 @@
|
|
|
246
272
|
if (action === 'steer') {
|
|
247
273
|
const item = (await readQueueItems(request, threadId)).find(item => item.id === queuedSubmissionId);
|
|
248
274
|
if (!item) throw new Error('该排队消息已不在队列中');
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
275
|
+
const page = await request('thread/turns/list', {
|
|
276
|
+
threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
277
|
+
});
|
|
278
|
+
const active = page.data?.[0];
|
|
279
|
+
if (active?.status === 'inProgress') {
|
|
252
280
|
await request('turn/steer', { threadId, expectedTurnId: active.id, input: item.input,
|
|
253
281
|
clientUserMessageId: item.clientUserMessageId });
|
|
254
282
|
const { deleted } = await request('thread/queue/delete', { threadId, queuedSubmissionId });
|
|
@@ -261,6 +289,6 @@
|
|
|
261
289
|
}
|
|
262
290
|
return readQueue(request, threadId);
|
|
263
291
|
}
|
|
264
|
-
return { createThread, readQueue, addQueue, changeQueue, mergeItems, applyNotification, mergeUsage, UsageCache, readProjects, orderDesktopProjects, threadProjectId, projectlessThreadTarget, updateSettings,
|
|
292
|
+
return { createThread, readQueue, addQueue, changeQueue, compactItem, compactTurn, mergeItems, applyNotification, mergeUsage, UsageCache, readProjects, orderDesktopProjects, threadProjectId, projectlessThreadTarget, updateSettings,
|
|
265
293
|
permissionState, availablePermissionModes, PermissionOptionsCache, updatePermissions };
|
|
266
294
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
+
else root.CodeRelaxP2pDataFramer = factory();
|
|
4
|
+
})(globalThis, () => {
|
|
5
|
+
const CHUNK_CHARS = 12000;
|
|
6
|
+
const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const MAX_PARTS = 1024;
|
|
8
|
+
const byteLength = value => new TextEncoder().encode(value).length;
|
|
9
|
+
|
|
10
|
+
return class {
|
|
11
|
+
constructor() { this.inbound = undefined; }
|
|
12
|
+
create(payload, id) {
|
|
13
|
+
if (typeof payload !== 'string' || !/^[a-f0-9]{32}-[0-9]{1,10}$/.test(id)) throw new Error('Invalid P2P payload');
|
|
14
|
+
const bytes = byteLength(payload);
|
|
15
|
+
if (bytes > MAX_MESSAGE_BYTES) throw new Error('Code Relax 直连消息超过 8 MiB');
|
|
16
|
+
const total = Math.max(1, Math.ceil(payload.length / CHUNK_CHARS));
|
|
17
|
+
if (total > MAX_PARTS) throw new Error('Code Relax 直连消息分片过多');
|
|
18
|
+
return { id, payload, bytes, index: 0, total };
|
|
19
|
+
}
|
|
20
|
+
frame(item) {
|
|
21
|
+
if (!item || item.index >= item.total) return undefined;
|
|
22
|
+
const start = item.index * CHUNK_CHARS;
|
|
23
|
+
return JSON.stringify({ type: 'data', id: item.id, index: item.index, total: item.total,
|
|
24
|
+
payload: item.payload.slice(start, start + CHUNK_CHARS) });
|
|
25
|
+
}
|
|
26
|
+
commit(item) { item.index++; return item.index >= item.total; }
|
|
27
|
+
accept(message) {
|
|
28
|
+
if (!message || message.type !== 'data') return { handled: false };
|
|
29
|
+
if (!/^[a-f0-9]{32}-[0-9]{1,10}$/.test(message.id || '') || !Number.isSafeInteger(message.index) ||
|
|
30
|
+
!Number.isSafeInteger(message.total) || message.total < 1 || message.total > MAX_PARTS ||
|
|
31
|
+
message.index < 0 || message.index >= message.total || typeof message.payload !== 'string' ||
|
|
32
|
+
message.payload.length > CHUNK_CHARS) throw new Error('Invalid P2P data frame');
|
|
33
|
+
if (!this.inbound) {
|
|
34
|
+
if (message.index !== 0) throw new Error('Invalid P2P data order');
|
|
35
|
+
this.inbound = { id: message.id, total: message.total, next: 0, bytes: 0, parts: [] };
|
|
36
|
+
}
|
|
37
|
+
const current = this.inbound;
|
|
38
|
+
if (current.id !== message.id || current.total !== message.total || current.next !== message.index) {
|
|
39
|
+
throw new Error('Invalid P2P data order');
|
|
40
|
+
}
|
|
41
|
+
current.bytes += byteLength(message.payload);
|
|
42
|
+
if (current.bytes > MAX_MESSAGE_BYTES) throw new Error('P2P data message too large');
|
|
43
|
+
current.parts.push(message.payload); current.next++;
|
|
44
|
+
if (current.next !== current.total) return { handled: true, complete: false };
|
|
45
|
+
this.inbound = undefined;
|
|
46
|
+
return { handled: true, complete: true, payload: current.parts.join('') };
|
|
47
|
+
}
|
|
48
|
+
reset() { this.inbound = undefined; }
|
|
49
|
+
};
|
|
50
|
+
});
|
|
@@ -5,7 +5,7 @@ description: 启动、检查或停止 Code Relax 工作站服务,生成并展
|
|
|
5
5
|
|
|
6
6
|
# Code Relax
|
|
7
7
|
|
|
8
|
-
-
|
|
8
|
+
- `relax start --json` 启动 Bridge 和受管 Codex 内核,无需打开 Desktop;支持纯 CLI 安装。`shared-start` 仅用于额外打开 Desktop 并接入同一内核。已配置 Code Relax 中继时,启动命令同时恢复中继连接器和已有设备授权。
|
|
9
9
|
- npm 全局安装会自动执行服务部署和 Skill 安装;只有修复安装时才单独运行 `relax setup --json` 或 `relax skill-install --json`。
|
|
10
10
|
- `setup` 默认启用当前用户登录启动工作站服务,不自动打开 Codex;用 `autostart-status / autostart-disable / autostart-enable` 管理。恢复完整控制会保留 Bridge 和中继,但仍须明确确认可能中断全部完整控制任务。
|
|
11
11
|
- 用户要求启动 Code Relax 时,执行一次 `relax start --json`。用户要求二维码或当前启动结果包含二维码时,展示 CLI 返回的 `qrPath` 和 `displayText`;没有设备授权时可用 `relax pair --json` 生成新的配对二维码。
|
|
@@ -5,10 +5,10 @@ import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
|
5
5
|
import { ManagedAppServer, managedAppServerUrl as sharedUrl, managedPortAvailable as portAvailable } from './managed-app-server.mjs';
|
|
6
6
|
export { portAvailable };
|
|
7
7
|
export async function launchDesktop(mode, { stateRoot, host, environment = process.env, available = portAvailable, wait = delay, timeout = 30000, recoveryId, progress = () => {} }) {
|
|
8
|
-
if (
|
|
8
|
+
if (mode !== 'shared') throw new Error('只支持接入 Code Relax 管理的 Codex 内核。');
|
|
9
9
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
10
10
|
const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
|
|
11
|
-
if (recovery && recovery.operationId !== recoveryId) throw new Error('
|
|
11
|
+
if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请先查看 shared-recovery-status。');
|
|
12
12
|
const lock = path.join(stateRoot, 'desktop-launch.lock');
|
|
13
13
|
let descriptor;
|
|
14
14
|
let managed;
|
|
@@ -18,7 +18,8 @@ export async function launchDesktop(mode, { stateRoot, host, environment = proce
|
|
|
18
18
|
throw error;
|
|
19
19
|
}
|
|
20
20
|
try {
|
|
21
|
-
const info = await host.inspect();
|
|
21
|
+
const info = await host.inspect();
|
|
22
|
+
if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
|
|
22
23
|
const connected = await host.sharedServiceUrl();
|
|
23
24
|
if (info.running) {
|
|
24
25
|
if ((mode === 'shared') === Boolean(connected)) {
|
|
@@ -29,18 +30,17 @@ export async function launchDesktop(mode, { stateRoot, host, environment = proce
|
|
|
29
30
|
}
|
|
30
31
|
const childEnv = { ...environment };
|
|
31
32
|
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Ordinary Desktop must not terminate a shared server that may still be executing tasks.
|
|
33
|
+
progress('starting-app-server');
|
|
34
|
+
managed = new ManagedAppServer({ stateRoot, host, environment, available, wait, timeout });
|
|
35
|
+
if (!await managed.start()) throw new Error(managed.snapshot().error || '受管 app-server 未就绪。');
|
|
36
|
+
const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
|
|
37
|
+
if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
|
|
38
|
+
childEnv.CODEX_APP_SERVER_WS_URL = sharedUrl;
|
|
39
39
|
progress('starting-desktop');
|
|
40
40
|
await host.start(info.desktop, [], childEnv, []);
|
|
41
41
|
const deadline = Date.now() + timeout;
|
|
42
42
|
while (true) {
|
|
43
|
-
const ready =
|
|
43
|
+
const ready = await host.sharedServiceUrl();
|
|
44
44
|
if (ready) break;
|
|
45
45
|
if (Date.now() >= deadline) throw new Error('Desktop 尚未按所选模式就绪,未启动 Bridge。请查看 Desktop 窗口后重试。');
|
|
46
46
|
await wait(500);
|
|
@@ -13,8 +13,14 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
13
13
|
let allowed = request.url === '/api/socket';
|
|
14
14
|
// Packaged ArkWeb pages have an opaque ("null") origin; device authentication remains required.
|
|
15
15
|
if (origin && origin !== 'null') {
|
|
16
|
-
try {
|
|
17
|
-
|
|
16
|
+
try {
|
|
17
|
+
const originUrl = new URL(origin);
|
|
18
|
+
const requestUrl = new URL(`http://${request.headers.host}`);
|
|
19
|
+
const nativeClient = request.headers['x-code-relax-native'] === '1';
|
|
20
|
+
allowed &&= ['http:', 'https:'].includes(originUrl.protocol) &&
|
|
21
|
+
(originUrl.host === request.headers.host || (nativeClient && originUrl.hostname === requestUrl.hostname));
|
|
22
|
+
}
|
|
23
|
+
catch { allowed = false; }
|
|
18
24
|
}
|
|
19
25
|
if (!allowed) { socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); return; }
|
|
20
26
|
wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws));
|