device-center 0.2.0-beta.5 → 0.2.0-beta.6
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 +20 -5
- package/SECURITY.md +6 -0
- package/bin/device-center.mjs +26 -2
- package/docs/agent-install.md +2 -2
- package/docs/cli.md +97 -0
- package/docs/friend-trial.md +2 -2
- package/docs/npm.md +9 -9
- package/docs/windows-acceptance.md +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/server/cli/commands.mjs +181 -0
- package/server/cloudAccess.mjs +1 -1
- package/server/drivers/browserPairing.mjs +1 -1
- package/server/drivers/cliClient.mjs +87 -0
- package/server/drivers/cliManagement.mjs +119 -0
- package/server/drivers/cloudRegistry.mjs +6 -0
- package/server/index.mjs +10 -2
- package/src/cli-approval.js +49 -0
- package/src/cli-handoff.js +19 -0
- package/src/login.js +2 -0
- package/src/main.js +1 -1
- package/src/messages.js +171 -27
- package/src/onboarding.js +1 -1
- package/src/pairing-handoff.js +6 -2
- package/src/styles.css +1 -0
- package/start.html +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## 快速使用
|
|
10
10
|
|
|
11
|
-
### 简化接入(0.2.0-beta.
|
|
11
|
+
### 简化接入(0.2.0-beta.6)
|
|
12
12
|
|
|
13
13
|
一页指南明确提供 **交给 Agent/手动安装**,目标设备不需要先安装 AI Agent:
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
有浏览器的设备无需搬运配对码,也不需要回 Agent 回复“已批准”。无浏览器服务器可选手动安装:目标终端运行脚本,在另一台电脑登录控制面,填入终端码、核对指纹并批准。全新安装脚本会配置用户级后台服务;已有安装时停止并保留设置。AI Agent/MCP 仅是管理电脑的可选能力。身份私钥留在设备;安装绑定只允许资源回报,远程日志仍需另外授权。[手动步骤与排查](docs/agent-install.md#手动安装目标设备没有-ai-agent)
|
|
23
23
|
|
|
24
|
-
本指南固定 **0.2.0-beta.
|
|
24
|
+
本指南固定 **0.2.0-beta.6**,提供统一 CLI 管理与 Windows 11 预览接入。无需 AI Agent 或 AD 域,使用普通用户 PowerShell、Node.js 24+,按 [Windows 11 验收](docs/windows-acceptance.md)操作;安装前以公开 npm registry 的版本记录为准。Windows 11 后台与真实绑定仍需实机验收。官方控制面已包含登录恢复修复,旧客户端仍可终端填码。
|
|
25
25
|
|
|
26
26
|
希望交给本机 Agent 安装时,按[Agent 安装指南](docs/agent-install.md)选择本机、官方托管或已有自建服务。“交给 Agent 安装”可直接复制完整说明;登录和设备绑定仍由用户核对。朋友试用从[一页指南](https://devices.owenshen.top/start)开始,无需先登录。
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
只用本机,不注册账号:
|
|
37
37
|
|
|
38
38
|
```sh
|
|
39
|
-
npx --yes device-center@0.2.0-beta.
|
|
39
|
+
npx --yes device-center@0.2.0-beta.6 start
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
打开 `http://127.0.0.1:5174`,将 `/mcp` 添加到本机智能体。侧栏“开始使用”按管理电脑 → 智能体 → 设备引导;导入 SSH 后检查一次连通性。没有可用 Agent/SSH 的设备保持未知;资源快照过期显示“待刷新”,不代表密钥过期。列表刷新不主动连接 SSH。
|
|
@@ -44,8 +44,8 @@ npx --yes device-center@0.2.0-beta.5 start
|
|
|
44
44
|
需要在外查看设备、使用日志中转:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
npx --yes device-center@0.2.0-beta.
|
|
48
|
-
npx --yes device-center@0.2.0-beta.
|
|
47
|
+
npx --yes device-center@0.2.0-beta.6 plan
|
|
48
|
+
npx --yes device-center@0.2.0-beta.6 connect --browser --confirm
|
|
49
49
|
# 自建控制面可指定 --cloud https://devices.example.com
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -55,6 +55,21 @@ npx --yes device-center@0.2.0-beta.5 connect --browser --confirm
|
|
|
55
55
|
|
|
56
56
|
## 能做什么
|
|
57
57
|
|
|
58
|
+
### CLI 管理(beta.6 起)
|
|
59
|
+
|
|
60
|
+
设备列表/详情、命名/标签/用途/声明服务、手动采集、连接诊断、SSH 引用管理及 MCP 工具调用,供人和 Agent 共用。这些命令需要 beta.6 或更新版;beta.5 不包含。安装前核对 npm 公开版本,源码验收可使用下方命令。完整示例、错误码与权限说明见 [CLI 手册](docs/cli.md)。
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
node bin/device-center.mjs capabilities --json
|
|
64
|
+
node bin/device-center.mjs devices list --port 5174 --json
|
|
65
|
+
node bin/device-center.mjs doctor --port 5174
|
|
66
|
+
# 云端管理需要本人在浏览器另行批准最长 8 小时的权限
|
|
67
|
+
node bin/device-center.mjs auth login --confirm
|
|
68
|
+
node bin/device-center.mjs devices list --cloud --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`auth logout --confirm` 撤销 CLI 管理权限,保留资源回报。CLI 可调用已有的指定日志授权功能;不能替目标设备批准日志或获得任意远程执行权限。
|
|
72
|
+
|
|
58
73
|
| 能力 | 当前实现 |
|
|
59
74
|
| --- | --- |
|
|
60
75
|
| 资源清单 | 本机真实资源;已有 SSH 主机经用户触发检查;云端只显示获准回报 |
|
package/SECURITY.md
CHANGED
|
@@ -6,6 +6,12 @@ Device keys remain in owner-private local files. Pairing is short-lived, one-tim
|
|
|
6
6
|
|
|
7
7
|
Only bounded log tail reads are available. No arbitrary commands, generic file transfer or cloud MCP. Symlink/multiple-hardlink/non-regular log files are rejected. Cloud revocation is checked on both routes. Local audit is bounded and fails closed at capacity. OS administrators, compromised endpoints and already-authorized readers remain trust boundaries. Keys are not TPM/Keychain wrapped. See [permissions, quotas, certificate renewal and limitations](docs/remote-access.md).
|
|
8
8
|
|
|
9
|
+
## CLI management (beta.6 and later)
|
|
10
|
+
|
|
11
|
+
Cloud CLI management is a separate capability, never implied by telemetry binding or log grants. The account owner approves a five-minute, one-time browser handoff after comparing the full device fingerprint. Approval permits account device metadata reads, descriptive profile edits and requests for previously approved resource fields, for at most eight hours. It does not permit account administration, SSH configuration changes, log grants, arbitrary commands or file access. Browser tickets are hashed at rest, removed from the URL before login, and omitted from automated CLI results. CLI requests are signed by the existing device identity and checked for path, origin, time, nonce, tenant, revocation and grant expiry. No browser cookies or long-lived cloud tokens are exported to the CLI.
|
|
12
|
+
|
|
13
|
+
The grant belongs to the approved device identity, not an isolated OS process. Other software with access to that user's private identity can use its active scope; same-user compromise remains outside this boundary. Logout or owner revocation cancels pending requests and invalidates CLI management immediately, while preserving device telemetry. Device revocation invalidates both. Descriptive names, purposes, tags and services are untrusted data, never Agent instructions or grants. Profile changes require an optimistic revision. Local CLI calls only fixed loopback routes; SSH checks and mutations need explicit confirmation. Added grant tables are backward-compatible; rollback does not erase identity or device data, and grant records are not transferable credentials.
|
|
14
|
+
|
|
9
15
|
## Windows candidate
|
|
10
16
|
|
|
11
17
|
Windows identity JSON is encrypted using local DPAPI CurrentUser, never LocalMachine; only ciphertext is persisted. The application directory and newly created private files allow the current user and SYSTEM only, with owner and reparse-point checks. Existing insecure directories are rejected rather than silently repaired. POSIX owner/mode checks remain unchanged. Windows ACL checks are refreshed on metadata changes and at least every 30 seconds; decrypted identity is cached only in the running process. This does not defend against malicious software running as the same user or an OS administrator, and copying the file to another computer/user is not a supported migration method. See [Microsoft DPAPI](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata.protect).
|
package/bin/device-center.mjs
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { parseManagement } from '../server/cli/commands.mjs';
|
|
4
5
|
|
|
5
6
|
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
6
7
|
const version = () => JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
8
|
+
const MANAGEMENT = ['devices', 'auth', 'doctor', 'ssh', 'mcp', 'capabilities'];
|
|
7
9
|
export function parseCli(argv = []) {
|
|
8
10
|
const command = argv[0] ?? 'help';
|
|
9
11
|
if (['help', '--help', '-h', '--version', '-v'].includes(command)) {
|
|
10
12
|
if (argv.length > 1) throw new Error('帮助和版本命令不接受其他参数。');
|
|
11
13
|
return { command: command === '-v' ? '--version' : ['-h', '--help'].includes(command) ? 'help' : command };
|
|
12
14
|
}
|
|
15
|
+
if (MANAGEMENT.includes(command)) { parseManagement(argv); return { command, args: argv.slice(1) }; }
|
|
13
16
|
if (!['start', 'plan', 'connect', 'run', 'status', 'disconnect', 'service', 'access'].includes(command)) throw new Error('未知命令。运行 device-center --help 查看用法。');
|
|
14
17
|
const args = argv.slice(1);
|
|
15
18
|
if (command === 'start') {
|
|
@@ -41,6 +44,16 @@ device-center status 查看已有绑定,不显示私
|
|
|
41
44
|
device-center run [--port 5174] 前台恢复已绑定 Driver
|
|
42
45
|
device-center disconnect --confirm 通知云端撤销,保留本机身份
|
|
43
46
|
device-center service plan|status|install|uninstall [--port 5174] [--confirm]
|
|
47
|
+
device-center devices help 列表、详情、命名、标签、用途、服务与手动采集
|
|
48
|
+
device-center auth login --confirm 本人另行批准限时云端 CLI 管理权限
|
|
49
|
+
device-center auth status|logout 查看/撤销 CLI 权限,保留设备绑定
|
|
50
|
+
device-center doctor [--cloud] 检查本机服务和已绑定控制面,不扫描
|
|
51
|
+
device-center ssh help 导入本机 SSH 引用、显式检查及移除
|
|
52
|
+
device-center mcp setup|tools|call 接入说明与现有只读工具
|
|
53
|
+
device-center capabilities --json Agent 可读命令能力、权限和退出码
|
|
54
|
+
|
|
55
|
+
新管理命令支持 --json,默认本机;devices --cloud 使用已绑定的控制面。
|
|
56
|
+
详情和资料修改使用不可变 ID;--revision 防止覆盖并发修改。变更需 --confirm。
|
|
44
57
|
|
|
45
58
|
默认端口5174,数据位于本用户的应用数据目录。关闭网页不会停止前台进程。
|
|
46
59
|
默认回报本机名称、系统、内存和磁盘;--share-ssh 需命令申请及面板另行批准。
|
|
@@ -53,10 +66,15 @@ Windows 身份由 DPAPI 当前用户保护,后台在当前用户登录后启
|
|
|
53
66
|
`;
|
|
54
67
|
}
|
|
55
68
|
export async function main(argv = process.argv.slice(2)) {
|
|
69
|
+
if (argv.includes('--json')) {
|
|
70
|
+
if (argv.filter(v => v === '--json').length !== 1 || ![...MANAGEMENT, 'access', 'status', 'plan', '--version'].includes(argv[0])) throw Object.assign(new Error('--json 适用于设备管理、access、status、plan 和 --version。'), { cliCode: 'invalid_arguments', statusCode: 400 });
|
|
71
|
+
argv = argv.filter(v => v !== '--json');
|
|
72
|
+
}
|
|
56
73
|
const plan = parseCli(argv);
|
|
57
74
|
if (plan.command === 'help') return helpText();
|
|
58
75
|
if (plan.command === '--version') return version();
|
|
59
76
|
if (Number(process.versions.node.split('.')[0]) < 24) throw new Error('需要 Node.js 24 或更高版本。');
|
|
77
|
+
if (MANAGEMENT.includes(plan.command)) return (await import('../server/cli/commands.mjs')).main([plan.command, ...plan.args]);
|
|
60
78
|
if (plan.command === 'access') return (await import('../scripts/access.mjs')).main(plan.args);
|
|
61
79
|
if (plan.command === 'service') {
|
|
62
80
|
if (plan.args[0] === 'install' && /(?:^|[\\/])_npx[\\/]/.test(ROOT)) throw new Error('npx 缓存会被清理,不能用作常驻目录。请先 npm install -g device-center@固定版本,再运行 device-center service install --confirm。');
|
|
@@ -70,8 +88,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
70
88
|
}
|
|
71
89
|
// npm links bin entries through a symlink; compare the actual script, not the link path.
|
|
72
90
|
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
91
|
+
const json = process.argv.includes('--json');
|
|
73
92
|
try {
|
|
74
93
|
const result = await main();
|
|
75
|
-
if (result != null) console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
|
76
|
-
} catch (error) {
|
|
94
|
+
if (result != null) console.log(json ? JSON.stringify({ schemaVersion: 1, ok: true, data: result }) : typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const { safeCode, exitCode } = await import('../server/cli/commands.mjs');
|
|
97
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code: safeCode(error), status: error.statusCode ?? null } }));
|
|
98
|
+
else console.error(error.cliCode ? `${error.cliCode}: ${error.message}` : error.message);
|
|
99
|
+
process.exitCode = MANAGEMENT.includes(process.argv[2]) ? exitCode(error) : 1;
|
|
100
|
+
}
|
|
77
101
|
}
|
package/docs/agent-install.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
目标设备只需要 Device Center Driver,不要求已经安装 AI Agent。一页指南提供 **交给 Agent** 和 **手动安装** 两个入口。网页只复制内容,安装发生在你执行脚本的目标设备上。
|
|
4
4
|
|
|
5
|
-
**本指南固定 `device-center@0.2.0-beta.
|
|
5
|
+
**本指南固定 `device-center@0.2.0-beta.6`(npm `beta`)**。macOS/Linux 使用下方终端命令;Windows 11 预览版按 [PowerShell 安装图示](windows-acceptance.md)操作。实际发布状态以 npm registry 为准;旧 beta.4 不支持原生 Windows。
|
|
6
6
|
|
|
7
7
|
## 用户只需要选择一种方式
|
|
8
8
|
|
|
@@ -57,7 +57,7 @@ AI Agent 和 MCP 是管理端的可选能力:只有以后想让某台电脑上
|
|
|
57
57
|
全新安装可以使用:
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --global --prefix "$HOME/.local" --ignore-scripts device-center@0.2.0-beta.
|
|
60
|
+
npm install --global --prefix "$HOME/.local" --ignore-scripts device-center@0.2.0-beta.6
|
|
61
61
|
"$HOME/.local/bin/device-center" --version
|
|
62
62
|
```
|
|
63
63
|
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CLI 与 Agent 使用手册
|
|
2
|
+
|
|
3
|
+
本手册中的统一管理命令需要 `0.2.0-beta.6` 或更新版,beta.5 不包含。需要 Node.js 24;安装前核对 npm 公开版本,源码验收使用 `node bin/device-center.mjs` 代替下文的 `device-center`。云端 CLI 管理还需要同版或更新的控制面;网页指南只在新包公开安装验证通过后更新版本。
|
|
4
|
+
|
|
5
|
+
## 从一台电脑开始
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
device-center --help
|
|
9
|
+
device-center service status --port 5174
|
|
10
|
+
# 没有运行服务时,在另一个终端前台启动;保持窗口打开
|
|
11
|
+
device-center start --port 5174
|
|
12
|
+
device-center devices list
|
|
13
|
+
device-center doctor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
本机模式不需要账号、绑定或云端。Docker 里看到的是容器,不能代表宿主机。CLI 不自动安装、启动服务或改 Agent 配置。
|
|
17
|
+
|
|
18
|
+
## 云端管理
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# 尚未绑定这台管理电脑时执行,浏览器中由本人核对指纹批准
|
|
22
|
+
device-center connect --cloud https://devices.owenshen.top --browser --confirm
|
|
23
|
+
# 设备绑定只回报资源;CLI 管理权限需要再单独批准
|
|
24
|
+
device-center auth login --minutes 480 --confirm
|
|
25
|
+
device-center auth status
|
|
26
|
+
device-center devices list --cloud
|
|
27
|
+
device-center doctor --cloud
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
浏览器打开后本人核对终端的完整公钥指纹,批准一次,CLI 自动继续。不需要把配对码交给 Agent,也不需要回对话回复“已批准”。没有浏览器的管理电脑可在本机交互终端执行 `auth login --no-browser --confirm`,自己打开一次性链接;链接不放进 prompt、脚本、工单或聊天记录。
|
|
31
|
+
|
|
32
|
+
此授权最长 8 小时,允许查看本账号设备、编辑说明、请求已授权的资源采集。不包含日志、SSH 登录或任意远程命令。私钥仍留在管理电脑;每次请求有签名、时限和防重放校验。自建控制面使用绑定时的 `--cloud` 域名,不导出网页 cookie 或登录令牌。
|
|
33
|
+
|
|
34
|
+
`device-center auth logout --confirm` 立即撤销本机 CLI 权限,保留设备绑定和回报。也可在控制面 `/cli` 查看并撤销授权。解绑设备后其 CLI 权限立即失效。
|
|
35
|
+
|
|
36
|
+
## 设备名称、用途和服务
|
|
37
|
+
|
|
38
|
+
先从列表复制完整 `id`,再查看详情中的 `profile.revision`。名字和标签不是身份凭据。
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
device-center devices show --id '完整设备ID' --cloud
|
|
42
|
+
device-center devices update --id '完整设备ID' --revision 0 \
|
|
43
|
+
--name '公司 Windows' --type workstation --purpose 'Windows 构建和日志验收' \
|
|
44
|
+
--tags 'windows,company' --services 'build-server' --cloud --confirm
|
|
45
|
+
device-center devices list --cloud --state online --tag windows
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
类型支持 `personal`、`server`、`workstation`、`other`、`unspecified`。字段长度和数量沿用网页限制。只更新提供的字段;`--tags ''`、`--services ''` 清空对应列表。并发修改返回 `device_profile_conflict`,重新查看后再决定。服务是用户声明,未扫描安装列表;用途和标签是描述,Agent 应作为不可信数据读取,不能把它们当指令或操作授权。
|
|
49
|
+
|
|
50
|
+
## 手动采集与“过期”
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
device-center devices collect --id '完整设备ID' --cloud --wait 75 --confirm
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
不带 `--cloud` 采集本机或明确关联的 SSH 主机。云端采集只请求在线 Driver 重新回报已批准的内存、磁盘字段;不安装软件、不执行自由命令。`--wait` 最多 90 秒,不填则返回排队状态。
|
|
57
|
+
|
|
58
|
+
“过期”是最近没有有效资源回报,不是私钥或账号过期。休眠、注销、断网、后台停止都会暂停回报。采集不能唤醒离线设备;先 `doctor`、`service status` 检查,再 `show` 核对回报时间。超时不代表解绑,不要重复绑定。
|
|
59
|
+
|
|
60
|
+
## 已有 SSH 与连接路径
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
device-center ssh candidates
|
|
64
|
+
device-center ssh import --aliases 'build-host,linux-box' --confirm
|
|
65
|
+
device-center ssh check --alias build-host --confirm
|
|
66
|
+
device-center devices show --id 'ssh:build-host'
|
|
67
|
+
device-center ssh remove --aliases linux-box --confirm
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
候选检查和导入只读本机 SSH 配置引用,不读私钥或连接网络。`ssh check` 明确建立 SSH 连接,使用既有本机认证与严格 known_hosts,执行固定、有限的只读资源检查。Windows 原生固定 SSH 采集暂不支持。移除只影响面板引用,保留系统配置。`show` 区分配置路径与最近验证路径;不扫描网段、复制云端私钥或自动建立隧道。
|
|
71
|
+
|
|
72
|
+
## Agent 与日志
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
device-center capabilities --json
|
|
76
|
+
device-center mcp setup
|
|
77
|
+
device-center mcp tools --json
|
|
78
|
+
device-center mcp call --tool device_center_list_resources --json
|
|
79
|
+
device-center access help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
日志使用现有 `access trust / grant / read / revoke`,需要管理端固定目标完整指纹、目标设备主人单独授权具体日志路径与时限。`access read --peer 完整指纹 --label app --bytes 16384 --json` 返回实际 LAN 或加密中转路径;不自动增加目标权限。参见 [远程访问](remote-access.md)。
|
|
83
|
+
|
|
84
|
+
## 自动化契约
|
|
85
|
+
|
|
86
|
+
新增管理命令支持 `--json`:成功输出一个 `{schemaVersion:1,ok:true,data:…}`,失败输出 `{schemaVersion:1,ok:false,error:{code,status}}`。进度写 stderr;不把私钥、一次性浏览器凭据或原始网络错误放进 JSON。云端列表分页传输,CLI 自动汇总(每账号最多 256 条资源)。本机端口只允许 `127.0.0.1`,不接受任意 API URL,拒绝重定向。
|
|
87
|
+
|
|
88
|
+
| 退出码 | 含义 |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| 0 | 命令完成;诊断结果还需检查 `healthy` |
|
|
91
|
+
| 1 | 操作失败或未知错误 |
|
|
92
|
+
| 2 | 新管理命令参数错误 |
|
|
93
|
+
| 3 | 缺少确认、绑定或授权 |
|
|
94
|
+
| 4 | 并发冲突或当前能力不可用 |
|
|
95
|
+
| 5 | 网络、服务、限流或等待超时 |
|
|
96
|
+
|
|
97
|
+
原有 `connect/run/service` 终端交互保持兼容;`connect` 不支持 `--json`,避免把绑定凭据当自动化输出。状态、plan 与 access 可使用 JSON;旧命令失败仍保持退出码 1。
|
package/docs/friend-trial.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Device Center · 朋友试用指南
|
|
2
2
|
|
|
3
|
-
让本机 Agent 看见自己的电脑与闲置设备:真实内存、磁盘、设备用途,以及单独授权的指定日志。本指南版本 `0.2.0-beta.
|
|
3
|
+
让本机 Agent 看见自己的电脑与闲置设备:真实内存、磁盘、设备用途,以及单独授权的指定日志。本指南版本 `0.2.0-beta.6`,发布状态以 npm registry 为准。
|
|
4
4
|
|
|
5
5
|
**入口:https://devices.owenshen.top/start**。支持 macOS / Linux,需要 Node.js 24+;Windows 11 提供 PowerShell 预览版,可选择“手动安装”,不需要 AI Agent 或 AD。
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Agent 按说明检查已有配置、固定版本安装、设置用户级后台服务、配置 MCP 并核验结果。已有配置冲突时解释并让你选择,不自动覆盖。系统授权或当前 Agent 无法配置 MCP 时,会请你完成对应一步。
|
|
16
16
|
|
|
17
|
-
不使用 Agent 时,可在全新环境运行 `npx --yes device-center@0.2.0-beta.
|
|
17
|
+
不使用 Agent 时,可在全新环境运行 `npx --yes device-center@0.2.0-beta.6 start` 体验本机面板;这是前台方式,终端关闭后停止。长期使用按[完整安装指南](https://devices.owenshen.top/install/agent.md)固定安装。
|
|
18
18
|
|
|
19
19
|
## 2. 托管模式才需要登录绑定
|
|
20
20
|
|
package/docs/npm.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# npm / npx 分发
|
|
2
2
|
|
|
3
|
-
MIT,Node.js 24。本指南固定 `0.2.0-beta.
|
|
3
|
+
MIT,Node.js 24。本指南固定 `0.2.0-beta.6`,支持 macOS/Linux,并提供 Windows 11 预览接入。使用 npm `beta` 渠道;`latest` 仍可能是旧版本,安装请写明版本,实际发布状态以公开 registry 为准。
|
|
4
4
|
|
|
5
|
-
beta.5
|
|
5
|
+
beta.6 增加统一 CLI 与独立云端管理授权,见 [CLI 手册](cli.md)。沿用 beta.5 的 PowerShell 安装、DPAPI CurrentUser 身份保护与当前用户登录后台任务,详见 [Windows 验收](windows-acceptance.md)。发布先后顺序是新 CLI → 匿名下载验包 → 对应指南与控制面上线。旧客户端的终端填码仍兼容;回退网页或旧 CLI 不删除既有身份。
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx --yes device-center@0.2.0-beta.
|
|
9
|
-
npx --yes device-center@0.2.0-beta.
|
|
8
|
+
npx --yes device-center@0.2.0-beta.6 --help
|
|
9
|
+
npx --yes device-center@0.2.0-beta.6 plan
|
|
10
10
|
# 主动接入默认官方服务;有浏览器时加 --browser 自动打开本次绑定页
|
|
11
|
-
npx --yes device-center@0.2.0-beta.
|
|
11
|
+
npx --yes device-center@0.2.0-beta.6 connect --browser --confirm
|
|
12
12
|
# 无浏览器:在终端与另一台电脑的面板之间核对配对码
|
|
13
|
-
npx --yes device-center@0.2.0-beta.
|
|
13
|
+
npx --yes device-center@0.2.0-beta.6 connect --confirm
|
|
14
14
|
# 自建服务
|
|
15
|
-
npx --yes device-center@0.2.0-beta.
|
|
15
|
+
npx --yes device-center@0.2.0-beta.6 connect --cloud https://你的控制面域名 --confirm
|
|
16
16
|
# 仅本机,无账号要求
|
|
17
|
-
npx --yes device-center@0.2.0-beta.
|
|
17
|
+
npx --yes device-center@0.2.0-beta.6 start
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
安装与无参/help/version/plan 不生成身份,不配对,不自动启动后台进程。复制命令不含凭据。只有 connect --confirm 才生成本机身份并申请绑定;默认官方地址不会自动批准,用户需登录自己的账号。SSH 缓存分享须 --share-ssh 与面板再次批准。
|
|
@@ -22,7 +22,7 @@ npx --yes device-center@0.2.0-beta.5 start
|
|
|
22
22
|
面板和本机 MCP:`http://127.0.0.1:5174/`、`/mcp`。常驻先固定安装:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
npm install -g device-center@0.2.0-beta.
|
|
25
|
+
npm install -g device-center@0.2.0-beta.6
|
|
26
26
|
device-center service plan --port 5174
|
|
27
27
|
# 自行停止同端口前台进程,再主动安装
|
|
28
28
|
device-center service install --port 5174 --confirm
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Windows 11 手动接入验收
|
|
2
2
|
|
|
3
|
-
**0.2.0-beta.
|
|
3
|
+
**0.2.0-beta.6 · Windows 11 预览版。** 安装前可用 `npm.cmd view device-center@0.2.0-beta.6 version` 确认发布状态。Windows CI 的脚本、安全存储及安装检查不能替代你的 Windows 11 实机验收。
|
|
4
4
|
|
|
5
5
|
## 准备与安装
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
## 验收清单
|
|
25
25
|
|
|
26
|
-
1. `--version` 是 `0.2.0-beta.
|
|
26
|
+
1. `--version` 是 `0.2.0-beta.6`;后台状态显示 `windows-task`,本机面板 `http://127.0.0.1:5174` 正常加载,页面脚本无 404。
|
|
27
27
|
2. 浏览器只批准刚由本机发起的申请;完整指纹与终端一致。终端有“云端已接收首次资源回报”,`firstReport.verified=true`。仅 `configured=true` 不算连通。
|
|
28
28
|
3. 云端出现这台 Windows 电脑,内存和用户目录所在卷磁盘分别显示真实数值;没有测试设备或伪造在线。可填写设备名、用途与标签。
|
|
29
29
|
4. 点击云端“采集一次”,采集时间在约 30–75 秒内更新。关闭安装终端后仍更新;注销/休眠会停止回报,登录/唤醒且网络恢复后继续。
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "device-center",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "device-center",
|
|
9
|
-
"version": "0.2.0-beta.
|
|
9
|
+
"version": "0.2.0-beta.6",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@peculiar/x509": "2.1.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "device-center",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Local device connector with account-bound pairing, TLS encrypted relay and explicitly authorized log access",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"SECURITY.md",
|
|
35
35
|
"LICENSE",
|
|
36
36
|
"scripts/access.mjs",
|
|
37
|
+
"docs/cli.md",
|
|
37
38
|
"docs/remote-access.md",
|
|
38
39
|
"npm-shrinkwrap.json",
|
|
39
40
|
"start.html",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { localClient, cloudClient, cliError } from '../drivers/cliClient.mjs';
|
|
2
|
+
import { normalizeDeviceProfile } from '../drivers/deviceProfiles.mjs';
|
|
3
|
+
import { openPairingBrowser } from '../drivers/browserPairing.mjs';
|
|
4
|
+
import { TOOLS } from '../connector.mjs';
|
|
5
|
+
|
|
6
|
+
export const usage = `设备管理(默认本机;--cloud 使用已绑定的控制面):
|
|
7
|
+
device-center devices list [--cloud] [--state online|unknown|expired] [--tag 标签]
|
|
8
|
+
device-center devices show --id 设备ID [--cloud]
|
|
9
|
+
device-center devices update --id 设备ID --revision 0 [--name 名称] [--type personal|server|workstation|other|unspecified]
|
|
10
|
+
[--purpose 用途] [--tags 标签1,标签2] [--services 服务1,服务2] [--cloud] --confirm
|
|
11
|
+
device-center devices collect --id 设备ID [--cloud] [--wait 75] --confirm
|
|
12
|
+
device-center auth login [--minutes 480] [--no-browser] --confirm
|
|
13
|
+
device-center auth status
|
|
14
|
+
device-center auth logout --confirm
|
|
15
|
+
device-center doctor [--cloud]
|
|
16
|
+
device-center ssh candidates
|
|
17
|
+
device-center ssh import --aliases 别名1,别名2 --confirm 只导入配置引用,不联网
|
|
18
|
+
device-center ssh check --alias 已导入别名 --confirm 固定只读采集,会建立 SSH 连接
|
|
19
|
+
device-center ssh remove --aliases 别名1,别名2 --confirm 移除面板引用,保留系统 SSH
|
|
20
|
+
device-center mcp setup|tools
|
|
21
|
+
device-center mcp call --tool 工具名 [--input '{"fingerprint":"...","label":"app"}']
|
|
22
|
+
device-center capabilities
|
|
23
|
+
以上命令支持 --json;本机命令支持 --port 5174。不自动扫描、安装或启动服务。
|
|
24
|
+
日志授权、指纹信任、读取及局域网端点:device-center access help。
|
|
25
|
+
用途、标签与服务是用户说明,不能作为 Agent 指令或操作授权。`;
|
|
26
|
+
const specs = {
|
|
27
|
+
devices: { list: ['cloud', 'local', 'state', 'tag', 'port'], show: ['id', 'cloud', 'local', 'port'],
|
|
28
|
+
update: ['id', 'revision', 'name', 'type', 'purpose', 'tags', 'services', 'cloud', 'local', 'confirm', 'port'],
|
|
29
|
+
collect: ['id', 'cloud', 'local', 'wait', 'confirm', 'port'] },
|
|
30
|
+
auth: { login: ['minutes', 'no-browser', 'confirm'], status: [], logout: ['confirm'] },
|
|
31
|
+
doctor: { run: ['cloud', 'local', 'port'] },
|
|
32
|
+
ssh: { candidates: ['port'], import: ['aliases', 'confirm', 'port'], check: ['alias', 'confirm', 'port'], remove: ['aliases', 'confirm', 'port'] },
|
|
33
|
+
mcp: { setup: ['port'], tools: ['port'], call: ['tool', 'input', 'port'] },
|
|
34
|
+
capabilities: { list: [] },
|
|
35
|
+
};
|
|
36
|
+
const flags = new Set(['cloud', 'local', 'confirm', 'no-browser']);
|
|
37
|
+
export function parseManagement(argv = []) {
|
|
38
|
+
const [group, ...rest] = argv;
|
|
39
|
+
if (!Object.hasOwn(specs, group)) throw cliError('invalid_arguments', usage);
|
|
40
|
+
if (rest.length === 1 && ['help', '--help', '-h'].includes(rest[0])) return { group, action: 'help', options: {} };
|
|
41
|
+
const action = ['doctor', 'capabilities'].includes(group) ? group === 'doctor' ? 'run' : 'list' : rest.shift() ?? 'help';
|
|
42
|
+
if (action === 'help') { if (rest.length) throw cliError('invalid_arguments', usage); return { group, action, options: {} }; }
|
|
43
|
+
if (!Object.hasOwn(specs[group], action)) throw cliError('invalid_arguments', usage);
|
|
44
|
+
const options = {};
|
|
45
|
+
for (let i = 0; i < rest.length; i++) {
|
|
46
|
+
const key = rest[i].slice(2);
|
|
47
|
+
if (!rest[i].startsWith('--') || !specs[group][action].includes(key) || Object.hasOwn(options, key)) throw cliError('invalid_arguments', usage);
|
|
48
|
+
if (flags.has(key)) options[key] = true;
|
|
49
|
+
else {
|
|
50
|
+
if (rest[i+1] === undefined || rest[i+1].startsWith('--')) throw cliError('invalid_arguments', usage);
|
|
51
|
+
options[key] = rest[++i];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (options.cloud && options.local || group === 'devices' && options.cloud && options.port) throw cliError('invalid_arguments', '--cloud 不能与 --local 同用;云端设备命令不接受 --port。');
|
|
55
|
+
if (options.port !== undefined && (!/^\d+$/.test(options.port) || Number(options.port) < 1024 || Number(options.port) > 65535)) throw cliError('invalid_arguments', '端口须为 1024–65535。');
|
|
56
|
+
if (options.state && !['online', 'unknown', 'expired'].includes(options.state)) throw cliError('invalid_arguments', '状态为 online、unknown 或 expired。');
|
|
57
|
+
if (['show', 'update', 'collect'].includes(action) && group === 'devices' && (typeof options.id !== 'string' || !options.id || options.id.length > 200 || /[\x00-\x1f\x7f]/.test(options.id))) throw cliError('invalid_arguments', '请提供完整 --id;名称不能用作设备身份。');
|
|
58
|
+
if (action === 'update') {
|
|
59
|
+
if (!/^\d+$/.test(options.revision ?? '') || !Number.isSafeInteger(Number(options.revision))) throw cliError('invalid_arguments', '请通过 show 获取当前 --revision,防止覆盖他人修改。');
|
|
60
|
+
if (!['name', 'type', 'purpose', 'tags', 'services'].some(key => Object.hasOwn(options, key))) throw cliError('invalid_arguments', '至少提供一个设备说明字段。');
|
|
61
|
+
}
|
|
62
|
+
if (options.wait !== undefined && (!/^\d+$/.test(options.wait) || Number(options.wait) < 1 || Number(options.wait) > 90)) throw cliError('invalid_arguments', '--wait 为 1–90 秒。');
|
|
63
|
+
if (options.minutes !== undefined && (!/^\d+$/.test(options.minutes) || Number(options.minutes) < 5 || Number(options.minutes) > 480)) throw cliError('invalid_arguments', '授权有效期为 5–480 分钟。');
|
|
64
|
+
const mutation = group === 'devices' && ['update', 'collect'].includes(action) || group === 'auth' && ['login', 'logout'].includes(action) || group === 'ssh' && action !== 'candidates';
|
|
65
|
+
if (mutation && !options.confirm) throw cliError('confirmation_required', '该操作需显式 --confirm;尚未更改或联系设备。', 403);
|
|
66
|
+
if (group === 'ssh' && ['import', 'remove'].includes(action) && (!options.aliases || options.aliases.split(',').some(v => !v.trim()))) throw cliError('invalid_arguments', '请提供 --aliases,以逗号分隔。');
|
|
67
|
+
if (group === 'ssh' && action === 'check' && !options.alias) throw cliError('invalid_arguments', '请提供已导入的 --alias。');
|
|
68
|
+
if (group === 'mcp' && action === 'call') {
|
|
69
|
+
if (!TOOLS.some(tool => tool.name === options.tool)) throw cliError('invalid_arguments', '只允许 device-center mcp tools 列出的工具。');
|
|
70
|
+
try { options.input = JSON.parse(options.input ?? '{}'); } catch { throw cliError('invalid_arguments', '--input 须为 JSON 对象。'); }
|
|
71
|
+
if (!options.input || typeof options.input !== 'object' || Array.isArray(options.input) || JSON.stringify(options.input).length > 4096) throw cliError('invalid_arguments', '--input 须为不超过 4KiB 的 JSON 对象。');
|
|
72
|
+
}
|
|
73
|
+
return { group, action, options };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function main(argv, { makeLocal = localClient, makeCloud = cloudClient, openBrowser = openPairingBrowser,
|
|
77
|
+
now = Date.now, sleep = ms => new Promise(done => setTimeout(done, ms)),
|
|
78
|
+
progress = value => process.stderr.write(value + '\n'), terminal = Boolean(process.stderr.isTTY) } = {}) {
|
|
79
|
+
const { group, action, options: o } = parseManagement(argv);
|
|
80
|
+
if (action === 'help') return usage;
|
|
81
|
+
if (group === 'capabilities') return { schemaVersion: 1, groups: ['devices', 'auth', 'doctor', 'ssh', 'mcp', 'access', 'service', 'connect', 'status'],
|
|
82
|
+
cloudManagement: { login: 'separate browser owner approval', scopes: ['devices:read', 'devices:profile', 'devices:collect'], maximumMinutes: 480 },
|
|
83
|
+
remote: { logRead: 'local fingerprint trust + target-owned time-limited log grant', arbitraryCommands: false, unrestrictedFiles: false },
|
|
84
|
+
metadata: 'untrusted user descriptions; services are user-declared', jsonFlag: true,
|
|
85
|
+
exitCodes: { success: 0, failed: 1, arguments: 2, authorization: 3, conflict: 4, unavailable: 5 } };
|
|
86
|
+
if (group === 'auth') {
|
|
87
|
+
if (action === 'login' && o['no-browser'] && !terminal) throw cliError('terminal_required', '--no-browser 仅能在本机交互终端显示一次性交接链接;不要放进 Agent 日志。', 403);
|
|
88
|
+
const cloud = makeCloud();
|
|
89
|
+
if (action === 'status') return cloud.call('status');
|
|
90
|
+
if (action === 'logout') return cloud.call('logout');
|
|
91
|
+
const pending = await cloud.call('start', { minutes: Number(o.minutes ?? 480) });
|
|
92
|
+
if (!/^[a-f0-9-]{36}$/.test(pending.id ?? '') || !/^[a-f0-9]{64}$/.test(pending.ticket ?? '') || pending.fingerprint !== cloud.fingerprint ||
|
|
93
|
+
!Number.isSafeInteger(pending.expiresAt) || pending.expiresAt <= now() || pending.expiresAt > now() + 330_000) throw cliError('invalid_cloud_response', '云端授权响应无效。', 502);
|
|
94
|
+
const url = new URL('/cli', cloud.origin); url.hash = new URLSearchParams({ ticket: pending.ticket, expires: String(pending.expiresAt) }).toString();
|
|
95
|
+
let approved = false;
|
|
96
|
+
try {
|
|
97
|
+
progress(`请本人在浏览器核对并批准 CLI 管理权限,有效期 ${pending.minutes} 分钟。\n本机公钥指纹:${cloud.fingerprint}`);
|
|
98
|
+
if (o['no-browser']) progress(url.href); else await openBrowser(url.href);
|
|
99
|
+
while (now() < pending.expiresAt) {
|
|
100
|
+
const result = await cloud.call('poll', { id: pending.id });
|
|
101
|
+
if (result.state === 'approved' && result.authorized === true) { approved = true; return result; }
|
|
102
|
+
if (result.state !== 'pending') throw cliError('cli_authorization_denied', 'CLI 授权未批准;设备绑定保留。', 403);
|
|
103
|
+
await sleep(3000);
|
|
104
|
+
}
|
|
105
|
+
throw cliError('cli_request_expired', 'CLI 授权请求已过期,可重新登录。', 410);
|
|
106
|
+
} finally { if (!approved) await cloud.call('cancel', { id: pending.id }).catch(() => {}); }
|
|
107
|
+
}
|
|
108
|
+
const local = () => makeLocal({ port: Number(o.port ?? 5174) });
|
|
109
|
+
if (group === 'doctor') {
|
|
110
|
+
const checks = [];
|
|
111
|
+
async function check(name, fn) { try { checks.push({ name, ok: true, result: await fn() }); } catch (error) { checks.push({ name, ok: false, code: safeCode(error) }); } }
|
|
112
|
+
checks.push({ name: 'node', ok: Number(process.versions.node.split('.')[0]) >= 24, version: process.versions.node });
|
|
113
|
+
const client = local();
|
|
114
|
+
await check('local-health', client.health); await check('local-publisher', client.link); await check('mcp-service', client.connector);
|
|
115
|
+
if (o.cloud) await check('cloud-authorization', () => makeCloud().call('status'));
|
|
116
|
+
return { healthy: checks.every(c => c.ok && (c.name !== 'cloud-authorization' || c.result.authorized) &&
|
|
117
|
+
(c.name !== 'local-publisher' || !c.result.configured || c.result.publisher?.state === 'connected')), checks,
|
|
118
|
+
networkScope: o.cloud ? 'loopback + already-bound HTTPS control plane' : 'loopback only',
|
|
119
|
+
advice: ['过期表示最近没有有效资源回报,不表示密钥过期。', '设备休眠、注销、断网或后台停止都会暂停回报;collect 不能唤醒离线电脑。', 'SSH 路径以最近固定采集结果为证,配置偏好不是已连通。'], scans: false, remoteCommands: false };
|
|
120
|
+
}
|
|
121
|
+
if (group === 'mcp') {
|
|
122
|
+
const client = local();
|
|
123
|
+
if (action === 'setup') return client.setup();
|
|
124
|
+
return client.mcp(action === 'tools' ? 'tools/list' : 'tools/call', action === 'tools' ? {} : { name: o.tool, arguments: o.input });
|
|
125
|
+
}
|
|
126
|
+
if (group === 'ssh') {
|
|
127
|
+
const client = local();
|
|
128
|
+
if (action === 'candidates') return client.candidates();
|
|
129
|
+
if (action === 'check') { const result = await client.checkSsh({ alias: o.alias }); if (!result.ok) throw cliError(result.error ?? 'ssh_check_failed', 'SSH 固定采集未通过;检查现有网络、known_hosts 与认证配置。', 502); return result; }
|
|
130
|
+
const input = { aliases: [...new Set(o.aliases.split(',').map(v => v.trim()))] };
|
|
131
|
+
return action === 'import' ? client.importSsh(input) : client.removeSsh(input);
|
|
132
|
+
}
|
|
133
|
+
const client = o.cloud ? makeCloud() : local();
|
|
134
|
+
const list = async () => o.cloud ? client.devices() : client.overview();
|
|
135
|
+
const show = async () => {
|
|
136
|
+
const device = o.cloud ? (await client.call('show', { deviceId: o.id })).device : (await list()).devices.find(d => d.id === o.id);
|
|
137
|
+
if (!device) throw cliError('device_not_found', '没有找到设备,请使用 list 返回的完整 ID。', 404);
|
|
138
|
+
return { device };
|
|
139
|
+
};
|
|
140
|
+
if (action === 'list') {
|
|
141
|
+
const { devices } = await list();
|
|
142
|
+
const filtered = devices.filter(d => (!o.state || d.state === o.state) && (!o.tag || d.profile?.tags.includes(o.tag)));
|
|
143
|
+
return { mode: o.cloud ? 'cloud' : 'local', devices: filtered, total: filtered.length };
|
|
144
|
+
}
|
|
145
|
+
if (action === 'show') return show();
|
|
146
|
+
if (action === 'update') {
|
|
147
|
+
const { device } = await show();
|
|
148
|
+
if (device.profile.revision !== Number(o.revision)) throw cliError('device_profile_conflict', '资料已变化,请重新 show 核对 revision。', 409);
|
|
149
|
+
const profile = Object.fromEntries(['displayName', 'type', 'purpose', 'tags', 'services'].map(k => [k, device.profile[k]]));
|
|
150
|
+
for (const [flag, field] of [['name', 'displayName'], ['type', 'type'], ['purpose', 'purpose'], ['tags', 'tags'], ['services', 'services']]) {
|
|
151
|
+
if (Object.hasOwn(o, flag)) profile[field] = ['tags', 'services'].includes(flag) ? o[flag].trim() ? o[flag].split(',').map(v => v.trim()) : [] : o[flag];
|
|
152
|
+
}
|
|
153
|
+
const input = { deviceId: o.id, profile: normalizeDeviceProfile(profile), revision: Number(o.revision) };
|
|
154
|
+
return o.cloud ? client.call('profile', input) : client.profile(input);
|
|
155
|
+
}
|
|
156
|
+
const input = { deviceId: o.id };
|
|
157
|
+
let result = o.cloud ? await client.call('collect', input) : await client.collect(input);
|
|
158
|
+
if (result.status === 'failed') throw cliError(result.error ?? 'collection_failed', '采集失败。', 502);
|
|
159
|
+
if (result.status !== 'pending' || !o.wait) return result;
|
|
160
|
+
const requestedAt = result.collection.requestedAt, deadline = now() + Number(o.wait) * 1000;
|
|
161
|
+
while (now() < deadline) {
|
|
162
|
+
await sleep(Math.min(3000, Math.max(1, deadline - now())));
|
|
163
|
+
const { device } = await show(), collection = device.collection;
|
|
164
|
+
if (collection?.requestedAt !== requestedAt) throw cliError('collection_changed', '采集请求已变化,请查看设备最新状态。', 409);
|
|
165
|
+
if (collection.status === 'completed') return { status: 'completed', device };
|
|
166
|
+
if (collection.status === 'timeout') break;
|
|
167
|
+
}
|
|
168
|
+
throw cliError('collection_timeout', '等待采集超时;请求可能仍在队列,先 show 查看结果,不要重复绑定。', 503);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function safeCode(error) {
|
|
172
|
+
const code = error.cliCode ?? error.message;
|
|
173
|
+
return typeof code === 'string' && /^[a-z][a-z_]{1,80}$/.test(code) ? code : 'command_failed';
|
|
174
|
+
}
|
|
175
|
+
export function exitCode(error) {
|
|
176
|
+
if (error.cliCode === 'invalid_arguments' || error.statusCode === 400) return 2;
|
|
177
|
+
if ([401, 403].includes(error.statusCode)) return 3;
|
|
178
|
+
if (error.statusCode === 409) return 4;
|
|
179
|
+
if (error.statusCode >= 500 || error.statusCode === 429 || error.statusCode === 410) return 5;
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
package/server/cloudAccess.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createUnifiedAccess } from './unifiedAccess.mjs';
|
|
|
4
4
|
|
|
5
5
|
const SESSION_AGE = 8 * 60 * 60 * 1000;
|
|
6
6
|
const COOKIE = '__Host-device-center';
|
|
7
|
-
const ENTRY_PAGES = new Set(['/', '/index.html', '/login', '/start', '/start.html', '/pair']);
|
|
7
|
+
const ENTRY_PAGES = new Set(['/', '/index.html', '/login', '/start', '/start.html', '/pair', '/cli']);
|
|
8
8
|
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
9
9
|
const reject = (message) => { throw new Error(message); };
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ export function pairingBrowserUrl(origin, pending) {
|
|
|
16
16
|
export async function openPairingBrowser(url, { platform = process.platform, spawnImpl = spawn, environment = process.env } = {}) {
|
|
17
17
|
const target = new URL(url);
|
|
18
18
|
cloudOrigin(target.origin);
|
|
19
|
-
if (
|
|
19
|
+
if (!['/pair', '/cli'].includes(target.pathname) || target.search || !/^#ticket=[a-f0-9]{64}&expires=\d+$/.test(target.hash)) throw new Error('无效的浏览器交接。');
|
|
20
20
|
const windows = platform === 'win32';
|
|
21
21
|
const command = platform === 'darwin' ? '/usr/bin/open' : platform === 'linux' ? 'xdg-open' : windows ? powershellPath(environment) : null;
|
|
22
22
|
if (!command) throw new Error('此系统尚不支持浏览器接入。');
|