@fengye404/termpilot 0.2.4 → 0.2.5
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/.env.example +2 -2
- package/README.md +7 -3
- package/dist/cli.js +95 -15
- package/package.json +2 -1
package/.env.example
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
PORT=8787
|
|
2
2
|
HOST=127.0.0.1
|
|
3
3
|
DATABASE_URL=postgresql://<your-user>@127.0.0.1:5432/termpilot
|
|
4
|
-
TERMPILOT_DEVICE_ID=pc-main
|
|
5
4
|
TERMPILOT_AGENT_TOKEN=demo-agent-token
|
|
6
|
-
TERMPILOT_CLIENT_TOKEN=demo-client-token
|
|
7
5
|
TERMPILOT_PAIRING_TTL_MINUTES=10
|
|
8
6
|
TERMPILOT_RELAY_URL=ws://127.0.0.1:8787/ws
|
|
9
7
|
TERMPILOT_POLL_INTERVAL_MS=500
|
|
8
|
+
# 可选:仅在你明确需要“管理端看所有设备”时才设置
|
|
9
|
+
# TERMPILOT_CLIENT_TOKEN=your-admin-client-token
|
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ termpilot agent
|
|
|
44
44
|
2. 端口,默认 `8787`
|
|
45
45
|
|
|
46
46
|
然后它会自动保存配置、后台启动 agent,并打印一次性配对码。
|
|
47
|
+
第一次配置时还会为这台电脑生成一个唯一设备名,避免多台电脑在同一个 relay 上都挤到 `pc-main`。
|
|
47
48
|
|
|
48
49
|
### 3. 手机完成配对
|
|
49
50
|
|
|
@@ -101,6 +102,8 @@ termpilot kill --sid <sid>
|
|
|
101
102
|
2. `termpilot agent` 适合作为长期后台入口。第一次配置完之后,日常只需要记住这一条命令。
|
|
102
103
|
3. relay 长期使用时,优先挂到 HTTPS/WSS 域名后面;本地演示再用裸 IP 和 `8787`。
|
|
103
104
|
4. 手机更适合看输出、发短命令和轻控制;电脑前的重输入仍然建议在本地终端完成。
|
|
105
|
+
5. 不要手动给多台电脑复用同一个 `deviceId`。新版会自动生成唯一设备名,除非你明确知道自己在做什么,不要覆盖它。
|
|
106
|
+
6. `TERMPILOT_CLIENT_TOKEN` 现在默认不启用。只有你明确需要“管理端查看所有设备”时,才单独配置它。
|
|
104
107
|
|
|
105
108
|
## 本地开发
|
|
106
109
|
|
|
@@ -110,9 +113,8 @@ pnpm build
|
|
|
110
113
|
pnpm docs:dev
|
|
111
114
|
pnpm test:ui-smoke
|
|
112
115
|
pnpm check:stability
|
|
116
|
+
pnpm test:isolation
|
|
113
117
|
```
|
|
114
|
-
10. 想排查控制历史时先看 `termpilot audit --limit 30`。
|
|
115
|
-
11. 服务器上日常用 `termpilot relay` 后台运行;只有排查问题时才用 `termpilot relay run`。
|
|
116
118
|
|
|
117
119
|
## 常见坑
|
|
118
120
|
|
|
@@ -121,8 +123,10 @@ pnpm check:stability
|
|
|
121
123
|
- 手机上看不到任务时,先确认这个任务是不是通过 `termpilot ...` 或 `termpilot create` 启动的。
|
|
122
124
|
- 首次配对优先用 `termpilot agent` 拿配对码;重新给手机配对时用 `termpilot agent --pair`。
|
|
123
125
|
- 外网正式使用时,不要长期直接裸奔 `ws://IP:8787/ws`,最好上域名和反代。
|
|
126
|
+
- 旧版本如果还保留着 `pc-main`,新版 `termpilot agent` 会自动迁移成唯一设备名,并提示你重新配对手机。
|
|
127
|
+
- 想排查控制历史时先看 `termpilot audit --limit 30`。
|
|
124
128
|
|
|
125
|
-
##
|
|
129
|
+
## 更多文档
|
|
126
130
|
|
|
127
131
|
更多实现说明:
|
|
128
132
|
|
package/dist/cli.js
CHANGED
|
@@ -7,10 +7,6 @@ import { cwd as processCwd2 } from "process";
|
|
|
7
7
|
import { createInterface } from "readline/promises";
|
|
8
8
|
import { setTimeout as delay2 } from "timers/promises";
|
|
9
9
|
|
|
10
|
-
// agent/src/daemon.ts
|
|
11
|
-
import { setTimeout as delay } from "timers/promises";
|
|
12
|
-
import WebSocket from "ws";
|
|
13
|
-
|
|
14
10
|
// packages/protocol/src/index.ts
|
|
15
11
|
var DEFAULT_DEVICE_ID = "pc-main";
|
|
16
12
|
var DEFAULT_AGENT_TOKEN = "demo-agent-token";
|
|
@@ -23,10 +19,14 @@ function parseJsonMessage(raw) {
|
|
|
23
19
|
}
|
|
24
20
|
}
|
|
25
21
|
|
|
22
|
+
// agent/src/daemon.ts
|
|
23
|
+
import { setTimeout as delay } from "timers/promises";
|
|
24
|
+
import WebSocket from "ws";
|
|
25
|
+
|
|
26
26
|
// agent/src/state-store.ts
|
|
27
27
|
import { randomUUID } from "crypto";
|
|
28
28
|
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
29
|
-
import { homedir } from "os";
|
|
29
|
+
import { homedir, hostname } from "os";
|
|
30
30
|
import path from "path";
|
|
31
31
|
var INITIAL_STATE = {
|
|
32
32
|
version: 1,
|
|
@@ -53,6 +53,9 @@ function getAgentLogFilePath() {
|
|
|
53
53
|
function getAgentConfigFilePath() {
|
|
54
54
|
return path.join(getAgentHome(), "config.json");
|
|
55
55
|
}
|
|
56
|
+
function getGeneratedDeviceIdFilePath() {
|
|
57
|
+
return path.join(getAgentHome(), "device-id");
|
|
58
|
+
}
|
|
56
59
|
function getStateLockPath() {
|
|
57
60
|
return `${getStateFilePath()}.lock`;
|
|
58
61
|
}
|
|
@@ -119,6 +122,42 @@ function withStateLock(action) {
|
|
|
119
122
|
rmSync(lockPath, { force: true });
|
|
120
123
|
}
|
|
121
124
|
}
|
|
125
|
+
function sanitizeDeviceLabel(value) {
|
|
126
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
127
|
+
}
|
|
128
|
+
function generateDeviceId() {
|
|
129
|
+
const base = sanitizeDeviceLabel(hostname().split(".")[0] ?? "") || "pc";
|
|
130
|
+
return `${base}-${randomUUID().slice(0, 8)}`;
|
|
131
|
+
}
|
|
132
|
+
function getOrCreateGeneratedDeviceId() {
|
|
133
|
+
ensureAgentHome();
|
|
134
|
+
const filePath = getGeneratedDeviceIdFilePath();
|
|
135
|
+
try {
|
|
136
|
+
const existing = readFileSync(filePath, "utf8").trim();
|
|
137
|
+
if (existing && existing !== DEFAULT_DEVICE_ID) {
|
|
138
|
+
return existing;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
const deviceId = generateDeviceId();
|
|
143
|
+
writeFileSync(filePath, `${deviceId}
|
|
144
|
+
`, "utf8");
|
|
145
|
+
return deviceId;
|
|
146
|
+
}
|
|
147
|
+
function rewriteSessionsDeviceId(previousDeviceId, nextDeviceId) {
|
|
148
|
+
return withStateLock((filePath) => {
|
|
149
|
+
const state = loadStateFromDisk(filePath);
|
|
150
|
+
const nextState = {
|
|
151
|
+
version: 1,
|
|
152
|
+
sessions: state.sessions.map((session) => session.deviceId === previousDeviceId ? {
|
|
153
|
+
...session,
|
|
154
|
+
deviceId: nextDeviceId
|
|
155
|
+
} : session)
|
|
156
|
+
};
|
|
157
|
+
saveStateToDisk(filePath, nextState);
|
|
158
|
+
return nextState;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
122
161
|
function upsertSession(session) {
|
|
123
162
|
return withStateLock((filePath) => {
|
|
124
163
|
const state = loadStateFromDisk(filePath);
|
|
@@ -244,7 +283,8 @@ async function createSession(input = {}) {
|
|
|
244
283
|
const name = input.name?.trim() || `session-${sid.slice(0, 6)}`;
|
|
245
284
|
const shell = input.shell?.trim() || process.env.SHELL || "/bin/zsh";
|
|
246
285
|
const workingDirectory = input.cwd?.trim() || processCwd();
|
|
247
|
-
const
|
|
286
|
+
const requestedDeviceId = input.deviceId?.trim() || process.env.TERMPILOT_DEVICE_ID?.trim();
|
|
287
|
+
const deviceId = requestedDeviceId && requestedDeviceId !== DEFAULT_DEVICE_ID ? requestedDeviceId : getOrCreateGeneratedDeviceId();
|
|
248
288
|
const startedAt = now();
|
|
249
289
|
const tmuxSessionName = buildTmuxSessionName(sid, name);
|
|
250
290
|
await runTmux(["new-session", "-d", "-s", tmuxSessionName, "-c", workingDirectory, shell]);
|
|
@@ -649,8 +689,8 @@ function createDaemonFromEnv() {
|
|
|
649
689
|
}
|
|
650
690
|
|
|
651
691
|
// agent/src/relay-admin.ts
|
|
652
|
-
function isLocalRelayHost(
|
|
653
|
-
return
|
|
692
|
+
function isLocalRelayHost(hostname2) {
|
|
693
|
+
return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || /^10\./.test(hostname2) || /^192\.168\./.test(hostname2) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname2);
|
|
654
694
|
}
|
|
655
695
|
function getRelayBaseCandidates(relayUrl = process.env.TERMPILOT_RELAY_URL ?? "ws://127.0.0.1:8787/ws") {
|
|
656
696
|
let url;
|
|
@@ -715,7 +755,11 @@ function getAgentToken() {
|
|
|
715
755
|
return process.env.TERMPILOT_AGENT_TOKEN ?? DEFAULT_AGENT_TOKEN;
|
|
716
756
|
}
|
|
717
757
|
function resolveDeviceId(value) {
|
|
718
|
-
|
|
758
|
+
const normalized = value?.trim() || process.env.TERMPILOT_DEVICE_ID?.trim();
|
|
759
|
+
if (normalized && normalized !== DEFAULT_DEVICE_ID) {
|
|
760
|
+
return normalized;
|
|
761
|
+
}
|
|
762
|
+
return getOrCreateGeneratedDeviceId();
|
|
719
763
|
}
|
|
720
764
|
async function readJsonOrThrow(response, message) {
|
|
721
765
|
if (!response.ok) {
|
|
@@ -905,8 +949,28 @@ function getDeviceId(argv) {
|
|
|
905
949
|
const saved = loadAgentConfig();
|
|
906
950
|
return resolveDeviceId(saved?.deviceId);
|
|
907
951
|
}
|
|
908
|
-
function
|
|
909
|
-
|
|
952
|
+
function maybeMigrateLegacyDeviceId(config, source) {
|
|
953
|
+
if (source === "cli" || source === "env") {
|
|
954
|
+
return { config };
|
|
955
|
+
}
|
|
956
|
+
if (config.deviceId !== DEFAULT_DEVICE_ID) {
|
|
957
|
+
return { config };
|
|
958
|
+
}
|
|
959
|
+
const nextDeviceId = getOrCreateGeneratedDeviceId();
|
|
960
|
+
if (nextDeviceId === config.deviceId) {
|
|
961
|
+
return { config };
|
|
962
|
+
}
|
|
963
|
+
rewriteSessionsDeviceId(config.deviceId, nextDeviceId);
|
|
964
|
+
return {
|
|
965
|
+
config: {
|
|
966
|
+
...config,
|
|
967
|
+
deviceId: nextDeviceId
|
|
968
|
+
},
|
|
969
|
+
migratedFrom: config.deviceId
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function isLocalRelayHost2(hostname2) {
|
|
973
|
+
return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || /^10\./.test(hostname2) || /^192\.168\./.test(hostname2) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname2);
|
|
910
974
|
}
|
|
911
975
|
function normalizeRelayUrl(rawHost, rawPort) {
|
|
912
976
|
const hostInput = rawHost.trim();
|
|
@@ -1004,7 +1068,11 @@ function getResolvedConfig(argv) {
|
|
|
1004
1068
|
async function ensureConfigured(argv) {
|
|
1005
1069
|
const resolved = getResolvedConfig(argv);
|
|
1006
1070
|
if (resolved) {
|
|
1007
|
-
|
|
1071
|
+
const migrated = maybeMigrateLegacyDeviceId(resolved.config, resolved.source);
|
|
1072
|
+
if (migrated.migratedFrom) {
|
|
1073
|
+
saveAgentConfig(migrated.config);
|
|
1074
|
+
}
|
|
1075
|
+
return { config: migrated.config, source: resolved.source };
|
|
1008
1076
|
}
|
|
1009
1077
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1010
1078
|
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`);
|
|
@@ -1104,6 +1172,8 @@ async function runStart(argv) {
|
|
|
1104
1172
|
const resolved = await ensureConfigured(argv);
|
|
1105
1173
|
let { config } = resolved;
|
|
1106
1174
|
const { source } = resolved;
|
|
1175
|
+
const previousConfig = loadAgentConfig();
|
|
1176
|
+
const migratedDeviceId = previousConfig?.deviceId === DEFAULT_DEVICE_ID && config.deviceId !== previousConfig.deviceId ? previousConfig.deviceId : void 0;
|
|
1107
1177
|
const preferredRelayUrl = await resolvePreferredRelayUrl(config.relayUrl);
|
|
1108
1178
|
if (preferredRelayUrl !== config.relayUrl) {
|
|
1109
1179
|
config = {
|
|
@@ -1116,6 +1186,10 @@ async function runStart(argv) {
|
|
|
1116
1186
|
if (source === "cli" || source === "prompt" || source === "saved") {
|
|
1117
1187
|
saveAgentConfig(config);
|
|
1118
1188
|
}
|
|
1189
|
+
if (migratedDeviceId) {
|
|
1190
|
+
console.log(`\u68C0\u6D4B\u5230\u65E7\u7684\u5171\u4EAB\u8BBE\u5907\u540D ${migratedDeviceId}\uFF0C\u5DF2\u81EA\u52A8\u8FC1\u79FB\u4E3A\u552F\u4E00\u8BBE\u5907\u540D ${config.deviceId}\u3002`);
|
|
1191
|
+
console.log("\u8FD9\u662F\u4E00\u6B21\u5B89\u5168\u4FEE\u590D\uFF1A\u5171\u4EAB relay \u4E0A\u4E0D\u540C\u7535\u8111\u4E0D\u80FD\u7EE7\u7EED\u5171\u7528 pc-main\u3002\u8BF7\u91CD\u65B0\u7ED9\u624B\u673A\u914D\u5BF9\u3002");
|
|
1192
|
+
}
|
|
1119
1193
|
if (args.foreground) {
|
|
1120
1194
|
await runDaemon();
|
|
1121
1195
|
return;
|
|
@@ -1407,11 +1481,13 @@ import { setTimeout as delay3 } from "timers/promises";
|
|
|
1407
1481
|
|
|
1408
1482
|
// relay/src/config.ts
|
|
1409
1483
|
function loadConfig() {
|
|
1484
|
+
const rawClientToken = process.env.TERMPILOT_CLIENT_TOKEN?.trim();
|
|
1485
|
+
const clientToken = rawClientToken && rawClientToken !== DEFAULT_CLIENT_TOKEN ? rawClientToken : void 0;
|
|
1410
1486
|
return {
|
|
1411
1487
|
host: process.env.HOST ?? "0.0.0.0",
|
|
1412
1488
|
port: Number(process.env.PORT ?? 8787),
|
|
1413
1489
|
agentToken: process.env.TERMPILOT_AGENT_TOKEN ?? DEFAULT_AGENT_TOKEN,
|
|
1414
|
-
clientToken
|
|
1490
|
+
clientToken,
|
|
1415
1491
|
databaseUrl: process.env.DATABASE_URL?.trim() || void 0,
|
|
1416
1492
|
pairingTtlMinutes: Number(process.env.TERMPILOT_PAIRING_TTL_MINUTES ?? 10)
|
|
1417
1493
|
};
|
|
@@ -1990,6 +2066,9 @@ async function startRelayServer(options = {}) {
|
|
|
1990
2066
|
const sessionCache = /* @__PURE__ */ new Map();
|
|
1991
2067
|
const outputBuffers = /* @__PURE__ */ new Map();
|
|
1992
2068
|
const webDir = options.webDir ?? resolveDefaultWebDir(import.meta.url);
|
|
2069
|
+
if ((process.env.TERMPILOT_CLIENT_TOKEN ?? "").trim() === DEFAULT_CLIENT_TOKEN) {
|
|
2070
|
+
app.log.warn("\u68C0\u6D4B\u5230 TERMPILOT_CLIENT_TOKEN \u4ECD\u4E3A\u9ED8\u8BA4 demo-client-token\u3002\u51FA\u4E8E\u9694\u79BB\u5B89\u5168\u8003\u8651\uFF0Crelay \u5DF2\u81EA\u52A8\u7981\u7528\u5168\u5C40\u5BA2\u6237\u7AEF\u8BBF\u95EE\u4EE4\u724C\u3002");
|
|
2071
|
+
}
|
|
1993
2072
|
const storesPromise = (async () => {
|
|
1994
2073
|
if (!config.databaseUrl) {
|
|
1995
2074
|
app.log.warn("\u672A\u63D0\u4F9B DATABASE_URL\uFF0C\u5F53\u524D relay \u4F7F\u7528\u5185\u5B58\u5B58\u50A8\u4F1A\u8BDD\u5143\u6570\u636E\u3002");
|
|
@@ -2232,7 +2311,8 @@ async function startRelayServer(options = {}) {
|
|
|
2232
2311
|
storeMode: sessionStore.mode,
|
|
2233
2312
|
agentsOnline: agents.size,
|
|
2234
2313
|
clientsOnline: clients.size,
|
|
2235
|
-
webUiReady: existsSync(webDir)
|
|
2314
|
+
webUiReady: existsSync(webDir),
|
|
2315
|
+
adminClientTokenEnabled: Boolean(config.clientToken)
|
|
2236
2316
|
};
|
|
2237
2317
|
});
|
|
2238
2318
|
app.post("/api/pairing-codes", async (request, reply) => {
|
|
@@ -2380,7 +2460,7 @@ async function startRelayServer(options = {}) {
|
|
|
2380
2460
|
if (role === "client") {
|
|
2381
2461
|
void (async () => {
|
|
2382
2462
|
let client = null;
|
|
2383
|
-
if (token === config.clientToken) {
|
|
2463
|
+
if (config.clientToken && token === config.clientToken) {
|
|
2384
2464
|
client = {
|
|
2385
2465
|
socket,
|
|
2386
2466
|
deviceScope: "*"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fengye404/termpilot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@10.31.0",
|
|
6
6
|
"description": "一个基于 tmux 的终端会话跨端查看与控制原型。",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"agent:revoke": "pnpm --filter @termpilot/agent run revoke",
|
|
42
42
|
"agent:doctor": "pnpm --filter @termpilot/agent run doctor",
|
|
43
43
|
"check:stability": "pnpm build && node scripts/check-relay-agent-stability.mjs",
|
|
44
|
+
"test:isolation": "pnpm build && node scripts/check-device-isolation.mjs",
|
|
44
45
|
"test:ui-smoke": "pnpm build && TERMPILOT_APP_URL=http://127.0.0.1:18787 TERMPILOT_RELAY_URL=ws://127.0.0.1:18787/ws python3 /Users/fengye/.agents/skills/webapp-testing/scripts/with_server.py --server \"PORT=18787 node dist/cli.js relay\" --port 18787 -- python3 scripts/ui_smoke.py",
|
|
45
46
|
"prepack": "pnpm build"
|
|
46
47
|
},
|