device-center 0.2.0-beta.7 → 0.2.0-beta.8
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 +5 -5
- package/docs/SKILL.md +1 -1
- package/docs/agent-install.md +2 -2
- package/docs/friend-trial.md +2 -2
- package/docs/hosted-access.md +8 -4
- package/docs/npm.md +8 -8
- package/docs/offline-install.md +6 -2
- package/docs/troubleshooting.md +9 -1
- package/docs/windows-acceptance.md +2 -2
- package/login.html +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/driver.mjs +57 -29
- package/scripts/service.mjs +18 -0
- package/server/drivers/cloudRegistry.mjs +19 -6
- package/server/drivers/connectionRecovery.mjs +10 -0
- package/server/drivers/windowsService.mjs +38 -9
- package/server/index.mjs +2 -2
- package/server/unifiedAccess.mjs +1 -1
- package/src/agent-setup.js +3 -3
- package/src/api.js +2 -1
- package/src/install-packages.js +9 -1
- package/src/installed-cli.js +6 -0
- package/src/login-flow.js +35 -0
- package/src/login.js +15 -20
- package/src/messages.js +210 -54
- package/src/onboarding.js +3 -3
- package/src/pair-recovery.js +4 -0
- package/src/pair.js +37 -5
- package/src/pairing-handoff.js +7 -2
- package/src/start.css +3 -0
- package/src/start.js +8 -5
- package/start.html +24 -17
package/src/pair.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { capturePairing, clearPairing } from './pairing-handoff.js';
|
|
2
2
|
import { api } from './api.js';
|
|
3
3
|
import { initLanguage, getLocale, t, h } from './i18n.js';
|
|
4
|
+
import { pairingFailureKind } from './pair-recovery.js';
|
|
5
|
+
import { installedCliCommand } from './installed-cli.js';
|
|
4
6
|
|
|
5
7
|
let handoff = capturePairing();
|
|
6
8
|
document.querySelector('link[href="/src/login.css"]').href = '/src/pair.css';
|
|
@@ -18,8 +20,12 @@ function render() {
|
|
|
18
20
|
else if (phase === 'waiting') root.innerHTML = heading('pair.bound') + `<p>${h('pair.reporting')}</p><p>${h('pair.noReply')}</p>${dashboard()}`;
|
|
19
21
|
else if (phase === 'done') root.innerHTML = `<div class="status-mark" aria-hidden="true">✓</div>` + heading('pair.done') + `<p>${h('pair.doneBody')}</p><p>${h('pair.noReply')}</p>${dashboard()}`;
|
|
20
22
|
else if (phase === 'report-timeout') root.innerHTML = heading('pair.bound') + `<p>${h('pair.reportTimeout')}</p>${dashboard()}`;
|
|
21
|
-
else if (phase === 'approval-unknown') root.innerHTML = heading('pair.approvalUnknown') + `<p>${h('pair.approvalUnknownBody')}</p>${dashboard()}`;
|
|
22
|
-
else
|
|
23
|
+
else if (phase === 'approval-unknown') root.innerHTML = heading('pair.approvalUnknown') + `<p>${h('pair.approvalUnknownBody')}</p>${handoff ? `<button id="check-result">${h('pair.checkResult')}</button>` : ''}${dashboard()}`;
|
|
24
|
+
else if (phase === 'retry') root.innerHTML = heading('pair.retryTitle') + `<p>${h('pair.retryBody')}</p><button id="retry">${h('pair.retry')}</button>`;
|
|
25
|
+
else {
|
|
26
|
+
const command = installedCliCommand(`connect --cloud '${location.origin}' --port 5174 --browser --confirm`, navigator.userAgent.includes('Windows') ? 'windows' : 'posix');
|
|
27
|
+
root.innerHTML = heading(phase === 'cancelled' ? 'pair.cancelled' : 'pair.missing') + `<p>${h(phase === 'cancelled' ? 'pair.cancelledBody' : 'pair.resumeBody')}</p>${phase === 'cancelled' ? '' : `<details><summary>${h('pair.resumeCommand')}</summary><p>${h('pair.targetTerminal')}</p><textarea readonly rows="5" aria-label="${h('pair.resumeCommand')}">${escape(command)}</textarea></details>`}<a class="primary" href="/start?lang=${getLocale()}">${h('pair.start')}</a>`;
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
async function watchReport(driverId, deadline) {
|
|
25
31
|
try {
|
|
@@ -31,6 +37,22 @@ async function watchReport(driverId, deadline) {
|
|
|
31
37
|
}
|
|
32
38
|
root.addEventListener('change', event => { if (event.target.id === 'share') shareSsh = event.target.checked; });
|
|
33
39
|
root.addEventListener('click', async event => {
|
|
40
|
+
if (event.target.id === 'retry' && phase === 'retry') { await loadPairing(); return; }
|
|
41
|
+
if (event.target.id === 'check-result' && phase === 'approval-unknown' && handoff) {
|
|
42
|
+
phase = 'loading'; render();
|
|
43
|
+
try {
|
|
44
|
+
const result = await api.browserPairingResult(handoff.ticket);
|
|
45
|
+
if (result.state === 'approved') { clearPairing(); handoff = null; phase = 'waiting'; render(); void watchReport(result.driverId, Date.now() + 90_000); }
|
|
46
|
+
else if (result.state === 'pending') await loadPairing();
|
|
47
|
+
else { phase = 'approval-unknown'; render(); }
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
if (pairingFailureKind(cause) === 'login') return;
|
|
50
|
+
if (pairingFailureKind(cause) === 'expired') { clearPairing(); handoff = null; phase = 'missing'; }
|
|
51
|
+
else phase = 'approval-unknown';
|
|
52
|
+
render();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
34
56
|
if (event.target.id === 'cancel' && phase === 'ready') { clearTimeout(expiresTimer); clearPairing(); handoff = null; phase = 'cancelled'; render(); return; }
|
|
35
57
|
if (event.target.id !== 'approve' || phase !== 'ready' || !handoff) return;
|
|
36
58
|
phase = 'approving'; render();
|
|
@@ -42,19 +64,29 @@ root.addEventListener('click', async event => {
|
|
|
42
64
|
if (cause.message === 'authentication_required') return;
|
|
43
65
|
if (['driver_rate_limited', 'driver_limit'].includes(cause.message)) { phase = 'ready'; error = cause.message === 'driver_limit' ? 'panel.386' : 'panel.385'; }
|
|
44
66
|
else if (['driver_pairing_expired', 'driver_pairing_unavailable', 'invalid_pairing_code', 'driver_pairing_confirmation_required'].includes(cause.message)) { clearPairing(); handoff = null; phase = 'missing'; }
|
|
45
|
-
else { clearTimeout(expiresTimer);
|
|
67
|
+
else { clearTimeout(expiresTimer); phase = 'approval-unknown'; }
|
|
46
68
|
render();
|
|
47
69
|
}
|
|
48
70
|
});
|
|
49
71
|
initLanguage({ onChange: render });
|
|
50
72
|
render();
|
|
51
|
-
|
|
73
|
+
async function loadPairing() {
|
|
74
|
+
if (!handoff || handoff.expiresAt <= Date.now()) { clearPairing(); handoff = null; phase = 'missing'; render(); return; }
|
|
75
|
+
clearTimeout(expiresTimer); phase = 'loading'; render();
|
|
52
76
|
try {
|
|
53
77
|
pairing = await api.lookupBrowserPairing(handoff.ticket);
|
|
54
78
|
phase = 'ready'; render();
|
|
55
79
|
expiresTimer = setTimeout(() => { if (phase !== 'ready') return; clearPairing(); handoff = null; phase = 'missing'; render(); }, Math.max(0, pairing.expiresAt - Date.now()));
|
|
56
80
|
} catch (cause) {
|
|
57
|
-
|
|
81
|
+
const kind = pairingFailureKind(cause);
|
|
82
|
+
if (kind === 'login') return;
|
|
83
|
+
if (kind === 'expired' && cause.message === 'driver_pairing_unavailable') {
|
|
84
|
+
phase = 'approval-unknown'; render(); return;
|
|
85
|
+
}
|
|
86
|
+
if (kind === 'expired') { clearPairing(); handoff = null; phase = 'missing'; }
|
|
87
|
+
else phase = 'retry';
|
|
88
|
+
render();
|
|
58
89
|
}
|
|
59
90
|
}
|
|
91
|
+
if (handoff) await loadPairing();
|
|
60
92
|
window.addEventListener('pagehide', () => { clearTimeout(timer); clearTimeout(expiresTimer); });
|
package/src/pairing-handoff.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pendingCli, clearCli } from './cli-handoff.js';
|
|
2
2
|
const KEY = 'device-center-pairing-v1';
|
|
3
|
+
const EXPIRED = 'device-center-pairing-expired';
|
|
3
4
|
const safeStorage = () => { try { return window.sessionStorage; } catch { return null; } };
|
|
4
5
|
const valid = (value, now) => value && /^[a-f0-9]{64}$/.test(value.ticket ?? '') && Number.isSafeInteger(value.expiresAt) && value.expiresAt > now && value.expiresAt <= now + 330_000;
|
|
5
6
|
|
|
@@ -7,11 +8,15 @@ export function pendingPairing({ storage = safeStorage(), now = Date.now() } = {
|
|
|
7
8
|
try {
|
|
8
9
|
const value = JSON.parse(storage.getItem(KEY));
|
|
9
10
|
if (valid(value, now)) return value;
|
|
11
|
+
if (/^[a-f0-9]{64}$/.test(value?.ticket ?? '') && Number.isSafeInteger(value?.expiresAt) && value.expiresAt <= now && now - value.expiresAt < 600_000) storage.setItem(EXPIRED, String(now));
|
|
10
12
|
storage.removeItem(KEY);
|
|
11
13
|
} catch { /* No token in error messages. */ }
|
|
12
14
|
return null;
|
|
13
15
|
}
|
|
14
|
-
export function clearPairing({ storage = safeStorage() } = {}) { try { storage.removeItem(KEY); } catch { /* memory is cleared by the caller */ } }
|
|
16
|
+
export function clearPairing({ storage = safeStorage() } = {}) { try { storage.removeItem(KEY); storage.removeItem(EXPIRED); } catch { /* memory is cleared by the caller */ } }
|
|
17
|
+
export function recentlyExpiredPairing({ storage = safeStorage(), now = Date.now() } = {}) {
|
|
18
|
+
try { const at = Number(storage.getItem(EXPIRED)); return at > 0 && at <= now && now - at < 600_000; } catch { return false; }
|
|
19
|
+
}
|
|
15
20
|
|
|
16
21
|
export function capturePairing({ location = window.location, history = window.history, storage = safeStorage(), now = Date.now() } = {}) {
|
|
17
22
|
if (!location.hash) return pendingPairing({ storage, now });
|
|
@@ -30,5 +35,5 @@ export function capturePairing({ location = window.location, history = window.hi
|
|
|
30
35
|
export function loginDestination(locale, { search = globalThis.window?.location?.search ?? '' } = {}) {
|
|
31
36
|
// Only this fixed management page can be selected without an active handoff.
|
|
32
37
|
// Never accept an arbitrary return URL from the login query.
|
|
33
|
-
return `${pendingCli() ? '/cli' : pendingPairing() ? '/pair' : new URLSearchParams(search).get('continue') === 'cli' ? '/cli' : '/'}?lang=${encodeURIComponent(locale)}`;
|
|
38
|
+
return `${pendingCli() ? '/cli' : pendingPairing() || recentlyExpiredPairing() ? '/pair' : new URLSearchParams(search).get('continue') === 'cli' ? '/cli' : '/'}?lang=${encodeURIComponent(locale)}`;
|
|
34
39
|
}
|
package/src/start.css
CHANGED
|
@@ -195,3 +195,6 @@ footer span { width: 100%; color: #8c807a; }
|
|
|
195
195
|
.package-download .primary { display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 10px 16px; border-radius: 4px; font-size: 14px; text-decoration: none; }
|
|
196
196
|
.package-digest { display: block; overflow-wrap: anywhere; font-size: 11px; line-height: 1.6; color: #a79892; }
|
|
197
197
|
@media (max-width: 480px) { .method-switch span { padding: 9px 4px; font-size: 12px; } }
|
|
198
|
+
.action-workbench > #os-options { margin-bottom: 22px; }
|
|
199
|
+
.package-followup { margin: 20px 0; padding-left: 22px; color: #c9bdb4; font-size: 13px; line-height: 1.9; }
|
|
200
|
+
.package-followup li { padding-left: 4px; margin: 8px 0; }
|
package/src/start.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderPackageChoices } from './install-packages.js';
|
|
1
|
+
import { renderPackageChoices, architectureChoices } from './install-packages.js';
|
|
2
2
|
import { agentInstallPlan, terminalInstallScript, OFFICIAL_CONTROL_ORIGIN } from './onboarding.js';
|
|
3
3
|
import { agentSetupPrompt } from './agent-setup.js';
|
|
4
4
|
import { initLanguage, getLocale, t } from './i18n.js';
|
|
@@ -9,11 +9,10 @@ const result = document.querySelector('#copy-result');
|
|
|
9
9
|
const customOrigin = document.querySelector('#custom-origin');
|
|
10
10
|
// Shared links only preselect UI choices; strict plan validation still gates copying.
|
|
11
11
|
const guideParams = new URLSearchParams(location.search);
|
|
12
|
-
let mode = ['local', 'official', 'self-hosted'].includes(guideParams.get('mode')) ? guideParams.get('mode') : '
|
|
13
|
-
let method = ['manual', 'package'].includes(guideParams.get('method')) ? guideParams.get('method') : '
|
|
12
|
+
let mode = ['local', 'official', 'self-hosted'].includes(guideParams.get('mode')) ? guideParams.get('mode') : 'official';
|
|
13
|
+
let method = ['agent', 'manual', 'package'].includes(guideParams.get('method')) ? guideParams.get('method') : 'package';
|
|
14
14
|
let os = ['windows', 'macos', 'linux'].includes(guideParams.get('os')) ? guideParams.get('os') : navigator.userAgent.includes('Linux') ? 'linux' : navigator.userAgent.includes('Windows') ? 'windows' : 'macos';
|
|
15
15
|
let packageArch = os === 'macos' ? 'arm64' : 'x64';
|
|
16
|
-
document.querySelector('#package-arch').value = packageArch;
|
|
17
16
|
let packages = [];
|
|
18
17
|
let packagesLoading = true;
|
|
19
18
|
let packagesFailed = false;
|
|
@@ -66,11 +65,15 @@ function translatedText(selector, key) {
|
|
|
66
65
|
}
|
|
67
66
|
function updateGuidance() {
|
|
68
67
|
const manual = method === 'manual';
|
|
68
|
+
document.querySelector('.mode-options').hidden = method === 'package';
|
|
69
69
|
document.querySelector('#windows-candidate').hidden = os !== 'windows';
|
|
70
70
|
document.querySelector('#package-options').hidden = method !== 'package';
|
|
71
71
|
document.querySelector('#copy-options').hidden = method === 'package';
|
|
72
72
|
document.querySelector('#next-action').hidden = method === 'package';
|
|
73
|
-
|
|
73
|
+
const select = document.querySelector('#package-arch');
|
|
74
|
+
select.replaceChildren(...architectureChoices(os).map(([value, label]) => new Option(label, value)));
|
|
75
|
+
select.value = packageArch;
|
|
76
|
+
translatedText('#package-run', `guide.package.archHelp.${os}`);
|
|
74
77
|
renderPackageChoices(document.querySelector('#package-downloads'), { packages, os, arch: packageArch, loading: packagesLoading, failed: packagesFailed });
|
|
75
78
|
translatedText('#copy-label', manual ? 'guide.manual.copy' : { local: 'guide.localCopy', official: 'guide.hostedCopy', 'self-hosted': 'guide.customCopy' }[mode]);
|
|
76
79
|
translatedText('#copy-hint', manual ? 'guide.manual.copyHint' : 'guide.focus.copyHint');
|
package/start.html
CHANGED
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
<h1 id="hero-title"><span data-i18n="guide.visual.heroLine1">少切电脑。</span><span data-i18n="guide.visual.heroLine2">少搬日志。</span></h1>
|
|
32
32
|
<p class="lead" data-i18n="guide.visual.lead">让本机 Agent 查看其他设备的状态与授权日志。</p>
|
|
33
33
|
<div class="hero-actions"><a class="button primary hero-cta" href="#install-title"><span data-i18n="guide.visual.begin">开始接入</span><svg class="icon" aria-hidden="true"><use href="#icon-arrow"/></svg></a><span class="cta-hint" data-i18n="guide.focus.firstTry">第一次用?先选本机,无需账号。</span></div>
|
|
34
|
-
<p class="platform" data-i18n="guide.platform">macOS / Linux / Windows 11
|
|
35
|
-
<p class="release-note"><span class="release-dot" aria-hidden="true"></span><span data-i18n="guide.badge">开发预览 · 0.2.0-beta.
|
|
34
|
+
<p class="platform" data-i18n="guide.platform">macOS / Linux / Windows 11 · beta</p>
|
|
35
|
+
<p class="release-note"><span class="release-dot" aria-hidden="true"></span><span data-i18n="guide.badge">开发预览 · 0.2.0-beta.8 · MIT</span></p>
|
|
36
36
|
</div>
|
|
37
37
|
<figure class="scene" data-paused="false">
|
|
38
38
|
<figcaption class="scene-caption"><span><span class="caption-cross" aria-hidden="true">+</span><span data-i18n="guide.focus.example">工作原理示意 · 无需操作</span></span><button class="motion-toggle" type="button" data-i18n-aria-label="guide.visual.pauseMotion" data-i18n-title="guide.visual.pauseMotion" aria-label="暂停动效" title="暂停动效"><svg class="icon pause-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M8 5v14M16 5v14"/></svg><svg class="icon play-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="m8 5 11 7-11 7Z"/></svg></button></figcaption>
|
|
@@ -81,20 +81,29 @@
|
|
|
81
81
|
<div class="section-heading">
|
|
82
82
|
<p class="eyebrow"><span class="action-marker" aria-hidden="true">↘</span><span data-i18n="guide.focus.start">开始操作</span></p>
|
|
83
83
|
<h2 id="install-title" tabindex="-1" data-i18n="guide.focus.startTitle">接入,从这里开始</h2>
|
|
84
|
-
<p class="section-intro" data-i18n="guide.focus.startBody"
|
|
84
|
+
<p class="section-intro" data-i18n="guide.focus.startBody">先选择目标系统和安装方式。推荐下载内置运行环境的安装包;使用模式在安装器里确认。</p>
|
|
85
85
|
<p class="section-note" data-i18n="guide.visual.noInstall">复制不会执行安装</p>
|
|
86
86
|
</div>
|
|
87
87
|
<div class="action-workbench">
|
|
88
|
+
<fieldset class="pairing-options" id="os-options">
|
|
89
|
+
<legend data-i18n="guide.os.title">目标电脑的系统</legend>
|
|
90
|
+
<div class="pairing-choices">
|
|
91
|
+
<label><input type="radio" name="install-os" value="macos" checked><span>macOS</span></label>
|
|
92
|
+
<label><input type="radio" name="install-os" value="linux"><span>Linux</span></label>
|
|
93
|
+
<label><input type="radio" name="install-os" value="windows"><span>Windows 11</span></label>
|
|
94
|
+
</div>
|
|
95
|
+
</fieldset>
|
|
96
|
+
|
|
88
97
|
<fieldset class="mode-options">
|
|
89
98
|
<legend><span class="action-number" aria-hidden="true">01</span><span data-i18n="guide.focus.select">选择使用方式</span></legend>
|
|
90
99
|
<div class="choices">
|
|
91
100
|
<label class="mode-choice">
|
|
92
|
-
<input type="radio" name="install-mode" value="local"
|
|
101
|
+
<input type="radio" name="install-mode" value="local">
|
|
93
102
|
<span class="choice-icon"><svg class="icon" aria-hidden="true"><use href="#icon-computer"/></svg></span>
|
|
94
103
|
<span class="choice-copy"><strong data-i18n="guide.local">本机使用</strong><span data-i18n="guide.visual.localBody">本机面板 + MCP,无需账号。</span></span>
|
|
95
104
|
</label>
|
|
96
105
|
<label class="mode-choice">
|
|
97
|
-
<input type="radio" name="install-mode" value="official">
|
|
106
|
+
<input type="radio" name="install-mode" value="official" checked>
|
|
98
107
|
<span class="choice-icon"><svg class="icon" aria-hidden="true"><use href="#icon-cloud"/></svg></span>
|
|
99
108
|
<span class="choice-copy"><strong data-i18n="guide.hosted">官方托管</strong><span data-i18n="guide.visual.hostedBody">用自己的账号绑定设备,使用加密中转。</span></span>
|
|
100
109
|
</label>
|
|
@@ -110,31 +119,29 @@
|
|
|
110
119
|
</details>
|
|
111
120
|
</fieldset>
|
|
112
121
|
<div class="copy-step">
|
|
113
|
-
<fieldset class="pairing-options" id="os-options">
|
|
114
|
-
<legend data-i18n="guide.os.title">目标电脑的系统</legend>
|
|
115
|
-
<div class="pairing-choices">
|
|
116
|
-
<label><input type="radio" name="install-os" value="macos" checked><span>macOS</span></label>
|
|
117
|
-
<label><input type="radio" name="install-os" value="linux"><span>Linux</span></label>
|
|
118
|
-
<label><input type="radio" name="install-os" value="windows"><span>Windows 11</span></label>
|
|
119
|
-
</div>
|
|
120
|
-
</fieldset>
|
|
121
122
|
<p class="hint" id="windows-candidate" data-i18n="guide.windowsCandidate" hidden>Windows 11 预览版 · 安装后核对绑定与后台状态。</p>
|
|
122
123
|
<fieldset class="method-options">
|
|
123
124
|
<legend class="action-heading"><span class="action-number" aria-hidden="true">02</span><span data-i18n="guide.method.title">选择安装方式</span></legend>
|
|
124
125
|
<div class="method-switch">
|
|
125
|
-
<label><input type="radio" name="install-method" value="agent"
|
|
126
|
-
<label><input type="radio" name="install-method" value="package"><span data-i18n="guide.method.package">下载安装包</span></label>
|
|
126
|
+
<label><input type="radio" name="install-method" value="agent"><span data-i18n="guide.method.agent">交给 Agent</span></label>
|
|
127
|
+
<label><input type="radio" name="install-method" value="package" checked><span data-i18n="guide.method.package">下载安装包</span></label>
|
|
127
128
|
<label><input type="radio" name="install-method" value="manual"><span data-i18n="guide.method.script">复制脚本</span></label>
|
|
128
129
|
</div>
|
|
129
130
|
</fieldset>
|
|
130
131
|
<div id="package-options" hidden>
|
|
131
132
|
<p class="hint" data-i18n="guide.package.description">内含程序、运行环境和依赖,无需系统 Node.js 或 npm 下载。</p>
|
|
132
133
|
<label class="package-architecture"><span data-i18n="guide.package.arch">处理器架构</span>
|
|
133
|
-
<select id="package-arch"
|
|
134
|
+
<select id="package-arch" aria-describedby="package-run"></select>
|
|
134
135
|
</label>
|
|
135
136
|
<div id="package-downloads" aria-live="polite"></div>
|
|
136
137
|
<p class="hint" id="package-run"></p>
|
|
137
138
|
<p class="hint" data-i18n="guide.package.confirmations">在安装向导中确认本机、官方或已有自建服务,再确认后台运行与本人配对。</p>
|
|
139
|
+
<ol class="package-followup">
|
|
140
|
+
<li data-i18n="guide.package.step1"></li>
|
|
141
|
+
<li data-i18n="guide.package.step2"></li>
|
|
142
|
+
<li data-i18n="guide.package.step3"></li>
|
|
143
|
+
<li data-i18n="guide.package.step4"></li>
|
|
144
|
+
</ol>
|
|
138
145
|
<a href="/install/offline.md" data-i18n="guide.package.details">查看安装包说明</a>
|
|
139
146
|
</div>
|
|
140
147
|
<div id="copy-options">
|
|
@@ -170,7 +177,7 @@
|
|
|
170
177
|
<details><summary data-i18n="guide.faq">遇到问题先查什么?</summary><div class="detail-content"><p data-i18n="guide.faqBody"></p><p data-i18n="guide.feedback"></p></div></details>
|
|
171
178
|
</section>
|
|
172
179
|
</main>
|
|
173
|
-
<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.
|
|
180
|
+
<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.8" target="_blank" rel="noopener noreferrer" data-i18n="guide.source">MIT npm 源码包 ↗</a><span data-i18n="guide.footer">本机安装 · 私钥留在设备 · 操作单独授权</span></footer>
|
|
174
181
|
<script type="module" src="/src/start.js"></script>
|
|
175
182
|
</body>
|
|
176
183
|
</html>
|