device-center 0.2.0-beta.2 → 0.2.0-beta.3
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 +8 -4
- package/docs/agent-install.md +2 -2
- package/docs/friend-trial.md +2 -2
- package/docs/hosted-access.md +1 -1
- package/docs/i18n.md +32 -0
- package/docs/npm.md +7 -7
- package/docs/onboarding.md +1 -1
- package/docs/remote-access.md +2 -2
- package/docs/roadmap.md +6 -0
- package/index.html +1 -0
- package/login.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/server/index.mjs +2 -2
- package/src/api.js +2 -1
- package/src/device-state.js +13 -12
- package/src/i18n.css +61 -0
- package/src/i18n.js +174 -0
- package/src/install-prompt.js +19 -0
- package/src/login.js +14 -11
- package/src/main.js +126 -120
- package/src/messages.js +5880 -0
- package/src/onboarding.js +14 -9
- package/src/start.js +22 -6
- package/src/styles.css +1 -1
- package/start.html +23 -22
package/src/onboarding.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import { localizedInstallPrompt } from './install-prompt.js';
|
|
2
|
+
import { normalizeLocale } from './i18n.js';
|
|
3
|
+
|
|
1
4
|
export const OFFICIAL_CONTROL_ORIGIN = 'https://devices.owenshen.top';
|
|
2
5
|
// Pin the CLI shipped with this release; never silently install a moving tag.
|
|
3
|
-
export const PUBLISHED_DRIVER_VERSION = '0.2.0-beta.
|
|
6
|
+
export const PUBLISHED_DRIVER_VERSION = '0.2.0-beta.3';
|
|
7
|
+
const inputError = (key, message) => Object.assign(new Error(message), { translationKey: key });
|
|
4
8
|
export function connectCommand(value, shareSsh = false) {
|
|
5
9
|
let url;
|
|
6
|
-
try { url = new URL(value); } catch { throw
|
|
10
|
+
try { url = new URL(value); } catch { throw inputError('error.https', '请输入完整的 HTTPS 域名,例如 https://devices.example.com'); }
|
|
7
11
|
if (url.protocol !== 'https:' || url.origin !== value || url.username || url.password ||
|
|
8
|
-
!/^(?:[a-z0-9.-]+|\[[a-f0-9:]+\])$/i.test(url.hostname)) throw
|
|
12
|
+
!/^(?:[a-z0-9.-]+|\[[a-f0-9:]+\])$/i.test(url.hostname)) throw inputError('error.origin', '请输入 HTTPS 控制面地址,不包含路径、账号或参数。');
|
|
9
13
|
// The allowlist excludes shell syntax. Quotes also work in POSIX shells and PowerShell.
|
|
10
14
|
return `npx --yes device-center@${PUBLISHED_DRIVER_VERSION} connect --cloud '${url.origin}'${shareSsh ? ' --share-ssh' : ''} --confirm`;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
// A reproducible handoff, not an installer. Reading/copying it has no machine effects.
|
|
14
18
|
export function agentInstallPlan({ mode = 'local', origin, port = 5174, background = true } = {}) {
|
|
15
|
-
if (!['local', 'official', 'self-hosted'].includes(mode) || !Number.isInteger(port) || port < 1024 || port > 65535 || typeof background !== 'boolean') throw
|
|
19
|
+
if (!['local', 'official', 'self-hosted'].includes(mode) || !Number.isInteger(port) || port < 1024 || port > 65535 || typeof background !== 'boolean') throw inputError('error.install', '安装模式或本机端口无效。');
|
|
16
20
|
const controlOrigin = mode === 'local' ? null : mode === 'official' ? OFFICIAL_CONTROL_ORIGIN : origin;
|
|
17
21
|
if (controlOrigin) connectCommand(controlOrigin); // Same strict HTTPS and shell-safe origin validation.
|
|
18
|
-
if (mode === 'self-hosted' && !controlOrigin) throw
|
|
19
|
-
if (origin !== undefined && mode !== 'self-hosted') throw
|
|
22
|
+
if (mode === 'self-hosted' && !controlOrigin) throw inputError('error.custom', '请输入自建控制面的 HTTPS 地址。');
|
|
23
|
+
if (origin !== undefined && mode !== 'self-hosted') throw inputError('error.selfOnly', '只有自建模式可以填写控制面地址。');
|
|
20
24
|
const cli = '"$HOME/.local/bin/device-center"';
|
|
21
25
|
const url = `http://127.0.0.1:${port}`;
|
|
22
26
|
return { mode, version: PUBLISHED_DRIVER_VERSION, controlOrigin, background, url, mcpUrl: `${url}/mcp`, commands: {
|
|
@@ -33,6 +37,7 @@ export function agentInstallPlan({ mode = 'local', origin, port = 5174, backgrou
|
|
|
33
37
|
|
|
34
38
|
export function agentInstallPrompt(options) {
|
|
35
39
|
const plan = agentInstallPlan(options), c = plan.commands;
|
|
40
|
+
if (options?.locale && normalizeLocale(options.locale) !== 'zh-CN') return localizedInstallPrompt(plan, options.locale);
|
|
36
41
|
return `请帮我在正在操作的这台电脑安装 Device Center,并接入这台电脑上的智能体。
|
|
37
42
|
|
|
38
43
|
我的选择:${plan.mode === 'local' ? '本机使用,无需账号,不申请云端绑定' : plan.mode === 'official' ? '使用 Device Center 官方托管控制面和加密中转' : '接入我已经部署的自建控制面'}。
|
|
@@ -54,9 +59,9 @@ ${plan.controlOrigin ? `控制面:${plan.controlOrigin}。设备 Driver 仍在
|
|
|
54
59
|
此说明固定使用 ${plan.version},包含设备说明与手动采集。已有旧版需先核对升级;不能假称未运行的设备已接通。`;
|
|
55
60
|
}
|
|
56
61
|
export function logAccessCommands({ manager, target, path, label = 'app' }) {
|
|
57
|
-
if (![manager, target].every(value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)) || manager === target) throw
|
|
58
|
-
if (typeof path !== 'string' || !path.startsWith('/') || path.length > 4096 || /[\x00-\x1f\x7f]/.test(path)) throw
|
|
59
|
-
if (!/^[a-zA-Z0-9_-]{1,40}$/.test(label)) throw
|
|
62
|
+
if (![manager, target].every(value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)) || manager === target) throw inputError('error.devices', '请选择两台不同的已绑定设备。');
|
|
63
|
+
if (typeof path !== 'string' || !path.startsWith('/') || path.length > 4096 || /[\x00-\x1f\x7f]/.test(path)) throw inputError('error.path', '填写目标设备上日志的绝对路径。');
|
|
64
|
+
if (!/^[a-zA-Z0-9_-]{1,40}$/.test(label)) throw inputError('error.label', '标签仅允许字母、数字、下划线或连字符。');
|
|
60
65
|
const quotedPath = "'" + path.replaceAll("'", "'\"'\"'") + "'";
|
|
61
66
|
return { trust: `device-center access trust --peer ${target} --confirm`, grant: `device-center access grant --peer ${manager} --log ${quotedPath} --label ${label} --minutes 60 --confirm` };
|
|
62
67
|
}
|
package/src/start.js
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
|
-
import { agentInstallPrompt } from './onboarding.js';
|
|
1
|
+
import { agentInstallPrompt, agentInstallPlan, OFFICIAL_CONTROL_ORIGIN } from './onboarding.js';
|
|
2
|
+
import { initLanguage, getLocale, t } from './i18n.js';
|
|
2
3
|
|
|
3
4
|
const text = document.querySelector('#install-instructions');
|
|
4
5
|
const result = document.querySelector('#copy-result');
|
|
5
|
-
|
|
6
|
+
let mode = 'local';
|
|
7
|
+
function instructions() {
|
|
8
|
+
const options = { locale: getLocale(), mode, ...(mode === 'self-hosted' ? { origin: document.querySelector('#custom-origin').value.trim() } : {}) };
|
|
9
|
+
const link = document.querySelector('[data-hosted-dashboard]');
|
|
10
|
+
try {
|
|
11
|
+
const plan = agentInstallPlan(options);
|
|
12
|
+
const url = new URL('/login', plan.controlOrigin ?? OFFICIAL_CONTROL_ORIGIN);
|
|
13
|
+
url.searchParams.set('lang', getLocale()); link.href = url.href; link.hidden = false;
|
|
14
|
+
return agentInstallPrompt(options);
|
|
15
|
+
} catch (error) { link.hidden = true; throw error; }
|
|
16
|
+
}
|
|
17
|
+
const errorKey = error => error.translationKey ?? 'error.custom';
|
|
18
|
+
let copyGeneration = 0;
|
|
19
|
+
initLanguage({ onChange: () => { copyGeneration++; result.textContent = ''; try { text.value = instructions(); } catch (error) { text.value = ''; result.textContent = t(errorKey(error)); result.className = 'error'; } } });
|
|
6
20
|
document.addEventListener('click', async event => {
|
|
7
21
|
const button = event.target.closest('button[data-mode]');
|
|
8
22
|
if (!button || button.disabled) return;
|
|
9
|
-
const
|
|
23
|
+
const generation = ++copyGeneration;
|
|
24
|
+
mode = button.dataset.mode;
|
|
10
25
|
try {
|
|
11
|
-
text.value =
|
|
12
|
-
} catch (error) { text.value = ''; result.textContent = error
|
|
26
|
+
text.value = instructions();
|
|
27
|
+
} catch (error) { text.value = ''; result.textContent = t(errorKey(error)); result.className = 'error'; return; }
|
|
13
28
|
button.disabled = true;
|
|
14
29
|
let copied = false;
|
|
15
30
|
try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text.value); copied = true; } } catch { /* Keep manual copy available. */ }
|
|
31
|
+
if (generation !== copyGeneration) { button.disabled = false; return; }
|
|
16
32
|
if (!copied) {
|
|
17
33
|
document.querySelector('#instruction-details').open = true; text.focus(); text.select();
|
|
18
34
|
try { copied = document.execCommand('copy'); } catch { /* Selection is the fallback. */ }
|
|
19
35
|
}
|
|
20
36
|
result.className = copied ? 'success' : '';
|
|
21
|
-
result.textContent = copied ? '
|
|
37
|
+
result.textContent = t(copied ? 'guide.copied' : 'guide.manual');
|
|
22
38
|
button.disabled = false;
|
|
23
39
|
});
|
package/src/styles.css
CHANGED
|
@@ -8,7 +8,7 @@ button:disabled { opacity: .55; cursor: wait; }
|
|
|
8
8
|
button:focus-visible, input:focus-visible, textarea:focus-visible, summary:focus-visible, a:focus-visible { outline: 3px solid #b5d7bf; outline-offset: 3px; }
|
|
9
9
|
.app-shell { height: 100vh; height: 100dvh; display: grid; grid-template-columns: 192px minmax(0, 1fr); overflow: hidden; }
|
|
10
10
|
.sidebar { min-height: 0; padding: 20px 14px; background: #fff; border-right: 1px solid var(--line); display: flex; flex-direction: column; overflow: auto; overscroll-behavior: contain; }
|
|
11
|
-
.sidebar
|
|
11
|
+
.sidebar .project-navigation { min-height: 0; flex: 1; overflow: auto; }
|
|
12
12
|
.brand { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 10px; }
|
|
13
13
|
.brand-mark { width: 34px; height: 34px; flex: 0 0 auto; display: grid; place-items: center; color: white; background: #377c57; border-radius: 10px; }
|
|
14
14
|
.brand strong { display: block; font-size: 13px; }
|
package/start.html
CHANGED
|
@@ -3,43 +3,44 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<meta name="description" content="
|
|
7
|
-
<title>开始试用 · Device Center</title>
|
|
6
|
+
<meta name="description" data-i18n-content="guide.lead" content="把自己的闲置电脑接入本机智能体。">
|
|
7
|
+
<title data-i18n="guide.title">开始试用 · Device Center</title>
|
|
8
8
|
<link rel="stylesheet" href="/src/start.css">
|
|
9
|
+
<link rel="stylesheet" href="/src/i18n.css">
|
|
9
10
|
</head>
|
|
10
|
-
<body>
|
|
11
|
-
<header><a class="brand" href="/start">DC <span>Device Center</span></a><a href="/
|
|
11
|
+
<body class="start-page">
|
|
12
|
+
<header><a class="brand" href="/start" data-language-link>DC <span>Device Center</span></a><div class="header-actions"><div class="language-picker" data-language-picker></div><a href="/" data-language-link data-i18n="guide.open">进入设备面板 ↗</a></div></header>
|
|
12
13
|
<main>
|
|
13
14
|
<section class="hero">
|
|
14
|
-
<p class="eyebrow">公开试用 · 0.2.0-beta.
|
|
15
|
-
<h1>让你的 Agent
|
|
16
|
-
<p class="lead">查看设备资源、记住每台电脑的用途,并在单独授权后读取指定日志。把安装交给本机 Agent,从一台电脑开始。</p>
|
|
17
|
-
<p class="platform">macOS / Linux · Node.js 24+ · Windows 原生版暂未支持</p>
|
|
15
|
+
<p class="eyebrow" data-i18n="guide.badge">公开试用 · 0.2.0-beta.3 · MIT</p>
|
|
16
|
+
<h1 data-i18n="guide.hero">让你的 Agent 看见自己的闲置电脑。</h1>
|
|
17
|
+
<p class="lead" data-i18n="guide.lead">查看设备资源、记住每台电脑的用途,并在单独授权后读取指定日志。把安装交给本机 Agent,从一台电脑开始。</p>
|
|
18
|
+
<p class="platform" data-i18n="guide.platform">macOS / Linux · Node.js 24+ · Windows 原生版暂未支持</p>
|
|
18
19
|
</section>
|
|
19
20
|
<section aria-labelledby="install-title">
|
|
20
|
-
<h2 id="install-title"><span class="step">1</span>
|
|
21
|
+
<h2 id="install-title"><span class="step">1</span> <span data-i18n="guide.install">选一种方式,复制给本机 Agent</span></h2>
|
|
21
22
|
<div class="choices">
|
|
22
|
-
<article><h3>本机使用</h3><p>本机面板和 MCP,无需账号。适合先了解自己的电脑与已有设备资源。</p><button data-mode="local">复制本机安装说明</button></article>
|
|
23
|
-
<article><h3>官方托管</h3><p>用自己的账号管理设备,使用官方加密中转。Driver 和私钥仍留在设备上。</p><button class="primary" data-mode="official">复制托管安装说明</button></article>
|
|
23
|
+
<article><h3 data-i18n="guide.local">本机使用</h3><p data-i18n="guide.localDesc">本机面板和 MCP,无需账号。适合先了解自己的电脑与已有设备资源。</p><button data-mode="local" data-i18n="guide.localCopy">复制本机安装说明</button></article>
|
|
24
|
+
<article><h3 data-i18n="guide.hosted">官方托管</h3><p data-i18n="guide.hostedDesc">用自己的账号管理设备,使用官方加密中转。Driver 和私钥仍留在设备上。</p><button class="primary" data-mode="official" data-i18n="guide.hostedCopy">复制托管安装说明</button></article>
|
|
24
25
|
</div>
|
|
25
|
-
<p class="hint"
|
|
26
|
-
<details class="custom"><summary>已有自己的控制面?</summary><label for="custom-origin">自建控制面的 HTTPS 地址</label><div class="custom-row"><input id="custom-origin" type="url" placeholder="https://devices.example.com" autocomplete="off" spellcheck="false"><button data-mode="self-hosted">复制自建接入说明</button></div><p class="hint">连接已经部署的服务;不会替你部署服务器或迁移原有绑定。</p></details>
|
|
26
|
+
<p class="hint" data-i18n="guide.paste">粘贴给正在目标电脑上运行、能安装软件的 Agent。它会检查已有配置、安装固定版本、配置用户级后台服务及 MCP,再核验真实结果。网页本身不会安装软件。</p>
|
|
27
|
+
<details class="custom"><summary data-i18n="guide.custom">已有自己的控制面?</summary><label for="custom-origin" data-i18n="guide.address">自建控制面的 HTTPS 地址</label><div class="custom-row"><input id="custom-origin" type="url" placeholder="https://devices.example.com" autocomplete="off" spellcheck="false"><button data-mode="self-hosted" data-i18n="guide.customCopy">复制自建接入说明</button></div><p class="hint" data-i18n="guide.customDesc">连接已经部署的服务;不会替你部署服务器或迁移原有绑定。</p></details>
|
|
27
28
|
<p id="copy-result" role="status" aria-live="polite"></p>
|
|
28
|
-
<details id="instruction-details"><summary>查看或手动复制完整说明</summary><textarea id="install-instructions" aria-label="Agent 安装说明" rows="9" readonly spellcheck="false"></textarea></details>
|
|
29
|
+
<details id="instruction-details"><summary data-i18n="guide.preview">查看或手动复制完整说明</summary><textarea id="install-instructions" aria-label="Agent 安装说明" data-i18n-aria-label="guide.instructions" rows="9" readonly spellcheck="false"></textarea></details>
|
|
29
30
|
</section>
|
|
30
31
|
<section aria-labelledby="pair-title">
|
|
31
|
-
<h2 id="pair-title"><span class="step">2</span>
|
|
32
|
-
<ol><li>按 Agent
|
|
33
|
-
<p class="hint">本机模式跳过此步。绑定只允许已说明的资源回报;读取日志需要目标设备另外授权。</p>
|
|
32
|
+
<h2 id="pair-title"><span class="step">2</span> <span data-i18n="guide.pair">使用托管时,登录并认领自己的电脑</span></h2>
|
|
33
|
+
<ol><li><span data-i18n="guide.pair1">按 Agent 提示打开设备面板,使用自己的 App Services/个人站账号登录。没有账号时在账号页注册。</span> <a href="https://devices.owenshen.top/login" data-hosted-dashboard data-i18n="guide.loginLink">打开云端设备面板</a></li><li data-i18n="guide.pair2">在面板输入本机终端显示的五分钟配对码,核对完整公钥指纹后批准。密码、私钥和配对码不要发给别人或贴到 Agent 对话。</li><li data-i18n="guide.pair3">在另一台自己的电脑重复安装并绑定同一账号。没有真实回报的设备会显示未知或未回报。</li></ol>
|
|
34
|
+
<p class="hint" data-i18n="guide.localHint">本机模式跳过此步。绑定只允许已说明的资源回报;读取日志需要目标设备另外授权。</p>
|
|
34
35
|
</section>
|
|
35
36
|
<section aria-labelledby="try-title">
|
|
36
|
-
<h2 id="try-title"><span class="step">3</span>
|
|
37
|
-
<div class="checks"><div><h3>资源真实吗?</h3><p>分别查看内存、磁盘和最近采集时间。</p></div><div><h3>设备容易辨认吗?</h3><p>在“设备说明”填写名称、用途、标签。服务清单由你登记。</p></div><div><h3>更新状态顺畅吗?</h3><p>点“采集一次”;休眠或停止的 Driver 不会被伪装在线。旧 Driver 需要更新。</p></div><div><h3>Agent 能读清单吗?</h3><p>让本机 Agent 调用
|
|
38
|
-
<details><summary>遇到问题先查什么?</summary><p>确认 Node.js 为 24+、Driver
|
|
37
|
+
<h2 id="try-title"><span class="step">3</span> <span data-i18n="guide.try">试用时看这四件事</span></h2>
|
|
38
|
+
<div class="checks"><div><h3 data-i18n="guide.resources">资源真实吗?</h3><p data-i18n="guide.resourcesBody">分别查看内存、磁盘和最近采集时间。</p></div><div><h3 data-i18n="guide.names">设备容易辨认吗?</h3><p data-i18n="guide.namesBody">在“设备说明”填写名称、用途、标签。服务清单由你登记。</p></div><div><h3 data-i18n="guide.refresh">更新状态顺畅吗?</h3><p data-i18n="guide.refreshBody">点“采集一次”;休眠或停止的 Driver 不会被伪装在线。旧 Driver 需要更新。</p></div><div><h3 data-i18n="guide.agent">Agent 能读清单吗?</h3><p data-i18n="guide.agentBody">让本机 Agent 调用 device_center_list_resources,核对设备名称与用途。</p></div></div>
|
|
39
|
+
<details><summary data-i18n="guide.faq">遇到问题先查什么?</summary><p data-i18n="guide.faqBody">确认 Node.js 为 24+、Driver 正在运行、电脑已唤醒、网络及系统时间正常。</p><p data-i18n="guide.feedback">反馈时说明系统、版本、所选模式、失败步骤和去掉敏感信息的错误。</p></details>
|
|
39
40
|
</section>
|
|
40
|
-
<aside><strong>这版的范围</strong><p
|
|
41
|
+
<aside><strong data-i18n="guide.scope">这版的范围</strong><p data-i18n="guide.scopeBody">支持真实资源、设备说明、手动采集和经单独授权的日志尾读。</p><p data-i18n="guide.quota">官方中转为试用额度:每账号每日 100 MiB(UTC),每方向 16 KiB/s;不是无限带宽服务。</p></aside>
|
|
41
42
|
</main>
|
|
42
|
-
<footer><a href="/install/agent.md">Agent
|
|
43
|
+
<footer><a href="/install/agent.md" data-i18n="guide.full">Agent 完整安装指南(中文)</a><a href="/install/trial.md" data-i18n="guide.markdown">文字版试用指南(中文)</a><a href="https://www.npmjs.com/package/device-center/v/0.2.0-beta.3" target="_blank" rel="noopener noreferrer" data-i18n="guide.source">MIT npm 源码包 ↗</a><span data-i18n="guide.footer">本机安装 · 私钥留在设备 · 操作单独授权</span></footer>
|
|
43
44
|
<script type="module" src="/src/start.js"></script>
|
|
44
45
|
</body>
|
|
45
46
|
</html>
|