dsh-remote-plugin 0.6.20 → 0.6.22
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/apk/dsh-remote.apk +0 -0
- package/index.mjs +128 -9
- package/package.json +1 -1
- package/public/announcements.json +8 -0
- package/public/app.js +326 -13
- package/public/desktop/desktop.css +33 -2
- package/public/desktop/desktop.html +23 -4
- package/public/desktop/desktop.js +246 -4
- package/public/index.html +32 -6
- package/public/styles.css +42 -1
- package/public/update.json +12 -12
- package/public/version.json +1 -1
|
@@ -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:
|
|
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-
|
|
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
|
-
<
|
|
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">
|
|
@@ -154,7 +169,7 @@
|
|
|
154
169
|
<section id="session-pending" class="ds-session-pending hidden" aria-live="polite"></section>
|
|
155
170
|
<div id="history" class="ds-history" aria-live="polite"></div>
|
|
156
171
|
<div class="ds-composer">
|
|
157
|
-
<div id="composer-status" class="ds-composer-status hidden" role="status" aria-live="polite"><span class="ds-composer-status-dot" aria-hidden="true"></span><span data-i18n="ds.composerRunning">运行中…</span></div>
|
|
172
|
+
<div id="composer-status" class="ds-composer-status hidden" role="status" aria-live="polite"><span class="ds-composer-status-dot" aria-hidden="true"></span><span id="composer-status-text" data-i18n="ds.composerRunning">运行中…</span></div>
|
|
158
173
|
<textarea id="composer" rows="1" data-i18n-placeholder="ds.composerPlaceholder" placeholder="输入消息…"></textarea>
|
|
159
174
|
<div class="ds-composer-actions">
|
|
160
175
|
<div class="ds-composer-left">
|
|
@@ -462,7 +477,7 @@
|
|
|
462
477
|
'ds.files': '文件传输', 'ds.settings': '设置', 'ds.stats': '统计', 'ds.menu': '导航',
|
|
463
478
|
'ds.send': '发送', 'ds.composerPlaceholder': '输入消息,Enter 发送…', 'ds.composerRunning': '运行中…', 'ds.presets': '预设', 'ds.sessionRename': '重命名会话', 'ds.sessionRenameTitle': '重命名会话', 'ds.sessionRenamePlaceholder': '输入新的会话名称', 'ds.sessionRenameConfirm': '保存', 'ds.sessionRenameFailed': '重命名失败', 'ds.sessionRenameEmpty': '会话名称不能为空', 'ds.sessionRenamed': '会话名称已更新', 'ds.sessionArchive': '归档', 'ds.sessionArchiveConfirm': '归档这个会话?归档后可在“显示已归档”中打开。', 'ds.sessionArchived': '会话已归档', 'ds.sessionStop': '停止本轮', 'ds.sessionStopConfirm': '停止当前回合?排队中的消息不会被删除。', 'ds.sessionStopFailed': '停止失败', 'ds.sessionStopRequested': '已请求停止本轮', 'ds.sessionRecovering': '恢复会话中…', 'ds.sessionReady': '会话已恢复', 'ds.sessionRecoveryFailed': '会话恢复失败',
|
|
464
479
|
'ds.commands': '指令',
|
|
465
|
-
'ds.cmdCompact': '/compact 压缩对话历史', 'ds.cmdExport': '/export 导出会话日志 ZIP',
|
|
480
|
+
'ds.cmdCompact': '/compact 压缩对话历史', 'ds.cmdExport': '/export 导出会话日志 ZIP', 'ds.commandTimedOut': '命令执行超时,未作为普通消息发送', 'ds.exportStarted': '会话日志已开始下载,请在浏览器下载中查看', 'ds.exportFailed': '无法导出会话日志:{msg}', 'ds.compacting': '正在压缩对话 · 已用时 {elapsed}', 'ds.compactComplete': '对话压缩完成', 'ds.compactFailed': '对话压缩未完成:{msg}', 'ds.commandRunning': '正在执行 /{command} · 已用时 {elapsed}', 'ds.commandComplete': '/{command} 已完成', 'ds.commandFailed': '/{command} 未完成:{msg}',
|
|
466
481
|
'ds.cmdFeedback': '/feedback 反馈当前会话', 'ds.cmdGoal': '/goal 设置/查看任务目标',
|
|
467
482
|
'ds.cmdPermission': '/permission 切换权限预设', 'ds.cmdPlan': '/plan 进入/退出计划模式',
|
|
468
483
|
'ds.fsUp': '上级', 'ds.fsRoot': '允许根目录', 'ds.fsNewWorkspace': '新建工作区', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
|
|
@@ -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': '系统提示',
|
|
@@ -565,7 +582,7 @@
|
|
|
565
582
|
'ds.files': 'Files', 'ds.settings': 'Settings', 'ds.stats': 'Stats', 'ds.menu': 'Menu',
|
|
566
583
|
'ds.send': 'Send', 'ds.composerPlaceholder': 'Type a message, Enter to send…', 'ds.composerRunning': 'Running…', 'ds.presets': 'Presets', 'ds.sessionRename': 'Rename session', 'ds.sessionRenameTitle': 'Rename session', 'ds.sessionRenamePlaceholder': 'Enter a new session name', 'ds.sessionRenameConfirm': 'Save', 'ds.sessionRenameFailed': 'Rename failed', 'ds.sessionRenameEmpty': 'Session name cannot be empty', 'ds.sessionRenamed': 'Session name updated', 'ds.sessionArchive': 'Archive', 'ds.sessionArchiveConfirm': 'Archive this session? You can open it from “Show archived”.', 'ds.sessionArchived': 'Session archived', 'ds.sessionStop': 'Stop turn', 'ds.sessionStopConfirm': 'Stop the current turn? Queued messages will be kept.', 'ds.sessionStopFailed': 'Stop failed', 'ds.sessionStopRequested': 'Stop requested', 'ds.sessionRecovering': 'Restoring session…', 'ds.sessionReady': 'Session ready', 'ds.sessionRecoveryFailed': 'Session restore failed',
|
|
567
584
|
'ds.commands': 'Commands',
|
|
568
|
-
'ds.cmdCompact': '/compact Compress conversation history', 'ds.cmdExport': '/export Export session log ZIP',
|
|
585
|
+
'ds.cmdCompact': '/compact Compress conversation history', 'ds.cmdExport': '/export Export session log ZIP', 'ds.commandTimedOut': 'Command timed out and was not sent as a chat message', 'ds.exportStarted': 'Session log download started; check your browser downloads', 'ds.exportFailed': 'Could not export session log: {msg}', 'ds.compacting': 'Compressing conversation · {elapsed} elapsed', 'ds.compactComplete': 'Conversation compression complete', 'ds.compactFailed': 'Conversation compression did not finish: {msg}', 'ds.commandRunning': 'Running /{command} · {elapsed} elapsed', 'ds.commandComplete': '/{command} complete', 'ds.commandFailed': '/{command} did not finish: {msg}',
|
|
569
586
|
'ds.cmdFeedback': '/feedback Feedback current session', 'ds.cmdGoal': '/goal Set/view task goal',
|
|
570
587
|
'ds.cmdPermission': '/permission Switch permission preset', 'ds.cmdPlan': '/plan Enter/exit plan mode',
|
|
571
588
|
'ds.fsUp': 'Up', 'ds.fsRoot': 'Allowed root', 'ds.fsNewWorkspace': 'New workspace', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
|
|
@@ -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',
|
|
@@ -79,6 +79,10 @@ const state = {
|
|
|
79
79
|
questions: [],
|
|
80
80
|
queues: {},
|
|
81
81
|
queueSteering: {},
|
|
82
|
+
compactions: {},
|
|
83
|
+
pendingCommands: {},
|
|
84
|
+
compactionPollTimer: null,
|
|
85
|
+
compactionClockTimer: null,
|
|
82
86
|
sessionTurnTimes: {},
|
|
83
87
|
questionModal: null,
|
|
84
88
|
streamsOk: { mux: false, host: false },
|
|
@@ -840,10 +844,79 @@ function renderGroupSelect() {
|
|
|
840
844
|
function toggleGroupMenu() { $('group-select-menu').classList.toggle('hidden') }
|
|
841
845
|
function closeGroupMenu() { $('group-select-menu').classList.add('hidden') }
|
|
842
846
|
|
|
847
|
+
let desktopGroupDrawerReturnFocus = null
|
|
848
|
+
|
|
849
|
+
function desktopGroupDrawerOpen() {
|
|
850
|
+
return !$('ds-group-drawer')?.classList.contains('hidden')
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function renderDesktopGroupDrawer() {
|
|
854
|
+
const trigger = $('ds-group-drawer-trigger')
|
|
855
|
+
const brandGroup = $('ds-brand-group-name')
|
|
856
|
+
const current = $('ds-group-drawer-current')
|
|
857
|
+
const list = $('ds-group-drawer-list')
|
|
858
|
+
if (brandGroup) brandGroup.textContent = state.activeGroup
|
|
859
|
+
if (trigger) trigger.setAttribute('aria-label', t('ds.groupsDrawerOpen') + ':' + state.activeGroup)
|
|
860
|
+
if (current) current.textContent = state.activeGroup
|
|
861
|
+
if (!list) return
|
|
862
|
+
list.innerHTML = state.groups.map(group => {
|
|
863
|
+
const servers = groupServers(group)
|
|
864
|
+
const selected = group === state.activeGroup
|
|
865
|
+
return `<button type="button" class="ds-group-drawer-option ${selected ? 'current' : ''}" data-ds-quick-group="${esc(group)}" ${selected ? 'aria-current="true"' : ''}>
|
|
866
|
+
<span class="ds-group-drawer-mark" aria-hidden="true"></span>
|
|
867
|
+
<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>
|
|
868
|
+
<span class="ds-group-drawer-check" aria-hidden="true">${selected ? '✓' : ''}</span>
|
|
869
|
+
</button>`
|
|
870
|
+
}).join('')
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function openDesktopGroupDrawer() {
|
|
874
|
+
const drawer = $('ds-group-drawer')
|
|
875
|
+
const backdrop = $('ds-group-drawer-backdrop')
|
|
876
|
+
const trigger = $('ds-group-drawer-trigger')
|
|
877
|
+
if (!drawer || !backdrop || !trigger) return
|
|
878
|
+
renderDesktopGroupDrawer()
|
|
879
|
+
desktopGroupDrawerReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger
|
|
880
|
+
drawer.classList.remove('hidden')
|
|
881
|
+
backdrop.classList.remove('hidden')
|
|
882
|
+
trigger.setAttribute('aria-expanded', 'true')
|
|
883
|
+
requestAnimationFrame(() => (drawer.querySelector('[data-ds-quick-group].current') || drawer.querySelector('[data-ds-quick-group]') || drawer).focus({ preventScroll: true }))
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function closeDesktopGroupDrawer({ restoreFocus = true } = {}) {
|
|
887
|
+
const drawer = $('ds-group-drawer')
|
|
888
|
+
const backdrop = $('ds-group-drawer-backdrop')
|
|
889
|
+
const trigger = $('ds-group-drawer-trigger')
|
|
890
|
+
if (!drawer || !backdrop || drawer.classList.contains('hidden')) return
|
|
891
|
+
drawer.classList.add('hidden')
|
|
892
|
+
backdrop.classList.add('hidden')
|
|
893
|
+
trigger?.setAttribute('aria-expanded', 'false')
|
|
894
|
+
if (restoreFocus) (desktopGroupDrawerReturnFocus || trigger)?.focus({ preventScroll: true })
|
|
895
|
+
desktopGroupDrawerReturnFocus = null
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function keepDesktopGroupDrawerFocus(event) {
|
|
899
|
+
if (!desktopGroupDrawerOpen()) return
|
|
900
|
+
if (event.key === 'Escape') {
|
|
901
|
+
event.preventDefault()
|
|
902
|
+
closeDesktopGroupDrawer()
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
if (event.key !== 'Tab') return
|
|
906
|
+
const drawer = $('ds-group-drawer')
|
|
907
|
+
const targets = [...drawer.querySelectorAll('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')]
|
|
908
|
+
if (!targets.length) return
|
|
909
|
+
const first = targets[0]
|
|
910
|
+
const last = targets[targets.length - 1]
|
|
911
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
|
912
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
|
913
|
+
}
|
|
914
|
+
|
|
843
915
|
function renderServers() {
|
|
844
916
|
const box = $('server-list')
|
|
845
917
|
if (!box) return
|
|
846
918
|
renderGroupSelect()
|
|
919
|
+
renderDesktopGroupDrawer()
|
|
847
920
|
box.innerHTML = state.groups.map(g => {
|
|
848
921
|
const list = groupServers(g)
|
|
849
922
|
const auto = state.autoSelect[g] !== false
|
|
@@ -1306,6 +1379,88 @@ function onHostFrame(full) {
|
|
|
1306
1379
|
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
|
|
1307
1380
|
}
|
|
1308
1381
|
}
|
|
1382
|
+
function activeCompaction(sessionId = state.current) {
|
|
1383
|
+
const compact = state.compactions[sessionId]
|
|
1384
|
+
return compact?.active === true ? compact : null
|
|
1385
|
+
}
|
|
1386
|
+
function compactElapsed(startedAt) {
|
|
1387
|
+
const seconds = Math.max(0, Math.floor((Date.now() - Number(startedAt || Date.now())) / 1000))
|
|
1388
|
+
const minutes = Math.floor(seconds / 60)
|
|
1389
|
+
const remain = seconds % 60
|
|
1390
|
+
return minutes > 0 ? `${minutes}:${String(remain).padStart(2, '0')}` : `${remain}s`
|
|
1391
|
+
}
|
|
1392
|
+
function setCompactionStatus(sessionId, next) {
|
|
1393
|
+
if (!sessionId) return
|
|
1394
|
+
const previous = state.compactions[sessionId]
|
|
1395
|
+
if (next?.active === true) {
|
|
1396
|
+
state.compactions[sessionId] = {
|
|
1397
|
+
active: true,
|
|
1398
|
+
phase: next.phase || previous?.phase || 'running',
|
|
1399
|
+
command: String(next.command || previous?.command || 'compact'),
|
|
1400
|
+
startedAt: Number(next.startedAt) || previous?.startedAt || Date.now(),
|
|
1401
|
+
message: String(next.message || ''),
|
|
1402
|
+
source: next.source || previous?.source || 'event',
|
|
1403
|
+
}
|
|
1404
|
+
} else {
|
|
1405
|
+
delete state.compactions[sessionId]
|
|
1406
|
+
if (previous?.active) {
|
|
1407
|
+
const command = previous.command || 'compact'
|
|
1408
|
+
if (next?.phase === 'failed') toast(command === 'compact'
|
|
1409
|
+
? t('ds.compactFailed', { msg: next.message || t('ds.sessionRecoveryFailed') })
|
|
1410
|
+
: t('ds.commandFailed', { command, msg: next.message || t('ds.sessionRecoveryFailed') }), 'err')
|
|
1411
|
+
else toast(command === 'compact' ? t('ds.compactComplete') : t('ds.commandComplete', { command }), 'ok')
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
ensureCompactionMonitoring()
|
|
1415
|
+
if (state.current === sessionId) updateComposerStatus()
|
|
1416
|
+
}
|
|
1417
|
+
async function refreshCompactionStatus(sessionId = state.current) {
|
|
1418
|
+
if (!sessionId) return
|
|
1419
|
+
try {
|
|
1420
|
+
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
|
|
1421
|
+
const url = new URL(apiUrl('/remote/api/command-status'), location.href)
|
|
1422
|
+
url.searchParams.set('sessionId', sessionId)
|
|
1423
|
+
const res = await fetch(url, { headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }, ...(signal ? { signal } : {}) })
|
|
1424
|
+
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
1425
|
+
if (!res.ok) return
|
|
1426
|
+
const body = await res.json().catch(() => null)
|
|
1427
|
+
const operation = body?.operation || body?.compact
|
|
1428
|
+
if (!operation) return
|
|
1429
|
+
const pending = state.pendingCommands[sessionId]
|
|
1430
|
+
if (operation.active) {
|
|
1431
|
+
delete state.pendingCommands[sessionId]
|
|
1432
|
+
setCompactionStatus(sessionId, { ...operation, source: 'status' })
|
|
1433
|
+
} else if (activeCompaction(sessionId) && activeCompaction(sessionId).source !== 'event') {
|
|
1434
|
+
setCompactionStatus(sessionId, operation)
|
|
1435
|
+
} else if (pending) {
|
|
1436
|
+
if (operation.phase === 'failed') toast(t('ds.commandFailed', { command: pending, msg: operation.message || t('ds.sessionRecoveryFailed') }), 'err')
|
|
1437
|
+
delete state.pendingCommands[sessionId]
|
|
1438
|
+
}
|
|
1439
|
+
} catch {}
|
|
1440
|
+
}
|
|
1441
|
+
function ensureCompactionMonitoring() {
|
|
1442
|
+
const active = Object.values(state.compactions).some(compact => compact?.active)
|
|
1443
|
+
if (!active) {
|
|
1444
|
+
if (state.compactionPollTimer) clearInterval(state.compactionPollTimer)
|
|
1445
|
+
if (state.compactionClockTimer) clearInterval(state.compactionClockTimer)
|
|
1446
|
+
state.compactionPollTimer = null
|
|
1447
|
+
state.compactionClockTimer = null
|
|
1448
|
+
return
|
|
1449
|
+
}
|
|
1450
|
+
if (!state.compactionClockTimer) state.compactionClockTimer = setInterval(() => { if (activeCompaction()) updateComposerStatus() }, 1000)
|
|
1451
|
+
if (!state.compactionPollTimer) state.compactionPollTimer = setInterval(() => {
|
|
1452
|
+
const compact = activeCompaction()
|
|
1453
|
+
if (compact && compact.source !== 'event') void refreshCompactionStatus()
|
|
1454
|
+
}, 3000)
|
|
1455
|
+
}
|
|
1456
|
+
function observeCompactionEvent(sessionId, event) {
|
|
1457
|
+
if (event?.type === 'compaction/start') {
|
|
1458
|
+
const existing = activeCompaction(sessionId)
|
|
1459
|
+
setCompactionStatus(sessionId, { active: true, phase: 'running', startedAt: existing?.startedAt || Date.now(), source: existing?.source || 'event' })
|
|
1460
|
+
} else if (event?.type === 'compaction/end' && activeCompaction(sessionId)?.source === 'event') {
|
|
1461
|
+
setCompactionStatus(sessionId, { active: false, phase: 'complete' })
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1309
1464
|
function hydrateSessionProjections(sessionId, projections) {
|
|
1310
1465
|
const s = state.byId.get(sessionId)
|
|
1311
1466
|
if (!s || !projections || typeof projections !== 'object') return
|
|
@@ -1401,6 +1556,7 @@ function setGoalCollapsed(sessionId, goal, collapsed) {
|
|
|
1401
1556
|
LS.set(COLLAPSED_GOALS_KEY, JSON.stringify(next.slice(-100)))
|
|
1402
1557
|
}
|
|
1403
1558
|
function onSessionEvent(sessionId, event) {
|
|
1559
|
+
observeCompactionEvent(sessionId, event)
|
|
1404
1560
|
if (event?.type === 'turn/start' || event?.type === 'turn/end') {
|
|
1405
1561
|
noteSessionTurnTime(sessionId, event)
|
|
1406
1562
|
renderSessions()
|
|
@@ -1568,6 +1724,7 @@ async function openSession(id) {
|
|
|
1568
1724
|
renderQueue()
|
|
1569
1725
|
renderSessionPendingDesktop()
|
|
1570
1726
|
updateComposerStatus()
|
|
1727
|
+
void refreshCompactionStatus(id)
|
|
1571
1728
|
await loadHistory()
|
|
1572
1729
|
}
|
|
1573
1730
|
function closeSession() {
|
|
@@ -1869,12 +2026,55 @@ async function interruptSubagent(childId) {
|
|
|
1869
2026
|
setTimeout(renderSessionCards, 600)
|
|
1870
2027
|
}
|
|
1871
2028
|
|
|
2029
|
+
const NO_FALLBACK_SLASH_COMMANDS = new Set(['compact', 'export'])
|
|
2030
|
+
const SLASH_COMMAND_TIMEOUT_MS = 20_000
|
|
2031
|
+
// 比插件端的 120 秒多留 5 秒,让服务端能返回确定的失败结果而非客户端先中断。
|
|
2032
|
+
const LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS = 125_000
|
|
2033
|
+
|
|
2034
|
+
function slashCommandName(text) {
|
|
2035
|
+
const match = /^\/+([^\s/]+)/.exec(String(text || '').trim())
|
|
2036
|
+
return match ? match[1].toLowerCase() : ''
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function sessionLogFilename(sessionId) {
|
|
2040
|
+
return `dsh-session-${String(sessionId).replace(/[^A-Za-z0-9_-]/g, '_')}.zip`
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
async function downloadSessionExport(sessionId) {
|
|
2044
|
+
const url = new URL(apiUrl('/api/session.export'), location.href)
|
|
2045
|
+
url.searchParams.set('sessionId', sessionId)
|
|
2046
|
+
url.searchParams.set('includeDescendants', 'true')
|
|
2047
|
+
if (state.server && state.token) url.searchParams.set('token', state.token)
|
|
2048
|
+
const headers = state.token ? { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } : {}
|
|
2049
|
+
try {
|
|
2050
|
+
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
2051
|
+
? AbortSignal.timeout(LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS)
|
|
2052
|
+
: undefined
|
|
2053
|
+
const preflight = await fetch(url, { method: 'HEAD', headers, ...(signal ? { signal } : {}) })
|
|
2054
|
+
if (preflight.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2055
|
+
if (!preflight.ok) throw new Error('HTTP ' + preflight.status)
|
|
2056
|
+
const anchor = document.createElement('a')
|
|
2057
|
+
anchor.href = url.href
|
|
2058
|
+
anchor.download = sessionLogFilename(sessionId)
|
|
2059
|
+
document.body.appendChild(anchor)
|
|
2060
|
+
anchor.click()
|
|
2061
|
+
anchor.remove()
|
|
2062
|
+
toast(t('ds.exportStarted'), 'ok')
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
console.error('session export download failed', e)
|
|
2065
|
+
toast(t('ds.exportFailed', { msg: e?.message || '' }), 'err')
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1872
2069
|
async function runSlashCommand(text) {
|
|
1873
2070
|
const clean = String(text || '').trim()
|
|
1874
2071
|
if (!clean.startsWith('/') || !state.current) return false
|
|
2072
|
+
const command = slashCommandName(clean)
|
|
2073
|
+
const noFallback = NO_FALLBACK_SLASH_COMMANDS.has(command)
|
|
2074
|
+
const longRunning = command === 'export'
|
|
1875
2075
|
try {
|
|
1876
2076
|
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
1877
|
-
? AbortSignal.timeout(
|
|
2077
|
+
? AbortSignal.timeout(longRunning ? LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS : SLASH_COMMAND_TIMEOUT_MS)
|
|
1878
2078
|
: undefined
|
|
1879
2079
|
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
1880
2080
|
method: 'POST',
|
|
@@ -1883,12 +2083,30 @@ async function runSlashCommand(text) {
|
|
|
1883
2083
|
...(signal ? { signal } : {})
|
|
1884
2084
|
})
|
|
1885
2085
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return true }
|
|
1886
|
-
if (!res.ok)
|
|
2086
|
+
if (!res.ok) {
|
|
2087
|
+
if (noFallback) toast(t('ds.commandTimedOut'), 'err')
|
|
2088
|
+
return noFallback
|
|
2089
|
+
}
|
|
1887
2090
|
const data = await res.json().catch(() => null)
|
|
1888
2091
|
if (data?.ok === false) return true
|
|
1889
|
-
|
|
2092
|
+
if (data?.ok === true && data.executed === true) {
|
|
2093
|
+
if (data.accepted) {
|
|
2094
|
+
if (command === 'compact') {
|
|
2095
|
+
setCompactionStatus(state.current, { ...(data.operation || data.compact), active: true, command, source: 'command' })
|
|
2096
|
+
} else {
|
|
2097
|
+
const sessionId = state.current
|
|
2098
|
+
state.pendingCommands[sessionId] = command
|
|
2099
|
+
setTimeout(() => { if (state.current === sessionId) void refreshCompactionStatus(sessionId) }, 600)
|
|
2100
|
+
}
|
|
2101
|
+
} else if (command === 'export') await downloadSessionExport(state.current)
|
|
2102
|
+
return true
|
|
2103
|
+
}
|
|
1890
2104
|
} catch (e) {
|
|
1891
2105
|
console.error('slash command bridge failed', e)
|
|
2106
|
+
if (noFallback) {
|
|
2107
|
+
toast(t('ds.commandTimedOut'), 'err')
|
|
2108
|
+
return true
|
|
2109
|
+
}
|
|
1892
2110
|
}
|
|
1893
2111
|
return false
|
|
1894
2112
|
}
|
|
@@ -1964,7 +2182,15 @@ async function archiveCurrentSession() {
|
|
|
1964
2182
|
function updateComposerStatus() {
|
|
1965
2183
|
const status = $('composer-status')
|
|
1966
2184
|
if (!status) return
|
|
1967
|
-
|
|
2185
|
+
const compact = activeCompaction()
|
|
2186
|
+
status.classList.toggle('hidden', !state.byId.get(state.current)?.running && !compact)
|
|
2187
|
+
status.classList.toggle('compacting', !!compact)
|
|
2188
|
+
const text = $('composer-status-text')
|
|
2189
|
+
if (text) text.textContent = compact
|
|
2190
|
+
? (compact.command === 'compact'
|
|
2191
|
+
? t('ds.compacting', { elapsed: compactElapsed(compact.startedAt) })
|
|
2192
|
+
: t('ds.commandRunning', { command: compact.command, elapsed: compactElapsed(compact.startedAt) }))
|
|
2193
|
+
: t('ds.composerRunning')
|
|
1968
2194
|
updateSessionActions()
|
|
1969
2195
|
}
|
|
1970
2196
|
function queuePreview(item) {
|
|
@@ -3077,6 +3303,22 @@ function bindUi() {
|
|
|
3077
3303
|
$('btn-server-add').addEventListener('click', addServer)
|
|
3078
3304
|
$('btn-group-add').addEventListener('click', addGroup)
|
|
3079
3305
|
$('group-select-btn').addEventListener('click', (e) => { e.stopPropagation(); toggleGroupMenu() })
|
|
3306
|
+
$('ds-group-drawer-trigger').addEventListener('click', () => { desktopGroupDrawerOpen() ? closeDesktopGroupDrawer() : openDesktopGroupDrawer() })
|
|
3307
|
+
$('ds-group-drawer-close').addEventListener('click', () => closeDesktopGroupDrawer())
|
|
3308
|
+
$('ds-group-drawer-backdrop').addEventListener('click', () => closeDesktopGroupDrawer())
|
|
3309
|
+
$('ds-group-drawer-list').addEventListener('click', (e) => {
|
|
3310
|
+
const option = e.target.closest('[data-ds-quick-group]')
|
|
3311
|
+
if (!option) return
|
|
3312
|
+
const group = option.dataset.dsQuickGroup
|
|
3313
|
+
closeDesktopGroupDrawer({ restoreFocus: false })
|
|
3314
|
+
if (group !== state.activeGroup) switchGroup(group)
|
|
3315
|
+
})
|
|
3316
|
+
$('ds-group-drawer-manage').addEventListener('click', () => {
|
|
3317
|
+
closeDesktopGroupDrawer({ restoreFocus: false })
|
|
3318
|
+
showView('view-settings')
|
|
3319
|
+
showSettingsPage('servers')
|
|
3320
|
+
})
|
|
3321
|
+
document.addEventListener('keydown', keepDesktopGroupDrawerFocus)
|
|
3080
3322
|
document.addEventListener('click', (e) => { if (!e.target.closest('#group-select')) closeGroupMenu() })
|
|
3081
3323
|
$('server-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); addServer() } })
|
|
3082
3324
|
|
package/public/index.html
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<header class="topbar">
|
|
18
|
-
<
|
|
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
|
-
|
|
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">
|
|
@@ -120,7 +135,7 @@
|
|
|
120
135
|
</div>
|
|
121
136
|
|
|
122
137
|
<div id="composer-wrap" class="composer-wrap">
|
|
123
|
-
<div id="composer-status" class="composer-status hidden" role="status" aria-live="polite"><span class="composer-status-dot" aria-hidden="true"></span><span data-i18n="composer.running">运行中…</span></div>
|
|
138
|
+
<div id="composer-status" class="composer-status hidden" role="status" aria-live="polite"><span class="composer-status-dot" aria-hidden="true"></span><span id="composer-status-text" data-i18n="composer.running">运行中…</span></div>
|
|
124
139
|
<div id="composer-menu" class="composer-menu hidden">
|
|
125
140
|
<div class="menu-group">
|
|
126
141
|
<div class="menu-title" data-i18n="menu.commandsTitle">输入指令</div>
|
|
@@ -866,6 +881,11 @@
|
|
|
866
881
|
<div id="modal-announcement" class="modal hidden" role="dialog" aria-modal="true">
|
|
867
882
|
<div class="modal-card announcement-card">
|
|
868
883
|
<div class="announcement-kicker" data-i18n="announcement.label">公告</div>
|
|
884
|
+
<div id="announcement-pagination" class="announcement-pagination hidden" aria-live="polite">
|
|
885
|
+
<button id="announcement-prev" class="mini-btn" type="button" data-i18n-aria="announcement.previous" data-i18n-title="announcement.previous" aria-label="上一条公告" title="上一条公告">‹</button>
|
|
886
|
+
<span id="announcement-page"></span>
|
|
887
|
+
<button id="announcement-next" class="mini-btn" type="button" data-i18n-aria="announcement.next" data-i18n-title="announcement.next" aria-label="下一条公告" title="下一条公告">›</button>
|
|
888
|
+
</div>
|
|
869
889
|
<div id="announcement-title" class="modal-title"></div>
|
|
870
890
|
<div id="announcement-content" class="modal-body announcement-content"></div>
|
|
871
891
|
<div id="announcement-poll" class="announcement-poll hidden">
|
|
@@ -958,6 +978,8 @@
|
|
|
958
978
|
'groups.added': '已创建组:{group}', 'groups.promptRename': '重命名组「{group}」:',
|
|
959
979
|
'groups.renamed': '组已重命名', 'groups.cannotDeleteDefault': '默认组不可删除',
|
|
960
980
|
'groups.confirmDelete': '删除组「{group}」?组内服务器将归入默认组。', 'groups.deleted': '组已删除',
|
|
981
|
+
'groups.drawerOpen': '切换服务器组', 'groups.drawerClose': '关闭组列表', 'groups.drawerEyebrow': '快速切换', 'groups.drawerTitle': '切换服务器组', 'groups.drawerCurrent': '当前连接组',
|
|
982
|
+
'groups.drawerManage': '管理服务器与组', 'groups.drawerManageDesc': '编辑、测速和调整组', 'groups.serverCount': '{count} 台服务器',
|
|
961
983
|
'stream.error': '事件流错误:{msg}',
|
|
962
984
|
'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
|
|
963
985
|
'session.stop': '停止本轮', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}', 'session.recovering': '恢复会话中…', 'session.recoveryReady': '会话已恢复', 'session.recoveryFailed': '会话恢复失败', 'session.recoveryCached': '正在查看缓存历史',
|
|
@@ -986,6 +1008,7 @@
|
|
|
986
1008
|
'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
|
|
987
1009
|
'queue.title': '排队消息', 'queue.steer': '插话', 'queue.steerUnavailable': '仅运行中可插话', 'queue.steerSubmitted': '插话请求已提交', 'queue.steerFailed': '插话失败:{msg}', 'queue.image': '图片消息',
|
|
988
1010
|
'send.failed': '发送失败', 'send.commandSent': '指令已发送', 'send.sent': '已发送', 'send.imageSent': '图片已发送', 'send.commandExecuted': '命令已执行',
|
|
1011
|
+
'session.commandTimedOut': '命令执行超时,未作为普通消息发送', 'session.exportStarted': '会话日志已开始下载,请在浏览器下载中查看', 'session.exportFailed': '无法导出会话日志:{msg}', 'session.compacting': '正在压缩对话 · 已用时 {elapsed}', 'session.compactComplete': '对话压缩完成', 'session.compactFailed': '对话压缩未完成:{msg}', 'session.commandRunning': '正在执行 /{command} · 已用时 {elapsed}', 'session.commandComplete': '/{command} 已完成', 'session.commandFailed': '/{command} 未完成:{msg}',
|
|
989
1012
|
'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
|
|
990
1013
|
'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
|
|
991
1014
|
'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}', 'models.effortLow': '低', 'models.effortHigh': '高', 'models.effortMax': '极高', 'models.effortOff': '关闭', 'models.effortCustomHint': '该路由未公布档位,按 DSH 兼容值尝试;不支持时不会更改当前设置。',
|
|
@@ -1122,7 +1145,7 @@
|
|
|
1122
1145
|
'modal.approvalTitle': '工具审批', 'modal.reject': '拒绝', 'modal.allowOnce': '允许一次',
|
|
1123
1146
|
'modal.questionTitle': 'DSH 需要你回答', 'modal.later': '稍后', 'modal.submit': '提交',
|
|
1124
1147
|
'modal.goalTitle': '目标控制', 'modal.close': '关闭', 'modal.updateGoal': '更新目标',
|
|
1125
|
-
'announcement.label': '公告', 'announcement.later': '稍后再看', 'announcement.gotIt': '
|
|
1148
|
+
'announcement.label': '公告', 'announcement.later': '稍后再看', 'announcement.gotIt': '全部知道了', 'announcement.open': '查看详情', 'announcement.previous': '上一条公告', 'announcement.next': '下一条公告', 'announcement.page': '第 {current} / {total} 条', 'announcement.historyLabel': '公告记录', 'announcement.historyTitle': '历史公告', 'announcement.historyEmpty': '暂无历史公告', 'announcement.close': '关闭', 'announcement.noDate': '未标注日期',
|
|
1126
1149
|
'announcement.voteSubmit': '提交投票', 'announcement.voteChoose': '请先选择一项', 'announcement.voteThanks': '已投票:{option}', 'announcement.voteFailed': '投票失败:{msg}', 'announcement.voteNetworkError': '网络错误', 'announcement.voteAgainLater': '提交太频繁,请稍后再试', 'announcement.voteFromHistory': '参与投票',
|
|
1127
1150
|
'modal.statsTitle': '本轮统计',
|
|
1128
1151
|
'time.justNow': '刚刚', 'time.minAgo': ' 分钟前', 'time.hourAgo': ' 小时前'
|
|
@@ -1188,6 +1211,8 @@
|
|
|
1188
1211
|
'groups.added': 'Created group: {group}', 'groups.promptRename': 'Rename group "{group}":',
|
|
1189
1212
|
'groups.renamed': 'Group renamed', 'groups.cannotDeleteDefault': 'The default group cannot be deleted',
|
|
1190
1213
|
'groups.confirmDelete': 'Delete group "{group}"? Its servers move to the default group.', 'groups.deleted': 'Group deleted',
|
|
1214
|
+
'groups.drawerOpen': 'Switch server group', 'groups.drawerClose': 'Close group list', 'groups.drawerEyebrow': 'QUICK SWITCH', 'groups.drawerTitle': 'Switch server group', 'groups.drawerCurrent': 'Current connection group',
|
|
1215
|
+
'groups.drawerManage': 'Manage servers and groups', 'groups.drawerManageDesc': 'Edit, test, and organize groups', 'groups.serverCount': '{count} servers',
|
|
1191
1216
|
'stream.error': 'Event stream error: {msg}',
|
|
1192
1217
|
'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
|
|
1193
1218
|
'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',
|
|
@@ -1216,6 +1241,7 @@
|
|
|
1216
1241
|
'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
|
|
1217
1242
|
'queue.title': 'Queued messages', 'queue.steer': 'Interject', 'queue.steerUnavailable': 'Only available while running', 'queue.steerSubmitted': 'Interjection requested', 'queue.steerFailed': 'Interjection failed: {msg}', 'queue.image': 'Image message',
|
|
1218
1243
|
'send.failed': 'Send failed', 'send.commandSent': 'Command sent', 'send.sent': 'Sent', 'send.imageSent': 'Image sent', 'send.commandExecuted': 'Command executed',
|
|
1244
|
+
'session.commandTimedOut': 'Command timed out and was not sent as a chat message', 'session.exportStarted': 'Session log download started; check your browser downloads', 'session.exportFailed': 'Could not export session log: {msg}', 'session.compacting': 'Compressing conversation · {elapsed} elapsed', 'session.compactComplete': 'Conversation compression complete', 'session.compactFailed': 'Conversation compression did not finish: {msg}', 'session.commandRunning': 'Running /{command} · {elapsed} elapsed', 'session.commandComplete': '/{command} complete', 'session.commandFailed': '/{command} did not finish: {msg}',
|
|
1219
1245
|
'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
|
|
1220
1246
|
'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
|
|
1221
1247
|
'models.effortFailed': 'Failed to switch reasoning effort', 'models.effortSwitched': 'Reasoning effort: {effort}', 'models.effortLow': 'Low', 'models.effortHigh': 'High', 'models.effortMax': 'Max', 'models.effortOff': 'Off', 'models.effortCustomHint': 'This route does not publish effort metadata. DSH compatibility values are tried; unsupported values leave the current setting unchanged.',
|
|
@@ -1352,7 +1378,7 @@
|
|
|
1352
1378
|
'modal.approvalTitle': 'Tool approval', 'modal.reject': 'Reject', 'modal.allowOnce': 'Allow once',
|
|
1353
1379
|
'modal.questionTitle': 'DSH needs your answer', 'modal.later': 'Later', 'modal.submit': 'Submit',
|
|
1354
1380
|
'modal.goalTitle': 'Goal control', 'modal.close': 'Close', 'modal.updateGoal': 'Update goal',
|
|
1355
|
-
'announcement.label': 'Announcement', 'announcement.later': 'Later', 'announcement.gotIt': '
|
|
1381
|
+
'announcement.label': 'Announcement', 'announcement.later': 'Later', 'announcement.gotIt': 'Mark all read', 'announcement.open': 'View details', 'announcement.previous': 'Previous announcement', 'announcement.next': 'Next announcement', 'announcement.page': 'Announcement {current} of {total}', 'announcement.historyLabel': 'Announcement archive', 'announcement.historyTitle': 'Announcement history', 'announcement.historyEmpty': 'No announcement history', 'announcement.close': 'Close', 'announcement.noDate': 'No date',
|
|
1356
1382
|
'announcement.voteSubmit': 'Submit vote', 'announcement.voteChoose': 'Choose an option first', 'announcement.voteThanks': 'Voted: {option}', 'announcement.voteFailed': 'Vote failed: {msg}', 'announcement.voteNetworkError': 'Network error', 'announcement.voteAgainLater': 'Too many submissions; try again later', 'announcement.voteFromHistory': 'Vote now',
|
|
1357
1383
|
'modal.statsTitle': 'This round',
|
|
1358
1384
|
'time.justNow': 'just now', 'time.minAgo': ' min ago', 'time.hourAgo': ' hr ago'
|