@fengye404/termpilot 0.1.5 → 0.1.7
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 +148 -40
- package/dist/cli.js +421 -59
- package/docs/README.md +1 -0
- package/docs/architecture.md +10 -7
- package/docs/operations-guide.md +445 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
TermPilot 是一个终端优先的远程控制工具。电脑上跑 `tmux` 会话,手机直接打开 relay 域名查看和控制同一批会话。
|
|
4
4
|
|
|
5
|
+
如果你已经准备长期部署,直接看完整运维文档:
|
|
6
|
+
|
|
7
|
+
- [部署与运维指南](/Users/fengye/workspace/TermPilot/docs/operations-guide.md)
|
|
8
|
+
|
|
5
9
|
## 产品形态
|
|
6
10
|
|
|
7
11
|
- 一个 npm 包:`@fengye404/termpilot`
|
|
@@ -10,7 +14,112 @@ TermPilot 是一个终端优先的远程控制工具。电脑上跑 `tmux` 会
|
|
|
10
14
|
- 手机端不安装,直接打开 relay 域名
|
|
11
15
|
- relay 同时负责消息中继和网页托管
|
|
12
16
|
|
|
13
|
-
##
|
|
17
|
+
## 5 分钟快速上手
|
|
18
|
+
|
|
19
|
+
### 1. 启动 relay
|
|
20
|
+
|
|
21
|
+
在云服务器或一台能被手机访问到的机器上执行:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @fengye404/termpilot
|
|
25
|
+
termpilot relay
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
默认情况下,`termpilot relay` 会直接在后台启动 relay,不占当前窗口。
|
|
29
|
+
|
|
30
|
+
常用 relay 管理命令:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
termpilot relay
|
|
34
|
+
termpilot relay stop
|
|
35
|
+
termpilot relay run
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `termpilot relay` 或 `termpilot relay start`:后台启动
|
|
39
|
+
- `termpilot relay stop`:停止后台 relay
|
|
40
|
+
- `termpilot relay run`:前台运行,适合看日志和排查问题
|
|
41
|
+
|
|
42
|
+
如果你只是先本地体验,也可以直接在自己电脑上跑 relay,然后让手机走局域网访问。
|
|
43
|
+
|
|
44
|
+
### 2. 启动电脑 agent
|
|
45
|
+
|
|
46
|
+
在你的电脑上执行:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g @fengye404/termpilot
|
|
50
|
+
termpilot agent
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
如果这是第一次运行,`termpilot agent` 会直接在终端里引导你:
|
|
54
|
+
|
|
55
|
+
1. 输入 relay 域名或 IP
|
|
56
|
+
2. 输入端口,直接回车默认 `8787`
|
|
57
|
+
3. 自动保存本机配置
|
|
58
|
+
4. 后台启动 agent
|
|
59
|
+
5. 输出一次性配对码
|
|
60
|
+
|
|
61
|
+
以后日常只需要继续执行:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
termpilot agent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
这条命令会根据当前状态自动处理:
|
|
68
|
+
|
|
69
|
+
- 没有后台 agent:按本机已保存配置启动
|
|
70
|
+
- 已经有后台 agent:直接显示当前状态
|
|
71
|
+
- 想重新给手机配对:执行 `termpilot agent --pair`
|
|
72
|
+
|
|
73
|
+
常用管理命令:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
termpilot agent status
|
|
77
|
+
termpilot agent stop
|
|
78
|
+
termpilot agent --pair
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. 手机完成配对
|
|
82
|
+
|
|
83
|
+
手机浏览器直接打开 relay 域名:
|
|
84
|
+
|
|
85
|
+
- `http://your-domain.com:8787`
|
|
86
|
+
- 或反代后的 `https://your-domain.com`
|
|
87
|
+
|
|
88
|
+
然后:
|
|
89
|
+
|
|
90
|
+
1. 输入电脑端刚打印出来的配对码
|
|
91
|
+
2. 点“配对”
|
|
92
|
+
3. 成功后直接进入会话列表
|
|
93
|
+
|
|
94
|
+
### 4. 直接跑一个可同步的任务
|
|
95
|
+
|
|
96
|
+
日常最短路径是:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
termpilot claude code
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
或者:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
termpilot open code
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
这会直接:
|
|
109
|
+
|
|
110
|
+
- 创建一个受 TermPilot 管理的 `tmux` 会话
|
|
111
|
+
- 把命令写进这个会话
|
|
112
|
+
- 当前终端自动 attach 进去
|
|
113
|
+
- 手机端同步看到同一个会话
|
|
114
|
+
|
|
115
|
+
### 5. 你现在应该能做到什么
|
|
116
|
+
|
|
117
|
+
此时你可以:
|
|
118
|
+
|
|
119
|
+
- 在电脑上看 `claude code` / `open code` 的流式输出
|
|
120
|
+
- 在手机上看同一份输出
|
|
121
|
+
- 在手机上补一条命令、发快捷键、关闭会话
|
|
122
|
+
- 随时在电脑和手机之间切换
|
|
14
123
|
|
|
15
124
|
### 服务器
|
|
16
125
|
|
|
@@ -33,7 +142,9 @@ termpilot relay
|
|
|
33
142
|
常用参数:
|
|
34
143
|
|
|
35
144
|
```bash
|
|
36
|
-
termpilot relay
|
|
145
|
+
termpilot relay
|
|
146
|
+
termpilot relay run
|
|
147
|
+
termpilot relay stop
|
|
37
148
|
DATABASE_URL=postgresql://user:pass@127.0.0.1:5432/termpilot termpilot relay
|
|
38
149
|
```
|
|
39
150
|
|
|
@@ -41,19 +152,13 @@ DATABASE_URL=postgresql://user:pass@127.0.0.1:5432/termpilot termpilot relay
|
|
|
41
152
|
|
|
42
153
|
```bash
|
|
43
154
|
npm install -g @fengye404/termpilot
|
|
44
|
-
termpilot agent
|
|
155
|
+
termpilot agent
|
|
45
156
|
```
|
|
46
157
|
|
|
47
|
-
这条命令现在会:
|
|
48
|
-
|
|
49
|
-
- 在后台启动 agent
|
|
50
|
-
- 判断这台电脑是否已经有本地 agent 在运行
|
|
51
|
-
- 直接输出一次性配对码
|
|
52
|
-
|
|
53
158
|
如果你只是想看调试日志,可以显式前台运行:
|
|
54
159
|
|
|
55
160
|
```bash
|
|
56
|
-
termpilot agent --
|
|
161
|
+
termpilot agent --foreground
|
|
57
162
|
```
|
|
58
163
|
|
|
59
164
|
查看后台状态:
|
|
@@ -71,7 +176,7 @@ termpilot agent stop
|
|
|
71
176
|
本地测试:
|
|
72
177
|
|
|
73
178
|
```bash
|
|
74
|
-
termpilot agent
|
|
179
|
+
termpilot agent
|
|
75
180
|
```
|
|
76
181
|
|
|
77
182
|
### 手机
|
|
@@ -80,7 +185,7 @@ termpilot agent --relay ws://127.0.0.1:8787/ws
|
|
|
80
185
|
|
|
81
186
|
- `https://your-domain.com`
|
|
82
187
|
|
|
83
|
-
首次使用时,直接执行上面的 `termpilot agent
|
|
188
|
+
首次使用时,直接执行上面的 `termpilot agent` 就会进入配置引导并拿到配对码;如果你已经跑着后台 agent、只是想重新给手机配对,用 `termpilot agent --pair`。
|
|
84
189
|
|
|
85
190
|
配对成功后:
|
|
86
191
|
|
|
@@ -89,29 +194,24 @@ termpilot agent --relay ws://127.0.0.1:8787/ws
|
|
|
89
194
|
- 点进一个会话后才进入终端详情页
|
|
90
195
|
- 连接信息和设备设置都在页面底部折叠区
|
|
91
196
|
|
|
92
|
-
##
|
|
93
|
-
|
|
94
|
-
电脑上直接启动后台 agent:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
termpilot agent --relay ws://your-domain.com/ws
|
|
98
|
-
```
|
|
197
|
+
## 日常使用
|
|
99
198
|
|
|
100
|
-
|
|
199
|
+
### 直接把命令交给 TermPilot
|
|
101
200
|
|
|
102
201
|
```bash
|
|
202
|
+
termpilot agent
|
|
103
203
|
termpilot claude code
|
|
204
|
+
termpilot open code
|
|
104
205
|
```
|
|
105
206
|
|
|
106
|
-
|
|
207
|
+
如果你想跑别的命令,也可以直接:
|
|
107
208
|
|
|
108
209
|
```bash
|
|
109
|
-
termpilot
|
|
210
|
+
termpilot npm run dev
|
|
211
|
+
termpilot python worker.py
|
|
110
212
|
```
|
|
111
213
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## 日常使用
|
|
214
|
+
### 手动管理会话
|
|
115
215
|
|
|
116
216
|
创建会话并进入:
|
|
117
217
|
|
|
@@ -121,13 +221,7 @@ termpilot list
|
|
|
121
221
|
termpilot attach --sid <sid>
|
|
122
222
|
```
|
|
123
223
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
termpilot claude code
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
在会话里运行:
|
|
224
|
+
进入会话以后,你仍然可以自己手动运行:
|
|
131
225
|
|
|
132
226
|
```bash
|
|
133
227
|
claude code
|
|
@@ -141,10 +235,12 @@ open code
|
|
|
141
235
|
|
|
142
236
|
```bash
|
|
143
237
|
termpilot relay
|
|
144
|
-
termpilot
|
|
238
|
+
termpilot relay stop
|
|
239
|
+
termpilot relay run
|
|
240
|
+
termpilot agent
|
|
241
|
+
termpilot agent --pair
|
|
145
242
|
termpilot agent status
|
|
146
243
|
termpilot agent stop
|
|
147
|
-
termpilot pair
|
|
148
244
|
termpilot create --name claude-main
|
|
149
245
|
termpilot list
|
|
150
246
|
termpilot attach --sid <sid>
|
|
@@ -158,12 +254,24 @@ termpilot doctor
|
|
|
158
254
|
## 最佳实践
|
|
159
255
|
|
|
160
256
|
1. 需要跨端同步的任务,一开始就用 `termpilot create` 创建,不要先在普通终端里跑再想着接管。
|
|
161
|
-
2.
|
|
162
|
-
3.
|
|
163
|
-
4.
|
|
164
|
-
5.
|
|
165
|
-
6.
|
|
166
|
-
7.
|
|
257
|
+
2. 第一次先跑一次 `termpilot agent` 完成本机配置,之后日常就只需要记住这一条命令。
|
|
258
|
+
3. 如果只是想“开一个会话然后立刻跑起来”,优先用 `termpilot claude code` 这类直达命令,不必手动 `create + attach`。
|
|
259
|
+
4. 一个长期任务用一个独立会话,名称直接写任务语义,比如 `claude-main`、`deploy-watch`、`batch-fix`。
|
|
260
|
+
5. 电脑前重操作优先 `termpilot attach`;手机更适合看进度、发短命令、补快捷键和关闭会话。
|
|
261
|
+
6. 普通 iTerm / Terminal 标签页不是 TermPilot 管理对象,不要指望后面“无缝接管”进来。
|
|
262
|
+
7. 手机优先走一次性配对码,不要长期传播访问令牌。
|
|
263
|
+
8. 要长期使用 relay,优先放到 HTTPS/WSS 域名后面,并接 PostgreSQL;本地演示可以先用内存模式。
|
|
264
|
+
9. 换手机或访问权变更时,先 `termpilot grants`,再 `termpilot revoke --token ...`。
|
|
265
|
+
10. 想排查控制历史时先看 `termpilot audit --limit 30`。
|
|
266
|
+
11. 服务器上日常用 `termpilot relay` 后台运行;只有排查问题时才用 `termpilot relay run`。
|
|
267
|
+
|
|
268
|
+
## 常见坑
|
|
269
|
+
|
|
270
|
+
- `termpilot agent` 不会停在前台,这是正常的;它默认就是后台守护进程。
|
|
271
|
+
- `termpilot relay` 默认也不会停在前台;想看日志请用 `termpilot relay run`。
|
|
272
|
+
- 手机上看不到任务时,先确认这个任务是不是通过 `termpilot ...` 或 `termpilot create` 启动的。
|
|
273
|
+
- 首次配对优先用 `termpilot agent` 拿配对码;重新给手机配对时用 `termpilot agent --pair`。
|
|
274
|
+
- 外网正式使用时,不要长期直接裸奔 `ws://IP:8787/ws`,最好上域名和反代。
|
|
167
275
|
|
|
168
276
|
## 本地开发
|
|
169
277
|
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import path3 from "path";
|
|
5
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
-
|
|
7
3
|
// agent/src/cli.ts
|
|
8
4
|
import { spawn as spawn2 } from "child_process";
|
|
9
5
|
import { openSync as openSync2 } from "fs";
|
|
10
6
|
import { cwd as processCwd2 } from "process";
|
|
7
|
+
import { createInterface } from "readline/promises";
|
|
11
8
|
import { setTimeout as delay2 } from "timers/promises";
|
|
12
9
|
|
|
13
10
|
// agent/src/daemon.ts
|
|
@@ -53,6 +50,9 @@ function getAgentRuntimeFilePath() {
|
|
|
53
50
|
function getAgentLogFilePath() {
|
|
54
51
|
return path.join(getAgentHome(), "agent.log");
|
|
55
52
|
}
|
|
53
|
+
function getAgentConfigFilePath() {
|
|
54
|
+
return path.join(getAgentHome(), "config.json");
|
|
55
|
+
}
|
|
56
56
|
function getStateLockPath() {
|
|
57
57
|
return `${getStateFilePath()}.lock`;
|
|
58
58
|
}
|
|
@@ -175,6 +175,29 @@ function clearAgentRuntime(expectedPid) {
|
|
|
175
175
|
}
|
|
176
176
|
rmSync(getAgentRuntimeFilePath(), { force: true });
|
|
177
177
|
}
|
|
178
|
+
function loadAgentConfig() {
|
|
179
|
+
ensureAgentHome();
|
|
180
|
+
try {
|
|
181
|
+
const raw = readFileSync(getAgentConfigFilePath(), "utf8");
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
if (typeof parsed.relayUrl !== "string" || typeof parsed.deviceId !== "string") {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const relayUrl = parsed.relayUrl.trim();
|
|
187
|
+
const deviceId = parsed.deviceId.trim();
|
|
188
|
+
if (!relayUrl || !deviceId) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return { relayUrl, deviceId };
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function saveAgentConfig(config) {
|
|
197
|
+
ensureAgentHome();
|
|
198
|
+
writeFileSync(getAgentConfigFilePath(), `${JSON.stringify(config, null, 2)}
|
|
199
|
+
`, "utf8");
|
|
200
|
+
}
|
|
178
201
|
|
|
179
202
|
// agent/src/tmux-backend.ts
|
|
180
203
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -716,6 +739,7 @@ function printHelp() {
|
|
|
716
739
|
console.log(`TermPilot agent \u7528\u6CD5\uFF1A
|
|
717
740
|
|
|
718
741
|
termpilot agent
|
|
742
|
+
termpilot agent --pair
|
|
719
743
|
termpilot agent --foreground
|
|
720
744
|
termpilot agent status
|
|
721
745
|
termpilot agent stop
|
|
@@ -813,10 +837,130 @@ async function runDoctor() {
|
|
|
813
837
|
}
|
|
814
838
|
function getDeviceId(argv) {
|
|
815
839
|
const args = parseArgs(argv);
|
|
816
|
-
|
|
840
|
+
const explicitDeviceId = typeof args.deviceId === "string" ? args.deviceId : void 0;
|
|
841
|
+
if (explicitDeviceId) {
|
|
842
|
+
return resolveDeviceId(explicitDeviceId);
|
|
843
|
+
}
|
|
844
|
+
const saved = loadAgentConfig();
|
|
845
|
+
return resolveDeviceId(saved?.deviceId);
|
|
846
|
+
}
|
|
847
|
+
function isLocalRelayHost(hostname) {
|
|
848
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
|
|
849
|
+
}
|
|
850
|
+
function normalizeRelayUrl(rawHost, rawPort) {
|
|
851
|
+
const hostInput = rawHost.trim();
|
|
852
|
+
const portInput = rawPort.trim() || "8787";
|
|
853
|
+
const normalizedPort = Number(portInput);
|
|
854
|
+
if (!Number.isFinite(normalizedPort) || normalizedPort <= 0 || normalizedPort > 65535) {
|
|
855
|
+
throw new Error("\u7AEF\u53E3\u65E0\u6548\uFF0C\u8BF7\u8F93\u5165 1 \u5230 65535 \u4E4B\u95F4\u7684\u6570\u5B57\u3002");
|
|
856
|
+
}
|
|
857
|
+
if (hostInput.includes("://")) {
|
|
858
|
+
const parsed = new URL(hostInput);
|
|
859
|
+
if (parsed.protocol === "http:") {
|
|
860
|
+
parsed.protocol = "ws:";
|
|
861
|
+
} else if (parsed.protocol === "https:") {
|
|
862
|
+
parsed.protocol = "wss:";
|
|
863
|
+
}
|
|
864
|
+
if (!parsed.port) {
|
|
865
|
+
parsed.port = String(normalizedPort);
|
|
866
|
+
}
|
|
867
|
+
if (!parsed.pathname || parsed.pathname === "/") {
|
|
868
|
+
parsed.pathname = "/ws";
|
|
869
|
+
}
|
|
870
|
+
parsed.search = "";
|
|
871
|
+
parsed.hash = "";
|
|
872
|
+
return parsed.toString();
|
|
873
|
+
}
|
|
874
|
+
const protocol = isLocalRelayHost(hostInput) ? "ws:" : "wss:";
|
|
875
|
+
return `${protocol}//${hostInput}:${normalizedPort}/ws`;
|
|
876
|
+
}
|
|
877
|
+
async function promptForAgentConfig(deviceId) {
|
|
878
|
+
const rl = createInterface({
|
|
879
|
+
input: process.stdin,
|
|
880
|
+
output: process.stdout
|
|
881
|
+
});
|
|
882
|
+
try {
|
|
883
|
+
console.log("\u8FD8\u6CA1\u6709\u627E\u5230\u672C\u673A\u7684 relay \u914D\u7F6E\uFF0C\u5148\u505A\u4E00\u6B21\u521D\u59CB\u5316\u3002");
|
|
884
|
+
const host = (await rl.question("\u8BF7\u8F93\u5165 relay \u57DF\u540D\u6216 IP: ")).trim();
|
|
885
|
+
if (!host) {
|
|
886
|
+
throw new Error("\u672A\u8F93\u5165 relay \u57DF\u540D\u6216 IP\uFF0C\u5DF2\u53D6\u6D88\u3002");
|
|
887
|
+
}
|
|
888
|
+
const port = await rl.question("\u8BF7\u8F93\u5165 relay \u7AEF\u53E3\uFF08\u76F4\u63A5\u56DE\u8F66\u9ED8\u8BA4 8787\uFF09: ");
|
|
889
|
+
const relayUrl = normalizeRelayUrl(host, port);
|
|
890
|
+
console.log(`\u5C06\u4F7F\u7528 relay: ${relayUrl}`);
|
|
891
|
+
return { relayUrl, deviceId };
|
|
892
|
+
} finally {
|
|
893
|
+
rl.close();
|
|
894
|
+
}
|
|
817
895
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
896
|
+
function getResolvedConfig(argv) {
|
|
897
|
+
const args = parseArgs(argv);
|
|
898
|
+
const deviceId = getDeviceId(argv);
|
|
899
|
+
const cliRelayUrl = typeof args.relay === "string" ? args.relay.trim() : "";
|
|
900
|
+
if (cliRelayUrl) {
|
|
901
|
+
return {
|
|
902
|
+
source: "cli",
|
|
903
|
+
config: {
|
|
904
|
+
relayUrl: cliRelayUrl,
|
|
905
|
+
deviceId
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const envRelayUrl = process.env.TERMPILOT_RELAY_URL?.trim();
|
|
910
|
+
if (envRelayUrl) {
|
|
911
|
+
return {
|
|
912
|
+
source: "env",
|
|
913
|
+
config: {
|
|
914
|
+
relayUrl: envRelayUrl,
|
|
915
|
+
deviceId
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
const saved = loadAgentConfig();
|
|
920
|
+
if (saved) {
|
|
921
|
+
return {
|
|
922
|
+
source: "saved",
|
|
923
|
+
config: {
|
|
924
|
+
relayUrl: saved.relayUrl,
|
|
925
|
+
deviceId
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
async function ensureConfigured(argv) {
|
|
932
|
+
const resolved = getResolvedConfig(argv);
|
|
933
|
+
if (resolved) {
|
|
934
|
+
return resolved;
|
|
935
|
+
}
|
|
936
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
937
|
+
throw new Error(`\u8FD8\u6CA1\u6709\u914D\u7F6E relay\uFF0C\u8BF7\u5148\u6267\u884C\uFF1Atermpilot agent --relay wss://\u4F60\u7684\u57DF\u540D/ws\uFF0C\u6216\u5728\u4EA4\u4E92\u7EC8\u7AEF\u91CC\u76F4\u63A5\u8FD0\u884C termpilot agent\u3002`);
|
|
938
|
+
}
|
|
939
|
+
const config = await promptForAgentConfig(getDeviceId(argv));
|
|
940
|
+
saveAgentConfig(config);
|
|
941
|
+
return { config, source: "prompt" };
|
|
942
|
+
}
|
|
943
|
+
function applyAgentConfig(config) {
|
|
944
|
+
process.env.TERMPILOT_RELAY_URL = config.relayUrl;
|
|
945
|
+
process.env.TERMPILOT_DEVICE_ID = config.deviceId;
|
|
946
|
+
}
|
|
947
|
+
function printRuntimeStatus(runtime = readRuntimeStatus().runtime) {
|
|
948
|
+
if (!runtime) {
|
|
949
|
+
console.log("\u540E\u53F0 agent \u5F53\u524D\u672A\u8FD0\u884C\u3002");
|
|
950
|
+
console.log(`\u72B6\u6001\u76EE\u5F55: ${getAgentHome()}`);
|
|
951
|
+
console.log(`\u914D\u7F6E\u6587\u4EF6: ${getAgentConfigFilePath()}`);
|
|
952
|
+
console.log(`\u65E5\u5FD7: ${getAgentLogFilePath()}`);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const sessions = loadState().sessions.filter((session) => session.deviceId === runtime.deviceId);
|
|
956
|
+
const runningSessions = sessions.filter((session) => session.status === "running").length;
|
|
957
|
+
console.log("\u540E\u53F0 agent \u6B63\u5728\u8FD0\u884C\u3002");
|
|
958
|
+
console.log(`PID: ${runtime.pid}`);
|
|
959
|
+
console.log(`\u8BBE\u5907: ${runtime.deviceId}`);
|
|
960
|
+
console.log(`relay: ${runtime.relayUrl}`);
|
|
961
|
+
console.log(`\u542F\u52A8\u65F6\u95F4: ${runtime.startedAt}`);
|
|
962
|
+
console.log(`\u65E5\u5FD7: ${getAgentLogFilePath()}`);
|
|
963
|
+
console.log(`\u4F1A\u8BDD: ${runningSessions} \u4E2A\u8FD0\u884C\u4E2D / ${sessions.length} \u4E2A\u603B\u8BA1`);
|
|
820
964
|
}
|
|
821
965
|
function isProcessAlive(pid) {
|
|
822
966
|
try {
|
|
@@ -855,23 +999,37 @@ async function waitForPairingCode(deviceId) {
|
|
|
855
999
|
}
|
|
856
1000
|
async function runStart(argv) {
|
|
857
1001
|
const args = parseArgs(argv);
|
|
1002
|
+
const shouldPair = Boolean(args.pair);
|
|
1003
|
+
const { config, source } = await ensureConfigured(argv);
|
|
1004
|
+
applyAgentConfig(config);
|
|
1005
|
+
if (source === "cli" || source === "prompt") {
|
|
1006
|
+
saveAgentConfig(config);
|
|
1007
|
+
}
|
|
858
1008
|
if (args.foreground) {
|
|
859
1009
|
await runDaemon();
|
|
860
1010
|
return;
|
|
861
1011
|
}
|
|
862
|
-
const deviceId =
|
|
863
|
-
const relayUrl =
|
|
1012
|
+
const deviceId = config.deviceId;
|
|
1013
|
+
const relayUrl = config.relayUrl;
|
|
864
1014
|
const existing = readRuntimeStatus();
|
|
865
1015
|
if (existing.runtime && existing.alive) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1016
|
+
const sameRuntime = existing.runtime.relayUrl === relayUrl && existing.runtime.deviceId === deviceId;
|
|
1017
|
+
if (!sameRuntime) {
|
|
1018
|
+
console.log("\u68C0\u6D4B\u5230\u540E\u53F0 agent \u5DF2\u5728\u8FD0\u884C\uFF0C\u4F46\u914D\u7F6E\u548C\u5F53\u524D\u547D\u4EE4\u4E0D\u4E00\u81F4\uFF0C\u6B63\u5728\u91CD\u542F\u3002");
|
|
1019
|
+
await runStop();
|
|
1020
|
+
} else {
|
|
1021
|
+
printRuntimeStatus(existing.runtime);
|
|
1022
|
+
if (shouldPair) {
|
|
1023
|
+
const pairing = await waitForPairingCode(deviceId);
|
|
1024
|
+
if (pairing) {
|
|
1025
|
+
console.log(`\u914D\u5BF9\u7801: ${pairing.pairingCode}`);
|
|
1026
|
+
console.log(`\u6709\u6548\u671F\u81F3: ${pairing.expiresAt}`);
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
console.log("\u5982\u9700\u91CD\u65B0\u7ED9\u624B\u673A\u914D\u5BF9\uFF0C\u8BF7\u6267\u884C\uFF1Atermpilot agent --pair");
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
873
1032
|
}
|
|
874
|
-
return;
|
|
875
1033
|
}
|
|
876
1034
|
clearAgentRuntime();
|
|
877
1035
|
const logFilePath = getAgentLogFilePath();
|
|
@@ -895,11 +1053,18 @@ async function runStart(argv) {
|
|
|
895
1053
|
console.log(`\u8BBE\u5907: ${deviceId}`);
|
|
896
1054
|
console.log(`relay: ${relayUrl}`);
|
|
897
1055
|
console.log(`\u65E5\u5FD7: ${logFilePath}`);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1056
|
+
if (source === "prompt") {
|
|
1057
|
+
console.log("\u672C\u6B21 relay \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u4EE5\u540E\u76F4\u63A5\u8FD0\u884C termpilot agent \u5373\u53EF\u3002");
|
|
1058
|
+
}
|
|
1059
|
+
if (shouldPair || source !== "saved") {
|
|
1060
|
+
const pairing = await waitForPairingCode(deviceId);
|
|
1061
|
+
if (pairing) {
|
|
1062
|
+
console.log(`\u914D\u5BF9\u7801: ${pairing.pairingCode}`);
|
|
1063
|
+
console.log(`\u6709\u6548\u671F\u81F3: ${pairing.expiresAt}`);
|
|
1064
|
+
console.log("\u624B\u673A\u7AEF\u76F4\u63A5\u6253\u5F00 relay \u9875\u9762\u5E76\u8F93\u5165\u8FD9\u4E2A\u914D\u5BF9\u7801\u5373\u53EF\u3002");
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
console.log("\u5982\u9700\u91CD\u65B0\u7ED9\u624B\u673A\u914D\u5BF9\uFF0C\u8BF7\u6267\u884C\uFF1Atermpilot agent --pair");
|
|
903
1068
|
}
|
|
904
1069
|
}
|
|
905
1070
|
function runStatus() {
|
|
@@ -907,18 +1072,21 @@ function runStatus() {
|
|
|
907
1072
|
if (!runtime || !alive) {
|
|
908
1073
|
console.log("\u540E\u53F0 agent \u5F53\u524D\u672A\u8FD0\u884C\u3002");
|
|
909
1074
|
console.log(`\u72B6\u6001\u76EE\u5F55: ${getAgentHome()}`);
|
|
1075
|
+
console.log(`\u914D\u7F6E\u6587\u4EF6: ${getAgentConfigFilePath()}`);
|
|
910
1076
|
console.log(`\u65E5\u5FD7: ${getAgentLogFilePath()}`);
|
|
1077
|
+
const config2 = loadAgentConfig();
|
|
1078
|
+
if (config2) {
|
|
1079
|
+
console.log(`\u5DF2\u4FDD\u5B58 relay: ${config2.relayUrl}`);
|
|
1080
|
+
console.log(`\u5DF2\u4FDD\u5B58\u8BBE\u5907: ${config2.deviceId}`);
|
|
1081
|
+
}
|
|
911
1082
|
return;
|
|
912
1083
|
}
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
console.log(`\u542F\u52A8\u65F6\u95F4: ${runtime.startedAt}`);
|
|
920
|
-
console.log(`\u65E5\u5FD7: ${getAgentLogFilePath()}`);
|
|
921
|
-
console.log(`\u4F1A\u8BDD: ${runningSessions} \u4E2A\u8FD0\u884C\u4E2D / ${sessions.length} \u4E2A\u603B\u8BA1`);
|
|
1084
|
+
printRuntimeStatus(runtime);
|
|
1085
|
+
const config = loadAgentConfig();
|
|
1086
|
+
if (config && (config.relayUrl !== runtime.relayUrl || config.deviceId !== runtime.deviceId)) {
|
|
1087
|
+
console.log(`\u5DF2\u4FDD\u5B58 relay: ${config.relayUrl}`);
|
|
1088
|
+
console.log(`\u5DF2\u4FDD\u5B58\u8BBE\u5907: ${config.deviceId}`);
|
|
1089
|
+
}
|
|
922
1090
|
}
|
|
923
1091
|
async function runStop() {
|
|
924
1092
|
const { runtime, alive } = readRuntimeStatus();
|
|
@@ -962,8 +1130,10 @@ async function runManagedCommand(argv) {
|
|
|
962
1130
|
}
|
|
963
1131
|
async function runDaemon() {
|
|
964
1132
|
await ensureTmuxAvailable();
|
|
965
|
-
const
|
|
966
|
-
|
|
1133
|
+
const config = await ensureConfigured([]);
|
|
1134
|
+
applyAgentConfig(config.config);
|
|
1135
|
+
const relayUrl = config.config.relayUrl;
|
|
1136
|
+
const deviceId = config.config.deviceId;
|
|
967
1137
|
saveAgentRuntime({
|
|
968
1138
|
pid: process.pid,
|
|
969
1139
|
relayUrl,
|
|
@@ -985,6 +1155,8 @@ async function runDaemon() {
|
|
|
985
1155
|
await daemon.start();
|
|
986
1156
|
}
|
|
987
1157
|
async function runPair(argv) {
|
|
1158
|
+
const config = await ensureConfigured(argv);
|
|
1159
|
+
applyAgentConfig(config.config);
|
|
988
1160
|
const deviceId = getDeviceId(argv);
|
|
989
1161
|
const payload = await createPairingCode(deviceId);
|
|
990
1162
|
console.log(`\u8BBE\u5907: ${payload.deviceId}`);
|
|
@@ -993,6 +1165,8 @@ async function runPair(argv) {
|
|
|
993
1165
|
console.log("\u8BF7\u5728\u624B\u673A\u7AEF\u8F93\u5165\u8FD9\u4E2A\u914D\u5BF9\u7801\uFF0C\u6362\u53D6\u8BBE\u5907\u8BBF\u95EE\u4EE4\u724C\u3002");
|
|
994
1166
|
}
|
|
995
1167
|
async function runGrants(argv) {
|
|
1168
|
+
const config = await ensureConfigured(argv);
|
|
1169
|
+
applyAgentConfig(config.config);
|
|
996
1170
|
const deviceId = getDeviceId(argv);
|
|
997
1171
|
const payload = await listDeviceGrants(deviceId);
|
|
998
1172
|
if (payload.grants.length === 0) {
|
|
@@ -1013,12 +1187,16 @@ async function runRevoke(argv) {
|
|
|
1013
1187
|
if (!accessToken) {
|
|
1014
1188
|
throw new Error("\u8BF7\u901A\u8FC7 --token \u6307\u5B9A\u8981\u64A4\u9500\u7684\u8BBF\u95EE\u4EE4\u724C\u3002");
|
|
1015
1189
|
}
|
|
1190
|
+
const config = await ensureConfigured(argv);
|
|
1191
|
+
applyAgentConfig(config.config);
|
|
1016
1192
|
const deviceId = getDeviceId(argv);
|
|
1017
1193
|
await revokeDeviceGrant(deviceId, accessToken);
|
|
1018
1194
|
console.log(`\u5DF2\u64A4\u9500\u8BBE\u5907 ${deviceId} \u7684\u8BBF\u95EE\u4EE4\u724C ${accessToken}`);
|
|
1019
1195
|
}
|
|
1020
1196
|
async function runAudit(argv) {
|
|
1021
1197
|
const args = parseArgs(argv);
|
|
1198
|
+
const config = await ensureConfigured(argv);
|
|
1199
|
+
applyAgentConfig(config.config);
|
|
1022
1200
|
const deviceId = getDeviceId(argv);
|
|
1023
1201
|
const parsedLimit = typeof args.limit === "string" ? Number(args.limit) : 20;
|
|
1024
1202
|
if (!Number.isFinite(parsedLimit) || parsedLimit <= 0) {
|
|
@@ -1099,9 +1277,75 @@ async function runAgentCli(argv = process.argv.slice(2)) {
|
|
|
1099
1277
|
}
|
|
1100
1278
|
}
|
|
1101
1279
|
|
|
1280
|
+
// relay/src/cli.ts
|
|
1281
|
+
import { spawn as spawn3 } from "child_process";
|
|
1282
|
+
import { openSync as openSync3 } from "fs";
|
|
1283
|
+
import { setTimeout as delay3 } from "timers/promises";
|
|
1284
|
+
|
|
1285
|
+
// relay/src/config.ts
|
|
1286
|
+
function loadConfig() {
|
|
1287
|
+
return {
|
|
1288
|
+
host: process.env.HOST ?? "0.0.0.0",
|
|
1289
|
+
port: Number(process.env.PORT ?? 8787),
|
|
1290
|
+
agentToken: process.env.TERMPILOT_AGENT_TOKEN ?? DEFAULT_AGENT_TOKEN,
|
|
1291
|
+
clientToken: process.env.TERMPILOT_CLIENT_TOKEN ?? DEFAULT_CLIENT_TOKEN,
|
|
1292
|
+
databaseUrl: process.env.DATABASE_URL?.trim() || void 0,
|
|
1293
|
+
pairingTtlMinutes: Number(process.env.TERMPILOT_PAIRING_TTL_MINUTES ?? 10)
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// relay/src/runtime-store.ts
|
|
1298
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1299
|
+
import { homedir as homedir2 } from "os";
|
|
1300
|
+
import path2 from "path";
|
|
1301
|
+
function getRelayHome() {
|
|
1302
|
+
return process.env.TERMPILOT_HOME ?? path2.join(homedir2(), ".termpilot");
|
|
1303
|
+
}
|
|
1304
|
+
function ensureRelayHome() {
|
|
1305
|
+
const dir = getRelayHome();
|
|
1306
|
+
mkdirSync2(dir, { recursive: true });
|
|
1307
|
+
return dir;
|
|
1308
|
+
}
|
|
1309
|
+
function getRelayRuntimeFilePath() {
|
|
1310
|
+
return path2.join(getRelayHome(), "relay-runtime.json");
|
|
1311
|
+
}
|
|
1312
|
+
function getRelayLogFilePath() {
|
|
1313
|
+
return path2.join(getRelayHome(), "relay.log");
|
|
1314
|
+
}
|
|
1315
|
+
function loadRelayRuntime() {
|
|
1316
|
+
ensureRelayHome();
|
|
1317
|
+
try {
|
|
1318
|
+
const raw = readFileSync2(getRelayRuntimeFilePath(), "utf8");
|
|
1319
|
+
const parsed = JSON.parse(raw);
|
|
1320
|
+
if (typeof parsed.pid !== "number" || typeof parsed.host !== "string" || typeof parsed.port !== "number" || typeof parsed.startedAt !== "string") {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
pid: parsed.pid,
|
|
1325
|
+
host: parsed.host,
|
|
1326
|
+
port: parsed.port,
|
|
1327
|
+
startedAt: parsed.startedAt
|
|
1328
|
+
};
|
|
1329
|
+
} catch {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function saveRelayRuntime(runtime) {
|
|
1334
|
+
ensureRelayHome();
|
|
1335
|
+
writeFileSync2(getRelayRuntimeFilePath(), `${JSON.stringify(runtime, null, 2)}
|
|
1336
|
+
`, "utf8");
|
|
1337
|
+
}
|
|
1338
|
+
function clearRelayRuntime(expectedPid) {
|
|
1339
|
+
const current = loadRelayRuntime();
|
|
1340
|
+
if (expectedPid !== void 0 && current?.pid !== expectedPid) {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
rmSync2(getRelayRuntimeFilePath(), { force: true });
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1102
1346
|
// relay/src/server.ts
|
|
1103
1347
|
import { createReadStream, existsSync, statSync as statSync2 } from "fs";
|
|
1104
|
-
import
|
|
1348
|
+
import path3 from "path";
|
|
1105
1349
|
import { fileURLToPath } from "url";
|
|
1106
1350
|
import Fastify from "fastify";
|
|
1107
1351
|
import websocket from "@fastify/websocket";
|
|
@@ -1403,18 +1647,6 @@ var PostgresAuditStore = class {
|
|
|
1403
1647
|
}
|
|
1404
1648
|
};
|
|
1405
1649
|
|
|
1406
|
-
// relay/src/config.ts
|
|
1407
|
-
function loadConfig() {
|
|
1408
|
-
return {
|
|
1409
|
-
host: process.env.HOST ?? "0.0.0.0",
|
|
1410
|
-
port: Number(process.env.PORT ?? 8787),
|
|
1411
|
-
agentToken: process.env.TERMPILOT_AGENT_TOKEN ?? DEFAULT_AGENT_TOKEN,
|
|
1412
|
-
clientToken: process.env.TERMPILOT_CLIENT_TOKEN ?? DEFAULT_CLIENT_TOKEN,
|
|
1413
|
-
databaseUrl: process.env.DATABASE_URL?.trim() || void 0,
|
|
1414
|
-
pairingTtlMinutes: Number(process.env.TERMPILOT_PAIRING_TTL_MINUTES ?? 10)
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
1650
|
// relay/src/session-store.ts
|
|
1419
1651
|
var MemorySessionStore = class {
|
|
1420
1652
|
mode = "memory";
|
|
@@ -1588,24 +1820,24 @@ var STATIC_CONTENT_TYPES = {
|
|
|
1588
1820
|
".webmanifest": "application/manifest+json; charset=utf-8"
|
|
1589
1821
|
};
|
|
1590
1822
|
function getMimeType(filePath) {
|
|
1591
|
-
return STATIC_CONTENT_TYPES[
|
|
1823
|
+
return STATIC_CONTENT_TYPES[path3.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
1592
1824
|
}
|
|
1593
1825
|
function createStaticPath(webDir, urlPath) {
|
|
1594
1826
|
const requestPath = decodeURIComponent(urlPath.split("?")[0] ?? "/");
|
|
1595
1827
|
const relativePath = requestPath === "/" ? "index.html" : requestPath.replace(/^\/+/, "");
|
|
1596
|
-
const resolvedPath =
|
|
1597
|
-
if (!resolvedPath.startsWith(
|
|
1598
|
-
return
|
|
1828
|
+
const resolvedPath = path3.resolve(webDir, relativePath);
|
|
1829
|
+
if (!resolvedPath.startsWith(path3.resolve(webDir))) {
|
|
1830
|
+
return path3.join(webDir, "index.html");
|
|
1599
1831
|
}
|
|
1600
1832
|
if (!existsSync(resolvedPath)) {
|
|
1601
|
-
return
|
|
1833
|
+
return path3.join(webDir, "index.html");
|
|
1602
1834
|
}
|
|
1603
1835
|
try {
|
|
1604
1836
|
if (statSync2(resolvedPath).isDirectory()) {
|
|
1605
|
-
return
|
|
1837
|
+
return path3.join(webDir, "index.html");
|
|
1606
1838
|
}
|
|
1607
1839
|
} catch {
|
|
1608
|
-
return
|
|
1840
|
+
return path3.join(webDir, "index.html");
|
|
1609
1841
|
}
|
|
1610
1842
|
return resolvedPath;
|
|
1611
1843
|
}
|
|
@@ -2080,6 +2312,129 @@ async function startRelayServer(options = {}) {
|
|
|
2080
2312
|
return app;
|
|
2081
2313
|
}
|
|
2082
2314
|
|
|
2315
|
+
// relay/src/cli.ts
|
|
2316
|
+
function isProcessAlive2(pid) {
|
|
2317
|
+
try {
|
|
2318
|
+
process.kill(pid, 0);
|
|
2319
|
+
return true;
|
|
2320
|
+
} catch {
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
function readRuntimeStatus2() {
|
|
2325
|
+
const runtime = loadRelayRuntime();
|
|
2326
|
+
if (!runtime) {
|
|
2327
|
+
return { runtime: null, alive: false };
|
|
2328
|
+
}
|
|
2329
|
+
const alive = isProcessAlive2(runtime.pid);
|
|
2330
|
+
if (!alive) {
|
|
2331
|
+
clearRelayRuntime(runtime.pid);
|
|
2332
|
+
return { runtime: null, alive: false };
|
|
2333
|
+
}
|
|
2334
|
+
return { runtime, alive };
|
|
2335
|
+
}
|
|
2336
|
+
function printRuntime(runtime = readRuntimeStatus2().runtime) {
|
|
2337
|
+
if (!runtime) {
|
|
2338
|
+
console.log("\u540E\u53F0 relay \u5F53\u524D\u672A\u8FD0\u884C\u3002");
|
|
2339
|
+
console.log(`\u8FD0\u884C\u65F6\u6587\u4EF6: ${getRelayRuntimeFilePath()}`);
|
|
2340
|
+
console.log(`\u65E5\u5FD7: ${getRelayLogFilePath()}`);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
console.log("\u540E\u53F0 relay \u6B63\u5728\u8FD0\u884C\u3002");
|
|
2344
|
+
console.log(`PID: ${runtime.pid}`);
|
|
2345
|
+
console.log(`\u76D1\u542C: http://${runtime.host}:${runtime.port}`);
|
|
2346
|
+
console.log(`\u542F\u52A8\u65F6\u95F4: ${runtime.startedAt}`);
|
|
2347
|
+
console.log(`\u65E5\u5FD7: ${getRelayLogFilePath()}`);
|
|
2348
|
+
}
|
|
2349
|
+
async function runForeground() {
|
|
2350
|
+
const config = loadConfig();
|
|
2351
|
+
saveRelayRuntime({
|
|
2352
|
+
pid: process.pid,
|
|
2353
|
+
host: config.host,
|
|
2354
|
+
port: config.port,
|
|
2355
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2356
|
+
});
|
|
2357
|
+
process.on("exit", () => {
|
|
2358
|
+
clearRelayRuntime(process.pid);
|
|
2359
|
+
});
|
|
2360
|
+
await startRelayServer({ webDir: resolveDefaultWebDir(import.meta.url), config });
|
|
2361
|
+
await new Promise(() => {
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
async function runStart2() {
|
|
2365
|
+
const config = loadConfig();
|
|
2366
|
+
const existing = readRuntimeStatus2();
|
|
2367
|
+
if (existing.runtime && existing.alive) {
|
|
2368
|
+
const sameConfig = existing.runtime.host === config.host && existing.runtime.port === config.port;
|
|
2369
|
+
if (sameConfig) {
|
|
2370
|
+
printRuntime(existing.runtime);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
console.log("\u68C0\u6D4B\u5230\u540E\u53F0 relay \u5DF2\u5728\u8FD0\u884C\uFF0C\u4F46\u76D1\u542C\u914D\u7F6E\u548C\u5F53\u524D\u547D\u4EE4\u4E0D\u4E00\u81F4\uFF0C\u6B63\u5728\u91CD\u542F\u3002");
|
|
2374
|
+
await runStop2();
|
|
2375
|
+
}
|
|
2376
|
+
clearRelayRuntime();
|
|
2377
|
+
const logFilePath = getRelayLogFilePath();
|
|
2378
|
+
const logFd = openSync3(logFilePath, "a");
|
|
2379
|
+
const child = spawn3(process.execPath, [process.argv[1], "relay-daemon"], {
|
|
2380
|
+
detached: true,
|
|
2381
|
+
stdio: ["ignore", logFd, logFd],
|
|
2382
|
+
env: process.env
|
|
2383
|
+
});
|
|
2384
|
+
child.unref();
|
|
2385
|
+
if (!child.pid) {
|
|
2386
|
+
throw new Error("\u540E\u53F0 relay \u542F\u52A8\u5931\u8D25\uFF0C\u672A\u83B7\u53D6\u5230\u5B50\u8FDB\u7A0B PID\u3002");
|
|
2387
|
+
}
|
|
2388
|
+
saveRelayRuntime({
|
|
2389
|
+
pid: child.pid,
|
|
2390
|
+
host: config.host,
|
|
2391
|
+
port: config.port,
|
|
2392
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2393
|
+
});
|
|
2394
|
+
console.log(`\u540E\u53F0 relay \u5DF2\u542F\u52A8\uFF0CPID: ${child.pid}`);
|
|
2395
|
+
console.log(`\u76D1\u542C: http://${config.host}:${config.port}`);
|
|
2396
|
+
console.log(`\u65E5\u5FD7: ${logFilePath}`);
|
|
2397
|
+
await delay3(300);
|
|
2398
|
+
}
|
|
2399
|
+
async function runStop2() {
|
|
2400
|
+
const { runtime, alive } = readRuntimeStatus2();
|
|
2401
|
+
if (!runtime || !alive) {
|
|
2402
|
+
console.log("\u540E\u53F0 relay \u5F53\u524D\u672A\u8FD0\u884C\u3002");
|
|
2403
|
+
clearRelayRuntime();
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
process.kill(runtime.pid, "SIGTERM");
|
|
2407
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
2408
|
+
if (!isProcessAlive2(runtime.pid)) {
|
|
2409
|
+
clearRelayRuntime(runtime.pid);
|
|
2410
|
+
console.log(`\u540E\u53F0 relay \u5DF2\u505C\u6B62\uFF0CPID: ${runtime.pid}`);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
await delay3(100);
|
|
2414
|
+
}
|
|
2415
|
+
process.kill(runtime.pid, "SIGKILL");
|
|
2416
|
+
clearRelayRuntime(runtime.pid);
|
|
2417
|
+
console.log(`\u540E\u53F0 relay \u5DF2\u5F3A\u5236\u505C\u6B62\uFF0CPID: ${runtime.pid}`);
|
|
2418
|
+
}
|
|
2419
|
+
async function runRelayCli(argv = process.argv.slice(2)) {
|
|
2420
|
+
const [command] = argv;
|
|
2421
|
+
if (!command || command === "start") {
|
|
2422
|
+
await runStart2();
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
switch (command) {
|
|
2426
|
+
case "run":
|
|
2427
|
+
case "daemon":
|
|
2428
|
+
await runForeground();
|
|
2429
|
+
return;
|
|
2430
|
+
case "stop":
|
|
2431
|
+
await runStop2();
|
|
2432
|
+
return;
|
|
2433
|
+
default:
|
|
2434
|
+
throw new Error(`\u672A\u77E5 relay \u5B50\u547D\u4EE4: ${command}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2083
2438
|
// src/cli.ts
|
|
2084
2439
|
var AGENT_ENV_FLAGS = [
|
|
2085
2440
|
{ flag: "--relay", envName: "TERMPILOT_RELAY_URL" },
|
|
@@ -2099,8 +2454,11 @@ var RELAY_ENV_FLAGS = [
|
|
|
2099
2454
|
function printHelp2() {
|
|
2100
2455
|
console.log(`TermPilot \u7528\u6CD5\uFF1A
|
|
2101
2456
|
|
|
2102
|
-
termpilot relay
|
|
2103
|
-
termpilot
|
|
2457
|
+
termpilot relay
|
|
2458
|
+
termpilot relay start
|
|
2459
|
+
termpilot relay stop
|
|
2460
|
+
termpilot relay run
|
|
2461
|
+
termpilot agent [--pair] [--relay ws://127.0.0.1:8787/ws] [--device-id pc-main]
|
|
2104
2462
|
termpilot agent status
|
|
2105
2463
|
termpilot agent stop
|
|
2106
2464
|
termpilot claude code
|
|
@@ -2135,9 +2493,6 @@ function applyEnvFlags(argv, mappings) {
|
|
|
2135
2493
|
}
|
|
2136
2494
|
return rest;
|
|
2137
2495
|
}
|
|
2138
|
-
function resolveBundledWebDir() {
|
|
2139
|
-
return path3.resolve(path3.dirname(fileURLToPath2(import.meta.url)), "../app/dist");
|
|
2140
|
-
}
|
|
2141
2496
|
async function main(argv = process.argv.slice(2)) {
|
|
2142
2497
|
const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
|
|
2143
2498
|
const [command, ...rest] = normalizedArgv;
|
|
@@ -2152,7 +2507,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2152
2507
|
printHelp2();
|
|
2153
2508
|
return;
|
|
2154
2509
|
}
|
|
2155
|
-
|
|
2510
|
+
if (relayArgs[0] === "status") {
|
|
2511
|
+
throw new Error("\u672A\u77E5 relay \u5B50\u547D\u4EE4: status");
|
|
2512
|
+
}
|
|
2513
|
+
await runRelayCli(relayArgs);
|
|
2156
2514
|
return;
|
|
2157
2515
|
}
|
|
2158
2516
|
case "agent": {
|
|
@@ -2168,6 +2526,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2168
2526
|
await runAgentCli(["daemon", ...rest]);
|
|
2169
2527
|
return;
|
|
2170
2528
|
}
|
|
2529
|
+
case "relay-daemon": {
|
|
2530
|
+
await runRelayCli(["daemon", ...rest]);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2171
2533
|
case "pair":
|
|
2172
2534
|
case "create":
|
|
2173
2535
|
case "list":
|
package/docs/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 文档索引
|
|
2
2
|
|
|
3
3
|
- [README.md](/Users/fengye/workspace/TermPilot/README.md):用户安装、启动和日常使用
|
|
4
|
+
- [operations-guide.md](/Users/fengye/workspace/TermPilot/docs/operations-guide.md):部署、反代、运维和排障手册
|
|
4
5
|
- [development.md](/Users/fengye/workspace/TermPilot/docs/development.md):仓库开发、测试和发布流程
|
|
5
6
|
- [architecture.md](/Users/fengye/workspace/TermPilot/docs/architecture.md):当前代码架构和运行时数据流
|
|
6
7
|
- [protocol.md](/Users/fengye/workspace/TermPilot/docs/protocol.md):三端协议和 HTTP 接口
|
package/docs/architecture.md
CHANGED
|
@@ -91,8 +91,10 @@ PC 端常驻进程和本地命令实现:
|
|
|
91
91
|
|
|
92
92
|
1. 服务器执行 `termpilot relay`
|
|
93
93
|
2. relay 监听 HTTP/WebSocket,并托管 `app/dist`
|
|
94
|
-
3.
|
|
95
|
-
4.
|
|
94
|
+
3. 电脑第一次执行 `termpilot agent`,在终端里输入 relay 域名和端口
|
|
95
|
+
4. agent 保存本地配置,并在后台启动常驻进程
|
|
96
|
+
5. 以后电脑直接执行 `termpilot agent`,自动按已保存配置启动或显示状态
|
|
97
|
+
6. 手机上直接打开 relay 域名
|
|
96
98
|
|
|
97
99
|
### 会话创建
|
|
98
100
|
|
|
@@ -112,11 +114,12 @@ PC 端常驻进程和本地命令实现:
|
|
|
112
114
|
|
|
113
115
|
### 配对与访问控制
|
|
114
116
|
|
|
115
|
-
1. 电脑端执行 `termpilot agent
|
|
116
|
-
2. relay
|
|
117
|
-
3.
|
|
118
|
-
4.
|
|
119
|
-
5.
|
|
117
|
+
1. 电脑端执行 `termpilot agent`
|
|
118
|
+
2. 如果本机还没有配置 relay,agent 会提示输入域名和端口,并保存到本地配置文件
|
|
119
|
+
3. relay 创建一次性配对码
|
|
120
|
+
4. 手机端输入配对码,兑换设备访问令牌
|
|
121
|
+
5. client WebSocket 以后携带设备令牌
|
|
122
|
+
6. relay 只向该 client 暴露允许访问的设备和会话
|
|
120
123
|
|
|
121
124
|
## 4. 当前实现边界
|
|
122
125
|
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# TermPilot 部署与运维指南
|
|
2
|
+
|
|
3
|
+
这份文档面向准备长期使用 TermPilot 的用户。它不重复 `README` 里的 5 分钟快速上手,而是把部署、反代、日常运维、排障和安全边界收成一份更完整的运行手册。
|
|
4
|
+
|
|
5
|
+
文档风格参考了成熟开源项目常见的写法:先说明适用场景,再给推荐拓扑、部署步骤、运维动作、排障清单和安全边界。你可以把它当成 TermPilot 的管理员手册来用。
|
|
6
|
+
|
|
7
|
+
## 0. 阅读这份文档前,你应该已经知道什么
|
|
8
|
+
|
|
9
|
+
建议你已经完成过下面这件事中的至少一件:
|
|
10
|
+
|
|
11
|
+
- 在本地把 `termpilot relay`、`termpilot agent` 跑通过一次
|
|
12
|
+
- 已经看过 [README.md](/Users/fengye/workspace/TermPilot/README.md) 里的快速上手
|
|
13
|
+
|
|
14
|
+
如果你还没有跑通过最小链路,请先回到 [README.md](/Users/fengye/workspace/TermPilot/README.md)。
|
|
15
|
+
|
|
16
|
+
## 1. 适用场景
|
|
17
|
+
|
|
18
|
+
适合下面这些情况:
|
|
19
|
+
|
|
20
|
+
- 你已经确认 TermPilot 的基本流程可用,准备长期跑在自己的服务器上
|
|
21
|
+
- 你希望用域名和 HTTPS/WSS 暴露 relay,而不是直接用裸 IP 和端口
|
|
22
|
+
- 你需要给自己或团队整理一份可维护的运行说明
|
|
23
|
+
|
|
24
|
+
不适合下面这些情况:
|
|
25
|
+
|
|
26
|
+
- 第一次体验产品
|
|
27
|
+
- 只想在局域网里临时试一下
|
|
28
|
+
|
|
29
|
+
第一次使用请先看 [README.md](/Users/fengye/workspace/TermPilot/README.md)。
|
|
30
|
+
|
|
31
|
+
## 2. 部署清单
|
|
32
|
+
|
|
33
|
+
开始之前,先确认下面这些前置条件:
|
|
34
|
+
|
|
35
|
+
- 一台能被手机访问到的服务器
|
|
36
|
+
- 一个已经解析到服务器的域名
|
|
37
|
+
- 服务器已经放行 `80` 和 `443`
|
|
38
|
+
- 电脑端已经安装 `tmux`
|
|
39
|
+
- 服务器和电脑都已经安装 `@fengye404/termpilot`
|
|
40
|
+
|
|
41
|
+
推荐你按这个顺序推进:
|
|
42
|
+
|
|
43
|
+
1. 先让服务器上的 `termpilot relay` 跑起来
|
|
44
|
+
2. 再让域名和 HTTPS 反代跑起来
|
|
45
|
+
3. 再在电脑执行 `termpilot agent`
|
|
46
|
+
4. 最后用手机完成第一次配对
|
|
47
|
+
|
|
48
|
+
## 3. 运行模型
|
|
49
|
+
|
|
50
|
+
TermPilot 由三部分组成:
|
|
51
|
+
|
|
52
|
+
- `relay`:运行在云服务器上,负责网页托管、WebSocket 中继、配对、设备权限和会话元数据
|
|
53
|
+
- `agent`:运行在你的电脑上,负责管理本地 `tmux` 会话并连接 relay
|
|
54
|
+
- `app`:手机浏览器直接打开 relay 域名,不需要单独安装 App
|
|
55
|
+
|
|
56
|
+
推荐拓扑:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
手机浏览器 --https/wss--> 域名 / 反向代理 --> relay
|
|
60
|
+
^
|
|
61
|
+
|
|
|
62
|
+
agent --wss--> /ws
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 4. 推荐部署模式
|
|
66
|
+
|
|
67
|
+
### 模式 A:最低成本验证
|
|
68
|
+
|
|
69
|
+
- 服务器上直接运行 `termpilot relay`
|
|
70
|
+
- 对外暴露 `8787`
|
|
71
|
+
- 手机访问 `http://your-ip:8787`
|
|
72
|
+
- 电脑连接 `ws://your-ip:8787/ws`
|
|
73
|
+
|
|
74
|
+
适合:
|
|
75
|
+
|
|
76
|
+
- 自己先试通链路
|
|
77
|
+
- 不想先配置域名和 HTTPS
|
|
78
|
+
|
|
79
|
+
缺点:
|
|
80
|
+
|
|
81
|
+
- 没有 HTTPS/WSS
|
|
82
|
+
- 不适合长期使用
|
|
83
|
+
|
|
84
|
+
### 模式 B:推荐生产模式
|
|
85
|
+
|
|
86
|
+
- 服务器上运行 `termpilot relay`
|
|
87
|
+
- 前面放一个反向代理,例如 Caddy
|
|
88
|
+
- 域名直接指向服务器
|
|
89
|
+
- 手机访问 `https://your-domain.com`
|
|
90
|
+
- 电脑连接 `wss://your-domain.com/ws`
|
|
91
|
+
|
|
92
|
+
适合:
|
|
93
|
+
|
|
94
|
+
- 个人长期使用
|
|
95
|
+
- 多设备跨网络访问
|
|
96
|
+
- 想降低手机端访问阻力
|
|
97
|
+
|
|
98
|
+
## 5. 生产部署步骤
|
|
99
|
+
|
|
100
|
+
### 5.1 域名解析
|
|
101
|
+
|
|
102
|
+
把你的域名 A 记录指向服务器公网 IP,例如:
|
|
103
|
+
|
|
104
|
+
- `fengye404.top -> 你的服务器公网 IP`
|
|
105
|
+
|
|
106
|
+
### 5.2 服务器启动 relay
|
|
107
|
+
|
|
108
|
+
最简单的后台启动:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
termpilot relay
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
常用命令:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
termpilot relay
|
|
118
|
+
termpilot relay stop
|
|
119
|
+
termpilot relay run
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
说明:
|
|
123
|
+
|
|
124
|
+
- `termpilot relay` 或 `termpilot relay start`:后台启动
|
|
125
|
+
- `termpilot relay stop`:停止后台 relay
|
|
126
|
+
- `termpilot relay run`:前台运行,适合看日志
|
|
127
|
+
|
|
128
|
+
默认监听:
|
|
129
|
+
|
|
130
|
+
- `host=0.0.0.0`
|
|
131
|
+
- `port=8787`
|
|
132
|
+
|
|
133
|
+
### 5.3 反向代理
|
|
134
|
+
|
|
135
|
+
推荐用 Caddy。最小配置如下:
|
|
136
|
+
|
|
137
|
+
```caddyfile
|
|
138
|
+
fengye404.top {
|
|
139
|
+
reverse_proxy 127.0.0.1:8787
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
这会同时转发:
|
|
144
|
+
|
|
145
|
+
- 网页请求 `/`
|
|
146
|
+
- WebSocket `/ws`
|
|
147
|
+
|
|
148
|
+
### 5.4 电脑启动 agent
|
|
149
|
+
|
|
150
|
+
第一次:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
termpilot agent
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
然后在终端里输入:
|
|
157
|
+
|
|
158
|
+
1. relay 域名或 IP
|
|
159
|
+
2. 端口,直接回车默认 `8787`
|
|
160
|
+
|
|
161
|
+
TermPilot 会自动:
|
|
162
|
+
|
|
163
|
+
- 保存本地配置
|
|
164
|
+
- 后台启动 agent
|
|
165
|
+
- 输出一次性配对码
|
|
166
|
+
|
|
167
|
+
以后日常:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
termpilot agent
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
如果你只想重新生成一个配对码:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
termpilot agent --pair
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 5.5 首次上线后的验收
|
|
180
|
+
|
|
181
|
+
如果你刚完成一套新部署,建议按下面顺序验收:
|
|
182
|
+
|
|
183
|
+
1. 服务器执行 `termpilot relay`,确认后台已启动
|
|
184
|
+
2. 服务器本机执行 `curl http://127.0.0.1:8787/health`
|
|
185
|
+
3. 手机打开 `https://your-domain.com`
|
|
186
|
+
4. 电脑执行 `termpilot agent`
|
|
187
|
+
5. 确认终端里已经打印出配对码
|
|
188
|
+
6. 手机输入配对码并进入会话列表
|
|
189
|
+
7. 电脑执行 `termpilot claude code`
|
|
190
|
+
8. 确认手机端能看到同一个会话的输出
|
|
191
|
+
|
|
192
|
+
## 6. 目录、数据与状态文件
|
|
193
|
+
|
|
194
|
+
默认状态目录:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
~/.termpilot
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
常见文件:
|
|
201
|
+
|
|
202
|
+
- `config.json`:agent 本地保存的 relay 配置
|
|
203
|
+
- `agent-runtime.json`:后台 agent 运行时状态
|
|
204
|
+
- `relay-runtime.json`:后台 relay 运行时状态
|
|
205
|
+
- `agent.log`:agent 日志
|
|
206
|
+
- `relay.log`:relay 日志
|
|
207
|
+
- `state.json`:本地会话状态
|
|
208
|
+
|
|
209
|
+
如果你想切换状态目录,可以设置:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
TERMPILOT_HOME=/your/path termpilot agent
|
|
213
|
+
TERMPILOT_HOME=/your/path termpilot relay
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 7. 推荐的日常工作流
|
|
217
|
+
|
|
218
|
+
### 7.1 服务器
|
|
219
|
+
|
|
220
|
+
长期保持:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
termpilot relay
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
只有排障时才用:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
termpilot relay run
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 7.2 电脑
|
|
233
|
+
|
|
234
|
+
日常只记住:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
termpilot agent
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
如果你要跑任务,最短路径是:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
termpilot claude code
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
或者:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
termpilot open code
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 7.3 手机
|
|
253
|
+
|
|
254
|
+
长期固定访问:
|
|
255
|
+
|
|
256
|
+
- `https://your-domain.com`
|
|
257
|
+
|
|
258
|
+
第一次用配对码,之后正常重连不应该要求重新配对。
|
|
259
|
+
|
|
260
|
+
## 8. 运维动作速查
|
|
261
|
+
|
|
262
|
+
### 8.1 relay
|
|
263
|
+
|
|
264
|
+
后台启动:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
termpilot relay
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
查看前台日志:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
termpilot relay run
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
停止后台 relay:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
termpilot relay stop
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 8.2 agent
|
|
283
|
+
|
|
284
|
+
按本机配置启动或查看状态:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
termpilot agent
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
重新生成配对码:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
termpilot agent --pair
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
查看后台状态:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
termpilot agent status
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
停止后台 agent:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
termpilot agent stop
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 8.3 会话
|
|
309
|
+
|
|
310
|
+
直接启动常见任务:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
termpilot claude code
|
|
314
|
+
termpilot open code
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
手动管理会话:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
termpilot create --name my-task --cwd /path/to/project
|
|
321
|
+
termpilot list
|
|
322
|
+
termpilot attach --sid <sid>
|
|
323
|
+
termpilot kill --sid <sid>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## 9. 故障排查
|
|
327
|
+
|
|
328
|
+
### 9.1 手机打开域名,但页面进不去
|
|
329
|
+
|
|
330
|
+
优先检查:
|
|
331
|
+
|
|
332
|
+
- DNS 是否已经生效
|
|
333
|
+
- `80` / `443` 是否放行
|
|
334
|
+
- 反向代理是否已启动
|
|
335
|
+
- `termpilot relay` 是否真的在跑
|
|
336
|
+
|
|
337
|
+
服务器检查:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
termpilot relay
|
|
341
|
+
curl http://127.0.0.1:8787/health
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 9.2 电脑端执行 `termpilot agent` 后手机还是看不到设备
|
|
345
|
+
|
|
346
|
+
优先检查:
|
|
347
|
+
|
|
348
|
+
- 电脑是否真的能连到 relay
|
|
349
|
+
- agent 是否已经后台运行
|
|
350
|
+
- 是否第一次配对还没完成
|
|
351
|
+
|
|
352
|
+
电脑检查:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
termpilot agent
|
|
356
|
+
termpilot agent status
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 9.3 手机端看不到某个任务
|
|
360
|
+
|
|
361
|
+
最常见原因:
|
|
362
|
+
|
|
363
|
+
- 这个任务不是通过 TermPilot 管理的会话启动的
|
|
364
|
+
|
|
365
|
+
正确做法:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
termpilot claude code
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
或:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
termpilot create --name my-task --cwd /path/to/project
|
|
375
|
+
termpilot attach --sid <sid>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 9.4 想重新绑定手机
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
termpilot agent --pair
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
如果要撤销旧设备访问令牌:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
termpilot grants
|
|
388
|
+
termpilot revoke --token <accessToken>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 9.5 relay 或 agent 想看实时日志
|
|
392
|
+
|
|
393
|
+
relay:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
termpilot relay run
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
agent:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
termpilot agent --foreground
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### 9.6 我改了域名或端口,电脑还是连旧地址
|
|
406
|
+
|
|
407
|
+
先停掉后台 agent,再重新执行一次交互配置:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
termpilot agent stop
|
|
411
|
+
termpilot agent
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
如果你使用了自定义状态目录,也要确认是不是改到了另一个 `TERMPILOT_HOME`。
|
|
415
|
+
|
|
416
|
+
## 10. 安全建议
|
|
417
|
+
|
|
418
|
+
- 正式环境优先使用域名 + HTTPS/WSS,不要长期裸露 `ws://ip:8787/ws`
|
|
419
|
+
- 不要长期传播手机端访问令牌
|
|
420
|
+
- 换手机或多人共享设备后,及时撤销旧令牌
|
|
421
|
+
- 如果准备长期保存会话元数据,relay 建议接 PostgreSQL
|
|
422
|
+
- 不要把外网入口直接暴露到非预期端口和无 TLS 配置上
|
|
423
|
+
|
|
424
|
+
## 11. 升级建议
|
|
425
|
+
|
|
426
|
+
推荐的升级节奏:
|
|
427
|
+
|
|
428
|
+
1. 先在一台日常不关键的机器上升级验证
|
|
429
|
+
2. 确认 `termpilot relay` 和 `termpilot agent` 都能正常启动
|
|
430
|
+
3. 用手机完成一次真实配对和会话查看
|
|
431
|
+
4. 再升级主力机器
|
|
432
|
+
|
|
433
|
+
如果升级后出现异常,优先检查:
|
|
434
|
+
|
|
435
|
+
- `~/.termpilot/relay.log`
|
|
436
|
+
- `~/.termpilot/agent.log`
|
|
437
|
+
- `curl http://127.0.0.1:8787/health`
|
|
438
|
+
- `termpilot agent status`
|
|
439
|
+
|
|
440
|
+
## 12. 建议的文档阅读顺序
|
|
441
|
+
|
|
442
|
+
1. [README.md](/Users/fengye/workspace/TermPilot/README.md)
|
|
443
|
+
2. [architecture.md](/Users/fengye/workspace/TermPilot/docs/architecture.md)
|
|
444
|
+
3. [protocol.md](/Users/fengye/workspace/TermPilot/docs/protocol.md)
|
|
445
|
+
4. 本文档
|