dsh-remote-plugin 0.6.19 → 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.
- package/apk/dsh-remote.apk +0 -0
- package/client.js +9 -2
- package/gateway.cjs +610 -33
- package/index.mjs +79 -5
- package/package.json +1 -1
- package/public/app.js +129 -2
- package/public/desktop/desktop.css +42 -6
- package/public/desktop/desktop.html +21 -1
- package/public/desktop/desktop.js +123 -2
- package/public/index.html +24 -3
- package/public/styles.css +50 -4
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 浏览器侧入口由 client half 注册在 DSH 原生侧边栏(见 client.js)。
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync, spawn } from 'node:child_process'
|
|
9
|
-
import { appendFileSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { appendFileSync, chmodSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
10
|
import { stat } from 'node:fs/promises'
|
|
11
11
|
import net from 'node:net'
|
|
12
12
|
import { homedir, hostname, networkInterfaces } from 'node:os'
|
|
@@ -14,7 +14,7 @@ import { dirname, extname, normalize, resolve } from 'node:path'
|
|
|
14
14
|
import { fileURLToPath } from 'node:url'
|
|
15
15
|
|
|
16
16
|
export const name = 'dsh-remote'
|
|
17
|
-
export const inject = ['webServer', 'commands', 'agents']
|
|
17
|
+
export const inject = ['webServer', 'commands', 'agents', 'connection']
|
|
18
18
|
|
|
19
19
|
const MOUNT = '/remote'
|
|
20
20
|
const PUBLIC_DIR = fileURLToPath(new URL('./public/', import.meta.url))
|
|
@@ -68,6 +68,42 @@ try {
|
|
|
68
68
|
|
|
69
69
|
// DSH 实际监听地址由 apply 时从 webServer 服务读取
|
|
70
70
|
let dshListen = { host: '127.0.0.1', port: 3080 }
|
|
71
|
+
let dshConnection = null
|
|
72
|
+
|
|
73
|
+
function dshUpstreamCookieFile() {
|
|
74
|
+
return process.env.DSH_REMOTE_DSH_COOKIE_FILE || `${homedir()}/.dsh-remote/dsh-upstream.cookie`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* DSH 0.1.2-alpha.1 起,Host RPC 与 WebSocket 都要求浏览器会话 Cookie。
|
|
79
|
+
* 新版 connection 服务可把仅进程内可见的启动令牌兑换为 Cookie;旧版没有
|
|
80
|
+
* authenticatedUrl,直接跳过即可。文件只让同一用户的独立网关读取。
|
|
81
|
+
*/
|
|
82
|
+
async function refreshDshUpstreamCookie() {
|
|
83
|
+
if (typeof dshConnection?.authenticatedUrl !== 'function') return false
|
|
84
|
+
const upstream = `http://${dshListen.host}:${dshListen.port}`
|
|
85
|
+
try {
|
|
86
|
+
const loginUrl = dshConnection.authenticatedUrl(upstream)
|
|
87
|
+
const response = await fetch(loginUrl, {
|
|
88
|
+
redirect: 'manual',
|
|
89
|
+
signal: AbortSignal.timeout(2500),
|
|
90
|
+
})
|
|
91
|
+
const setCookie = response.headers.get('set-cookie') || ''
|
|
92
|
+
const cookie = setCookie.split(';', 1)[0].trim()
|
|
93
|
+
if (response.status !== 303 || !cookie.includes('=') || cookie.length > 4096 || /[\0\r\n]/.test(cookie)) {
|
|
94
|
+
throw new Error(`认证交换返回 HTTP ${response.status}`)
|
|
95
|
+
}
|
|
96
|
+
const file = dshUpstreamCookieFile()
|
|
97
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
98
|
+
writeFileSync(file, cookie + '\n', { mode: 0o600 })
|
|
99
|
+
chmodSync(file, 0o600)
|
|
100
|
+
return true
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// 禁止记录带启动令牌的 URL 或 Cookie,只保留不含凭据的错误摘要。
|
|
103
|
+
logGateway('刷新 DSH 上游认证失败: ' + (e?.message || String(e)))
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
71
107
|
|
|
72
108
|
function lanIPs() {
|
|
73
109
|
const out = [...configuredAdvertisedHosts()]
|
|
@@ -169,6 +205,7 @@ function runExit(cmd, args) {
|
|
|
169
205
|
|
|
170
206
|
const GATEWAY_ENV_KEYS = [
|
|
171
207
|
'TOKEN', 'TOKEN_FILE', 'DSH_REMOTE_TOKEN', 'DSH_REMOTE_DEVICE_KEYS', 'DSH_REMOTE_FS_ROOT', 'DSH_REMOTE_FS_WORKSPACE_CACHE_MS', 'DSH_REMOTE_FS_MAX_UPLOAD',
|
|
208
|
+
'DSH_REMOTE_DSH_COOKIE_FILE',
|
|
172
209
|
'DSH_REMOTE_NOTES', 'DSH_REMOTE_WORKBENCH', 'DSH_REMOTE_ADVERTISE_HOSTS', 'DSH_REMOTE_DSH_SERVICE', 'DSH_REMOTE_SYSTEMCTL', 'DSH_REMOTE_DSH_CONTROL_MODE',
|
|
173
210
|
'DSH_REMOTE_DSH_CONTROL_TIMEOUT_MS', 'DSH_REMOTE_DSH_CONTROL_POLL_MS', 'DSH_REMOTE_FEEDBACK_URL',
|
|
174
211
|
'UPDATE_CHECK_URL', 'UPDATE_INTERVAL_MS', 'UPDATE_PROXY', 'DSH_HEALTH_PATH',
|
|
@@ -363,6 +400,7 @@ function ensureGateway() {
|
|
|
363
400
|
if (ensurePromise) return ensurePromise
|
|
364
401
|
ensurePromise = (async () => {
|
|
365
402
|
try {
|
|
403
|
+
await refreshDshUpstreamCookie()
|
|
366
404
|
const health = await gatewayRunning()
|
|
367
405
|
if (!health.running) {
|
|
368
406
|
const out = await startGateway()
|
|
@@ -475,6 +513,23 @@ async function resolveAgent(ctx, sessionId) {
|
|
|
475
513
|
}
|
|
476
514
|
}
|
|
477
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
|
+
|
|
478
533
|
async function resolveFile(pathname) {
|
|
479
534
|
let abs = targetPath(pathname)
|
|
480
535
|
if (abs === null) return null
|
|
@@ -733,15 +788,27 @@ async function serveStatic(req, res, ctx) {
|
|
|
733
788
|
sendJson(res, 200, { ok: false, message: 'agent not found', debug: { resolvePath, commandNames: [] } })
|
|
734
789
|
return
|
|
735
790
|
}
|
|
791
|
+
let commandEntries = null
|
|
736
792
|
let commandNames = []
|
|
737
793
|
try {
|
|
738
|
-
|
|
794
|
+
const listed = await ctx.commands.list(agent)
|
|
795
|
+
commandEntries = Array.isArray(listed) ? listed : null
|
|
796
|
+
commandNames = commandEntries ? commandEntries.map(commandEntryName).filter(Boolean) : []
|
|
739
797
|
} catch (e) {
|
|
740
798
|
commandNames = ['list-error: ' + (e?.message || String(e))]
|
|
741
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
|
+
}
|
|
742
807
|
const signal = AbortSignal.timeout(30000)
|
|
743
|
-
const result =
|
|
744
|
-
|
|
808
|
+
const result = ctx.commands.execute.length === 3
|
|
809
|
+
? await ctx.commands.execute(agent, line, signal)
|
|
810
|
+
: await ctx.commands.execute(agent, line, [], signal)
|
|
811
|
+
sendJson(res, 200, { ok: true, executed: commandWasExecuted(result), debug: { resolvePath, commandNames } })
|
|
745
812
|
} catch (e) {
|
|
746
813
|
sendJson(res, 200, { ok: false, message: e?.message || String(e) })
|
|
747
814
|
}
|
|
@@ -783,6 +850,7 @@ async function serveStatic(req, res, ctx) {
|
|
|
783
850
|
|
|
784
851
|
export function apply(ctx) {
|
|
785
852
|
dshListen = { host: ctx.webServer.host, port: ctx.webServer.port }
|
|
853
|
+
dshConnection = ctx.connection
|
|
786
854
|
ctx.effect(() => ctx.webServer.register({
|
|
787
855
|
kind: 'prefix',
|
|
788
856
|
path: MOUNT,
|
|
@@ -794,4 +862,10 @@ export function apply(ctx) {
|
|
|
794
862
|
})
|
|
795
863
|
// DSH 启动/重启后自愈: 用户没关过网关就自动拉起(默认开, DSH_REMOTE_AUTOSTART=0 关闭)
|
|
796
864
|
void ensureGateway()
|
|
865
|
+
// apply 可能早于 Web 监听完成;延迟再交换一次新版 DSH 的会话 Cookie。
|
|
866
|
+
ctx.effect(() => {
|
|
867
|
+
const timer = setTimeout(() => { void ensureGateway() }, 5000)
|
|
868
|
+
timer.unref?.()
|
|
869
|
+
return () => clearTimeout(timer)
|
|
870
|
+
}, 'dsh-remote: refresh upstream authentication')
|
|
797
871
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
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
|
|
@@ -2108,6 +2179,7 @@ async function openSession(id) {
|
|
|
2108
2179
|
renderSessionTitle(); renderSessionSub(); updateCancelBtn(); updateSessionStatus()
|
|
2109
2180
|
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
2110
2181
|
renderQueue()
|
|
2182
|
+
renderSessionPending()
|
|
2111
2183
|
restoreCachedHistory()
|
|
2112
2184
|
await loadHistory(true)
|
|
2113
2185
|
renderSessionCards()
|
|
@@ -2140,6 +2212,7 @@ async function closeSession() {
|
|
|
2140
2212
|
const discard = await shouldDiscardEmptySession(sessionId)
|
|
2141
2213
|
if (state.current !== sessionId) return
|
|
2142
2214
|
state.current = null
|
|
2215
|
+
renderSessionPending()
|
|
2143
2216
|
if (discard) removeLocalSessionRecord(sessionId)
|
|
2144
2217
|
setComposerFullscreen(false)
|
|
2145
2218
|
clearComposerImages()
|
|
@@ -3329,7 +3402,7 @@ function renderOverview() {
|
|
|
3329
3402
|
$('overview-attention-list').innerHTML = pending.length ? pending.slice(0, 3).map(({ kind, item }) => {
|
|
3330
3403
|
const title = titleOf(state.byId.get(item.sessionId))
|
|
3331
3404
|
if (kind === 'approval') return `<div class="overview-attention-item" data-overview-approval="${esc(item.approvalId)}">
|
|
3332
|
-
<span class="overview-item-mark">⌁</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.toolName || t('tool.default'))}</span><span class="overview-item-desc">${esc(item
|
|
3405
|
+
<span class="overview-item-mark">⌁</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.toolName || t('tool.default'))}</span><span class="overview-item-desc">${esc(approvalDetail(item))} · ${esc(title)}</span></span>
|
|
3333
3406
|
<span class="overview-item-actions"><button type="button" class="mini-btn" data-overview-approve="1">${t('pending.allow')}</button><button type="button" class="mini-btn" data-overview-approve="0">${t('pending.reject')}</button></span>
|
|
3334
3407
|
</div>`
|
|
3335
3408
|
return `<button type="button" class="overview-attention-item question" data-overview-question="${esc(item.rpcId)}">
|
|
@@ -3394,7 +3467,7 @@ function renderPending() {
|
|
|
3394
3467
|
const title = titleOf(state.byId.get(a.sessionId))
|
|
3395
3468
|
return `<div class="pending-card approval" data-approval="${esc(a.approvalId)}">
|
|
3396
3469
|
<div class="pc-title">${esc(t('pending.approvalTitle', { tool: a.toolName || t('tool.default') }))}</div>
|
|
3397
|
-
<div class="pc-desc">${esc(a
|
|
3470
|
+
<div class="pc-desc">${esc(approvalDetail(a))}</div>
|
|
3398
3471
|
<div class="pc-session">${esc(title)}</div>
|
|
3399
3472
|
<div class="goal-actions"><button class="mini-btn" data-approve="1">${t('pending.allow')}</button><button class="mini-btn" data-approve="0">${t('pending.reject')}</button></div>
|
|
3400
3473
|
</div>`
|
|
@@ -3414,10 +3487,48 @@ function renderPending() {
|
|
|
3414
3487
|
})
|
|
3415
3488
|
list.querySelectorAll('[data-question]').forEach(btn =>
|
|
3416
3489
|
btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.question))))
|
|
3490
|
+
renderSessionPending()
|
|
3417
3491
|
updatePendingBadge()
|
|
3418
3492
|
renderOverview()
|
|
3419
3493
|
}
|
|
3420
3494
|
|
|
3495
|
+
function approvalDetail(a) {
|
|
3496
|
+
const lines = []
|
|
3497
|
+
if (a?.reason) lines.push(String(a.reason))
|
|
3498
|
+
if (a?.arguments !== undefined) lines.push(safeJson(a.arguments))
|
|
3499
|
+
if (a?.callId) lines.push(`callId: ${a.callId}`)
|
|
3500
|
+
return lines.join('\n') || t('pending.noReason')
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
function renderSessionPending() {
|
|
3504
|
+
const box = $('session-pending')
|
|
3505
|
+
if (!box) return
|
|
3506
|
+
const sessionId = state.current
|
|
3507
|
+
const items = [
|
|
3508
|
+
...state.approvals.filter(a => a.sessionId === sessionId).map(a => ({ kind: 'approval', item: a })),
|
|
3509
|
+
...state.questions.filter(q => q.sessionId === sessionId).map(q => ({ kind: 'question', item: q }))
|
|
3510
|
+
]
|
|
3511
|
+
box.classList.toggle('hidden', !sessionId || !items.length)
|
|
3512
|
+
if (!sessionId || !items.length) { box.innerHTML = ''; return }
|
|
3513
|
+
box.innerHTML = `<div class="session-pending-head"><span>${esc(t('overview.attention'))}</span><span>${esc(t('pending.count', { n: items.length }))}</span></div><div class="session-pending-list">${items.map(({ kind, item }) => {
|
|
3514
|
+
if (kind === 'approval') return `<article class="pending-card approval" data-session-approval="${esc(item.approvalId)}">
|
|
3515
|
+
<div class="pc-title">${esc(item.toolName || t('tool.default'))}</div>
|
|
3516
|
+
<div class="pc-desc">${esc(approvalDetail(item))}</div>
|
|
3517
|
+
<div class="goal-actions"><button type="button" class="mini-btn" data-session-approve="1">${t('pending.allow')}</button><button type="button" class="mini-btn" data-session-approve="0">${t('pending.reject')}</button></div>
|
|
3518
|
+
</article>`
|
|
3519
|
+
return `<article class="pending-card question" data-session-question="${esc(item.rpcId)}">
|
|
3520
|
+
<div class="pc-title">❓ ${esc((item.questions || []).map(question => question.question).filter(Boolean).join(' / ') || t('notify.questionTitle'))}</div>
|
|
3521
|
+
<div class="goal-actions"><button type="button" class="mini-btn" data-session-answer>${t('pending.answer')}</button></div>
|
|
3522
|
+
</article>`
|
|
3523
|
+
}).join('')}</div>`
|
|
3524
|
+
box.querySelectorAll('[data-session-approve]').forEach(button => {
|
|
3525
|
+
button.addEventListener('click', () => approveApproval(button.closest('[data-session-approval]')?.dataset.sessionApproval || '', button.dataset.sessionApprove === '1'))
|
|
3526
|
+
})
|
|
3527
|
+
box.querySelectorAll('[data-session-answer]').forEach(button => {
|
|
3528
|
+
button.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === button.closest('[data-session-question]')?.dataset.sessionQuestion)))
|
|
3529
|
+
})
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3421
3532
|
async function approveApproval(id, allow) {
|
|
3422
3533
|
const a = state.approvals.find(x => x.approvalId === id)
|
|
3423
3534
|
if (!a) return
|
|
@@ -6839,6 +6950,22 @@ function bindUi() {
|
|
|
6839
6950
|
$('btn-server-add').addEventListener('click', addServer)
|
|
6840
6951
|
$('btn-group-add').addEventListener('click', addGroup)
|
|
6841
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)
|
|
6842
6969
|
document.addEventListener('click', (e) => {
|
|
6843
6970
|
if (!e.target.closest('#group-select')) closeGroupMenu()
|
|
6844
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:
|
|
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; }
|
|
@@ -225,15 +232,15 @@ html.reorder-scroll-lock body { overscroll-behavior: none; }
|
|
|
225
232
|
.ds-overview-attention-list, .ds-overview-session-list { display:flex; flex-direction:column; gap:8px; }
|
|
226
233
|
.ds-overview-attention-item, .ds-overview-session-item { width:100%; min-width:0; display:flex; align-items:center; gap:10px; padding:11px 12px; border:1px solid var(--dsr-line); border-radius:12px; background:var(--dsr-bg-2); color:var(--dsr-text); text-align:left; }
|
|
227
234
|
button.ds-overview-attention-item, button.ds-overview-session-item { cursor:pointer; font:inherit; }
|
|
228
|
-
.ds-overview-attention-item { border-left:3px solid var(--dsr-warning); }
|
|
235
|
+
.ds-overview-attention-item { align-items:flex-start; border-left:3px solid var(--dsr-warning); }
|
|
229
236
|
.ds-overview-attention-item.question { border-left-color:var(--dsr-accent-2); }
|
|
230
237
|
.ds-overview-mark { flex:0 0 auto; width:26px; height:26px; display:grid; place-items:center; border-radius:8px; background:var(--dsr-warning-soft); color:var(--dsr-warning); font-size:12px; }
|
|
231
238
|
.ds-overview-attention-item.question .ds-overview-mark { background:var(--dsr-accent-2-soft); color:var(--dsr-accent-2); }
|
|
232
239
|
.ds-overview-copy { flex:1; min-width:0; }
|
|
233
|
-
.ds-overview-item-title, .ds-overview-item-desc { display:block;
|
|
240
|
+
.ds-overview-item-title, .ds-overview-item-desc { display:block; line-height:1.45; overflow-wrap:anywhere; word-break:break-word; }
|
|
234
241
|
.ds-overview-item-title { font-size:12px; font-weight:650; }
|
|
235
|
-
.ds-overview-item-desc { margin-top:3px; color:var(--dsr-muted); font-size:11px; }
|
|
236
|
-
.ds-overview-actions { flex:0 0 auto; display:flex; gap:4px; }
|
|
242
|
+
.ds-overview-item-desc { margin-top:3px; color:var(--dsr-muted); font-size:11px; white-space:normal; }
|
|
243
|
+
.ds-overview-actions { flex:0 0 auto; display:flex; flex-wrap:wrap; justify-content:flex-end; gap:4px; }
|
|
237
244
|
.ds-overview-actions .ds-btn { min-height:27px; padding:2px 8px; font-size:11px; }
|
|
238
245
|
.ds-overview-actions .allow { background:var(--dsr-success-soft); border-color:var(--dsr-success-line); color:var(--dsr-success); }
|
|
239
246
|
.ds-overview-actions .reject { background:var(--dsr-warning-soft); border-color:var(--dsr-warning-line); color:var(--dsr-warning); }
|
|
@@ -287,6 +294,11 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
287
294
|
.ds-presets-guide { padding: 2px 8px 10px; font-size: 11px; line-height: 1.5; }
|
|
288
295
|
.ds-session-cards { flex: none; max-height: 220px; overflow-y: auto; padding: 10px 22px 0; display: flex; flex-direction: column; gap: 8px; }
|
|
289
296
|
.ds-session-cards:empty { display: none; }
|
|
297
|
+
.ds-session-pending { flex:none; margin:10px 22px 0; padding:10px; border:1px solid var(--dsr-warning-line); border-radius:13px; background:var(--dsr-warning-soft); }
|
|
298
|
+
.ds-session-pending-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; color:var(--dsr-text); font-size:12px; font-weight:750; }
|
|
299
|
+
.ds-session-pending-list { display:flex; flex-direction:column; gap:8px; }
|
|
300
|
+
.ds-session-pending .ds-notif-card { min-width:0; box-shadow:none; animation:none; }
|
|
301
|
+
.ds-session-pending .ds-notif-title, .ds-session-pending .ds-notif-body { overflow-wrap:anywhere; word-break:break-word; }
|
|
290
302
|
.ds-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 12px; }
|
|
291
303
|
.ds-card-title { font-size: 11px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; margin-bottom: 6px; text-transform: uppercase; }
|
|
292
304
|
.ds-goal-disclosure { width: 100%; min-width: 0; display: flex; justify-content: flex-end; }
|
|
@@ -465,6 +477,30 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
465
477
|
.ds-drawer-body::-webkit-scrollbar-thumb { background: var(--dsr-line); border-radius: 999px; }
|
|
466
478
|
.ds-drawer-body::-webkit-scrollbar-thumb:hover { background: var(--dsr-muted); }
|
|
467
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
|
+
|
|
468
504
|
/* 通知卡片栈(右下角) */
|
|
469
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)); }
|
|
470
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">
|
|
@@ -151,6 +166,7 @@
|
|
|
151
166
|
<section id="view-chat" class="ds-view">
|
|
152
167
|
<div id="session-cards" class="ds-session-cards"></div>
|
|
153
168
|
<div id="queue-dock" class="ds-queue-dock hidden" aria-live="polite"></div>
|
|
169
|
+
<section id="session-pending" class="ds-session-pending hidden" aria-live="polite"></section>
|
|
154
170
|
<div id="history" class="ds-history" aria-live="polite"></div>
|
|
155
171
|
<div class="ds-composer">
|
|
156
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 data-i18n="ds.composerRunning">运行中…</span></div>
|
|
@@ -527,6 +543,8 @@
|
|
|
527
543
|
'ds.groupsPromptRename': '重命名组「{group}」:', 'ds.groupsRenamed': '组已重命名',
|
|
528
544
|
'ds.groupsCannotDeleteDefault': '默认组不可删除',
|
|
529
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} 台服务器',
|
|
530
548
|
'ds.theme.default': '默认深空', 'ds.theme.dark': '落日', 'ds.theme.light': '易北爱乐厅', 'ds.theme.neutral': '草原孤塔', 'ds.theme.mono': '黑曜白',
|
|
531
549
|
'ds.role.me': '我', 'ds.role.dsh': 'DSH', 'ds.toolDefault': '工具',
|
|
532
550
|
'ds.eventSystemReminder': '系统提示',
|
|
@@ -630,6 +648,8 @@
|
|
|
630
648
|
'ds.groupsPromptRename': 'Rename group "{group}":', 'ds.groupsRenamed': 'Group renamed',
|
|
631
649
|
'ds.groupsCannotDeleteDefault': 'The default group cannot be deleted',
|
|
632
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',
|
|
633
653
|
'ds.theme.default': 'Deep Space', 'ds.theme.dark': 'Sunset', 'ds.theme.light': 'Elbphilharmonie', 'ds.theme.neutral': 'Prairie Tower', 'ds.theme.mono': 'Monochrome',
|
|
634
654
|
'ds.role.me': 'Me', 'ds.role.dsh': 'DSH', 'ds.toolDefault': 'tool',
|
|
635
655
|
'ds.eventSystemReminder': 'System reminder',
|