dsh-remote-plugin 0.6.20 → 0.6.21

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.
Binary file
package/index.mjs CHANGED
@@ -513,6 +513,23 @@ async function resolveAgent(ctx, sessionId) {
513
513
  }
514
514
  }
515
515
 
516
+ function commandHead(line) {
517
+ const match = /^\/+(\S+)/.exec(String(line || '').trim())
518
+ return match ? match[1].replace(/^\/+/, '') : ''
519
+ }
520
+
521
+ function commandEntryName(entry) {
522
+ const value = typeof entry === 'string' ? entry : entry?.name
523
+ return String(value || '').trim().replace(/^\/+/, '')
524
+ }
525
+
526
+ function commandWasExecuted(result) {
527
+ // DSH 0.1.x 的 commands.execute() 成功时可以返回 void。只要 Promise 正常
528
+ // 完成,就不应把同一条 /command 再降级为 session.prompt 的普通文本;只有
529
+ // 执行器明确返回 false / { executed: false } 才表示未处理。
530
+ return result !== false && result?.executed !== false
531
+ }
532
+
516
533
  async function resolveFile(pathname) {
517
534
  let abs = targetPath(pathname)
518
535
  if (abs === null) return null
@@ -771,17 +788,27 @@ async function serveStatic(req, res, ctx) {
771
788
  sendJson(res, 200, { ok: false, message: 'agent not found', debug: { resolvePath, commandNames: [] } })
772
789
  return
773
790
  }
791
+ let commandEntries = null
774
792
  let commandNames = []
775
793
  try {
776
- commandNames = (await ctx.commands.list(agent)).map(c => c.name)
794
+ const listed = await ctx.commands.list(agent)
795
+ commandEntries = Array.isArray(listed) ? listed : null
796
+ commandNames = commandEntries ? commandEntries.map(commandEntryName).filter(Boolean) : []
777
797
  } catch (e) {
778
798
  commandNames = ['list-error: ' + (e?.message || String(e))]
779
799
  }
800
+ const name = commandHead(line)
801
+ if (commandEntries && (!name || !commandEntries.some(entry => commandEntryName(entry) === name))) {
802
+ // 仅把 DSH 已登记的 slash command 截留到命令服务。未知 /xxx 仍让客户端
803
+ // 按普通文本发送,保持 DSH 自己处理自定义提示词的兼容行为。
804
+ sendJson(res, 200, { ok: true, executed: false, debug: { resolvePath, commandNames, reason: 'unknown-command' } })
805
+ return
806
+ }
780
807
  const signal = AbortSignal.timeout(30000)
781
808
  const result = ctx.commands.execute.length === 3
782
809
  ? await ctx.commands.execute(agent, line, signal)
783
810
  : await ctx.commands.execute(agent, line, [], signal)
784
- sendJson(res, 200, { ok: true, executed: result !== undefined, debug: { resolvePath, commandNames } })
811
+ sendJson(res, 200, { ok: true, executed: commandWasExecuted(result), debug: { resolvePath, commandNames } })
785
812
  } catch (e) {
786
813
  sendJson(res, 200, { ok: false, message: e?.message || String(e) })
787
814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
package/public/app.js CHANGED
@@ -938,10 +938,81 @@ function closeGroupMenu() {
938
938
  if (menu) menu.classList.add('hidden')
939
939
  }
940
940
 
941
+ let groupDrawerReturnFocus = null
942
+
943
+ function groupDrawerOpen() {
944
+ return !$('group-drawer')?.classList.contains('hidden')
945
+ }
946
+
947
+ function renderGroupDrawer() {
948
+ const trigger = $('group-drawer-trigger')
949
+ const brandGroup = $('brand-group-name')
950
+ const current = $('group-drawer-current')
951
+ const list = $('group-drawer-list')
952
+ if (brandGroup) brandGroup.textContent = state.activeGroup
953
+ if (trigger) trigger.setAttribute('aria-label', t('groups.drawerOpen') + ':' + state.activeGroup)
954
+ if (current) current.textContent = state.activeGroup
955
+ if (!list) return
956
+ list.innerHTML = state.groups.map(group => {
957
+ const servers = groupServers(group)
958
+ const selected = group === state.activeGroup
959
+ return `<button type="button" class="group-drawer-option ${selected ? 'current' : ''}" data-quick-group="${esc(group)}" ${selected ? 'aria-current="true"' : ''}>
960
+ <span class="group-drawer-option-mark" aria-hidden="true"></span>
961
+ <span class="group-drawer-option-copy"><span class="group-drawer-option-name">${esc(group)}</span><span class="group-drawer-option-meta">${t('groups.serverCount', { count: servers.length })}</span></span>
962
+ <span class="group-drawer-option-check" aria-hidden="true">${selected ? '✓' : ''}</span>
963
+ </button>`
964
+ }).join('')
965
+ }
966
+
967
+ function openGroupDrawer() {
968
+ const drawer = $('group-drawer')
969
+ const backdrop = $('group-drawer-backdrop')
970
+ const trigger = $('group-drawer-trigger')
971
+ if (!drawer || !backdrop || !trigger) return
972
+ renderGroupDrawer()
973
+ groupDrawerReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger
974
+ drawer.classList.remove('hidden')
975
+ backdrop.classList.remove('hidden')
976
+ document.body.classList.add('group-drawer-open')
977
+ trigger.setAttribute('aria-expanded', 'true')
978
+ requestAnimationFrame(() => (drawer.querySelector('[data-quick-group].current') || drawer.querySelector('[data-quick-group]') || drawer).focus({ preventScroll: true }))
979
+ }
980
+
981
+ function closeGroupDrawer({ restoreFocus = true } = {}) {
982
+ const drawer = $('group-drawer')
983
+ const backdrop = $('group-drawer-backdrop')
984
+ const trigger = $('group-drawer-trigger')
985
+ if (!drawer || !backdrop || drawer.classList.contains('hidden')) return
986
+ drawer.classList.add('hidden')
987
+ backdrop.classList.add('hidden')
988
+ document.body.classList.remove('group-drawer-open')
989
+ trigger?.setAttribute('aria-expanded', 'false')
990
+ if (restoreFocus) (groupDrawerReturnFocus || trigger)?.focus({ preventScroll: true })
991
+ groupDrawerReturnFocus = null
992
+ }
993
+
994
+ function keepGroupDrawerFocus(event) {
995
+ if (!groupDrawerOpen()) return
996
+ if (event.key === 'Escape') {
997
+ event.preventDefault()
998
+ closeGroupDrawer()
999
+ return
1000
+ }
1001
+ if (event.key !== 'Tab') return
1002
+ const drawer = $('group-drawer')
1003
+ const targets = [...drawer.querySelectorAll('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')]
1004
+ if (!targets.length) return
1005
+ const first = targets[0]
1006
+ const last = targets[targets.length - 1]
1007
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
1008
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
1009
+ }
1010
+
941
1011
  function renderServers() {
942
1012
  const box = $('server-list')
943
1013
  if (!box) return
944
1014
  renderGroupSelect()
1015
+ renderGroupDrawer()
945
1016
  const groupsHtml = state.groups.map(g => {
946
1017
  const list = groupServers(g)
947
1018
  const auto = state.autoSelect[g] !== false
@@ -6879,6 +6950,22 @@ function bindUi() {
6879
6950
  $('btn-server-add').addEventListener('click', addServer)
6880
6951
  $('btn-group-add').addEventListener('click', addGroup)
6881
6952
  $('group-select-btn').addEventListener('click', (e) => { e.stopPropagation(); toggleGroupMenu() })
6953
+ $('group-drawer-trigger').addEventListener('click', () => { groupDrawerOpen() ? closeGroupDrawer() : openGroupDrawer() })
6954
+ $('group-drawer-close').addEventListener('click', () => closeGroupDrawer())
6955
+ $('group-drawer-backdrop').addEventListener('click', () => closeGroupDrawer())
6956
+ $('group-drawer-list').addEventListener('click', (e) => {
6957
+ const option = e.target.closest('[data-quick-group]')
6958
+ if (!option) return
6959
+ const group = option.dataset.quickGroup
6960
+ closeGroupDrawer({ restoreFocus: false })
6961
+ if (group !== state.activeGroup) switchGroup(group)
6962
+ })
6963
+ $('group-drawer-manage').addEventListener('click', () => {
6964
+ closeGroupDrawer({ restoreFocus: false })
6965
+ showView('view-settings')
6966
+ showSettingsPage('servers')
6967
+ })
6968
+ document.addEventListener('keydown', keepGroupDrawerFocus)
6882
6969
  document.addEventListener('click', (e) => {
6883
6970
  if (!e.target.closest('#group-select')) closeGroupMenu()
6884
6971
  })
@@ -9,9 +9,16 @@ html, body {
9
9
  .ds-app { position: fixed; inset: 0; display: flex; overflow: hidden; }
10
10
  .ds-sidebar { width: 280px; flex: none; display: flex; flex-direction: column; border-right: 1px solid var(--dsr-line); background: linear-gradient(180deg, var(--dsr-panel), var(--dsr-bg)); box-shadow: 12px 0 34px var(--dsr-shadow); overflow: hidden; }
11
11
  .ds-sidebar-backdrop { display: none; }
12
- .ds-brand { display: flex; align-items: center; gap: 8px; padding: 17px 18px 12px; font-weight: 750; font-size: 15px; letter-spacing: .1px; }
12
+ .ds-brand { display: flex; align-items: center; gap: 6px; padding: 12px 12px 8px 18px; font-weight: 750; font-size: 15px; letter-spacing: .1px; }
13
+ .ds-brand-trigger { min-width: 0; flex: 1; display: flex; align-items: center; gap: 8px; padding: 5px 6px 5px 0; border: 0; border-radius: 10px; background: transparent; color: var(--dsr-text); font: inherit; text-align: left; cursor: pointer; }
14
+ .ds-brand-trigger:hover { background: var(--dsr-accent-soft); }
15
+ .ds-brand-trigger:focus-visible { outline: 2px solid var(--dsr-accent); outline-offset: 2px; }
13
16
  .ds-logo { color: var(--dsr-accent-strong); }
14
- .ds-brand-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
17
+ .ds-brand-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 0; line-height: 1.14; }
18
+ .ds-brand-name { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
19
+ .ds-brand-group-name { min-width: 0; overflow: hidden; color: var(--dsr-muted); font-size: 10px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
20
+ .ds-brand-caret { flex: none; color: var(--dsr-muted); font-size: 15px; line-height: 1; transition: transform .18s ease, color .18s ease; }
21
+ .ds-brand-trigger[aria-expanded="true"] .ds-brand-caret { color: var(--dsr-accent-strong); transform: rotate(180deg); }
15
22
  .ds-gh { color: var(--dsr-accent-2); display: inline-flex; }
16
23
  .ds-gh svg { width: 17px; height: 17px; fill: currentColor; }
17
24
  .ds-new-session { padding: 0 12px 8px; }
@@ -470,6 +477,30 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
470
477
  .ds-drawer-body::-webkit-scrollbar-thumb { background: var(--dsr-line); border-radius: 999px; }
471
478
  .ds-drawer-body::-webkit-scrollbar-thumb:hover { background: var(--dsr-muted); }
472
479
 
480
+ /* 左上角应用名展开的组快捷切换抽屉 */
481
+ .ds-group-drawer-backdrop { position: fixed; inset: 0; z-index: 108; border: 0; background: var(--dsr-overlay); }
482
+ .ds-group-drawer { position: fixed; inset: 0 auto 0 0; z-index: 109; width: min(360px, calc(100vw - 18px)); display: flex; flex-direction: column; background: var(--dsr-panel); border-right: 1px solid var(--dsr-line); box-shadow: 18px 0 48px var(--dsr-shadow); animation: ds-group-drawer-in .22s cubic-bezier(.2,.8,.2,1); }
483
+ .ds-group-drawer.hidden, .ds-group-drawer-backdrop.hidden { display: none; }
484
+ @keyframes ds-group-drawer-in { from { opacity: .01; transform: translateX(-16px); } to { opacity: 1; transform: translateX(0); } }
485
+ .ds-group-drawer-head { flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 19px 16px 15px; border-bottom: 1px solid var(--dsr-line); }
486
+ .ds-group-drawer-eyebrow { margin-bottom: 3px; color: var(--dsr-accent-strong); font-size: 10px; font-weight: 750; letter-spacing: 1.5px; }
487
+ .ds-group-drawer-head h2 { margin: 0; color: var(--dsr-text); font-size: 19px; line-height: 1.2; }
488
+ .ds-group-drawer-current { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 14px 14px 8px; padding: 10px 12px; border: 1px solid var(--dsr-accent-line); border-radius: 12px; background: var(--dsr-accent-soft); color: var(--dsr-muted); font-size: 12px; }
489
+ .ds-group-drawer-current strong { min-width: 0; overflow: hidden; color: var(--dsr-accent-strong); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
490
+ .ds-group-drawer-list { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 10px 14px; display: flex; flex-direction: column; gap: 4px; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dsr-line) transparent; }
491
+ .ds-group-drawer-option { width: 100%; min-height: 56px; display: flex; align-items: center; gap: 11px; padding: 9px 11px; border: 1px solid transparent; border-radius: 13px; background: transparent; color: var(--dsr-text); font: inherit; text-align: left; cursor: pointer; }
492
+ .ds-group-drawer-option:hover { background: var(--dsr-bg-2); }
493
+ .ds-group-drawer-option.current { border-color: var(--dsr-accent-line); background: linear-gradient(100deg, var(--dsr-accent-soft), transparent); }
494
+ .ds-group-drawer-mark { width: 10px; height: 10px; flex: none; border: 2px solid var(--dsr-line); border-radius: 50%; }
495
+ .ds-group-drawer-option.current .ds-group-drawer-mark { border-color: var(--dsr-accent); background: var(--dsr-accent); box-shadow: 0 0 0 4px var(--dsr-accent-soft); }
496
+ .ds-group-drawer-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
497
+ .ds-group-drawer-name { overflow: hidden; font-size: 14px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
498
+ .ds-group-drawer-meta { overflow: hidden; color: var(--dsr-muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
499
+ .ds-group-drawer-check { flex: none; color: var(--dsr-accent-strong); font-size: 18px; font-weight: 750; }
500
+ .ds-group-drawer-foot { flex: none; padding: 12px 14px; border-top: 1px solid var(--dsr-line); background: var(--dsr-bg-2); }
501
+ .ds-group-drawer-foot .ds-btn { width: 100%; justify-content: space-between; }
502
+ .ds-group-drawer-foot p { margin: 7px 2px 0; color: var(--dsr-muted); font-size: 11px; }
503
+
473
504
  /* 通知卡片栈(右下角) */
474
505
  .ds-toast-stack { position: fixed; right: 16px; bottom: 16px; z-index: 100; display: flex; flex-direction: column; gap: 10px; width: min(380px, calc(100vw - 32px)); }
475
506
  .ds-notif-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-left: 3px solid var(--dsr-accent-strong); border-radius: 12px; box-shadow: 0 10px 28px rgba(0,0,0,.3); padding: 11px 13px; animation: dsSlideIn .22s ease; }
@@ -12,7 +12,9 @@
12
12
  <div class="ds-app" id="ds-app">
13
13
  <aside class="ds-sidebar" id="ds-sidebar">
14
14
  <div class="ds-brand">
15
- <span class="ds-logo">◆</span><span class="ds-brand-name">DSH Remote</span>
15
+ <button id="ds-group-drawer-trigger" class="ds-brand-trigger" type="button" aria-haspopup="dialog" aria-controls="ds-group-drawer" aria-expanded="false" data-i18n-aria="ds.groupsDrawerOpen">
16
+ <span class="ds-logo">◆</span><span class="ds-brand-copy"><span class="ds-brand-name">DSH Remote</span><span id="ds-brand-group-name" class="ds-brand-group-name">默认</span></span><span class="ds-brand-caret" aria-hidden="true">⌄</span>
17
+ </button>
16
18
  <a class="ds-gh" href="https://github.com/Blank-not-black/dsh-Remote" target="_blank" rel="noopener" data-i18n-title="ds.repo" data-i18n-aria="ds.repo">
17
19
  <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8a8.01 8.01 0 0 0 5.47 7.59c.4.08.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
18
20
  </a>
@@ -82,6 +84,19 @@
82
84
  </div>
83
85
  </aside>
84
86
  <button id="ds-sidebar-backdrop" class="ds-sidebar-backdrop hidden" type="button" data-i18n-aria="ds.close" aria-label="关闭菜单"></button>
87
+ <button id="ds-group-drawer-backdrop" class="ds-group-drawer-backdrop hidden" type="button" tabindex="-1" aria-hidden="true"></button>
88
+ <aside id="ds-group-drawer" class="ds-group-drawer hidden" role="dialog" aria-modal="true" aria-labelledby="ds-group-drawer-title" tabindex="-1">
89
+ <div class="ds-group-drawer-head">
90
+ <div><div class="ds-group-drawer-eyebrow" data-i18n="ds.groupsDrawerEyebrow">QUICK SWITCH</div><h2 id="ds-group-drawer-title" data-i18n="ds.groupsDrawerTitle">切换服务器组</h2></div>
91
+ <button id="ds-group-drawer-close" class="ds-icon-btn" type="button" data-i18n-aria="ds.groupsDrawerClose" data-i18n-title="ds.groupsDrawerClose">×</button>
92
+ </div>
93
+ <div class="ds-group-drawer-current"><span data-i18n="ds.groupsDrawerCurrent">当前连接组</span><strong id="ds-group-drawer-current">默认</strong></div>
94
+ <div id="ds-group-drawer-list" class="ds-group-drawer-list" role="group" data-i18n-aria="ds.groupsDrawerTitle"></div>
95
+ <div class="ds-group-drawer-foot">
96
+ <button id="ds-group-drawer-manage" class="ds-btn" type="button"><span data-i18n="ds.groupsDrawerManage">管理服务器与组</span><span aria-hidden="true">›</span></button>
97
+ <p data-i18n="ds.groupsDrawerManageDesc">编辑、测速和调整组</p>
98
+ </div>
99
+ </aside>
85
100
 
86
101
  <div class="ds-main">
87
102
  <header class="ds-topbar">
@@ -528,6 +543,8 @@
528
543
  'ds.groupsPromptRename': '重命名组「{group}」:', 'ds.groupsRenamed': '组已重命名',
529
544
  'ds.groupsCannotDeleteDefault': '默认组不可删除',
530
545
  'ds.groupsConfirmDelete': '删除组「{group}」?组内服务器将归入默认组。', 'ds.groupsDeleted': '组已删除',
546
+ 'ds.groupsDrawerOpen': '切换服务器组', 'ds.groupsDrawerClose': '关闭组列表', 'ds.groupsDrawerEyebrow': '快速切换', 'ds.groupsDrawerTitle': '切换服务器组', 'ds.groupsDrawerCurrent': '当前连接组',
547
+ 'ds.groupsDrawerManage': '管理服务器与组', 'ds.groupsDrawerManageDesc': '编辑、测速和调整组', 'ds.groupsServerCount': '{count} 台服务器',
531
548
  'ds.theme.default': '默认深空', 'ds.theme.dark': '落日', 'ds.theme.light': '易北爱乐厅', 'ds.theme.neutral': '草原孤塔', 'ds.theme.mono': '黑曜白',
532
549
  'ds.role.me': '我', 'ds.role.dsh': 'DSH', 'ds.toolDefault': '工具',
533
550
  'ds.eventSystemReminder': '系统提示',
@@ -631,6 +648,8 @@
631
648
  'ds.groupsPromptRename': 'Rename group "{group}":', 'ds.groupsRenamed': 'Group renamed',
632
649
  'ds.groupsCannotDeleteDefault': 'The default group cannot be deleted',
633
650
  'ds.groupsConfirmDelete': 'Delete group "{group}"? Its servers move to the default group.', 'ds.groupsDeleted': 'Group deleted',
651
+ 'ds.groupsDrawerOpen': 'Switch server group', 'ds.groupsDrawerClose': 'Close group list', 'ds.groupsDrawerEyebrow': 'QUICK SWITCH', 'ds.groupsDrawerTitle': 'Switch server group', 'ds.groupsDrawerCurrent': 'Current connection group',
652
+ 'ds.groupsDrawerManage': 'Manage servers and groups', 'ds.groupsDrawerManageDesc': 'Edit, test, and organize groups', 'ds.groupsServerCount': '{count} servers',
634
653
  'ds.theme.default': 'Deep Space', 'ds.theme.dark': 'Sunset', 'ds.theme.light': 'Elbphilharmonie', 'ds.theme.neutral': 'Prairie Tower', 'ds.theme.mono': 'Monochrome',
635
654
  'ds.role.me': 'Me', 'ds.role.dsh': 'DSH', 'ds.toolDefault': 'tool',
636
655
  'ds.eventSystemReminder': 'System reminder',
@@ -840,10 +840,79 @@ function renderGroupSelect() {
840
840
  function toggleGroupMenu() { $('group-select-menu').classList.toggle('hidden') }
841
841
  function closeGroupMenu() { $('group-select-menu').classList.add('hidden') }
842
842
 
843
+ let desktopGroupDrawerReturnFocus = null
844
+
845
+ function desktopGroupDrawerOpen() {
846
+ return !$('ds-group-drawer')?.classList.contains('hidden')
847
+ }
848
+
849
+ function renderDesktopGroupDrawer() {
850
+ const trigger = $('ds-group-drawer-trigger')
851
+ const brandGroup = $('ds-brand-group-name')
852
+ const current = $('ds-group-drawer-current')
853
+ const list = $('ds-group-drawer-list')
854
+ if (brandGroup) brandGroup.textContent = state.activeGroup
855
+ if (trigger) trigger.setAttribute('aria-label', t('ds.groupsDrawerOpen') + ':' + state.activeGroup)
856
+ if (current) current.textContent = state.activeGroup
857
+ if (!list) return
858
+ list.innerHTML = state.groups.map(group => {
859
+ const servers = groupServers(group)
860
+ const selected = group === state.activeGroup
861
+ return `<button type="button" class="ds-group-drawer-option ${selected ? 'current' : ''}" data-ds-quick-group="${esc(group)}" ${selected ? 'aria-current="true"' : ''}>
862
+ <span class="ds-group-drawer-mark" aria-hidden="true"></span>
863
+ <span class="ds-group-drawer-copy"><span class="ds-group-drawer-name">${esc(group)}</span><span class="ds-group-drawer-meta">${t('ds.groupsServerCount', { count: servers.length })}</span></span>
864
+ <span class="ds-group-drawer-check" aria-hidden="true">${selected ? '✓' : ''}</span>
865
+ </button>`
866
+ }).join('')
867
+ }
868
+
869
+ function openDesktopGroupDrawer() {
870
+ const drawer = $('ds-group-drawer')
871
+ const backdrop = $('ds-group-drawer-backdrop')
872
+ const trigger = $('ds-group-drawer-trigger')
873
+ if (!drawer || !backdrop || !trigger) return
874
+ renderDesktopGroupDrawer()
875
+ desktopGroupDrawerReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger
876
+ drawer.classList.remove('hidden')
877
+ backdrop.classList.remove('hidden')
878
+ trigger.setAttribute('aria-expanded', 'true')
879
+ requestAnimationFrame(() => (drawer.querySelector('[data-ds-quick-group].current') || drawer.querySelector('[data-ds-quick-group]') || drawer).focus({ preventScroll: true }))
880
+ }
881
+
882
+ function closeDesktopGroupDrawer({ restoreFocus = true } = {}) {
883
+ const drawer = $('ds-group-drawer')
884
+ const backdrop = $('ds-group-drawer-backdrop')
885
+ const trigger = $('ds-group-drawer-trigger')
886
+ if (!drawer || !backdrop || drawer.classList.contains('hidden')) return
887
+ drawer.classList.add('hidden')
888
+ backdrop.classList.add('hidden')
889
+ trigger?.setAttribute('aria-expanded', 'false')
890
+ if (restoreFocus) (desktopGroupDrawerReturnFocus || trigger)?.focus({ preventScroll: true })
891
+ desktopGroupDrawerReturnFocus = null
892
+ }
893
+
894
+ function keepDesktopGroupDrawerFocus(event) {
895
+ if (!desktopGroupDrawerOpen()) return
896
+ if (event.key === 'Escape') {
897
+ event.preventDefault()
898
+ closeDesktopGroupDrawer()
899
+ return
900
+ }
901
+ if (event.key !== 'Tab') return
902
+ const drawer = $('ds-group-drawer')
903
+ const targets = [...drawer.querySelectorAll('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')]
904
+ if (!targets.length) return
905
+ const first = targets[0]
906
+ const last = targets[targets.length - 1]
907
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
908
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
909
+ }
910
+
843
911
  function renderServers() {
844
912
  const box = $('server-list')
845
913
  if (!box) return
846
914
  renderGroupSelect()
915
+ renderDesktopGroupDrawer()
847
916
  box.innerHTML = state.groups.map(g => {
848
917
  const list = groupServers(g)
849
918
  const auto = state.autoSelect[g] !== false
@@ -3077,6 +3146,22 @@ function bindUi() {
3077
3146
  $('btn-server-add').addEventListener('click', addServer)
3078
3147
  $('btn-group-add').addEventListener('click', addGroup)
3079
3148
  $('group-select-btn').addEventListener('click', (e) => { e.stopPropagation(); toggleGroupMenu() })
3149
+ $('ds-group-drawer-trigger').addEventListener('click', () => { desktopGroupDrawerOpen() ? closeDesktopGroupDrawer() : openDesktopGroupDrawer() })
3150
+ $('ds-group-drawer-close').addEventListener('click', () => closeDesktopGroupDrawer())
3151
+ $('ds-group-drawer-backdrop').addEventListener('click', () => closeDesktopGroupDrawer())
3152
+ $('ds-group-drawer-list').addEventListener('click', (e) => {
3153
+ const option = e.target.closest('[data-ds-quick-group]')
3154
+ if (!option) return
3155
+ const group = option.dataset.dsQuickGroup
3156
+ closeDesktopGroupDrawer({ restoreFocus: false })
3157
+ if (group !== state.activeGroup) switchGroup(group)
3158
+ })
3159
+ $('ds-group-drawer-manage').addEventListener('click', () => {
3160
+ closeDesktopGroupDrawer({ restoreFocus: false })
3161
+ showView('view-settings')
3162
+ showSettingsPage('servers')
3163
+ })
3164
+ document.addEventListener('keydown', keepDesktopGroupDrawerFocus)
3080
3165
  document.addEventListener('click', (e) => { if (!e.target.closest('#group-select')) closeGroupMenu() })
3081
3166
  $('server-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); addServer() } })
3082
3167
 
package/public/index.html CHANGED
@@ -15,10 +15,11 @@
15
15
  </head>
16
16
  <body>
17
17
  <header class="topbar">
18
- <div class="brand">
18
+ <button id="group-drawer-trigger" class="brand brand-trigger" type="button" aria-haspopup="dialog" aria-controls="group-drawer" aria-expanded="false" data-i18n-aria="groups.drawerOpen">
19
19
  <span class="brand-dot"></span>
20
- <span class="brand-name">DSH Remote</span>
21
- </div>
20
+ <span class="brand-copy"><span class="brand-name">DSH Remote</span><span id="brand-group-name" class="brand-group-name">默认</span></span>
21
+ <svg class="brand-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg>
22
+ </button>
22
23
  <div class="topbar-right">
23
24
  <button id="btn-donate" class="topbar-btn icon-btn" data-i18n-title="settings.donate" data-i18n-aria="settings.donate"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8h11v6a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4Z"/><path d="M17 10h1.5a2.5 2.5 0 0 1 0 5H17M9 4c0 1 1 1.2 1 2M13 4c0 1 1 1.2 1 2M5 20h13"/></svg></button>
24
25
  <button id="btn-feedback" class="topbar-btn icon-btn" data-i18n-title="feedback.title" data-i18n-aria="feedback.title" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 11.5a7.5 7.5 0 0 1-7.5 7.5H7l-3 2 .8-3.1A7.5 7.5 0 1 1 20 11.5Z"/><path d="M8 11.5h.01M12 11.5h.01M16 11.5h.01"/></svg></button>
@@ -27,6 +28,20 @@
27
28
  </div>
28
29
  </header>
29
30
 
31
+ <button id="group-drawer-backdrop" class="group-drawer-backdrop hidden" type="button" tabindex="-1" aria-hidden="true"></button>
32
+ <aside id="group-drawer" class="group-drawer hidden" role="dialog" aria-modal="true" aria-labelledby="group-drawer-title" tabindex="-1">
33
+ <div class="group-drawer-head">
34
+ <div><div class="group-drawer-eyebrow" data-i18n="groups.drawerEyebrow">QUICK SWITCH</div><h2 id="group-drawer-title" data-i18n="groups.drawerTitle">切换服务器组</h2></div>
35
+ <button id="group-drawer-close" class="group-drawer-close" type="button" data-i18n-aria="groups.drawerClose" data-i18n-title="groups.drawerClose">×</button>
36
+ </div>
37
+ <div class="group-drawer-current"><span data-i18n="groups.drawerCurrent">当前连接组</span><strong id="group-drawer-current">默认</strong></div>
38
+ <div id="group-drawer-list" class="group-drawer-list" role="group" data-i18n-aria="groups.drawerTitle"></div>
39
+ <div class="group-drawer-foot">
40
+ <button id="group-drawer-manage" class="group-drawer-manage" type="button"><span data-i18n="groups.drawerManage">管理服务器与组</span><span aria-hidden="true">›</span></button>
41
+ <p data-i18n="groups.drawerManageDesc">编辑、测速和调整组</p>
42
+ </div>
43
+ </aside>
44
+
30
45
  <main id="main" class="main">
31
46
  <!-- 会话列表 -->
32
47
  <section id="view-home" class="view">
@@ -958,6 +973,8 @@
958
973
  'groups.added': '已创建组:{group}', 'groups.promptRename': '重命名组「{group}」:',
959
974
  'groups.renamed': '组已重命名', 'groups.cannotDeleteDefault': '默认组不可删除',
960
975
  'groups.confirmDelete': '删除组「{group}」?组内服务器将归入默认组。', 'groups.deleted': '组已删除',
976
+ 'groups.drawerOpen': '切换服务器组', 'groups.drawerClose': '关闭组列表', 'groups.drawerEyebrow': '快速切换', 'groups.drawerTitle': '切换服务器组', 'groups.drawerCurrent': '当前连接组',
977
+ 'groups.drawerManage': '管理服务器与组', 'groups.drawerManageDesc': '编辑、测速和调整组', 'groups.serverCount': '{count} 台服务器',
961
978
  'stream.error': '事件流错误:{msg}',
962
979
  'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
963
980
  'session.stop': '停止本轮', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}', 'session.recovering': '恢复会话中…', 'session.recoveryReady': '会话已恢复', 'session.recoveryFailed': '会话恢复失败', 'session.recoveryCached': '正在查看缓存历史',
@@ -1188,6 +1205,8 @@
1188
1205
  'groups.added': 'Created group: {group}', 'groups.promptRename': 'Rename group "{group}":',
1189
1206
  'groups.renamed': 'Group renamed', 'groups.cannotDeleteDefault': 'The default group cannot be deleted',
1190
1207
  'groups.confirmDelete': 'Delete group "{group}"? Its servers move to the default group.', 'groups.deleted': 'Group deleted',
1208
+ 'groups.drawerOpen': 'Switch server group', 'groups.drawerClose': 'Close group list', 'groups.drawerEyebrow': 'QUICK SWITCH', 'groups.drawerTitle': 'Switch server group', 'groups.drawerCurrent': 'Current connection group',
1209
+ 'groups.drawerManage': 'Manage servers and groups', 'groups.drawerManageDesc': 'Edit, test, and organize groups', 'groups.serverCount': '{count} servers',
1191
1210
  'stream.error': 'Event stream error: {msg}',
1192
1211
  'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
1193
1212
  'session.stop': 'Stop turn', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}', 'session.recovering': 'Restoring session…', 'session.recoveryReady': 'Session ready', 'session.recoveryFailed': 'Session restore failed', 'session.recoveryCached': 'Viewing cached history',
package/public/styles.css CHANGED
@@ -40,7 +40,15 @@ body { padding-bottom: calc(92px + var(--dsr-safe-b)); }
40
40
  background: var(--dsr-bg);
41
41
  border-bottom: 1px solid var(--dsr-line);
42
42
  }
43
- .brand { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: .5px; }
43
+ .brand { display: flex; align-items: center; gap: 8px; min-width: 0; font-weight: 700; letter-spacing: .5px; }
44
+ .brand-trigger { padding: 4px 5px 4px 2px; border-radius: 11px; text-align: left; color: var(--dsr-text); }
45
+ .brand-trigger:active { background: var(--dsr-accent-soft); }
46
+ .brand-trigger:focus-visible { outline: 2px solid var(--dsr-accent); outline-offset: 2px; }
47
+ .brand-copy { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 0; line-height: 1.14; }
48
+ .brand-name { white-space: nowrap; }
49
+ .brand-group-name { max-width: 16ch; overflow: hidden; color: var(--dsr-muted); font-size: 10.5px; font-weight: 600; letter-spacing: .15px; text-overflow: ellipsis; white-space: nowrap; }
50
+ .brand-caret { width: 14px; height: 14px; flex: none; color: var(--dsr-muted); transition: transform .18s ease, color .18s ease; }
51
+ .brand-trigger[aria-expanded="true"] .brand-caret { color: var(--dsr-accent-strong); transform: rotate(180deg); }
44
52
  .brand-dot {
45
53
  width: 10px; height: 10px; border-radius: 50%;
46
54
  background: linear-gradient(135deg, var(--dsr-brand-a), var(--dsr-brand-b));
@@ -77,6 +85,34 @@ button {
77
85
  .topbar-right #btn-feedback { color: var(--dsr-info); }
78
86
  .topbar-right #btn-refresh { color: var(--dsr-success); }
79
87
  .topbar-right .topbar-btn.conn-badge { padding: 0 14px; border-radius: 999px; font-size: 12.5px; }
88
+
89
+ /* ---------- 顶栏组切换抽屉 ---------- */
90
+ .group-drawer-backdrop { position: fixed; inset: 0; z-index: 70; border: 0; background: var(--dsr-overlay); }
91
+ .group-drawer { position: fixed; inset: 0 auto 0 0; z-index: 71; display: flex; flex-direction: column; width: min(360px, calc(100vw - 18px)); background: var(--dsr-panel); border-right: 1px solid var(--dsr-line); box-shadow: 18px 0 48px var(--dsr-shadow); animation: group-drawer-in .22s cubic-bezier(.2,.8,.2,1); }
92
+ .group-drawer.hidden, .group-drawer-backdrop.hidden { display: none; }
93
+ @keyframes group-drawer-in { from { opacity: .01; transform: translateX(-16px); } to { opacity: 1; transform: translateX(0); } }
94
+ body.group-drawer-open { overflow: hidden; }
95
+ .group-drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: calc(var(--dsr-safe-top) + 18px) 18px 15px; border-bottom: 1px solid var(--dsr-line); }
96
+ .group-drawer-eyebrow { margin-bottom: 3px; color: var(--dsr-accent-strong); font-size: 10px; font-weight: 750; letter-spacing: 1.5px; }
97
+ .group-drawer-head h2 { margin: 0; color: var(--dsr-text); font-size: 20px; line-height: 1.2; }
98
+ .group-drawer-close { width: 38px; height: 38px; display: grid; flex: none; place-items: center; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-bg-2); color: var(--dsr-muted); font-size: 24px; line-height: 1; }
99
+ .group-drawer-close:active { background: var(--dsr-accent-soft); color: var(--dsr-accent-strong); }
100
+ .group-drawer-current { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 14px 14px 8px; padding: 11px 12px; border: 1px solid var(--dsr-accent-line); border-radius: 12px; background: var(--dsr-accent-soft); color: var(--dsr-muted); font-size: 12px; }
101
+ .group-drawer-current strong { min-width: 0; overflow: hidden; color: var(--dsr-accent-strong); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
102
+ .group-drawer-list { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 10px 14px; display: flex; flex-direction: column; gap: 4px; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dsr-line) transparent; }
103
+ .group-drawer-option { width: 100%; min-height: 58px; display: flex; align-items: center; gap: 11px; padding: 9px 11px; border: 1px solid transparent; border-radius: 13px; color: var(--dsr-text); text-align: left; }
104
+ .group-drawer-option:active { background: var(--dsr-bg-2); }
105
+ .group-drawer-option.current { border-color: var(--dsr-accent-line); background: linear-gradient(100deg, var(--dsr-accent-soft), transparent); }
106
+ .group-drawer-option-mark { width: 10px; height: 10px; flex: none; border: 2px solid var(--dsr-line); border-radius: 50%; }
107
+ .group-drawer-option.current .group-drawer-option-mark { border-color: var(--dsr-accent); background: var(--dsr-accent); box-shadow: 0 0 0 4px var(--dsr-accent-soft); }
108
+ .group-drawer-option-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
109
+ .group-drawer-option-name { overflow: hidden; font-size: 14px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
110
+ .group-drawer-option-meta { overflow: hidden; color: var(--dsr-muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
111
+ .group-drawer-option-check { flex: none; color: var(--dsr-accent-strong); font-size: 18px; font-weight: 750; }
112
+ .group-drawer-foot { flex: none; padding: 12px 14px calc(12px + var(--dsr-safe-b)); border-top: 1px solid var(--dsr-line); background: var(--dsr-bg-2); }
113
+ .group-drawer-manage { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 12px; border: 1px solid var(--dsr-line); border-radius: 11px; background: var(--dsr-panel); color: var(--dsr-accent-strong); font-weight: 700; text-align: left; }
114
+ .group-drawer-manage:active { background: var(--dsr-accent-soft); }
115
+ .group-drawer-foot p { margin: 7px 2px 0; color: var(--dsr-muted); font-size: 11px; }
80
116
  .mini-btn {
81
117
  padding: 5px 11px; border-radius: 9px; font-size: 13px;
82
118
  background: var(--dsr-panel); border: 1px solid var(--dsr-line); color: var(--dsr-accent-strong);
@@ -1215,6 +1251,8 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
1215
1251
  .section-actions .session-sort { max-width: 98px; }
1216
1252
  .section-actions .mini-btn { max-width: 92px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1217
1253
  .topbar { padding-left: 10px; padding-right: 10px; }
1254
+ .brand { gap: 6px; }
1255
+ .brand-group-name { max-width: 10ch; }
1218
1256
  .topbar-right { gap: 4px; min-width: 0; }
1219
1257
  .topbar-right .topbar-btn.icon-btn { width: 38px; height: 38px; }
1220
1258
  .topbar-right .topbar-btn.conn-badge { max-width: 72px; padding-left: 8px; padding-right: 8px; overflow: hidden; text-overflow: ellipsis; }
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.6.20",
2
+ "version": "0.6.21",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "a5793085029414356a0a4faf411eb3c99bc934cdbf3ae387cc9c4c06738cf0c1",
5
- "releasedAt": "2026-08-30T14:51:29.621Z",
6
- "notes": "0.6.20:适配 DSH 0.1.2-alpha.1 的新版客户端模块、认证、斜杠 RPC 与 remote.mux 实时协议,同时保留旧版 DSH 的自动回退;修复斜杠命令因 commands.execute 参数变化触发 aborted 报错,以及新版历史、工作区、模型、目标和子代理等接口不兼容问题;审批与用户提问现在会在所属对话内完整显示并可直接处理,主页仍保留全局待办入口,长工具参数、原因和调用标识不再被截断。",
4
+ "sha256": "46fc5409dbe59300e352ae6dddfbc001856a349a85b81be025e958316a1c5dc0",
5
+ "releasedAt": "2026-08-31T04:10:09.396Z",
6
+ "notes": "0.6.21:左上角 DSH Remote 名称可展开服务器组快捷切换抽屉,当前组、组内服务器数量和管理入口一目了然,手机与桌面端均可一键切换;修复部分新版 DSH 命令执行成功返回 void 时被误判为未执行、继而把 /命令作为普通文本发送的问题,同时兼容旧版三参数、新版四参数及默认参数签名。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.21",
10
+ "notes": "0.6.21:左上角 DSH Remote 名称可展开服务器组快捷切换抽屉,当前组、组内服务器数量和管理入口一目了然,手机与桌面端均可一键切换;修复部分新版 DSH 命令执行成功返回 void 时被误判为未执行、继而把 /命令作为普通文本发送的问题,同时兼容旧版三参数、新版四参数及默认参数签名。"
11
+ },
8
12
  {
9
13
  "version": "0.6.20",
10
14
  "notes": "0.6.20:适配 DSH 0.1.2-alpha.1 的新版客户端模块、认证、斜杠 RPC 与 remote.mux 实时协议,同时保留旧版 DSH 的自动回退;修复斜杠命令因 commands.execute 参数变化触发 aborted 报错,以及新版历史、工作区、模型、目标和子代理等接口不兼容问题;审批与用户提问现在会在所属对话内完整显示并可直接处理,主页仍保留全局待办入口,长工具参数、原因和调用标识不再被截断。"
@@ -40,10 +44,6 @@
40
44
  {
41
45
  "version": "0.6.12",
42
46
  "notes": "0.6.12:修复远程启动和重启 DSH 时统一报 HTTP 502 且无法确认结果的问题;改为异步追踪 systemd 服务检查、命令提交、进程启动、DSH HTTP 恢复和 mux/host 实时通道重连阶段,成功时显示 PID、HTTP 状态和用时,失败时区分服务不存在、systemd 不可用、权限不足、命令失败、服务失败、启动超时、HTTP 恢复超时和实时通道恢复超时;优化工作区会话筛选、文件预览和应用内选择器;新增中央投票公告与反馈成功确认;主页公告栏常驻并在无未读公告时显示空状态,同时修正设置页异常右箭头、主页刷新图标居中和周末谷时提醒。"
43
- },
44
- {
45
- "version": "0.6.11",
46
- "notes": "0.6.11 正式版:新增按 DSH 工作区筛选、切换和创建会话,文件页支持切换工作区并直接预览常见文本与 Markdown;修复工作区筛选未正确收敛会话、工作区位于默认文件根外时无法查看,以及新会话工作区名称与完整路径显示异常;新增投票公告及隐私化汇总;统一手机端应用内选择抽屉;周末全天按谷时并修复重复峰谷提醒;插件侧栏图标采用亮色背景并随插件自身四套皮肤切换配色;加强网关安装、连接与故障排查引导。"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.20"
2
+ "version": "0.6.21"
3
3
  }