orbit-codex-bridge 0.1.0 → 0.1.1
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 +55 -150
- package/cli.mjs +113 -33
- package/lib/connector-service.mjs +259 -0
- package/package.json +6 -3
- package/public/admin.html +1 -1
- package/public/admin.js +1 -1
- package/public/app.js +113 -38
- package/public/connect.html +2 -2
- package/public/index.html +21 -2
- package/relay-agent.mjs +46 -9
- package/relay-store.mjs +1 -1
- package/server.mjs +10 -4
package/README.md
CHANGED
|
@@ -1,190 +1,95 @@
|
|
|
1
1
|
# Orbit · 会话随行
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
从手机连接自己的电脑,查看和继续 Codex 桌面里的原会话。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Orbit 将电脑连接到 [Orbit 工作台](https://orbit.cubesugar.tech)。在网页中选择电脑和会话,即可查看历史、接收实时回复、发送指令、停止任务和处理审批。桌面已加载的会话会自动热接管,沿用原会话 ID 和执行上下文。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 安装与连接
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
| --- | --- |
|
|
11
|
-
| 列出当前 CODEX_HOME 的原生会话 | 已接入;本机验收读取 22 条,包含 2 条归档、1 条子代理记录 |
|
|
12
|
-
| 按项目归属 / 工作目录分组 | 已接入;本机验收 13 个分组,不代表 13 个独立代码仓库 |
|
|
13
|
-
| 搜索、置顶筛选、分页阅读历史 | 已接入;Markdown、命令输出、退出码、MCP 工具记录及文件 diff 可展开,消息和代码可复制;图片本体尚不展示 |
|
|
14
|
-
| 电脑新增的持久化消息同步到网页 | SQLite/WAL 版本检测 + WebSocket 通知,约 1.2 秒轮询,前端防抖 0.8 秒 |
|
|
15
|
-
| 原 ID 接续闲置历史会话 | 实验通路已实现;用真实 Codex 引擎 + 本机确定性模型桩完成两轮同 ID 集成测试 |
|
|
16
|
-
| Orbit 管理执行的流式输出、停止、审批 | 已实现协议通路;单元测试覆盖,未向用户现有真实会话发送测试消息 |
|
|
17
|
-
| 无侵入热接管桌面活动执行 | 已接入内部桌面 IPC;隔离桩测试覆盖,尚未对用户真实会话写入验证;桌面协议不稳定,暂属实验能力 |
|
|
18
|
-
| 选择模型、推理级别与上下文显示 | 模型目录从当前 Codex `model/list` 动态读取,逐会话授权后可更改下一轮参数;桌面模式经 owner 校验走内部 IPC,未对真实桌面会话执行模型切换验收;上下文仅展示执行器提供的最新一轮真实 token 用量和窗口,不用累计量冒充 |
|
|
19
|
-
| 手机外网连接、自建出站中继 | 已实现云端 Relay 和电脑出站 Agent,需自行部署域名/TLS;已通过两台隔离电脑的 HTTP/WS 集成验证,未部署到真实云服务器 |
|
|
20
|
-
| 多用户控制面与设备撤销 | 已实现自助注册/自动登录、添加自己的电脑/生成连接配置、套餐标签/设备额度/到期、永久撤销、审计及后台;单节点 SQLite 预览版 |
|
|
21
|
-
| 支付、组织角色、推送 | 未实现;不应直接作为完整商业 SaaS 上线 |
|
|
22
|
-
|
|
23
|
-
“全量”限定为当前本地 `CODEX_HOME` 中的数据,不包含其他电脑、其他用户目录或只存在于云端的会话。子代理可查看但不可独立接续。锁状态是保守探测,不等价于任务正在推理。
|
|
24
|
-
|
|
25
|
-
## 快速启动
|
|
26
|
-
|
|
27
|
-
环境:Node.js 24+、npm、已安装 Codex 桌面 / CLI,且本机已有原生会话数据库。
|
|
9
|
+
需要 Node.js 24+,并在电脑上安装、运行 Codex 桌面应用。
|
|
28
10
|
|
|
29
|
-
|
|
11
|
+
Windows PowerShell:
|
|
30
12
|
|
|
31
13
|
```powershell
|
|
32
|
-
|
|
14
|
+
npx.cmd --yes --package=orbit-codex-bridge@latest orbit login
|
|
33
15
|
```
|
|
34
16
|
|
|
35
|
-
|
|
17
|
+
macOS / Linux:
|
|
36
18
|
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
npm start
|
|
19
|
+
```sh
|
|
20
|
+
npx --yes --package=orbit-codex-bridge@latest orbit login
|
|
40
21
|
```
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
### 通过 npm 连接云端电脑(待发布)
|
|
23
|
+
也可以全局安装,之后直接使用 `orbit` 命令:
|
|
45
24
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```powershell
|
|
49
|
-
npm install --global orbit-codex-bridge
|
|
25
|
+
```sh
|
|
26
|
+
npm install --global orbit-codex-bridge@latest
|
|
50
27
|
orbit login
|
|
51
28
|
```
|
|
52
29
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- 首版只打包 Orbit 程序,不包含 Codex 会话、模型密钥或账号凭证。
|
|
58
|
-
- 自建 Relay 可通过 `orbit login --server https://你的域名` 指定。
|
|
59
|
-
- 命令行当前以前台方式运行;关闭窗口会停止服务。再次运行 `orbit login` 会复用电脑凭证。
|
|
60
|
-
- 当前包尚未发布到 npm registry;发布并完成云端域名部署后,这组命令才可供普通用户直接安装。
|
|
61
|
-
|
|
62
|
-
首次访问需要本机配对密钥:
|
|
63
|
-
|
|
64
|
-
```powershell
|
|
65
|
-
Get-Content .runtime\pairing-key
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
密钥在第一次启动时生成,只存于本项目 `.runtime`,不是模型 API Key。不要发到群里、截图公开或提交到代码仓库。
|
|
69
|
-
|
|
70
|
-
启动脚本强制本机模式或显式 `-Lan`,可用 `-Port 4320` 改端口。生产环境自定义绑定请使用 `.env.local` + `npm start`。如果系统执行策略不允许运行脚本,使用 `npm start`,无需降低系统执行策略。
|
|
71
|
-
|
|
72
|
-
## 手机同 Wi-Fi 体验
|
|
30
|
+
1. 命令会打开电脑授权页面,登录或注册 Orbit 账号,确认添加这台电脑。
|
|
31
|
+
2. 手机打开 [orbit.cubesugar.tech](https://orbit.cubesugar.tech),登录同一账号。
|
|
32
|
+
3. 扫描终端显示的配对二维码,选择这台电脑,完成首次连接。
|
|
33
|
+
4. 选择原有会话,开始使用。尚未加载的会话可先在 Codex 桌面打开,再点“重新连接”。
|
|
73
34
|
|
|
74
|
-
|
|
75
|
-
2. 打开电脑端“设备与连接”,使用显示的局域网地址,不是手机上的 `127.0.0.1`。
|
|
76
|
-
3. 手机访问该地址,输入配对密钥;也可以在电脑页面主动展示配对二维码。
|
|
77
|
-
4. 如果无法访问,确认两台设备在同一可信网络、没有 AP 隔离,以及 Windows 防火墙是否允许此专用网络端口。程序不会自行修改防火墙。
|
|
35
|
+
授权信息保存在电脑本地,下次启动会自动复用。连接器默认在当前用户的后台运行,可以关闭安装终端;断网后自动重连,工作进程异常退出后自动恢复。电脑需要保持开机、联网,Codex 桌面需要保持运行。电脑重启后运行 `orbit start` 恢复连接。
|
|
78
36
|
|
|
79
|
-
|
|
37
|
+
## 常用命令
|
|
80
38
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## 原会话接续与热接管安全边界
|
|
84
|
-
|
|
85
|
-
- 浏览、搜索和刷新不会启动执行引擎,NativeStore 以 SQLite `readOnly` + `query_only` 打开数据库。
|
|
86
|
-
- 热接管需要逐条明确确认,授权 30 分钟后过期;订阅本身只读,不会授予控制权。桌面权限继承原 Codex 会话,不会扩大权限。
|
|
87
|
-
- 桌面热接管只路由到已发现的原会话 owner,保留原 conversation ID;任务追加、停止和审批均需匹配当前 owner / turn / request。
|
|
88
|
-
- 桌面 IPC 断线、owner 变化或状态补丁版本缺口会撤销控制授权;不会自动重发指令,也不会回退到 cold resume。恢复后需重新订阅和授权。
|
|
89
|
-
- 模型和推理级别仅能从当前执行器的可用模型目录选择;桌面模式只允许在任务空闲且已授权时更改下一轮设置,会校验原 owner、旧设置和确认结果,不改运行中的任务。独立执行模式在当前浏览器授权会话中保存下一轮覆盖参数,服务重启后会丢失。目录不兼容或无法读取时拒绝切换。
|
|
90
|
-
- 桌面任务运行时可以先选模型并将设置排入下一轮;Orbit 等到同一原会话 turn 空闲后,再用 owner 和旧设置复核并应用。排队设置保存在桥接进程内存,Orbit 服务重启、授权失效、owner 变化或桌面不确认时会撤销/丢弃,不写入真实 Codex 设置文件。
|
|
91
|
-
- 上下文进度采用桌面快照的 `latestTokenUsageInfo.last.totalTokens / modelContextWindow`;总历史用量 `total` 不是当前上下文。没有准确用量或窗口时显示“暂无用量”或只显示 token 数,不猜测百分比。
|
|
92
|
-
- 热接管依赖 Codex 桌面内部 IPC 协议,版本升级后可能失效。接入前要求 Codex 桌面运行并已加载目标会话;还没有对真实会话执行写操作的实机验证。
|
|
93
|
-
- 非桌面模式的历史会话接续使用 `thread/resume` 和 `turn/start`,不创建或 fork 新会话,并校验返回的原 ID。桌面热接管直接委托给原 Codex owner。
|
|
94
|
-
- 独立执行模式遇到原客户端持有 writer lock 时默认拒绝续接。**不要手动删除锁文件,不要让多个独立执行器同时写同一会话。**
|
|
95
|
-
- 桌面热接管要求 Codex 已加载目标会话;独立引擎模式则可能需要先从原客户端正常释放会话。
|
|
96
|
-
- 原生历史可读不代表桌面已加载该会话。遇到 `no-client-found` 时,Orbit 保留历史只读、显示逐会话的未连接原因;在 Codex 桌面打开同一条原会话后,点击提示中的“重新连接”或列表刷新按钮可重新订阅。重新连接不发送消息、不改模型、不自动恢复控制授权,仍需再次明确授权。
|
|
97
|
-
- owner 消失或操作返回 `no-client-found` 会使该会话的旧快照、审批、控制授权及排队模型设置失效;不会自动重试写操作,也不会退回独立执行器。桌面 IPC 本身在线时仍可能有个别会话未加载,界面分别显示这两种状态。
|
|
98
|
-
- 独立引擎沿用本机 `CODEX_HOME`,但桌面进程特有的运行时环境不保证被继承;实际中转配置、环境变量和模型可用性仍需逐机验证。
|
|
99
|
-
- 断线不会自动重放写操作;相同会话中的并发发送会被拒绝。请求 ID 去重窗口为 5 分钟,不能替代持久化事务。
|
|
100
|
-
- 扩大环境权限首版只能拒绝;未知宿主工具直接报错,不自动授权。
|
|
101
|
-
- 不提供私有 stdio 注入、进程内存修改、强制抢锁或自动重启桌面。
|
|
102
|
-
|
|
103
|
-
## 配置
|
|
104
|
-
|
|
105
|
-
复制 `.env.example` 为 `.env.local`,按需设置:
|
|
106
|
-
|
|
107
|
-
| 环境变量 | 说明 |
|
|
39
|
+
| 命令 | 用途 |
|
|
108
40
|
| --- | --- |
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
### 多用户云中继与管理后台
|
|
41
|
+
| `orbit login` | 首次授权,或复用已有授权启动后台连接器 |
|
|
42
|
+
| `orbit start` | 启动后台连接器 |
|
|
43
|
+
| `orbit status` | 查看后台运行状态、云端连接状态及日志路径 |
|
|
44
|
+
| `orbit stop` | 停止后台连接器,保留授权 |
|
|
45
|
+
| `orbit restart` | 重启后台连接器 |
|
|
46
|
+
| `orbit logout` | 停止连接并撤销这台电脑的授权 |
|
|
47
|
+
| `orbit rebind` | 清理旧绑定,重新打开网页授权这台电脑 |
|
|
48
|
+
| `orbit login --foreground` | 在当前终端运行,显示实时诊断信息 |
|
|
49
|
+
|
|
50
|
+
自建服务可在首次授权时指定地址:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
orbit login --server https://your-orbit-domain.com
|
|
54
|
+
```
|
|
125
55
|
|
|
126
|
-
|
|
56
|
+
## 更新
|
|
127
57
|
|
|
128
|
-
|
|
58
|
+
使用 npx 的用户重新执行安装命令即可获取最新版。全局安装的用户运行:
|
|
129
59
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
- 管理员停用账号、重置密码或撤销设备会断开相应访问;已撤销设备必须重新签发凭证。
|
|
135
|
-
- Relay 只转发会话协议,不保存 Codex 数据库或模型密钥;TLS 加密到 Relay,当前尚无端到端加密。
|
|
60
|
+
```sh
|
|
61
|
+
npm install --global orbit-codex-bridge@latest
|
|
62
|
+
orbit login
|
|
63
|
+
```
|
|
136
64
|
|
|
137
|
-
|
|
65
|
+
连接器会更新后台运行时,复用已有电脑授权。
|
|
138
66
|
|
|
139
|
-
|
|
67
|
+
从早期需要保持终端打开的版本升级时,先在旧终端按 `Ctrl+C` 停止旧连接器,再运行新版命令。
|
|
140
68
|
|
|
141
|
-
|
|
69
|
+
## 移除与重新绑定
|
|
142
70
|
|
|
143
|
-
|
|
71
|
+
在工作台的电脑列表点击“移除电脑”,确认后立即撤销连接凭证,释放设备名额。需要再次绑定时,在原电脑执行:
|
|
144
72
|
|
|
145
73
|
```powershell
|
|
146
|
-
|
|
147
|
-
npm test
|
|
148
|
-
npm run test:native
|
|
74
|
+
npx.cmd --yes --package=orbit-codex-bridge@latest orbit rebind
|
|
149
75
|
```
|
|
150
76
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- 另已对本机真实 App Server 做 `initialize` / `thread/list` 只读协议测试;没有恢复或发送现有用户会话。
|
|
155
|
-
- 桌面和手机布局实测截图位于 `previews/desktop.png` / `previews/mobile.png`。默认 npm 镜像没有漏洞审计接口,改用 npm 官方 registry 完成 `npm audit --omit=dev`,结果为 0 个已知依赖漏洞;这不等于应用安全审计。
|
|
156
|
-
|
|
157
|
-
## 结构
|
|
158
|
-
|
|
159
|
-
```text
|
|
160
|
-
手机 / 桌面网页
|
|
161
|
-
├─ HTTP(S):配对、状态、列表、历史分页
|
|
162
|
-
└─ WS(S):变更通知、消息、流式事件、停止、审批
|
|
163
|
-
│
|
|
164
|
-
server.mjs:单用户本地网关
|
|
165
|
-
├─ lib/store.mjs:原生历史只读适配器
|
|
166
|
-
├─ lib/desktop.mjs:桌面 IPC、owner 发现、实时状态与原会话控制
|
|
167
|
-
└─ lib/rpc.mjs:按需独立 App Server / 可选显式目标端点
|
|
168
|
-
```
|
|
77
|
+
重新登录并确认电脑授权即可。移除或重绑不会删除电脑上的 Codex 项目和原有会话。
|
|
78
|
+
|
|
79
|
+
## 常见问题
|
|
169
80
|
|
|
170
|
-
|
|
81
|
+
**Windows 提示禁止运行 `npx.ps1`**
|
|
171
82
|
|
|
172
|
-
|
|
83
|
+
在 PowerShell 中使用 `npx.cmd`。全局安装时也可使用 `npm.cmd`,无需修改系统脚本执行策略。
|
|
173
84
|
|
|
174
|
-
|
|
175
|
-
2. **云端扩展与可靠性**:现有账号、设备凭证、隔离路由和撤销继续补充限额、连接租约、审计保留与多节点调度。
|
|
176
|
-
3. **安全与可靠性**:考虑端到端加密、最小权限、审计脱敏、密钥轮换、速率限制、持久化幂等、多设备控制权、故障恢复和安全评审。当前预览不具备 E2EE。
|
|
177
|
-
4. **兼容与分发**:固定支持版本、协议能力协商、升级回归、桌面 agent 安装 / 更新 / 卸载、签名与跨平台测试。
|
|
178
|
-
5. **产品与合规**:组织管理、套餐支付和推送;核对依赖许可证和模型服务条款,明确独立品牌与第三方关系。当前不打包转售 Codex 可执行文件。
|
|
85
|
+
**电脑显示离线**
|
|
179
86
|
|
|
180
|
-
|
|
87
|
+
先确认电脑开机并联网,运行 `orbit status` 查看状态。短暂断网会自动恢复;电脑重启后运行 `orbit start`。仍有问题时,按状态命令显示的路径查看 `connector.log`,或使用 `orbit login --foreground` 诊断。
|
|
181
88
|
|
|
182
|
-
|
|
89
|
+
**能看历史,但会话尚未连接**
|
|
183
90
|
|
|
184
|
-
|
|
91
|
+
在 Codex 桌面打开同一条会话,再点击网页中的“重新连接”。桌面热接管依赖 Codex 内部接口,桌面版本更新后可能需要更新 Orbit。
|
|
185
92
|
|
|
186
|
-
|
|
93
|
+
**配对二维码与密钥**
|
|
187
94
|
|
|
188
|
-
|
|
189
|
-
- <https://developers.openai.com/codex/app-server/>
|
|
190
|
-
- <https://github.com/openai/codex/blob/main/codex-rs/app-server-daemon/README.md>
|
|
95
|
+
只在自己的设备上使用。每台电脑使用独立连接凭证,可在工作台移除电脑,或运行 `orbit logout` 撤销授权。连接中断时,Orbit 不会自动重发已提交的消息和审批。
|
package/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
import QRCode from 'qrcode';
|
|
10
10
|
import { createOrbitServer } from './server.mjs';
|
|
11
11
|
import { createRelayAgent } from './relay-agent.mjs';
|
|
12
|
+
import {connectorStatus,installConnectorRuntime,startConnector,stopConnector,superviseConnector} from './lib/connector-service.mjs';
|
|
12
13
|
|
|
13
14
|
const defaultServerUrl = 'https://orbit.cubesugar.tech';
|
|
14
15
|
const packageDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -136,42 +137,101 @@ async function authorizeComputer(serverUrl, name) {
|
|
|
136
137
|
return config;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
async function
|
|
140
|
+
export async function existingOrbitUrl(port) {
|
|
141
|
+
const url=`http://127.0.0.1:${port}`;
|
|
142
|
+
try {
|
|
143
|
+
const response=await fetch(`${url}/healthz`,{signal:AbortSignal.timeout(1500),cache:'no-store'});
|
|
144
|
+
if(!response.ok)return null;
|
|
145
|
+
const health=await response.json();
|
|
146
|
+
return health.ok===true?url:null;
|
|
147
|
+
} catch { return null; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function printPairing(pairingKey, publicUrl) {
|
|
151
|
+
const pairingUrl = `${publicUrl}/#pair=${encodeURIComponent(pairingKey)}`;
|
|
152
|
+
console.log('本机配对二维码和密钥仅供自己的手机使用,请勿公开。');
|
|
153
|
+
console.log(await QRCode.toString(pairingUrl,{type:'terminal',errorCorrectionLevel:'L',small:true}));
|
|
154
|
+
console.log(`本机配对密钥:${pairingKey}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runComputer(config, background = false) {
|
|
140
158
|
process.env.ORBIT_HOST = '127.0.0.1';
|
|
141
159
|
process.env.ORBIT_PORT = String(config.port || 4318);
|
|
142
160
|
process.env.ORBIT_PUBLIC_URL = config.publicUrl;
|
|
143
|
-
|
|
161
|
+
let app=null;
|
|
144
162
|
let agent;
|
|
145
|
-
let
|
|
163
|
+
let localUrl=await existingOrbitUrl(config.port || 4318);
|
|
164
|
+
if(localUrl)console.log(`检测到本机 Orbit 已运行,将复用 ${localUrl} 并连接云端。`);
|
|
146
165
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
if(!localUrl){
|
|
167
|
+
app=createOrbitServer({dataDir:configDirectory(),publicUrl:config.publicUrl});
|
|
168
|
+
const info=await app.listen();
|
|
169
|
+
localUrl=info.url;
|
|
170
|
+
console.log(`Orbit 电脑端运行中:${localUrl}`);
|
|
171
|
+
if (!background) await printPairing(app.pairingKey,config.publicUrl);
|
|
172
|
+
}
|
|
149
173
|
agent = createRelayAgent({
|
|
150
174
|
relayUrl:config.relayUrl,
|
|
151
|
-
localUrl
|
|
175
|
+
localUrl,
|
|
152
176
|
deviceId:config.deviceId,
|
|
153
177
|
agentToken:config.agentToken,
|
|
154
178
|
onReady:() => console.log(`电脑“${config.deviceName}”已安全连接 Orbit。`),
|
|
179
|
+
onState:state => {
|
|
180
|
+
if (background && process.connected) process.send({type:'connector-state',state:{...state,workerReady:true}},() => {});
|
|
181
|
+
},
|
|
155
182
|
onError:error => console.error(`Orbit 连接暂时中断:${error.message}`)
|
|
156
183
|
});
|
|
157
184
|
agent.start();
|
|
158
|
-
console.log(
|
|
185
|
+
if(!app)console.log(`复用本机 Orbit:${localUrl}`);
|
|
159
186
|
console.log(`手机工作台:${config.publicUrl}`);
|
|
160
|
-
console.log('
|
|
161
|
-
const pairingUrl = `${config.publicUrl}/#pair=${encodeURIComponent(app.pairingKey)}`;
|
|
162
|
-
console.log('本机配对二维码(含长期本机密钥,仅用于自己的手机;不要发布或转发):');
|
|
163
|
-
console.log(await QRCode.toString(pairingUrl, {type:'terminal', errorCorrectionLevel:'L', small:true}));
|
|
164
|
-
console.log(`本机配对密钥:${app.pairingKey}`);
|
|
187
|
+
if (!background) console.log('前台诊断模式;保持此窗口运行,按 Ctrl+C 停止。');
|
|
165
188
|
await new Promise(resolve => {
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
const stop = () => {
|
|
190
|
+
process.off('SIGINT',stop);process.off('SIGTERM',stop);process.off('message',message);process.off('disconnect',stop);
|
|
191
|
+
resolve();
|
|
192
|
+
};
|
|
193
|
+
const message = packet => { if (packet?.type === 'stop') stop(); };
|
|
194
|
+
process.once('SIGINT',stop);
|
|
195
|
+
process.once('SIGTERM',stop);
|
|
196
|
+
if (background) {process.on('message',message);process.once('disconnect',stop);}
|
|
168
197
|
});
|
|
169
198
|
} finally {
|
|
170
199
|
agent?.stop();
|
|
171
|
-
if (
|
|
200
|
+
if (app) await app.close();
|
|
172
201
|
}
|
|
173
202
|
}
|
|
174
203
|
|
|
204
|
+
async function runBackground(config) {
|
|
205
|
+
console.log('正在启动 Orbit 后台连接器…');
|
|
206
|
+
const runtime = await installConnectorRuntime(packageDirectory,configDirectory());
|
|
207
|
+
const service = await startConnector({home:configDirectory(),...runtime});
|
|
208
|
+
const label = service.status === 'online' ? '已连接云端' : service.status === 'blocked' ? service.reason : '正在自动连接云端';
|
|
209
|
+
console.log(`电脑“${config.deviceName}”后台运行中 · ${label}`);
|
|
210
|
+
console.log(`手机工作台:${config.publicUrl}`);
|
|
211
|
+
console.log('可以关闭此终端。断网自动重连,异常退出自动恢复;电脑休眠期间无法远程使用。');
|
|
212
|
+
console.log('停止:orbit stop;状态:orbit status;前台诊断:orbit login --foreground');
|
|
213
|
+
console.log(`诊断日志:${path.join(configDirectory(),'connector.log')}`);
|
|
214
|
+
try {
|
|
215
|
+
const key = (await fs.readFile(path.join(configDirectory(),'pairing-key'),'utf8')).trim();
|
|
216
|
+
if (key) await printPairing(key,config.publicUrl);
|
|
217
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function revokeComputer(config) {
|
|
221
|
+
const response = await fetch(`${config.publicUrl}/api/cloud/device-auth/revoke`, {
|
|
222
|
+
method:'POST',
|
|
223
|
+
headers:{'Authorization':`Bearer ${config.agentToken}`,'Content-Type':'application/json'},
|
|
224
|
+
body:JSON.stringify({deviceId:config.deviceId}),
|
|
225
|
+
signal:AbortSignal.timeout(15000)
|
|
226
|
+
});
|
|
227
|
+
if (!response.ok && response.status !== 404) {
|
|
228
|
+
const result = await response.json().catch(() => ({}));
|
|
229
|
+
throw new Error(result.error || `无法撤销电脑凭证 (${response.status})。请检查网络后重试。`);
|
|
230
|
+
}
|
|
231
|
+
await stopConnector(configDirectory());
|
|
232
|
+
await fs.unlink(configPath());
|
|
233
|
+
}
|
|
234
|
+
|
|
175
235
|
async function main() {
|
|
176
236
|
const [command = 'login', ...args] = process.argv.slice(2);
|
|
177
237
|
if (command === '--version' || command === '-v') {
|
|
@@ -179,48 +239,68 @@ async function main() {
|
|
|
179
239
|
return;
|
|
180
240
|
}
|
|
181
241
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
182
|
-
console.log('Orbit computer connector\n\nUsage:\n orbit login [--name "My computer"] [--server https://orbit.example.com]\n orbit status\n orbit logout');
|
|
242
|
+
console.log('Orbit computer connector\n\nUsage:\n orbit login [--name "My computer"] [--server https://orbit.example.com] [--foreground]\n orbit start\n orbit stop\n orbit restart\n orbit status\n orbit logout\n orbit rebind');
|
|
183
243
|
return;
|
|
184
244
|
}
|
|
185
245
|
const namedIndex = args.indexOf('--name');
|
|
186
|
-
|
|
246
|
+
let name = namedIndex >= 0 ? args[namedIndex + 1] : os.hostname();
|
|
187
247
|
const serverIndex = args.indexOf('--server');
|
|
188
|
-
|
|
189
|
-
|
|
248
|
+
let serverUrl = normalizedServer(serverIndex >= 0 ? args[serverIndex + 1] : process.env.ORBIT_SERVER_URL);
|
|
249
|
+
let existing = await loadConfig();
|
|
250
|
+
if (command === '__service' || command === '__worker') {
|
|
251
|
+
if (!existing) throw new Error('电脑凭证不存在,请运行 orbit login。');
|
|
252
|
+
if (command === '__worker') {
|
|
253
|
+
if (!process.connected) throw new Error('工作进程只能由后台连接器启动。');
|
|
254
|
+
await runComputer(existing,true);
|
|
255
|
+
} else {
|
|
256
|
+
const version = JSON.parse(await fs.readFile(path.join(packageDirectory,'package.json'),'utf8')).version;
|
|
257
|
+
await superviseConnector({home:configDirectory(),config:existing,cliPath:fileURLToPath(import.meta.url),version});
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
190
261
|
|
|
191
262
|
if (command === 'status') {
|
|
263
|
+
const service = await connectorStatus(configDirectory());
|
|
264
|
+
const labels = {online:'在线',connecting:'连接中',reconnecting:'断线重连中',blocked:'连接被拒绝',starting:'启动中',restarting:'异常恢复中'};
|
|
192
265
|
console.log(existing
|
|
193
|
-
? `电脑:${existing.deviceName}\n工作台:${existing.publicUrl}\n配置文件:${configPath()}`
|
|
266
|
+
? `电脑:${existing.deviceName}\n工作台:${existing.publicUrl}\n后台服务:${service ? labels[service.status] || service.status : '未运行'}${service?.reason ? ` · ${service.reason}` : ''}\n配置文件:${configPath()}\n日志文件:${path.join(configDirectory(),'connector.log')}`
|
|
194
267
|
: '此电脑尚未连接 Orbit。运行 orbit login 完成连接。');
|
|
195
268
|
return;
|
|
196
269
|
}
|
|
270
|
+
if (command === 'stop') {
|
|
271
|
+
console.log(await stopConnector(configDirectory()) ? 'Orbit 后台连接器已停止,授权已保留。' : 'Orbit 后台连接器未运行。');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
197
274
|
if (command === 'logout') {
|
|
198
275
|
if (!existing) {
|
|
199
276
|
console.log('此电脑尚未连接 Orbit。');
|
|
200
277
|
return;
|
|
201
278
|
}
|
|
202
|
-
|
|
203
|
-
method:'POST',
|
|
204
|
-
headers:{'Authorization':`Bearer ${existing.agentToken}`, 'Content-Type':'application/json'},
|
|
205
|
-
body:JSON.stringify({deviceId:existing.deviceId})
|
|
206
|
-
});
|
|
207
|
-
if (!response.ok && response.status !== 404) {
|
|
208
|
-
const result = await response.json().catch(() => ({}));
|
|
209
|
-
throw new Error(result.error || `无法撤销电脑凭证 (${response.status})。请检查网络后重试。`);
|
|
210
|
-
}
|
|
211
|
-
await fs.unlink(configPath());
|
|
279
|
+
await revokeComputer(existing);
|
|
212
280
|
console.log('电脑凭证已从服务器撤销,并从本机删除。');
|
|
213
281
|
return;
|
|
214
282
|
}
|
|
215
|
-
if (
|
|
283
|
+
if (!['login','start','restart','rebind'].includes(command)) throw new Error(`未知命令:${command}。运行 orbit --help 查看用法。`);
|
|
284
|
+
if (command === 'rebind' && existing) {
|
|
285
|
+
if (namedIndex < 0) name = existing.deviceName;
|
|
286
|
+
if (serverIndex < 0) serverUrl = normalizedServer(existing.publicUrl);
|
|
287
|
+
await revokeComputer(existing);
|
|
288
|
+
existing = null;
|
|
289
|
+
console.log('旧电脑授权已移除,正在重新绑定。');
|
|
290
|
+
}
|
|
291
|
+
if (command === 'restart') await stopConnector(configDirectory());
|
|
216
292
|
if (existing) {
|
|
217
|
-
|
|
293
|
+
if (args.includes('--foreground')) {
|
|
294
|
+
await stopConnector(configDirectory());
|
|
295
|
+
await runComputer(existing);
|
|
296
|
+
} else await runBackground(existing);
|
|
218
297
|
return;
|
|
219
298
|
}
|
|
220
299
|
console.log('正在为这台电脑申请 Orbit 连接授权…');
|
|
221
300
|
const config = await authorizeComputer(serverUrl, name);
|
|
222
301
|
console.log(`已保存电脑“${config.deviceName}”的专属凭证到 ${configPath()}。`);
|
|
223
|
-
await runComputer(config);
|
|
302
|
+
if (args.includes('--foreground')) await runComputer(config);
|
|
303
|
+
else await runBackground(config);
|
|
224
304
|
}
|
|
225
305
|
|
|
226
306
|
export async function runCli() {
|