@bulolo/hermes-link 0.3.5 → 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.
@@ -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
  }
@@ -9002,10 +9002,10 @@ async function startLinkService(options) {
9002
9002
  function escapeHtml(s) {
9003
9003
  return s.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;");
9004
9004
  }
9005
- function formatDate(iso) {
9005
+ function formatDate(iso, locale = "zh-CN") {
9006
9006
  const d = new Date(iso);
9007
9007
  if (!Number.isFinite(d.getTime())) return iso;
9008
- return d.toLocaleString("zh-CN", { hour12: false });
9008
+ return d.toLocaleString(locale, { hour12: locale !== "zh-CN" });
9009
9009
  }
9010
9010
  async function renderPairingPage(options) {
9011
9011
  const { session, claimed, version, linkId } = options;
@@ -9023,11 +9023,9 @@ async function renderPairingPage(options) {
9023
9023
  const qrDataUri = `data:image/svg+xml;base64,${Buffer.from(qrSvg).toString("base64")}`;
9024
9024
  const currentUrl = session.local_api_url.replace(/\/+$/u, "");
9025
9025
  const expiresAtMs = Date.parse(session.expires_at);
9026
- const statusLabel = claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : isExpired ? "\u914D\u5BF9\u5DF2\u8FC7\u671F" : "\u7B49\u5F85 App \u626B\u7801";
9027
- const statusHint = claimed ? "App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002" : isExpired ? "\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002" : "\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002";
9028
- const statusPillLabel = claimed ? "\u5DF2\u626B\u7801" : isExpired ? "\u5DF2\u8FC7\u671F" : "\u7B49\u5F85\u4E2D";
9026
+ const initialState = claimed ? "claimed" : isExpired ? "expired" : "waiting";
9029
9027
  return `<!doctype html>
9030
- <html lang="zh-CN">
9028
+ <html lang="en">
9031
9029
  <head>
9032
9030
  <meta charset="utf-8" />
9033
9031
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -9067,7 +9065,10 @@ async function renderPairingPage(options) {
9067
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; }
9068
9066
  .hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, 390px); gap: 0; }
9069
9067
  .copy { padding: 34px 34px 30px; border-right: 1px solid var(--line); }
9068
+ .header-row { display: flex; justify-content: space-between; align-items: center; }
9070
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; }
9071
9072
  h1 { margin: 18px 0 12px; font-size: clamp(34px, 4vw, 52px); line-height: 1.02; }
9072
9073
  .subtitle { max-width: 42ch; margin: 0; color: var(--muted); font-size: 16px; line-height: 1.7; }
9073
9074
  .meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-top: 26px; }
@@ -9087,7 +9088,12 @@ async function renderPairingPage(options) {
9087
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; }
9088
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); }
9089
9090
  .qr-frame img { width: min(100%, 300px); aspect-ratio: 1; display: block; border-radius: 18px; background: #fff; padding: 14px; }
9090
- .code { margin-top: 16px; padding: 14px 16px; border-radius: 18px; border: 1px solid var(--line); background: rgba(0,0,0,0.03); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 22px; letter-spacing: 0.18em; text-align: center; user-select: all; }
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; }
9091
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; }
9092
9098
  @media (max-width: 920px) { .hero { grid-template-columns: 1fr; } .copy { border-right: none; border-bottom: 1px solid var(--line); } }
9093
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; } }
@@ -9098,12 +9104,15 @@ async function renderPairingPage(options) {
9098
9104
  <section class="panel">
9099
9105
  <div class="hero">
9100
9106
  <div class="copy">
9101
- <span class="eyebrow">Hermes Link \xB7 ${escapeHtml(version)}</span>
9102
- <h1>\u5728 App \u91CC\u5B8C\u6210\u8FD9\u6B21\u914D\u5BF9</h1>
9103
- <p class="subtitle">\u8FD9\u4E0D\u662F\u4E00\u5F20\u5B64\u96F6\u96F6\u7684\u4E8C\u7EF4\u7801\u3002\u5B83\u540C\u65F6\u7ED9\u4F60\u914D\u5BF9\u72B6\u6001\u3001\u624B\u52A8\u7801\u3001\u6709\u6548\u671F\u548C\u5F53\u524D\u672C\u5730\u5730\u5740\uFF0C\u65B9\u4FBF Windows \u4F20\u7EDF\u63A7\u5236\u53F0\u3001\u8FDC\u7A0B\u7EC8\u7AEF\u548C\u6D4F\u89C8\u5668\u6253\u5F00\u65F6\u90FD\u80FD\u987A\u5229\u5B8C\u6210\u3002</p>
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>
9104
9113
  <div class="meta-grid">
9105
9114
  <div class="meta">
9106
- <span class="meta-label">\u672C\u5730\u5730\u5740</span>
9115
+ <span class="meta-label" data-i18n="metaLocalUrl">Local Address</span>
9107
9116
  <div class="meta-value">${escapeHtml(currentUrl)}</div>
9108
9117
  </div>
9109
9118
  <div class="meta">
@@ -9111,52 +9120,57 @@ async function renderPairingPage(options) {
9111
9120
  <div class="meta-value">${escapeHtml(linkId)}</div>
9112
9121
  </div>
9113
9122
  <div class="meta">
9114
- <span class="meta-label">\u914D\u5BF9\u7801</span>
9123
+ <span class="meta-label" data-i18n="metaConnectToken">Connect Token</span>
9115
9124
  <div class="meta-value">${escapeHtml(session.code)}</div>
9116
9125
  </div>
9117
9126
  <div class="meta">
9118
- <span class="meta-label">\u8FC7\u671F\u65F6\u95F4</span>
9119
- <div class="meta-value">${escapeHtml(formatDate(session.expires_at))}</div>
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>
9120
9129
  </div>
9121
9130
  </div>
9122
9131
  <div class="steps">
9123
9132
  <div class="step">
9124
9133
  <div class="step-badge">1</div>
9125
9134
  <div>
9126
- <p class="step-title">\u5728 App \u91CC\u6253\u5F00"\u8FDE\u63A5 Hermes Link"</p>
9127
- <p class="step-copy">\u5148\u767B\u5F55 HermesPilot \u8D26\u53F7\uFF0C\u518D\u8D70\u626B\u7801\u6216\u624B\u52A8\u7801\u3002\u914D\u5BF9\u6210\u529F\u540E\uFF0CApp \u4F1A\u81EA\u52A8\u5207\u5230\u8FD9\u53F0 Link\u3002</p>
9135
+ <p class="step-title" data-i18n="step1Title">Open &ldquo;Connect Hermes Link&rdquo; in the App</p>
9136
+ <p class="step-copy" data-i18n="step1Copy">Find the &ldquo;Connect Link&rdquo; 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>
9128
9137
  </div>
9129
9138
  </div>
9130
9139
  <div class="step">
9131
9140
  <div class="step-badge">2</div>
9132
9141
  <div>
9133
- <p class="step-title">\u626B\u4E8C\u7EF4\u7801\uFF0C\u6216\u76F4\u63A5\u8F93\u5165\u914D\u5BF9\u7801</p>
9134
- <p class="step-copy">\u5982\u679C\u626B\u7801\u4E0D\u65B9\u4FBF\uFF0CApp \u91CC\u4E5F\u53EF\u4EE5\u76F4\u63A5\u8F93\u5165\u8FD9\u4E2A\u9875\u9762\u663E\u793A\u7684\u914D\u5BF9\u7801\u3002</p>
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>
9135
9144
  </div>
9136
9145
  </div>
9137
9146
  <div class="step">
9138
9147
  <div class="step-badge">3</div>
9139
9148
  <div>
9140
- <p class="step-title">\u914D\u5BF9\u6210\u529F\u540E\uFF0C\u8FD9\u4E2A\u9875\u9762\u4F1A\u81EA\u52A8\u53D8\u6210\u5DF2\u5B8C\u6210\u72B6\u6001</p>
9141
- <p class="step-copy" id="statusHint">${escapeHtml(statusHint)}</p>
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>
9142
9151
  </div>
9143
9152
  </div>
9144
9153
  </div>
9145
- <p class="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</p>
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>
9146
9155
  </div>
9147
9156
  <div class="qr">
9148
9157
  <div class="card">
9149
9158
  <div class="status">
9150
- <h2 class="status-title" id="statusTitle">${escapeHtml(statusLabel)}</h2>
9151
- <span class="pill" id="statusPill">${escapeHtml(statusPillLabel)}</span>
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>
9152
9161
  </div>
9153
9162
  <div class="qr-frame">
9154
9163
  <img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
9155
9164
  </div>
9156
- <div class="code">${escapeHtml(session.code)}</div>
9157
- <div class="footer">
9158
- <span>Local API: ${escapeHtml(currentUrl)}</span>
9159
- <span>Relay / Server \u4F1A\u540C\u65F6\u51C6\u5907\u53EF\u7528\u8DEF\u7531</span>
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>
9160
9174
  </div>
9161
9175
  </div>
9162
9176
  </div>
@@ -9167,26 +9181,104 @@ async function renderPairingPage(options) {
9167
9181
  const sessionId = ${JSON.stringify(session.session_id)};
9168
9182
  const expiresAtMs = ${Number.isFinite(expiresAtMs) ? String(expiresAtMs) : "Number.NaN"};
9169
9183
  const initialClaimed = ${JSON.stringify(claimed)};
9170
- const statusTitle = document.querySelector('#statusTitle');
9171
- const statusPill = document.querySelector('#statusPill');
9184
+
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
+ };
9239
+
9240
+ let lang = localStorage.getItem('hl-lang') || 'en';
9241
+ let state = ${JSON.stringify(initialState)};
9242
+
9243
+ const statusTitleEl = document.querySelector('#statusTitle');
9244
+ const statusPillEl = document.querySelector('#statusPill');
9172
9245
  const statusHintEl = document.querySelector('#statusHint');
9173
- let refreshTimer = null;
9246
+ const langToggleEl = document.querySelector('#langToggle');
9247
+ const expiresEl = document.querySelector('#expiresValue');
9174
9248
 
9175
- const stopPolling = () => { if (refreshTimer !== null) { clearInterval(refreshTimer); refreshTimer = null; } };
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
+ }
9176
9268
 
9177
- const markClaimed = () => {
9178
- statusTitle.textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
9179
- statusPill.textContent = '\u5DF2\u626B\u7801';
9180
- statusHintEl.textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
9181
- stopPolling();
9182
- };
9269
+ langToggleEl.addEventListener('click', () => {
9270
+ lang = lang === 'zh' ? 'en' : 'zh';
9271
+ localStorage.setItem('hl-lang', lang);
9272
+ applyLang();
9273
+ });
9183
9274
 
9184
- const markExpired = () => {
9185
- statusTitle.textContent = '\u914D\u5BF9\u5DF2\u8FC7\u671F';
9186
- statusPill.textContent = '\u5DF2\u8FC7\u671F';
9187
- statusHintEl.textContent = '\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002';
9188
- stopPolling();
9189
- };
9275
+ if (lang === 'zh') applyLang();
9276
+
9277
+ let refreshTimer = null;
9278
+ const stopPolling = () => { if (refreshTimer !== null) { clearInterval(refreshTimer); refreshTimer = null; } };
9279
+
9280
+ const markClaimed = () => { state = 'claimed'; applyLang(); stopPolling(); };
9281
+ const markExpired = () => { state = 'expired'; applyLang(); stopPolling(); };
9190
9282
 
9191
9283
  const refresh = async () => {
9192
9284
  if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) { markExpired(); return; }
package/dist/cli/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  saveConfig,
22
22
  startLinkService,
23
23
  writeJsonFile
24
- } from "../chunk-EYXOHOAC.js";
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) {
@@ -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
- Pairing page: ${result.pageUrl}
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startLinkService
3
- } from "../chunk-EYXOHOAC.js";
3
+ } from "../chunk-7IVSOP5F.js";
4
4
  import "../chunk-NP3Y2NVF.js";
5
5
  export {
6
6
  startLinkService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulolo/hermes-link",
3
- "version": "0.3.5",
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",