@bulolo/hermes-link 0.3.4 → 0.3.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 +330 -439
- package/README.zh-CN.md +739 -0
- package/dist/{chunk-ELQBIHDQ.js → chunk-7IVSOP5F.js} +300 -132
- package/dist/cli/index.js +12 -7
- package/dist/http/app.js +1 -1
- package/package.json +2 -2
- package/README.en.md +0 -630
|
@@ -1546,9 +1546,9 @@ async function discoverRouteCandidates(options) {
|
|
|
1546
1546
|
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
1547
1547
|
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
1548
1548
|
const preferredUrls = [
|
|
1549
|
+
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
1549
1550
|
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
1550
|
-
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port))
|
|
1551
|
-
...lanIps.map((ip) => buildDirectUrl(ip, options.port))
|
|
1551
|
+
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port))
|
|
1552
1552
|
];
|
|
1553
1553
|
return { lanIps, publicIpv4s, publicIpv6s, preferredUrls, environment };
|
|
1554
1554
|
}
|
|
@@ -8885,10 +8885,29 @@ async function startLinkService(options) {
|
|
|
8885
8885
|
});
|
|
8886
8886
|
const rootRouter = new Router13();
|
|
8887
8887
|
rootRouter.get("/pair", async (ctx) => {
|
|
8888
|
-
const
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8888
|
+
const sessionId = typeof ctx.query.session_id === "string" ? ctx.query.session_id : "";
|
|
8889
|
+
if (!sessionId) {
|
|
8890
|
+
ctx.status = 400;
|
|
8891
|
+
ctx.type = "text/plain";
|
|
8892
|
+
ctx.body = "Missing session_id";
|
|
8893
|
+
return;
|
|
8894
|
+
}
|
|
8895
|
+
const session = await readPairingSession(sessionId, paths);
|
|
8896
|
+
if (!session) {
|
|
8897
|
+
ctx.status = 404;
|
|
8898
|
+
ctx.type = "text/plain";
|
|
8899
|
+
ctx.body = "Pairing session not found";
|
|
8900
|
+
return;
|
|
8901
|
+
}
|
|
8902
|
+
const claimed = await isPairingSessionClaimed(sessionId, paths);
|
|
8903
|
+
ctx.set("content-type", "text/html; charset=utf-8");
|
|
8904
|
+
ctx.set("cache-control", "no-store");
|
|
8905
|
+
ctx.body = await renderPairingPage({
|
|
8906
|
+
session,
|
|
8907
|
+
claimed,
|
|
8908
|
+
version: LINK_VERSION,
|
|
8909
|
+
linkId: identity.link_id ?? session.link_id
|
|
8910
|
+
});
|
|
8892
8911
|
});
|
|
8893
8912
|
rootRouter.get("/api/v1/status", async (ctx) => {
|
|
8894
8913
|
await authenticateRequest(ctx, paths);
|
|
@@ -8980,151 +8999,300 @@ async function startLinkService(options) {
|
|
|
8980
8999
|
};
|
|
8981
9000
|
return { app, server, stop };
|
|
8982
9001
|
}
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
9002
|
+
function escapeHtml(s) {
|
|
9003
|
+
return s.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """);
|
|
9004
|
+
}
|
|
9005
|
+
function formatDate(iso, locale = "zh-CN") {
|
|
9006
|
+
const d = new Date(iso);
|
|
9007
|
+
if (!Number.isFinite(d.getTime())) return iso;
|
|
9008
|
+
return d.toLocaleString(locale, { hour12: locale !== "zh-CN" });
|
|
9009
|
+
}
|
|
9010
|
+
async function renderPairingPage(options) {
|
|
9011
|
+
const { session, claimed, version, linkId } = options;
|
|
9012
|
+
const isExpired = !claimed && isPairingSessionExpired(session);
|
|
9013
|
+
const qrPayload = JSON.stringify({
|
|
8986
9014
|
kind: "hermes_link_pairing",
|
|
8987
9015
|
version: 1,
|
|
8988
|
-
link_id:
|
|
8989
|
-
display_name:
|
|
8990
|
-
session_id:
|
|
8991
|
-
code:
|
|
8992
|
-
preferred_urls:
|
|
8993
|
-
})
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
width: 240,
|
|
9001
|
-
errorCorrectionLevel: "M"
|
|
9002
|
-
});
|
|
9003
|
-
qrHtml = `<div class="qr">${qrSvg}</div>`;
|
|
9004
|
-
} catch {
|
|
9005
|
-
qrHtml = "";
|
|
9006
|
-
}
|
|
9007
|
-
}
|
|
9008
|
-
const baseUrl = `http://127.0.0.1:${port}`;
|
|
9009
|
-
return `<!DOCTYPE html>
|
|
9016
|
+
link_id: session.link_id,
|
|
9017
|
+
display_name: session.display_name,
|
|
9018
|
+
session_id: session.session_id,
|
|
9019
|
+
code: session.code,
|
|
9020
|
+
preferred_urls: session.preferred_urls
|
|
9021
|
+
});
|
|
9022
|
+
const qrSvg = await QRCode.toString(qrPayload, { type: "svg", margin: 1, width: 320, errorCorrectionLevel: "M" });
|
|
9023
|
+
const qrDataUri = `data:image/svg+xml;base64,${Buffer.from(qrSvg).toString("base64")}`;
|
|
9024
|
+
const currentUrl = session.local_api_url.replace(/\/+$/u, "");
|
|
9025
|
+
const expiresAtMs = Date.parse(session.expires_at);
|
|
9026
|
+
const initialState = claimed ? "claimed" : isExpired ? "expired" : "waiting";
|
|
9027
|
+
return `<!doctype html>
|
|
9010
9028
|
<html lang="en">
|
|
9011
9029
|
<head>
|
|
9012
|
-
<meta charset="
|
|
9013
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
9014
|
-
<
|
|
9030
|
+
<meta charset="utf-8" />
|
|
9031
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9032
|
+
<meta name="color-scheme" content="light dark" />
|
|
9033
|
+
<title>Hermes Link Pairing</title>
|
|
9015
9034
|
<style>
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9035
|
+
:root {
|
|
9036
|
+
color-scheme: light dark;
|
|
9037
|
+
--bg: #f4f5f7;
|
|
9038
|
+
--panel: rgba(255,255,255,0.78);
|
|
9039
|
+
--panel-strong: rgba(255,255,255,0.94);
|
|
9040
|
+
--text: #151922;
|
|
9041
|
+
--muted: #5f6673;
|
|
9042
|
+
--line: rgba(21,25,34,0.12);
|
|
9043
|
+
--accent: #2d5cff;
|
|
9044
|
+
--accent-soft: rgba(45,92,255,0.12);
|
|
9045
|
+
--good: #0b8457;
|
|
9046
|
+
--shadow: 0 24px 90px rgba(18,24,38,0.12);
|
|
9047
|
+
}
|
|
9048
|
+
@media (prefers-color-scheme: dark) {
|
|
9049
|
+
:root {
|
|
9050
|
+
--bg: #0c1017;
|
|
9051
|
+
--panel: rgba(16,20,28,0.78);
|
|
9052
|
+
--panel-strong: rgba(16,20,28,0.94);
|
|
9053
|
+
--text: #eef2f8;
|
|
9054
|
+
--muted: #9ba4b3;
|
|
9055
|
+
--line: rgba(255,255,255,0.12);
|
|
9056
|
+
--accent: #8ab4ff;
|
|
9057
|
+
--accent-soft: rgba(138,180,255,0.12);
|
|
9058
|
+
--good: #67d7a7;
|
|
9059
|
+
--shadow: 0 24px 90px rgba(0,0,0,0.45);
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
* { box-sizing: border-box; }
|
|
9063
|
+
body { margin: 0; min-height: 100vh; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: linear-gradient(180deg, var(--bg) 0%, color-mix(in srgb, var(--bg) 88%, var(--accent) 12%) 100%); }
|
|
9064
|
+
.shell { min-height: 100vh; display: grid; place-items: center; padding: 28px 18px; }
|
|
9065
|
+
.panel { width: min(1040px, 100%); border: 1px solid var(--line); border-radius: 28px; background: var(--panel); box-shadow: var(--shadow); backdrop-filter: blur(18px); overflow: hidden; }
|
|
9066
|
+
.hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, 390px); gap: 0; }
|
|
9067
|
+
.copy { padding: 34px 34px 30px; border-right: 1px solid var(--line); }
|
|
9068
|
+
.header-row { display: flex; justify-content: space-between; align-items: center; }
|
|
9069
|
+
.eyebrow { display: inline-flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font-size: 13px; font-weight: 600; }
|
|
9070
|
+
.lang-btn { background: var(--accent-soft); color: var(--accent); border: none; border-radius: 999px; padding: 6px 14px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; line-height: 1; }
|
|
9071
|
+
.lang-btn:hover { opacity: 0.75; }
|
|
9072
|
+
h1 { margin: 18px 0 12px; font-size: clamp(34px, 4vw, 52px); line-height: 1.02; }
|
|
9073
|
+
.subtitle { max-width: 42ch; margin: 0; color: var(--muted); font-size: 16px; line-height: 1.7; }
|
|
9074
|
+
.meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-top: 26px; }
|
|
9075
|
+
.meta { padding: 16px 16px 15px; border-radius: 18px; background: var(--panel-strong); border: 1px solid var(--line); }
|
|
9076
|
+
.meta-label { display: block; color: var(--muted); font-size: 12px; line-height: 1.4; margin-bottom: 8px; }
|
|
9077
|
+
.meta-value { font-size: 15px; line-height: 1.5; word-break: break-word; }
|
|
9078
|
+
.steps { display: grid; gap: 10px; margin-top: 18px; }
|
|
9079
|
+
.step { display: flex; gap: 12px; align-items: flex-start; padding: 14px 16px; border: 1px solid var(--line); border-radius: 18px; background: var(--panel-strong); }
|
|
9080
|
+
.step-badge { flex: none; width: 26px; height: 26px; border-radius: 999px; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent); font-size: 13px; font-weight: 600; }
|
|
9081
|
+
.step-title { font-size: 14px; line-height: 1.45; margin: 0; font-weight: 600; }
|
|
9082
|
+
.step-copy { margin: 3px 0 0; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
9083
|
+
.hint { margin: 10px 0 0; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
9084
|
+
.qr { padding: 26px 26px 30px; background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0)); }
|
|
9085
|
+
.card { border: 1px solid var(--line); border-radius: 24px; background: var(--panel-strong); padding: 20px; }
|
|
9086
|
+
.status { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
|
|
9087
|
+
.status-title { margin: 0; font-size: 18px; line-height: 1.3; }
|
|
9088
|
+
.pill { display: inline-flex; align-items: center; justify-content: center; padding: 7px 11px; border-radius: 999px; background: rgba(11,132,87,0.12); color: var(--good); font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
9089
|
+
.qr-frame { display: grid; place-items: center; padding: 18px; border-radius: 24px; background: linear-gradient(180deg, rgba(45,92,255,0.06), rgba(45,92,255,0)); border: 1px solid var(--line); }
|
|
9090
|
+
.qr-frame img { width: min(100%, 300px); aspect-ratio: 1; display: block; border-radius: 18px; background: #fff; padding: 14px; }
|
|
9091
|
+
.manual { margin-top: 16px; border: 1px solid var(--line); border-radius: 18px; overflow: hidden; }
|
|
9092
|
+
.manual-row { display: flex; flex-direction: column; gap: 2px; padding: 12px 16px; background: rgba(0,0,0,0.03); }
|
|
9093
|
+
.manual-row + .manual-row { border-top: 1px solid var(--line); }
|
|
9094
|
+
.manual-label { font-size: 11px; color: var(--muted); letter-spacing: 0.04em; }
|
|
9095
|
+
.manual-value { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 14px; word-break: break-all; user-select: all; }
|
|
9096
|
+
.manual-value.code { font-size: 20px; letter-spacing: 0.16em; text-align: center; }
|
|
9097
|
+
.footer { display: flex; justify-content: space-between; gap: 18px; flex-wrap: wrap; padding-top: 16px; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
9098
|
+
@media (max-width: 920px) { .hero { grid-template-columns: 1fr; } .copy { border-right: none; border-bottom: 1px solid var(--line); } }
|
|
9099
|
+
@media (max-width: 640px) { .copy, .qr { padding: 22px 18px; } .meta-grid { grid-template-columns: 1fr; } .status { align-items: flex-start; flex-direction: column; } .code { font-size: 18px; letter-spacing: 0.14em; } }
|
|
9036
9100
|
</style>
|
|
9037
9101
|
</head>
|
|
9038
9102
|
<body>
|
|
9039
|
-
<
|
|
9040
|
-
<
|
|
9041
|
-
|
|
9103
|
+
<main class="shell">
|
|
9104
|
+
<section class="panel">
|
|
9105
|
+
<div class="hero">
|
|
9106
|
+
<div class="copy">
|
|
9107
|
+
<div class="header-row">
|
|
9108
|
+
<span class="eyebrow">Hermes Link \xB7 ${escapeHtml(version)}</span>
|
|
9109
|
+
<button id="langToggle" class="lang-btn">\u4E2D\u6587</button>
|
|
9110
|
+
</div>
|
|
9111
|
+
<h1 data-i18n="h1">Complete Pairing in the App</h1>
|
|
9112
|
+
<p class="subtitle" data-i18n="subtitle">Scan the QR code or enter the connect token to link this device.</p>
|
|
9113
|
+
<div class="meta-grid">
|
|
9114
|
+
<div class="meta">
|
|
9115
|
+
<span class="meta-label" data-i18n="metaLocalUrl">Local Address</span>
|
|
9116
|
+
<div class="meta-value">${escapeHtml(currentUrl)}</div>
|
|
9117
|
+
</div>
|
|
9118
|
+
<div class="meta">
|
|
9119
|
+
<span class="meta-label">Link ID</span>
|
|
9120
|
+
<div class="meta-value">${escapeHtml(linkId)}</div>
|
|
9121
|
+
</div>
|
|
9122
|
+
<div class="meta">
|
|
9123
|
+
<span class="meta-label" data-i18n="metaConnectToken">Connect Token</span>
|
|
9124
|
+
<div class="meta-value">${escapeHtml(session.code)}</div>
|
|
9125
|
+
</div>
|
|
9126
|
+
<div class="meta">
|
|
9127
|
+
<span class="meta-label" data-i18n="metaExpires">Expires</span>
|
|
9128
|
+
<div class="meta-value" id="expiresValue" data-iso="${escapeHtml(session.expires_at)}">${escapeHtml(formatDate(session.expires_at, "en-US"))}</div>
|
|
9129
|
+
</div>
|
|
9130
|
+
</div>
|
|
9131
|
+
<div class="steps">
|
|
9132
|
+
<div class="step">
|
|
9133
|
+
<div class="step-badge">1</div>
|
|
9134
|
+
<div>
|
|
9135
|
+
<p class="step-title" data-i18n="step1Title">Open “Connect Hermes Link” in the App</p>
|
|
9136
|
+
<p class="step-copy" data-i18n="step1Copy">Find the “Connect Link” entry in the App, then scan the QR code or enter the token manually. The App will switch to this Link automatically after pairing.</p>
|
|
9137
|
+
</div>
|
|
9138
|
+
</div>
|
|
9139
|
+
<div class="step">
|
|
9140
|
+
<div class="step-badge">2</div>
|
|
9141
|
+
<div>
|
|
9142
|
+
<p class="step-title" data-i18n="step2Title">Scan the QR code, or enter the address and token manually</p>
|
|
9143
|
+
<p class="step-copy" data-i18n="step2Copy">If scanning is inconvenient, enter the address and connect token below in the App.</p>
|
|
9144
|
+
</div>
|
|
9145
|
+
</div>
|
|
9146
|
+
<div class="step">
|
|
9147
|
+
<div class="step-badge">3</div>
|
|
9148
|
+
<div>
|
|
9149
|
+
<p class="step-title" data-i18n="step3Title">This page will update automatically once pairing is complete</p>
|
|
9150
|
+
<p class="step-copy" id="statusHint">${initialState === "claimed" ? "Pairing complete. You can close this page." : initialState === "expired" ? "This QR code has expired. Please run hermeslink pair again." : "Open the App to scan, or copy the connect token for manual entry."}</p>
|
|
9151
|
+
</div>
|
|
9152
|
+
</div>
|
|
9153
|
+
</div>
|
|
9154
|
+
<p class="hint" data-i18n="hint">You can keep this page open to check pairing status later. If pairing has already succeeded, the page will not prompt for a re-scan.</p>
|
|
9155
|
+
</div>
|
|
9156
|
+
<div class="qr">
|
|
9157
|
+
<div class="card">
|
|
9158
|
+
<div class="status">
|
|
9159
|
+
<h2 class="status-title" id="statusTitle">${initialState === "claimed" ? "Pairing Complete" : initialState === "expired" ? "Pairing Expired" : "Waiting for App to Scan"}</h2>
|
|
9160
|
+
<span class="pill" id="statusPill">${initialState === "claimed" ? "Scanned" : initialState === "expired" ? "Expired" : "Waiting"}</span>
|
|
9161
|
+
</div>
|
|
9162
|
+
<div class="qr-frame">
|
|
9163
|
+
<img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
|
|
9164
|
+
</div>
|
|
9165
|
+
<div class="manual">
|
|
9166
|
+
<div class="manual-row">
|
|
9167
|
+
<span class="manual-label" data-i18n="manualAddrLabel">Address (pick any that works)</span>
|
|
9168
|
+
${session.preferred_urls.map((u) => `<span class="manual-value">${escapeHtml(u)}</span>`).join("\n ")}
|
|
9169
|
+
</div>
|
|
9170
|
+
<div class="manual-row">
|
|
9171
|
+
<span class="manual-label" data-i18n="manualTokenLabel">Connect Token</span>
|
|
9172
|
+
<span class="manual-value code">${escapeHtml(session.code)}</span>
|
|
9173
|
+
</div>
|
|
9174
|
+
</div>
|
|
9175
|
+
</div>
|
|
9176
|
+
</div>
|
|
9177
|
+
</div>
|
|
9178
|
+
</section>
|
|
9179
|
+
</main>
|
|
9180
|
+
<script>
|
|
9181
|
+
const sessionId = ${JSON.stringify(session.session_id)};
|
|
9182
|
+
const expiresAtMs = ${Number.isFinite(expiresAtMs) ? String(expiresAtMs) : "Number.NaN"};
|
|
9183
|
+
const initialClaimed = ${JSON.stringify(claimed)};
|
|
9042
9184
|
|
|
9043
|
-
|
|
9185
|
+
const T = {
|
|
9186
|
+
zh: {
|
|
9187
|
+
h1: '\u5728 App \u91CC\u5B8C\u6210\u8FD9\u6B21\u914D\u5BF9',
|
|
9188
|
+
subtitle: '\u626B\u7801\u6216\u624B\u52A8\u8F93\u5165\u914D\u5BF9\u7801\uFF0C\u5B8C\u6210 App \u4E0E\u672C\u673A\u7684\u8FDE\u63A5\u3002',
|
|
9189
|
+
metaLocalUrl: '\u672C\u5730\u5730\u5740',
|
|
9190
|
+
metaConnectToken: '\u914D\u5BF9\u7801',
|
|
9191
|
+
metaExpires: '\u8FC7\u671F\u65F6\u95F4',
|
|
9192
|
+
step1Title: '\u5728 App \u91CC\u6253\u5F00\u201C\u8FDE\u63A5 Hermes Link\u201D',
|
|
9193
|
+
step1Copy: '\u5728 App \u91CC\u627E\u5230\u201C\u8FDE\u63A5 Link\u201D\u5165\u53E3\uFF0C\u9009\u62E9\u626B\u7801\u6216\u624B\u52A8\u8F93\u5165\u914D\u5BF9\u7801\u3002\u914D\u5BF9\u6210\u529F\u540E\uFF0CApp \u4F1A\u81EA\u52A8\u5207\u5230\u8FD9\u53F0 Link\u3002',
|
|
9194
|
+
step2Title: '\u626B\u4E8C\u7EF4\u7801\uFF0C\u6216\u624B\u52A8\u586B\u5199\u5730\u5740\u548C\u914D\u5BF9\u7801',
|
|
9195
|
+
step2Copy: '\u5982\u679C\u626B\u7801\u4E0D\u65B9\u4FBF\uFF0C\u53EF\u5728 App \u91CC\u624B\u52A8\u8F93\u5165\u4E0B\u65B9\u7684\u5730\u5740\u548C\u914D\u5BF9\u7801\u5B8C\u6210\u8FDE\u63A5\u3002',
|
|
9196
|
+
step3Title: '\u914D\u5BF9\u6210\u529F\u540E\uFF0C\u8FD9\u4E2A\u9875\u9762\u4F1A\u81EA\u52A8\u53D8\u6210\u5DF2\u5B8C\u6210\u72B6\u6001',
|
|
9197
|
+
hint: '\u53EF\u5728\u7EC8\u7AEF\u7EE7\u7EED\u4FDD\u7559\u8FD9\u4E2A\u9875\u9762\uFF0C\u65B9\u4FBF\u7A0D\u540E\u6838\u5BF9\u72B6\u6001\uFF1B\u5982\u679C\u914D\u5BF9\u5DF2\u7ECF\u6210\u529F\uFF0C\u9875\u9762\u4E0D\u4F1A\u518D\u8981\u6C42\u91CD\u65B0\u626B\u7801\u3002',
|
|
9198
|
+
manualAddrLabel: '\u5730\u5740\uFF08\u4EFB\u9009\u4E00\u4E2A\u53EF\u7528\u7684\uFF09',
|
|
9199
|
+
manualTokenLabel: '\u914D\u5BF9\u7801',
|
|
9200
|
+
status_waiting: '\u7B49\u5F85 App \u626B\u7801',
|
|
9201
|
+
status_claimed: '\u5DF2\u5B8C\u6210\u914D\u5BF9',
|
|
9202
|
+
status_expired: '\u914D\u5BF9\u5DF2\u8FC7\u671F',
|
|
9203
|
+
pill_waiting: '\u7B49\u5F85\u4E2D',
|
|
9204
|
+
pill_claimed: '\u5DF2\u626B\u7801',
|
|
9205
|
+
pill_expired: '\u5DF2\u8FC7\u671F',
|
|
9206
|
+
hint_waiting: '\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002',
|
|
9207
|
+
hint_claimed: 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002',
|
|
9208
|
+
hint_expired: '\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002',
|
|
9209
|
+
expires_locale: 'zh-CN',
|
|
9210
|
+
langToggle: 'EN',
|
|
9211
|
+
},
|
|
9212
|
+
en: {
|
|
9213
|
+
h1: 'Complete Pairing in the App',
|
|
9214
|
+
subtitle: 'Scan the QR code or enter the connect token to link this device.',
|
|
9215
|
+
metaLocalUrl: 'Local Address',
|
|
9216
|
+
metaConnectToken: 'Connect Token',
|
|
9217
|
+
metaExpires: 'Expires',
|
|
9218
|
+
step1Title: 'Open \u201CConnect Hermes Link\u201D in the App',
|
|
9219
|
+
step1Copy: 'Find the \u201CConnect Link\u201D entry in the App, then scan the QR code or enter the token manually. The App will switch to this Link automatically after pairing.',
|
|
9220
|
+
step2Title: 'Scan the QR code, or enter the address and token manually',
|
|
9221
|
+
step2Copy: 'If scanning is inconvenient, enter the address and connect token below in the App.',
|
|
9222
|
+
step3Title: 'This page will update automatically once pairing is complete',
|
|
9223
|
+
hint: 'You can keep this page open to check pairing status later. If pairing has already succeeded, the page will not prompt for a re-scan.',
|
|
9224
|
+
manualAddrLabel: 'Address (pick any that works)',
|
|
9225
|
+
manualTokenLabel: 'Connect Token',
|
|
9226
|
+
status_waiting: 'Waiting for App to Scan',
|
|
9227
|
+
status_claimed: 'Pairing Complete',
|
|
9228
|
+
status_expired: 'Pairing Expired',
|
|
9229
|
+
pill_waiting: 'Waiting',
|
|
9230
|
+
pill_claimed: 'Scanned',
|
|
9231
|
+
pill_expired: 'Expired',
|
|
9232
|
+
hint_waiting: 'Open the App to scan, or copy the connect token for manual entry.',
|
|
9233
|
+
hint_claimed: 'Pairing complete. You can close this page.',
|
|
9234
|
+
hint_expired: 'This QR code has expired. Please run hermeslink pair again.',
|
|
9235
|
+
expires_locale: 'en-US',
|
|
9236
|
+
langToggle: '\u4E2D\u6587',
|
|
9237
|
+
},
|
|
9238
|
+
};
|
|
9044
9239
|
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
<div class="mono" title="\u70B9\u51FB\u590D\u5236" onclick="copyText(this)">${qrPayload.replace(/</g, "<")}</div>
|
|
9048
|
-
</div>
|
|
9240
|
+
let lang = localStorage.getItem('hl-lang') || 'en';
|
|
9241
|
+
let state = ${JSON.stringify(initialState)};
|
|
9049
9242
|
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9243
|
+
const statusTitleEl = document.querySelector('#statusTitle');
|
|
9244
|
+
const statusPillEl = document.querySelector('#statusPill');
|
|
9245
|
+
const statusHintEl = document.querySelector('#statusHint');
|
|
9246
|
+
const langToggleEl = document.querySelector('#langToggle');
|
|
9247
|
+
const expiresEl = document.querySelector('#expiresValue');
|
|
9054
9248
|
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9249
|
+
function applyLang() {
|
|
9250
|
+
const s = T[lang];
|
|
9251
|
+
document.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN';
|
|
9252
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
9253
|
+
const key = el.dataset.i18n;
|
|
9254
|
+
if (s[key] !== undefined) el.textContent = s[key];
|
|
9255
|
+
});
|
|
9256
|
+
statusTitleEl.textContent = s['status_' + state];
|
|
9257
|
+
statusPillEl.textContent = s['pill_' + state];
|
|
9258
|
+
statusHintEl.textContent = s['hint_' + state];
|
|
9259
|
+
if (expiresEl) {
|
|
9260
|
+
const iso = expiresEl.dataset.iso;
|
|
9261
|
+
const d = new Date(iso);
|
|
9262
|
+
expiresEl.textContent = isFinite(d.getTime())
|
|
9263
|
+
? d.toLocaleString(s.expires_locale, { hour12: lang === 'en' })
|
|
9264
|
+
: iso;
|
|
9265
|
+
}
|
|
9266
|
+
langToggleEl.textContent = s.langToggle;
|
|
9267
|
+
}
|
|
9059
9268
|
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9269
|
+
langToggleEl.addEventListener('click', () => {
|
|
9270
|
+
lang = lang === 'zh' ? 'en' : 'zh';
|
|
9271
|
+
localStorage.setItem('hl-lang', lang);
|
|
9272
|
+
applyLang();
|
|
9273
|
+
});
|
|
9064
9274
|
|
|
9065
|
-
|
|
9275
|
+
if (lang === 'zh') applyLang();
|
|
9066
9276
|
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
<div class="status" id="status"></div>
|
|
9070
|
-
<div id="results"></div>
|
|
9071
|
-
</div>
|
|
9277
|
+
let refreshTimer = null;
|
|
9278
|
+
const stopPolling = () => { if (refreshTimer !== null) { clearInterval(refreshTimer); refreshTimer = null; } };
|
|
9072
9279
|
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
let pollTimer = setInterval(async () => {
|
|
9076
|
-
try {
|
|
9077
|
-
const res = await fetch('/api/v1/pairing/session?session_id=${sessionId}');
|
|
9078
|
-
const data = await res.json();
|
|
9079
|
-
if (data.ok && data.session?.claimed) {
|
|
9080
|
-
clearInterval(pollTimer);
|
|
9081
|
-
showStatus('success', 'App \u5DF2\u5B8C\u6210\u914D\u5BF9 \u2713');
|
|
9082
|
-
}
|
|
9083
|
-
} catch {}
|
|
9084
|
-
}, 2000);
|
|
9085
|
-
` : ""}
|
|
9280
|
+
const markClaimed = () => { state = 'claimed'; applyLang(); stopPolling(); };
|
|
9281
|
+
const markExpired = () => { state = 'expired'; applyLang(); stopPolling(); };
|
|
9086
9282
|
|
|
9087
|
-
async
|
|
9088
|
-
|
|
9089
|
-
btn.disabled = true;
|
|
9090
|
-
btn.textContent = '\u914D\u5BF9\u4E2D...';
|
|
9283
|
+
const refresh = async () => {
|
|
9284
|
+
if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) { markExpired(); return; }
|
|
9091
9285
|
try {
|
|
9092
|
-
const
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
btn.textContent = '\u5DF2\u914D\u5BF9';
|
|
9100
|
-
showStatus('success', '\u914D\u5BF9\u6210\u529F\uFF01');
|
|
9101
|
-
const results = document.getElementById('results');
|
|
9102
|
-
results.innerHTML = \`
|
|
9103
|
-
<div class="result-row"><span class="tag">access_token \xB7 2h</span><div class="mono" onclick="copyText(this)">\${data.access_token.token}</div></div>
|
|
9104
|
-
<div class="result-row" style="margin-top:0.5rem"><span class="tag">refresh_token \xB7 90days</span><div class="mono" onclick="copyText(this)">\${data.refresh_token.token}</div></div>
|
|
9105
|
-
\`;
|
|
9106
|
-
} else {
|
|
9107
|
-
throw new Error(data.error?.message || JSON.stringify(data));
|
|
9108
|
-
}
|
|
9109
|
-
} catch (e) {
|
|
9110
|
-
btn.disabled = false;
|
|
9111
|
-
btn.textContent = '\u5728\u6B64\u8BBE\u5907\u4E0A\u914D\u5BF9';
|
|
9112
|
-
showStatus('error', '\u914D\u5BF9\u5931\u8D25: ' + e.message);
|
|
9113
|
-
}
|
|
9114
|
-
}
|
|
9115
|
-
|
|
9116
|
-
function showStatus(type, msg) {
|
|
9117
|
-
const el = document.getElementById('status');
|
|
9118
|
-
el.className = 'status ' + type;
|
|
9119
|
-
el.textContent = msg;
|
|
9120
|
-
}
|
|
9286
|
+
const response = await fetch('/api/v1/pairing/session?session_id=' + encodeURIComponent(sessionId), { headers: { accept: 'application/json' } });
|
|
9287
|
+
if (response.status === 404) { markExpired(); return; }
|
|
9288
|
+
if (!response.ok) return;
|
|
9289
|
+
const payload = await response.json();
|
|
9290
|
+
if (payload?.session?.claimed) markClaimed();
|
|
9291
|
+
} catch {}
|
|
9292
|
+
};
|
|
9121
9293
|
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
const orig = el.style.borderColor;
|
|
9125
|
-
el.style.borderColor = '#10b981';
|
|
9126
|
-
setTimeout(() => el.style.borderColor = orig, 800);
|
|
9127
|
-
}).catch(() => {});
|
|
9294
|
+
if (!initialClaimed) {
|
|
9295
|
+
refreshTimer = setInterval(refresh, 2000);
|
|
9128
9296
|
}
|
|
9129
9297
|
</script>
|
|
9130
9298
|
</body>
|
package/dist/cli/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
saveConfig,
|
|
22
22
|
startLinkService,
|
|
23
23
|
writeJsonFile
|
|
24
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-7IVSOP5F.js";
|
|
25
25
|
import "../chunk-NP3Y2NVF.js";
|
|
26
26
|
|
|
27
27
|
// src/cli/index.ts
|
|
@@ -259,7 +259,10 @@ async function runPairingPreflight(options) {
|
|
|
259
259
|
display_name: "Hermes Link",
|
|
260
260
|
session_id: sessionId,
|
|
261
261
|
code: token.token,
|
|
262
|
-
preferred_urls: preferredUrls
|
|
262
|
+
preferred_urls: preferredUrls,
|
|
263
|
+
lan_ips: routes?.lanIps ?? [],
|
|
264
|
+
public_ipv4s: routes?.publicIpv4s ?? [],
|
|
265
|
+
public_ipv6s: routes?.publicIpv6s ?? []
|
|
263
266
|
};
|
|
264
267
|
const pageUrl = buildLocalPairingPageUrl(preferredUrls[0] ?? `http://127.0.0.1:${options.config.port}`, sessionId, token.token);
|
|
265
268
|
if (options.openBrowser !== false) {
|
|
@@ -273,8 +276,8 @@ async function runPairingPreflight(options) {
|
|
|
273
276
|
preferredUrls
|
|
274
277
|
};
|
|
275
278
|
}
|
|
276
|
-
function buildLocalPairingPageUrl(baseUrl, sessionId,
|
|
277
|
-
const qs = new URLSearchParams({ session_id: sessionId
|
|
279
|
+
function buildLocalPairingPageUrl(baseUrl, sessionId, _connectToken) {
|
|
280
|
+
const qs = new URLSearchParams({ session_id: sessionId });
|
|
278
281
|
return `${baseUrl}/pair?${qs.toString()}`;
|
|
279
282
|
}
|
|
280
283
|
|
|
@@ -414,14 +417,16 @@ async function cmdPair(paths) {
|
|
|
414
417
|
const result = await runPairingPreflight({ identity, config, paths, openBrowser: false });
|
|
415
418
|
process.stdout.write("\n");
|
|
416
419
|
qrcode.generate(result.qrPayload, { small: true });
|
|
420
|
+
const pageUrls = result.preferredUrls.map((base) => buildLocalPairingPageUrl(base, result.sessionId));
|
|
421
|
+
const label = "Pairing page: ";
|
|
422
|
+
const indent = " ".repeat(label.length);
|
|
417
423
|
process.stdout.write(`
|
|
418
|
-
|
|
424
|
+
${label}${pageUrls.join(`
|
|
425
|
+
${indent}`)}
|
|
419
426
|
`);
|
|
420
427
|
process.stdout.write(`Session ID: ${result.sessionId}
|
|
421
428
|
`);
|
|
422
429
|
process.stdout.write(`Connect token: ${result.connectToken}
|
|
423
|
-
`);
|
|
424
|
-
process.stdout.write(`Preferred URLs: ${result.preferredUrls.join(", ")}
|
|
425
430
|
`);
|
|
426
431
|
process.stdout.write(`
|
|
427
432
|
App \u626B\u63CF\u4E8C\u7EF4\u7801\u540E\uFF0C\u8C03\u7528\u4EE5\u4E0B\u63A5\u53E3\u5B8C\u6210\u914D\u5BF9\uFF1A
|
package/dist/http/app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bulolo/hermes-link",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Provides full client API, multi-device auth and conversation management for Hermes Agent, with LAN and internet connectivity.",
|
|
5
5
|
"author": "Bulolo",
|
|
6
6
|
"license": "MIT",
|
|
@@ -75,4 +75,4 @@
|
|
|
75
75
|
"typescript": "^5.7.2",
|
|
76
76
|
"vitest": "^2.1.8"
|
|
77
77
|
}
|
|
78
|
-
}
|
|
78
|
+
}
|