dsh-remote-plugin 0.6.14 → 0.6.16

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/public/admin.html CHANGED
@@ -108,6 +108,28 @@
108
108
  .gateway-port-row input { width: 92px; background: var(--dsr-bg); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 8px; padding: 6px 8px; font: inherit; font-size: 13px; outline: none; }
109
109
  .gateway-port-row .muted { font-size: 12px; }
110
110
  .gateway-port-row .mini-btn { margin-left: auto; }
111
+ .host-ip-card { margin: 0 0 14px; padding: 18px; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); }
112
+ .host-ip-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
113
+ .host-ip-title { font-size: 16px; font-weight: 750; }
114
+ .host-ip-head .muted { margin-top: 4px; font-size: 12px; }
115
+ .host-ip-badge { flex: none; padding: 5px 9px; border: 1px solid var(--dsr-accent-line); border-radius: 999px; color: var(--dsr-accent-strong); background: var(--dsr-accent-soft); font-size: 11px; font-weight: 700; }
116
+ .host-ip-desc { margin: 12px 0; color: var(--dsr-muted); font-size: 12px; line-height: 1.55; }
117
+ .host-ip-table-wrap { overflow: hidden; border: 1px solid var(--dsr-line); border-radius: 13px; }
118
+ .host-ip-table { width: 100%; border-collapse: collapse; }
119
+ .host-ip-table th, .host-ip-table td { padding: 10px 12px; border-bottom: 1px solid var(--dsr-divider); text-align: left; font-size: 12px; }
120
+ .host-ip-table th { color: var(--dsr-muted); background: var(--dsr-bg-2); font-size: 11px; font-weight: 750; }
121
+ .host-ip-table tr:last-child td { border-bottom: none; }
122
+ .host-ip-toggle { width: 74px; }
123
+ .host-ip-value { font-size: 13px !important; }
124
+ .host-ip-use { color: var(--dsr-muted); }
125
+ .host-ip-switch { position: relative; display: inline-block; width: 38px; height: 22px; vertical-align: middle; }
126
+ .host-ip-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
127
+ .host-ip-switch span { position: absolute; inset: 0; border: 1px solid var(--dsr-line); border-radius: 999px; background: var(--dsr-bg-2); cursor: pointer; transition: .18s ease; }
128
+ .host-ip-switch span::after { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; border-radius: 50%; background: var(--dsr-muted); transition: .18s ease; }
129
+ .host-ip-switch input:checked + span { border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
130
+ .host-ip-switch input:checked + span::after { transform: translateX(16px); background: var(--dsr-accent-strong); }
131
+ .host-ip-switch input:focus-visible + span { outline: 2px solid var(--dsr-accent-strong); outline-offset: 2px; }
132
+ .host-ip-empty { padding: 18px; color: var(--dsr-muted); text-align: center; font-size: 12px; }
111
133
  .conn-badge { white-space: nowrap; }
112
134
  #btn-close-drawer { white-space: nowrap; }
113
135
  @media (max-width: 720px) {
@@ -220,7 +242,7 @@
220
242
  .tb-btn { border-radius: 11px; background: linear-gradient(135deg, var(--dsr-panel-2), var(--dsr-bg-2)); border-color: var(--dsr-line); transition: transform .18s ease, border-color .18s ease, background .18s ease; }
221
243
  .tb-btn:hover { transform: translateY(-1px); border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
222
244
  .tb-btn svg { width: 17px; height: 17px; }
223
- .login-card, .stat-card, .stats-chart, .table-wrap, .gateway-port-row, .pair-box { background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); border-color: var(--dsr-line); border-radius: 18px; box-shadow: 0 12px 30px var(--dsr-shadow); }
245
+ .login-card, .stat-card, .stats-chart, .table-wrap, .gateway-port-row, .host-ip-card, .pair-box { background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); border-color: var(--dsr-line); border-radius: 18px; box-shadow: 0 12px 30px var(--dsr-shadow); }
224
246
  .login-card { padding: 18px; }
225
247
  .stat-grid { gap: 12px; }
226
248
  .stat-card { padding: 15px 16px; box-shadow: inset 0 1px var(--dsr-divider), 0 10px 26px var(--dsr-shadow); }
@@ -293,6 +315,9 @@
293
315
  .token-row { display: flex; flex-direction: column; align-items: stretch; }
294
316
  .token-actions { justify-content: stretch; }
295
317
  .token-actions .mini-btn { flex: 1 1 auto; }
318
+ .host-ip-card { padding: 15px; }
319
+ .host-ip-table-wrap { overflow-x: auto; }
320
+ .host-ip-table { min-width: 360px; }
296
321
  .device-key-toggle { align-items:flex-start; }
297
322
  .device-key-toggle-actions { align-items:flex-end; flex-direction:column-reverse; }
298
323
  .device-key-grid.head { display:none; }
@@ -434,6 +459,24 @@
434
459
  <button id="btn-save-port" class="mini-btn" data-i18n="savePort">保存</button>
435
460
  </div>
436
461
 
462
+ <section id="host-ip-card" class="host-ip-card" aria-live="polite">
463
+ <div class="host-ip-head">
464
+ <div>
465
+ <div class="host-ip-title" data-i18n="hostIPs.title">主机 IP 地址</div>
466
+ <div id="host-ip-summary" class="muted" data-i18n="hostIPs.loading">正在读取主机地址…</div>
467
+ </div>
468
+ <div class="host-ip-badge" data-i18n="hostIPs.badge">手机连接候选</div>
469
+ </div>
470
+ <div class="host-ip-desc" data-i18n="hostIPs.desc">关闭虚拟机、容器或其他无效地址后,它们不会再写入配对二维码,也不会作为诊断和防火墙建议地址。</div>
471
+ <div class="host-ip-table-wrap">
472
+ <table class="host-ip-table">
473
+ <thead><tr><th data-i18n="hostIPs.enabledColumn">启用</th><th data-i18n="hostIPs.addressColumn">主机 IP</th><th data-i18n="hostIPs.statusColumn">状态</th></tr></thead>
474
+ <tbody id="host-ip-rows"></tbody>
475
+ </table>
476
+ <div id="host-ip-empty" class="host-ip-empty hidden" data-i18n="hostIPs.empty">暂无可用的非回环 IPv4 地址</div>
477
+ </div>
478
+ </section>
479
+
437
480
  <div id="pair-box" class="pair-box hidden">
438
481
  <div class="pair-title" data-i18n="pairTitle">手机 App 扫码配对</div>
439
482
  <div class="pair-qr" id="pair-qr"></div>
@@ -564,6 +607,7 @@
564
607
  'stat.download': '去下载', 'stat.embedded': 'DSH 内嵌 · 免网关',
565
608
  'stat.updateCheck': '更新检查: {error}', 'stat.latest': '已是最新(来源检查)', 'stat.notChecked': '未检查更新',
566
609
  'stat.hostIP': '主机 IP · {hostname}', 'stat.ipSep': '、', 'stat.phoneGateway': ' (手机连 {port} 网关)', 'stat.phoneThis': ' (手机连这个地址)',
610
+ 'hostIPs.title': '主机 IP 地址', 'hostIPs.badge': '手机连接候选', 'hostIPs.loading': '正在读取主机地址…', 'hostIPs.summary': '已启用 {enabled}/{total} 个地址', 'hostIPs.desc': '关闭虚拟机、容器或其他无效地址后,它们不会再写入配对二维码,也不会作为诊断和防火墙建议地址。', 'hostIPs.enabledColumn': '启用', 'hostIPs.addressColumn': '主机 IP', 'hostIPs.statusColumn': '状态', 'hostIPs.enabled': '已启用 · 会用于配对', 'hostIPs.disabled': '已关闭 · 不用于配对', 'hostIPs.enable': '切换此地址', 'hostIPs.empty': '暂无可用的非回环 IPv4 地址', 'hostIPs.keepOne': '至少保留一个可用地址',
567
611
  'stat.reachable': '可达', 'stat.unreachable': '不可达', 'stat.dshUpstream': 'DSH 上游 {url}',
568
612
  'stat.devicesOnline': '设备在线 / 累计', 'stat.totalRequests': '总请求数', 'stat.authFailures': '认证失败',
569
613
  'stat.uptime': '运行时长 · {host}:{port}',
@@ -673,6 +717,7 @@
673
717
  'stat.download': 'Download', 'stat.embedded': 'Embedded in DSH · no gateway',
674
718
  'stat.updateCheck': 'Update check: {error}', 'stat.latest': 'Up to date (source check)', 'stat.notChecked': 'Not checked',
675
719
  'stat.hostIP': 'Host IP · {hostname}', 'stat.ipSep': ', ', 'stat.phoneGateway': ' (phone connects to gateway {port})', 'stat.phoneThis': ' (phone connects to this address)',
720
+ 'hostIPs.title': 'Host IP addresses', 'hostIPs.badge': 'Phone connection candidates', 'hostIPs.loading': 'Reading host addresses…', 'hostIPs.summary': '{enabled}/{total} addresses enabled', 'hostIPs.desc': 'Disabled virtual-machine, container, or other invalid addresses are excluded from pairing QR codes, diagnostics, and firewall suggestions.', 'hostIPs.enabledColumn': 'Enabled', 'hostIPs.addressColumn': 'Host IP', 'hostIPs.statusColumn': 'Status', 'hostIPs.enabled': 'Enabled · used for pairing', 'hostIPs.disabled': 'Disabled · excluded from pairing', 'hostIPs.enable': 'Toggle this address', 'hostIPs.empty': 'No usable non-loopback IPv4 address', 'hostIPs.keepOne': 'Keep at least one usable address enabled',
676
721
  'stat.reachable': 'Reachable', 'stat.unreachable': 'Unreachable', 'stat.dshUpstream': 'DSH upstream {url}',
677
722
  'stat.devicesOnline': 'Devices online / total', 'stat.totalRequests': 'Total requests', 'stat.authFailures': 'Auth failures',
678
723
  'stat.uptime': 'Uptime · {host}:{port}',
package/public/admin.js CHANGED
@@ -31,6 +31,71 @@ let gatewayPort = 8787
31
31
  let gatewayPortLoaded = false
32
32
  let doctorExpanded = store.get('dshAdminDoctorCollapsed') !== '1'
33
33
  let doctorChecks = []
34
+ const HOST_IP_SELECTION_KEY = 'dshAdminEnabledHostIPsV1'
35
+
36
+ function normalizedHostIPs(st) {
37
+ return Array.isArray(st?.lanIPs)
38
+ ? [...new Set(st.lanIPs.map(value => String(value || '').trim()).filter(value => value && value !== '127.0.0.1' && value !== '0.0.0.0'))]
39
+ : []
40
+ }
41
+
42
+ function hostIPScope(st) {
43
+ return [st?.hostname || location.hostname || 'host', st?.host || ''].join('|')
44
+ }
45
+
46
+ function hostIPPreferences() {
47
+ try {
48
+ const value = JSON.parse(store.get(HOST_IP_SELECTION_KEY) || '{}')
49
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
50
+ } catch {
51
+ return {}
52
+ }
53
+ }
54
+
55
+ function enabledHostIPs(st) {
56
+ const all = normalizedHostIPs(st)
57
+ if (!all.length) return []
58
+ const saved = hostIPPreferences()[hostIPScope(st)]
59
+ if (!Array.isArray(saved)) return all
60
+ const selected = all.filter(ip => saved.includes(ip))
61
+ return selected.length ? selected : all
62
+ }
63
+
64
+ function saveEnabledHostIPs(st, selected) {
65
+ const prefs = hostIPPreferences()
66
+ prefs[hostIPScope(st)] = normalizedHostIPs(st).filter(ip => selected.includes(ip))
67
+ store.set(HOST_IP_SELECTION_KEY, JSON.stringify(prefs))
68
+ }
69
+
70
+ function renderHostIPs(st) {
71
+ const all = normalizedHostIPs(st)
72
+ const selected = enabledHostIPs(st)
73
+ const rows = $('host-ip-rows')
74
+ const empty = $('host-ip-empty')
75
+ const summary = $('host-ip-summary')
76
+ if (!rows || !empty || !summary) return
77
+ summary.textContent = all.length
78
+ ? t('hostIPs.summary', { enabled: selected.length, total: all.length })
79
+ : t('hostIPs.empty')
80
+ rows.innerHTML = all.map(ip => `<tr>
81
+ <td class="host-ip-toggle"><label class="host-ip-switch" title="${esc(t('hostIPs.enable'))}"><input type="checkbox" data-host-ip-toggle="${esc(ip)}" ${selected.includes(ip) ? 'checked' : ''}><span aria-hidden="true"></span></label></td>
82
+ <td class="mono host-ip-value">${esc(ip)}</td>
83
+ <td class="host-ip-use">${esc(t(selected.includes(ip) ? 'hostIPs.enabled' : 'hostIPs.disabled'))}</td>
84
+ </tr>`).join('')
85
+ empty.classList.toggle('hidden', all.length > 0)
86
+ rows.querySelectorAll('[data-host-ip-toggle]').forEach(input => input.addEventListener('change', () => {
87
+ const ip = input.dataset.hostIpToggle
88
+ const next = enabledHostIPs(st).filter(value => value !== ip)
89
+ if (input.checked) next.push(ip)
90
+ if (!next.length) {
91
+ input.checked = true
92
+ toast(t('hostIPs.keepOne'), 'err')
93
+ return
94
+ }
95
+ saveEnabledHostIPs(st, next)
96
+ render(st)
97
+ }))
98
+ }
34
99
 
35
100
  function onlineClientDevices(st) {
36
101
  return (st.devices || []).filter(device => device.online && (device.kind === 'app' || device.kind === 'web'))
@@ -38,7 +103,7 @@ function onlineClientDevices(st) {
38
103
 
39
104
  function firewallCommand(st) {
40
105
  const port = Number(st.port || gatewayPort) || 8787
41
- const ip = (st.lanIPs || []).find(value => /^10\.|^192\.168\.|^172\.(1[6-9]|2\d|3[01])\.|^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(value || ''))
106
+ const ip = enabledHostIPs(st).find(value => /^10\.|^192\.168\.|^172\.(1[6-9]|2\d|3[01])\.|^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(value || ''))
42
107
  let cidr = 'LocalSubnet'
43
108
  if (/^10\./.test(ip || '')) cidr = '10.0.0.0/8'
44
109
  else if (/^192\.168\./.test(ip || '')) cidr = '192.168.0.0/16'
@@ -52,7 +117,7 @@ function firewallCommand(st) {
52
117
  function buildDoctorChecks(st) {
53
118
  const isGateway = st.mode === 'gateway'
54
119
  const port = Number(st.port || gatewayPort) || 8787
55
- const ip = (st.lanIPs || []).find(value => value && value !== '127.0.0.1' && value !== '0.0.0.0')
120
+ const ip = enabledHostIPs(st).find(value => value && value !== '127.0.0.1' && value !== '0.0.0.0')
56
121
  const base = ip ? `http://${ip}:${port}` : ''
57
122
  const clients = onlineClientDevices(st)
58
123
  const events = st.events || {}
@@ -322,6 +387,7 @@ function render(st) {
322
387
  $('btn-qr').classList.toggle('hidden', isGateway !== true || !shownToken)
323
388
  $('btn-rotate').classList.toggle('hidden', isGateway !== true || !shownToken || !!st.tokenFromEnv)
324
389
  renderDeviceKeys(st, isGateway)
390
+ renderHostIPs(st)
325
391
  renderQr(st)
326
392
  renderDoctor(st)
327
393
  // 网关开关: 仅插件内嵌页提供, 网关运行/停止两种状态
@@ -356,14 +422,12 @@ function render(st) {
356
422
  action.dataset.heroAction = heroState === 'plugin' ? 'start' : heroState === 'offline' ? 'copy' : 'devices'
357
423
  }
358
424
  }
359
- const hostIPs = (st.lanIPs || []).join(t('stat.ipSep')) || '127.0.0.1'
360
425
  const latestHtml = st.latest?.newer
361
426
  ? `<div class="v">${t('stat.updateAvailable', { version: st.latest.version })}</div><div class="k">${t('stat.currentV', { version: st.version })} · <a href="${st.latest.url || '#'}" target="_blank" rel="noopener" style="color:var(--dsr-accent-strong)">${t('stat.download')}</a></div>`
362
427
  : `<div class="v">v${st.version}</div><div class="k">${isPlugin ? t('stat.embedded') : st.latest?.error ? t('stat.updateCheck', { error: st.latest.error }) : st.latest?.version ? t('stat.latest') : t('stat.notChecked')}</div>`
363
428
  $('stats').innerHTML = `
364
429
  <div class="stat-card"><div class="v">v${st.version}</div><div class="k">${t(isPlugin ? 'stat.pluginVersion' : 'stat.gatewayVersion')}</div></div>
365
430
  <div class="stat-card ${st.latest?.newer ? 'warn' : 'ok'}">${latestHtml}</div>
366
- <div class="stat-card ok"><div class="v" style="font-size:13px">${hostIPs}</div><div class="k">${t('stat.hostIP', { hostname: st.hostname })}${isPlugin ? t('stat.phoneGateway', { port: gatewayPort }) : t('stat.phoneThis')}</div></div>
367
431
  <div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${t(upOk ? 'stat.reachable' : 'stat.unreachable')}</div><div class="k">${t('stat.dshUpstream', { url: st.upstream.url })}</div></div>
368
432
  <div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k">${t('stat.devicesOnline')}</div></div>
369
433
  <div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k">${t('stat.totalRequests')}</div></div>
@@ -424,13 +488,19 @@ function render(st) {
424
488
  }
425
489
 
426
490
  function pairTarget(st, accessToken) {
427
- const ip = (st.lanIPs || []).find(x => x && x !== '127.0.0.1' && x !== '0.0.0.0') || (st.lanIPs || [])[0]
428
- const host = ip || (st.host && st.host !== '0.0.0.0' ? st.host : location.hostname)
429
491
  const port = st.port || 8787
430
- const base = `http://${host}:${port}`
492
+ const hosts = enabledHostIPs(st).slice()
493
+ if (!hosts.length) {
494
+ const fallback = st.host && st.host !== '0.0.0.0' ? String(st.host).trim() : location.hostname
495
+ if (fallback) hosts.push(fallback)
496
+ }
497
+ const bases = hosts.map(host => `http://${host}:${port}`)
498
+ const query = new URLSearchParams({ token: String(accessToken || '') })
499
+ for (const base of bases) query.append('server', base)
431
500
  return {
432
- url: `dshremote://pair?token=${encodeURIComponent(accessToken)}&server=${encodeURIComponent(base)}`,
433
- base
501
+ url: `dshremote://pair?${query.toString()}`,
502
+ base: bases[0] || '',
503
+ bases
434
504
  }
435
505
  }
436
506
 
@@ -75,6 +75,39 @@
75
75
  "minVersion": "",
76
76
  "maxVersion": "",
77
77
  "publishedAt": "2026-08-25T00:24:27+08:00"
78
+ },
79
+ {
80
+ "id": "2026-08-26-dsh-local-android-rc1",
81
+ "title": "dsh-local-android v0.1.0-rc.1 发布公告",
82
+ "content": "大家好,dsh-local-android 现已发布 v0.1.0-rc.1 测试版本。\n\ndsh-local-android 是 DSH 的 Android 本地发行版,应用名称为 DSH for Android。它将 DSH 运行时、Local Gateway 和交互界面整合到 Android 设备本地运行,适合希望直接在手机或 Android 环境中使用 DSH 的用户。\n\n当前版本支持:\n\n- 在 Android 设备本地运行 DSH;\n- 使用本地 Gateway 和 WebView 界面进行交互;\n- 通过系统文件选择器访问本地文件;\n- 在“设置 → 模型”中配置模型提供方、API 地址、API 密钥和模型目录;\n- 分层显示安装、Engine、Gateway 和界面启动状态,便于排查问题。\n\n本次发布同时提供 arm64-v8a 和 x86_64 架构版本,请根据设备或模拟器的 CPU 架构选择对应 APK。\n\n目前版本仍处于 RC 测试阶段,不同设备、Android 版本和运行环境下可能存在兼容性或稳定性差异。默认运行时采用 minimal profile,部分扩展能力暂未包含。\n\n如果你有在 Android 环境中本地运行 DSH 的需求,欢迎下载尝试,并反馈实际使用体验、设备兼容性和遇到的问题。\n\n提交日志或截图时,请注意隐藏 API 密钥、Token 及其他敏感信息。感谢大家的支持与反馈。",
83
+ "minVersion": "",
84
+ "maxVersion": "",
85
+ "publishedAt": "2026-08-26T18:28:26+08:00",
86
+ "actionUrl": "https://github.com/Blank-not-black/dsh-local-android/releases",
87
+ "actionText": "下载 DSH for Android"
88
+ },
89
+ {
90
+ "id": "2026-08-27-meeting-asr-brand-poll",
91
+ "title": "投票:你的手机品牌是什么?(会议转写调研)",
92
+ "content": "大家好,我们正在研究为 dsh-Remote 增加会议模式:使用手机录音,调用语音识别生成完整文字,再由 DSH 整理会议纪要、待办事项或指定文件。\n\n第一阶段准备优先验证手机厂商提供的云端语音识别能力,因此先统计大家正在使用的手机品牌。本次投票只用于了解设备分布和安排后续适配顺序,不代表某个品牌已经确认支持,也不会立即改变现有功能。\n\n后续如果需要进一步确认识别能力,我们会在 App 内提供自愿参与的诊断测试,只记录识别服务是否可用、是否返回 partial results、连续识别时长和错误类型,不收集录音、识别文字或 API 密钥。",
93
+ "minVersion": "0.6.11",
94
+ "maxVersion": "",
95
+ "publishedAt": "2026-08-27T12:00:00+08:00",
96
+ "poll": {
97
+ "id": "meeting-asr-brand-2026-08",
98
+ "question": "你的手机品牌是什么?",
99
+ "options": [
100
+ { "id": "xiaomi-redmi", "label": "小米 / Redmi" },
101
+ { "id": "huawei", "label": "华为" },
102
+ { "id": "honor", "label": "荣耀" },
103
+ { "id": "oppo-oneplus", "label": "OPPO / 一加" },
104
+ { "id": "vivo-iqoo", "label": "vivo / iQOO" },
105
+ { "id": "samsung", "label": "三星" },
106
+ { "id": "meizu", "label": "魅族" },
107
+ { "id": "google-pixel", "label": "Google Pixel" },
108
+ { "id": "other", "label": "其他品牌" }
109
+ ]
110
+ }
78
111
  }
79
112
  ]
80
113
  }