@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 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 deviceId = input.deviceId?.trim() || process.env.TERMPILOT_DEVICE_ID || DEFAULT_DEVICE_ID;
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(hostname) {
653
- 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);
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
- return value?.trim() || process.env.TERMPILOT_DEVICE_ID || DEFAULT_DEVICE_ID;
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 isLocalRelayHost2(hostname) {
909
- 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);
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
- return resolved;
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: process.env.TERMPILOT_CLIENT_TOKEN ?? DEFAULT_CLIENT_TOKEN,
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.4",
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
  },