ysagc-agent 0.1.0 → 0.2.0

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 CHANGED
@@ -6,39 +6,30 @@
6
6
  - 通信:JSON-RPC 2.0 over WebSocket(控制通道)+ 二进制帧(终端通道)
7
7
  - 路径沙箱:只能操作 allowlist(用户显式添加的项目根目录)内路径
8
8
  - 终端:node-pty(Windows: PowerShell / macOS·Linux: 默认 shell),输入输出**不经过网站后端**
9
+ - **免配对码**(v0.2.0):浏览器打开网站「工作台」自动识别并连接本机代理,无需输入配对码、无需安装 exe
9
10
 
10
- ## 安装(两种渠道任选)
11
+ ## 安装(npm 方式,全球通用)
11
12
 
12
- ### 安装包(普通用户)
13
-
14
- 到网站「工作台 → 下载」页,按你的系统下载安装包:
15
-
16
- | 平台 | 形态 |
17
- |---|---|
18
- | Windows 10/11 | `ysagc-agent.exe`(单文件,免装 Node) |
19
- | macOS 12+ | 单文件二进制(未签名,首次需右键「打开」) |
20
- | Linux (Ubuntu 20.04+/Debian 11+) | 单文件二进制(AppImage 敬请期待) |
21
-
22
- Windows 若被杀软拦截:选择「仍要运行」,或在 Windows 安全中心放行。
23
-
24
- ### ② npm(开发者用户)
13
+ 要求 Node.js ≥ 20(Windows / macOS / Linux 均可)。
25
14
 
26
15
  ```bash
27
- # 免安装,直接跑
16
+ # 方式一:免安装,直接跑(推荐,首次会下载依赖包)
28
17
  npx ysagc-agent
29
18
 
30
- # 或全局安装
19
+ # 方式二:全局安装后运行
31
20
  npm i -g ysagc-agent
32
21
  ysagc-agent
33
22
  ```
34
23
 
35
- 要求 Node.js 20。
24
+ 启动后终端会显示 `ysagc-agent 运行中: ws://127.0.0.1:37890`,保持该终端窗口开启即可(最小化没问题,不要关闭)。
36
25
 
37
26
  ## 使用流程
38
27
 
39
- 1. 启动后终端会显示 **6 位配对码**(10 分钟有效)。
40
- 2. 打开网站 → 设置 → 设备管理 → 输入配对码 → 配对成功。
41
- 3. 在网站添加项目(本机文件夹 / 克隆仓库 / 从零创建),即可开始使用工作台。
28
+ 1. 打开终端执行 `npx ysagc-agent`(保持运行)。
29
+ 2. 打开网站「工作台」,点击「新增工作区」→ 选择本机文件夹。
30
+ 3. 在聊天中把话题「绑定工作区」后,AI 就能在对应文件夹里读文件、改代码、跑命令了。
31
+
32
+ > 旧版(0.1.x)安装包渠道与配对码流程已不再需要;如需卸载:`npm rm -g ysagc-agent`。
42
33
 
43
34
  ## 配置(agent.json)
44
35
 
@@ -47,8 +38,9 @@ ysagc-agent
47
38
  ```jsonc
48
39
  {
49
40
  "port": 37890, // 端口被占用自动 +1(最多 5 次)
50
- "allowedRoots": [], // 授权项目根目录(网站添加项目时自动写入)
51
- "deviceTokenWhitelist": [], // 已配对设备令牌(sha256
41
+ "allowedRoots": [], // 授权项目根目录(网站添加工作区时自动写入)
42
+ "deviceTokenWhitelist": [], // 旧版配对令牌(sha256,兼容保留)
43
+ "originWhitelist": [], // 允许直连的浏览器 Origin;留空=自动信任站点与 localhost
52
44
  "trashEnabled": true, // 删除进回收站
53
45
  "trashRetentionDays": 7,
54
46
  "enableTerminal": true, // 终端开关
@@ -64,13 +56,15 @@ ysagc-agent
64
56
 
65
57
  **Q:端口被占用怎么办?** 自动尝试 37891–37894;也可 `ysagc-agent --port <端口>` 指定。
66
58
 
67
- **Q:macOS 提示"无法打开"?** 未签名应用:右键(或 Control+点击)→ 打开;或 系统设置 隐私与安全性 → 仍要打开。
59
+ **Q:网页提示"未检测到本机代理"?** 确认 Agent 已在终端运行(能看到 `运行中: ws://127.0.0.1:37890`);首次 `npx` 会先下载依赖,稍等片刻。
68
60
 
69
- **Q:连不上网站?** 确认 Agent 正在运行、配对码未过期;网站「设备管理」页有一键自检。
61
+ **Q:macOS 提示"无法打开"?** npm 方式无需处理;仅安装包渠道需要右键 → 打开。
70
62
 
71
63
  **Q:日志在哪?** `~/.ysagc-agent/logs/agent.log`。日志**永不记录**密钥文件内容与 Git 凭证。
72
64
 
73
- **Q:Agent 会读取我电脑上的所有文件吗?** 不会。Agent 只能访问你在网站显式添加的项目目录(allowlist),越界操作一律拒绝并记录审计;终端除外(终端等同于你的命令行,可操作整台电脑——首次使用会在网页端弹安全确认)。
65
+ **Q:Agent 会读取我电脑上的所有文件吗?** 不会。Agent 只能访问你在网站显式添加的工作区目录(allowlist),越界操作一律拒绝并记录审计;终端除外(终端等同于你的命令行,可操作整台电脑——首次使用会在网页端弹安全确认)。
66
+
67
+ **Q:其他网站能控制我的 Agent 吗?** 不能。Agent 只信任站点域名与 localhost 来源(Origin 白名单),其他网页发起的连接会被拒绝。
74
68
 
75
69
  ## 开发
76
70
 
@@ -81,7 +75,7 @@ npm run smoke # 冒烟测试(WS 鉴权 / RPC / 终端全链路)
81
75
  node scripts/smoke.js <token> <port> # 自定义参数
82
76
  ```
83
77
 
84
- 打包(单文件):
78
+ 打包(单文件,可选):
85
79
 
86
80
  ```bash
87
81
  npx @yao-pkg/pkg . --target node22-win-x64 --output dist/ysagc-agent.exe
@@ -93,4 +87,4 @@ CI:`.gitlab-ci.yml`(build 产物 + tag 触发发布到 Package Registry / np
93
87
 
94
88
  ## 隐私
95
89
 
96
- Agent 与网站后端的交互仅限:配对码上报、版本握手、审计元数据。**项目文件内容、终端输入输出、Git 凭证一律不经过网站后端**。
90
+ Agent 与网站后端的交互仅限:设备自动注册、版本握手、审计元数据。**项目文件内容、终端输入输出、Git 凭证一律不经过网站后端**。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ysagc-agent",
3
- "version": "0.1.0",
4
- "description": "代码工作台本机代理:替浏览器在本机执行文件/Git/终端操作(JSON-RPC over WebSocket,直连 127.0.0.1",
3
+ "version": "0.2.0",
4
+ "description": "代码工作台本机代理:替浏览器在本机执行文件/Git/终端操作(JSON-RPC over WebSocket,直连 127.0.0.1,免配对码)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "ysagc-agent": "src/index.js"
package/src/config.js CHANGED
@@ -27,8 +27,13 @@ const DEFAULTS = {
27
27
  // 终端权限:ask(每次询问)/ allow(始终允许)/ deny(拒绝)
28
28
  terminalPermission: 'ask',
29
29
  // 协议版本(与后端握手比对)
30
- protocolVersion: 1,
30
+ protocolVersion: 2,
31
31
  agentVersion: null,
32
+ // ---- 免配对码直连(v0.2.0 起)----
33
+ // 允许的浏览器 Origin 列表(精确匹配 origin,如 https://chat.ysagc.top)。
34
+ // 为空时自动信任:backendUrl 对应的 origin + http://localhost / http://127.0.0.1 及其任意端口。
35
+ // 该列表之外的 Origin 发起的 WebSocket / 本地 HTTP 请求将被拒绝(防恶意网页操控本机代理)。
36
+ originWhitelist: [],
32
37
  };
33
38
 
34
39
  function ensureDir() {
package/src/http-fs.js CHANGED
@@ -13,6 +13,8 @@ const path = require('path');
13
13
  const crypto = require('crypto');
14
14
  const url = require('url');
15
15
 
16
+ const { isOriginTrusted } = require('./origin');
17
+
16
18
  const MAX_UPLOAD_SIZE = 512 * 1024 * 1024; // 512MB
17
19
  const MAX_ZIP_FILES = 2000;
18
20
 
@@ -24,7 +26,12 @@ function createHttpFsHandlers(ctxRef) {
24
26
  const sandbox = () => ctxRef().sandbox;
25
27
  const cfg = () => ctxRef().config;
26
28
 
27
- function checkToken(token) {
29
+ function checkToken(token, req) {
30
+ // 免配对码直连(v0.2.0):Origin 受信任的浏览器请求直接放行
31
+ if (req) {
32
+ const origin = req.headers && req.headers.origin;
33
+ if (origin && isOriginTrusted(origin, cfg())) return true;
34
+ }
28
35
  if (!token) return false;
29
36
  return (cfg().deviceTokenWhitelist || []).includes(sha256(token));
30
37
  }
@@ -37,7 +44,7 @@ function createHttpFsHandlers(ctxRef) {
37
44
  /** 上传:分片追加到临时文件,最后一片 rename 落盘 */
38
45
  function handleUpload(req, res, query) {
39
46
  const token = query.token;
40
- if (!checkToken(token)) return sendJson(res, 401, { code: 'E_AUTH_FAILED', msg: '未授权' });
47
+ if (!checkToken(token, req)) return sendJson(res, 401, { code: 'E_AUTH_FAILED', msg: '未授权' });
41
48
  const relPath = String(query.path || '');
42
49
  if (!relPath) return sendJson(res, 400, { code: 'E_VALIDATION', msg: '缺少 path' });
43
50
  let r;
@@ -89,7 +96,7 @@ function createHttpFsHandlers(ctxRef) {
89
96
  /** 单文件下载(流式) */
90
97
  function handleDownload(req, res, query) {
91
98
  const token = query.token;
92
- if (!checkToken(token)) return sendJson(res, 401, { code: 'E_AUTH_FAILED', msg: '未授权' });
99
+ if (!checkToken(token, req)) return sendJson(res, 401, { code: 'E_AUTH_FAILED', msg: '未授权' });
93
100
  const relPath = String(query.path || '');
94
101
  let r;
95
102
  try {
@@ -111,7 +118,7 @@ function createHttpFsHandlers(ctxRef) {
111
118
  /** zip 打包下载(yazl 流式) */
112
119
  function handleZip(req, res, query) {
113
120
  const token = query.token;
114
- if (!checkToken(token)) return sendJson(res, 401, { code: 'E_AUTH_FAILED', msg: '未授权' });
121
+ if (!checkToken(token, req)) return sendJson(res, 401, { code: 'E_AUTH_FAILED', msg: '未授权' });
115
122
  const paths = String(query.paths || '')
116
123
  .split(',')
117
124
  .map((p) => p.trim())
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ const fs = require('fs');
20
20
  const { loadConfig, saveConfig, CONFIG_DIR } = require('./config');
21
21
  const logger = require('./logger');
22
22
  const { register, handleRequest } = require('./rpc');
23
+ const { isOriginTrusted } = require('./origin');
23
24
  const { registerSystemMethods } = require('./methods/system');
24
25
  const { createTerminalMethods } = require('./methods/terminal');
25
26
  const { createProjectMethods } = require('./methods/project');
@@ -27,9 +28,10 @@ const { createFsMethods } = require('./methods/fs');
27
28
  const { createSkillMethods } = require('./methods/skill');
28
29
  const { createGitMethods } = require('./methods/git');
29
30
  const { createScaffoldMethods } = require('./methods/scaffold');
31
+ const { createDialogMethods } = require('./methods/dialog');
30
32
  const { createHttpFsHandlers } = require('./http-fs');
31
33
  const { Sandbox } = require('./sandbox');
32
- const { startPairing, stopPairing } = require('./pairing');
34
+ const { startPairing, stopPairing, agentId } = require('./pairing');
33
35
 
34
36
  const DEFAULT_PORT = 37890;
35
37
  const MAX_PORT_TRIES = 5;
@@ -47,6 +49,26 @@ function getArg(name) {
47
49
  return i >= 0 ? argv[i + 1] : null;
48
50
  }
49
51
 
52
+ const AGENT_VERSION = require('../package.json').version;
53
+ if (argv.includes('--help') || argv.includes('-h')) {
54
+ console.log(`ysagc-agent v${AGENT_VERSION} — 代码工作台本机代理
55
+ 用法: npx ysagc-agent [选项]
56
+
57
+ 选项:
58
+ --port N 指定监听端口(默认 37890,被占用自动 +1)
59
+ --init-token <t> 把指定 token 加入白名单(兼容旧版配对,一般无需使用)
60
+ --help, -h 显示帮助
61
+ --version, -v 显示版本
62
+
63
+ 启动后无需配对码:浏览器打开网站「工作台」即可自动识别本机代理并连接。
64
+ 代理仅监听 127.0.0.1,替浏览器在本机执行文件 / Git / 终端操作。`);
65
+ process.exit(0);
66
+ }
67
+ if (argv.includes('--version') || argv.includes('-v')) {
68
+ console.log(AGENT_VERSION);
69
+ process.exit(0);
70
+ }
71
+
50
72
  // ---------------- 配置 ----------------
51
73
  let config = loadConfig();
52
74
  logger.setLevel(config.logLevel);
@@ -80,6 +102,8 @@ const gitMethods = createGitMethods(() => configRef);
80
102
  gitMethods.register(register);
81
103
  const scaffoldMethods = createScaffoldMethods(() => configRef);
82
104
  scaffoldMethods.register(register);
105
+ const dialogMethods = createDialogMethods();
106
+ dialogMethods.register(register);
83
107
 
84
108
  // ---------------- WS 服务 ----------------
85
109
  const state = {
@@ -136,6 +160,8 @@ function setupConnection(ws, req) {
136
160
  ws.isAlive = true;
137
161
  ws.authed = false;
138
162
  ws.deviceId = null;
163
+ // 免配对码直连:Origin 可信的浏览器连接无需 deviceToken(v0.2.0)
164
+ ws.originTrusted = isOriginTrusted(req.headers.origin, configRef.config);
139
165
  ws.authTimer = setTimeout(() => {
140
166
  if (!ws.authed) closeWith(ws, 4401, 'auth timeout');
141
167
  }, AUTH_TIMEOUT_MS);
@@ -167,9 +193,10 @@ function setupConnection(ws, req) {
167
193
  // ---- 鉴权握手 ----
168
194
  if (msg && msg.type === 'auth') {
169
195
  if (ws.authed) return;
170
- const ok = checkAuthToken(msg.token);
171
- if (!ok) {
172
- logger.warn(`[ws] 鉴权失败:token 不在白名单(remote=${req.socket.remoteAddress})`);
196
+ const tokenOk = checkAuthToken(msg.token);
197
+ const autoOk = ws.originTrusted && (msg.mode === 'auto' || !msg.token);
198
+ if (!tokenOk && !autoOk) {
199
+ logger.warn(`[ws] 鉴权失败:token 不在白名单且 Origin 不受信任(origin=${req.headers.origin || 'none'} remote=${req.socket.remoteAddress})`);
173
200
  closeWith(ws, 4401, 'unauthorized');
174
201
  return;
175
202
  }
@@ -275,20 +302,52 @@ function setupConnection(ws, req) {
275
302
  function tryListen(port, triesLeft) {
276
303
  const httpFs = createHttpFsHandlers(() => configRef);
277
304
  const server = http.createServer((req, res) => {
278
- // 本地 HTTP:上传/下载/zip(T-M1-05)
279
305
  const parsed = url.parse(req.url, true);
280
306
  const p = parsed.pathname || '';
307
+ const origin = req.headers.origin || '';
308
+
309
+ // CORS 预检(浏览器跨源 fetch 到 http://127.0.0.1 需要)
310
+ if (req.method === 'OPTIONS') {
311
+ res.writeHead(204, {
312
+ 'Access-Control-Allow-Origin': origin || '*',
313
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
314
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Offset, X-Total, Authorization',
315
+ 'Access-Control-Max-Age': '86400',
316
+ });
317
+ res.end();
318
+ return;
319
+ }
320
+ const corsHeaders = (h) => {
321
+ h['Access-Control-Allow-Origin'] = origin || '*';
322
+ return h;
323
+ };
281
324
  try {
325
+ // 免配对码直连:浏览器探测本机 Agent 信息(v0.2.0)
326
+ if (req.method === 'GET' && p === '/api/bootstrap') {
327
+ res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
328
+ res.end(JSON.stringify({
329
+ agentId: agentId(),
330
+ agentVersion: require('../package.json').version,
331
+ protocolVersion: configRef.config.protocolVersion || 2,
332
+ port: configRef.config.port,
333
+ platform: `${process.platform} ${process.arch}`,
334
+ nodeVersion: process.version,
335
+ hostname: require('os').hostname(),
336
+ paired: (configRef.config.deviceTokenWhitelist || []).length > 0,
337
+ allowAuto: true,
338
+ }));
339
+ return;
340
+ }
282
341
  if (req.method === 'POST' && p === '/api/fs/upload') return httpFs.handleUpload(req, res, parsed.query);
283
342
  if (req.method === 'GET' && p === '/api/fs/download') return httpFs.handleDownload(req, res, parsed.query);
284
343
  if (req.method === 'GET' && p === '/api/fs/zip') return httpFs.handleZip(req, res, parsed.query);
285
344
  } catch (e) {
286
345
  logger.error(`[http] 处理 ${p} 失败: ${e && e.message}`);
287
- res.writeHead(500, { 'Content-Type': 'application/json' });
346
+ res.writeHead(500, corsHeaders({ 'Content-Type': 'application/json' }));
288
347
  res.end(JSON.stringify({ code: 'E_INTERNAL', msg: e && e.message }));
289
348
  return;
290
349
  }
291
- res.writeHead(404, { 'Content-Type': 'application/json' });
350
+ res.writeHead(404, corsHeaders({ 'Content-Type': 'application/json' }));
292
351
  res.end(JSON.stringify({ error: 'not found' }));
293
352
  });
294
353
 
@@ -343,7 +402,9 @@ function tryListen(port, triesLeft) {
343
402
  return server;
344
403
  }
345
404
 
346
- console.log(`ysagc-agent v${require('../package.json').version} 启动中(配置目录: ${CONFIG_DIR})`);
405
+ console.log(`ysagc-agent v${AGENT_VERSION} 启动中(配置目录: ${CONFIG_DIR},端口: ${config.port})`);
406
+ console.log(`免配对码直连已启用:打开网站「工作台」即可自动识别本机代理(agentId: ${agentId().slice(0, 8)}...)`);
407
+ console.log('提示: 输入 --help 查看完整选项。');
347
408
  tryListen(config.port, MAX_PORT_TRIES);
348
409
 
349
410
  process.on('SIGINT', () => {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * dialog.* RPC 方法:本机原生对话框(T-M3-xx)
3
+ * - dialog.pickFolder {} → { path } 打开系统文件夹选择器,返回绝对路径(取消返回 E_DIALOG_CANCELED)
4
+ * 实现:
5
+ * Windows: PowerShell FolderBrowserDialog(Agent 运行在用户本机,可弹原生窗口)
6
+ * macOS: osascript choose folder
7
+ * Linux: zenity / kdialog(依次尝试)
8
+ * 用途:添加项目时"点击选择文件夹"替代手输绝对路径(需求 4.3.1 方式 A)
9
+ */
10
+ 'use strict';
11
+
12
+ const { execFile } = require('child_process');
13
+ const { RpcError } = require('../rpc');
14
+
15
+ const DIALOG_TIMEOUT_MS = 5 * 60 * 1000; // 用户可能挑很久,5 分钟上限
16
+
17
+ function run(cmd, args, timeoutMs) {
18
+ return new Promise((resolve, reject) => {
19
+ execFile(
20
+ cmd,
21
+ args,
22
+ { timeout: timeoutMs || DIALOG_TIMEOUT_MS, windowsHide: true, maxBuffer: 4 * 1024 * 1024 },
23
+ (err, stdout) => {
24
+ if (err) return reject(err);
25
+ resolve(String(stdout || ''));
26
+ }
27
+ );
28
+ });
29
+ }
30
+
31
+ /** Windows:PowerShell FolderBrowserDialog */
32
+ async function pickFolderWindows() {
33
+ const ps = [
34
+ 'Add-Type -AssemblyName System.Windows.Forms',
35
+ "$f = New-Object System.Windows.Forms.FolderBrowserDialog",
36
+ "$f.Description = '选择要添加到工作台的项目文件夹'",
37
+ '$f.ShowNewFolderButton = $true',
38
+ "if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.SelectedPath }",
39
+ ].join('; ');
40
+ const out = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-STA', '-Command', ps]);
41
+ const p = String(out || '').trim().split(/\r?\n/).filter(Boolean).pop() || '';
42
+ return p;
43
+ }
44
+
45
+ /** macOS:osascript choose folder */
46
+ async function pickFolderMac() {
47
+ const out = await run('osascript', ['-e', 'POSIX path of (choose folder with prompt "选择要添加到工作台的项目文件夹")']);
48
+ return String(out || '').trim();
49
+ }
50
+
51
+ /** Linux:zenity → kdialog 依次尝试 */
52
+ async function pickFolderLinux() {
53
+ for (const [cmd, args] of [
54
+ ['zenity', ['--file-selection', '--directory', '--title=选择要添加到工作台的项目文件夹']],
55
+ ['kdialog', ['--getexistingdirectory', '选择要添加到工作台的项目文件夹']],
56
+ ]) {
57
+ try {
58
+ const out = await run(cmd, args, 30000);
59
+ const p = String(out || '').trim();
60
+ if (p) return p;
61
+ } catch {
62
+ /* 尝试下一种 */
63
+ }
64
+ }
65
+ throw new RpcError('E_DIALOG_UNSUPPORTED', '当前系统未安装文件夹选择工具(zenity/kdialog),请手动输入路径');
66
+ }
67
+
68
+ function createDialogMethods() {
69
+ return {
70
+ register(reg) {
71
+ reg('dialog.pickFolder', async () => {
72
+ let p = '';
73
+ if (process.platform === 'win32') p = await pickFolderWindows();
74
+ else if (process.platform === 'darwin') p = await pickFolderMac();
75
+ else p = await pickFolderLinux();
76
+ p = String(p || '').trim();
77
+ if (!p) throw new RpcError('E_DIALOG_CANCELED', '已取消选择文件夹');
78
+ return { path: p.replace(/\\/g, '/') };
79
+ });
80
+ },
81
+ };
82
+ }
83
+
84
+ module.exports = { createDialogMethods };
package/src/origin.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Origin 信任判断(免配对码直连,v0.2.0)
3
+ * 浏览器 WebSocket / fetch 会带 Origin 头;恶意网页发起的请求 Origin 不在白名单时被拒绝。
4
+ * originWhitelist 为空时自动信任:backendUrl 的 origin + localhost/127.0.0.1 任意端口。
5
+ */
6
+ 'use strict';
7
+
8
+ /**
9
+ * @param {string|null|undefined} origin 请求的 Origin 头
10
+ * @param {{backendUrl?: string, originWhitelist?: string[]}} config 当前配置
11
+ * @returns {boolean}
12
+ */
13
+ function isOriginTrusted(origin, config) {
14
+ if (!origin || typeof origin !== 'string') return false;
15
+ let o;
16
+ try {
17
+ o = new URL(origin).origin;
18
+ } catch {
19
+ return false;
20
+ }
21
+ const whitelist = (config && config.originWhitelist) || [];
22
+ if (Array.isArray(whitelist) && whitelist.length > 0) {
23
+ return whitelist.some((w) => String(w).replace(/\/$/, '') === o);
24
+ }
25
+ // 默认策略:站点 origin + 本机开发源
26
+ const backend = String((config && config.backendUrl) || '').replace(/\/$/, '');
27
+ if (backend) {
28
+ try {
29
+ if (new URL(backend).origin === o) return true;
30
+ } catch {
31
+ /* ignore */
32
+ }
33
+ }
34
+ if (o.startsWith('http://localhost') || o.startsWith('http://127.0.0.1')) return true;
35
+ return false;
36
+ }
37
+
38
+ module.exports = { isOriginTrusted };
package/src/pairing.js CHANGED
@@ -93,7 +93,18 @@ function postJson(url, body) {
93
93
 
94
94
  function rotateCode() {
95
95
  current = { code: generateCode(), agentId: agentId(), createdAt: Date.now() };
96
- console.log(`\n🔑 配对码: ${current.code}(10 分钟内有效,请在网站“设备管理”输入)\n`);
96
+ const W = 46;
97
+ const bar = '═'.repeat(W);
98
+ const pad = (s) => ` ${String(s).padEnd(W - 2)}`;
99
+ console.log('');
100
+ console.log(' ' + bar);
101
+ console.log(pad('代码工作台 · 配对码'));
102
+ console.log(' ' + '─'.repeat(W));
103
+ console.log(pad(`🔑 配对码: ${current.code}`));
104
+ console.log(pad('有效期: 10 分钟 · 单次使用'));
105
+ console.log(pad('请在网站「设备管理」或「首次配置」中输入'));
106
+ console.log(' ' + bar);
107
+ console.log('');
97
108
  logger.info(`[pairing] 生成新配对码 ${current.code}`);
98
109
  report();
99
110
  return current;