dsh-remote-plugin 0.6.18 → 0.6.20
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 +50 -3
- package/package.json +1 -1
- package/public/admin.html +16 -17
- package/public/admin.js +6 -5
- package/public/app.js +94 -16
- package/public/desktop/desktop.css +57 -11
- package/public/desktop/desktop.html +24 -20
- package/public/desktop/desktop.js +110 -18
- package/public/index.html +33 -29
- package/public/motion.js +105 -17
- package/public/plugin.html +6 -5
- package/public/styles.css +67 -27
- package/public/theme-vars.css +244 -167
- package/public/theme.js +2 -2
- 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()
|
|
@@ -740,7 +778,9 @@ async function serveStatic(req, res, ctx) {
|
|
|
740
778
|
commandNames = ['list-error: ' + (e?.message || String(e))]
|
|
741
779
|
}
|
|
742
780
|
const signal = AbortSignal.timeout(30000)
|
|
743
|
-
const result =
|
|
781
|
+
const result = ctx.commands.execute.length === 3
|
|
782
|
+
? await ctx.commands.execute(agent, line, signal)
|
|
783
|
+
: await ctx.commands.execute(agent, line, [], signal)
|
|
744
784
|
sendJson(res, 200, { ok: true, executed: result !== undefined, debug: { resolvePath, commandNames } })
|
|
745
785
|
} catch (e) {
|
|
746
786
|
sendJson(res, 200, { ok: false, message: e?.message || String(e) })
|
|
@@ -783,6 +823,7 @@ async function serveStatic(req, res, ctx) {
|
|
|
783
823
|
|
|
784
824
|
export function apply(ctx) {
|
|
785
825
|
dshListen = { host: ctx.webServer.host, port: ctx.webServer.port }
|
|
826
|
+
dshConnection = ctx.connection
|
|
786
827
|
ctx.effect(() => ctx.webServer.register({
|
|
787
828
|
kind: 'prefix',
|
|
788
829
|
path: MOUNT,
|
|
@@ -794,4 +835,10 @@ export function apply(ctx) {
|
|
|
794
835
|
})
|
|
795
836
|
// DSH 启动/重启后自愈: 用户没关过网关就自动拉起(默认开, DSH_REMOTE_AUTOSTART=0 关闭)
|
|
796
837
|
void ensureGateway()
|
|
838
|
+
// apply 可能早于 Web 监听完成;延迟再交换一次新版 DSH 的会话 Cookie。
|
|
839
|
+
ctx.effect(() => {
|
|
840
|
+
const timer = setTimeout(() => { void ensureGateway() }, 5000)
|
|
841
|
+
timer.unref?.()
|
|
842
|
+
return () => clearTimeout(timer)
|
|
843
|
+
}, 'dsh-remote: refresh upstream authentication')
|
|
797
844
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/public/admin.html
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
-
<meta name="theme-color" content="#
|
|
6
|
+
<meta name="theme-color" content="#0D1117">
|
|
7
7
|
<title>DSH Remote · 管理</title>
|
|
8
|
-
<script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
|
|
8
|
+
<script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'&&t!=='mono'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
|
|
9
9
|
<link rel="stylesheet" href="../styles.css">
|
|
10
10
|
<style>
|
|
11
11
|
.admin-wrap { max-width: 1180px; margin: 0 auto; padding: calc(env(safe-area-inset-top, 0px) + 14px) 14px 40px; }
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
.device-key-toggle-actions { display:flex; align-items:center; gap:8px; flex:none; }
|
|
88
88
|
.device-key-switch { position:relative; width:42px; height:24px; flex:none; }
|
|
89
89
|
.device-key-switch input { position:absolute; opacity:0; pointer-events:none; }
|
|
90
|
-
.device-key-switch span { position:absolute; inset:0; border:1px solid var(--dsr-line); border-radius:999px; background:var(--dsr-bg-2); cursor:pointer; transition
|
|
91
|
-
.device-key-switch span::after { content:''; position:absolute; width:18px; height:18px; left:2px; top:2px; border-radius:50%; background:var(--dsr-muted); transition
|
|
90
|
+
.device-key-switch span { position:absolute; inset:0; border:1px solid var(--dsr-line); border-radius:999px; background:var(--dsr-bg-2); cursor:pointer; transition:background-color .18s ease,border-color .18s ease; }
|
|
91
|
+
.device-key-switch span::after { content:''; position:absolute; width:18px; height:18px; left:2px; top:2px; border-radius:50%; background:var(--dsr-muted); transition:transform .18s ease,background-color .18s ease; }
|
|
92
92
|
.device-key-switch input:checked + span { border-color:var(--dsr-accent-line); background:var(--dsr-accent-soft); }
|
|
93
93
|
.device-key-switch input:checked + span::after { transform:translateX(18px); background:var(--dsr-accent-strong); }
|
|
94
94
|
.device-key-switch input:focus-visible + span { outline:2px solid var(--dsr-accent-strong); outline-offset:2px; }
|
|
@@ -126,8 +126,8 @@
|
|
|
126
126
|
.host-ip-use { color: var(--dsr-muted); }
|
|
127
127
|
.host-ip-switch { position: relative; display: inline-block; width: 38px; height: 22px; vertical-align: middle; }
|
|
128
128
|
.host-ip-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
129
|
-
.host-ip-switch span { position: absolute; inset: 0; border: 1px solid var(--dsr-line); border-radius: 999px; background: var(--dsr-bg-2); cursor: pointer; transition: .18s ease; }
|
|
130
|
-
.host-ip-switch span::after { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; border-radius: 50%; background: var(--dsr-muted); transition: .18s ease; }
|
|
129
|
+
.host-ip-switch span { position: absolute; inset: 0; border: 1px solid var(--dsr-line); border-radius: 999px; background: var(--dsr-bg-2); cursor: pointer; transition: background-color .18s ease, border-color .18s ease; }
|
|
130
|
+
.host-ip-switch span::after { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; border-radius: 50%; background: var(--dsr-muted); transition: transform .18s ease, background-color .18s ease; }
|
|
131
131
|
.host-ip-switch input:checked + span { border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
|
|
132
132
|
.host-ip-switch input:checked + span::after { transform: translateX(16px); background: var(--dsr-accent-strong); }
|
|
133
133
|
.host-ip-switch input:focus-visible + span { outline: 2px solid var(--dsr-accent-strong); outline-offset: 2px; }
|
|
@@ -186,7 +186,6 @@
|
|
|
186
186
|
width: 248px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 14px;
|
|
187
187
|
box-shadow: 0 12px 34px var(--dsr-shadow); padding: 6px;
|
|
188
188
|
display: flex; flex-direction: column; gap: 2px;
|
|
189
|
-
animation: fb-in .18s ease;
|
|
190
189
|
}
|
|
191
190
|
.fb-item {
|
|
192
191
|
min-height: 44px; display: flex; align-items: center; gap: 10px;
|
|
@@ -204,7 +203,6 @@
|
|
|
204
203
|
.fb-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
205
204
|
.fb-name { font-weight: 600; }
|
|
206
205
|
.fb-desc { font-size: 11px; color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
207
|
-
@keyframes fb-in { from { opacity: 0; transform: translateY(-6px) } to { opacity: 1; transform: translateY(0) } }
|
|
208
206
|
/* 窄屏压缩顶栏: 图标化文字按钮, 保证全部按钮完整可见 */
|
|
209
207
|
@media (max-width: 900px) {
|
|
210
208
|
.gh-label, .t-label { display: none; }
|
|
@@ -331,8 +329,7 @@
|
|
|
331
329
|
@media (max-width: 900px) {
|
|
332
330
|
.admin-title { display: grid; grid-template-columns: minmax(0, 1fr) auto; row-gap: 10px; }
|
|
333
331
|
.admin-title .left { min-width: 0; }
|
|
334
|
-
.admin-title .right { grid-column: 1 / -1; justify-content: flex-start; min-width: 0;
|
|
335
|
-
.admin-title .right::-webkit-scrollbar { display: none; }
|
|
332
|
+
.admin-title .right { grid-column: 1 / -1; justify-content: flex-start; min-width: 0; flex-wrap: wrap; overflow: visible; }
|
|
336
333
|
}
|
|
337
334
|
@media (max-width: 440px) {
|
|
338
335
|
.admin-title h1 { display: block; min-width: 0; overflow: visible; font-size: 18px; }
|
|
@@ -381,12 +378,12 @@
|
|
|
381
378
|
</div>
|
|
382
379
|
</div>
|
|
383
380
|
|
|
384
|
-
<div id="login-view" class="login-card hidden">
|
|
385
|
-
<input id="token-input" type="password" placeholder="输入网关访问令牌" autocomplete="off" data-i18n-placeholder="tokenPlaceholder">
|
|
381
|
+
<div id="login-view" class="login-card hidden" data-motion-view>
|
|
382
|
+
<input id="token-input" name="gateway-token" type="password" placeholder="输入网关访问令牌" autocomplete="off" aria-label="网关访问令牌" data-i18n-placeholder="tokenPlaceholder" data-i18n-aria="tokenPlaceholder">
|
|
386
383
|
<button id="btn-login" class="mini-btn" data-i18n="enter">进入</button>
|
|
387
384
|
</div>
|
|
388
385
|
|
|
389
|
-
<div id="main-view" class="hidden">
|
|
386
|
+
<div id="main-view" class="hidden" data-motion-view>
|
|
390
387
|
<section id="admin-hero" class="admin-hero" aria-live="polite">
|
|
391
388
|
<div class="admin-hero-copy">
|
|
392
389
|
<div class="admin-eyebrow" data-i18n="hero.eyebrow">GATEWAY CONTROL</div>
|
|
@@ -455,8 +452,8 @@
|
|
|
455
452
|
</div>
|
|
456
453
|
|
|
457
454
|
<div id="gateway-port-row" class="gateway-port-row hidden">
|
|
458
|
-
<label data-i18n="gatewayPort">网关端口</label>
|
|
459
|
-
<input id="gateway-port-input" type="number" min="1" max="65535" inputmode="numeric" placeholder="8787">
|
|
455
|
+
<label for="gateway-port-input" data-i18n="gatewayPort">网关端口</label>
|
|
456
|
+
<input id="gateway-port-input" name="gateway-port" type="number" min="1" max="65535" inputmode="numeric" autocomplete="off" placeholder="8787">
|
|
460
457
|
<span id="gateway-port-current" class="muted">—</span>
|
|
461
458
|
<button id="btn-save-port" class="mini-btn" data-i18n="savePort">保存</button>
|
|
462
459
|
</div>
|
|
@@ -560,7 +557,7 @@
|
|
|
560
557
|
'doctor.realtime': '实时消息通道', 'doctor.realtimePass': 'mux 与 host 通道均已连接', 'doctor.realtimeFail': '实时通道未全部建立;可先使用轮询,检查 DSH 后会自动恢复',
|
|
561
558
|
'doctor.start': '启动', 'doctor.showQr': '显示二维码', 'doctor.copyAddress': '复制地址', 'doctor.copyCommand': '复制命令',
|
|
562
559
|
'doctor.reportCopied': '诊断报告已复制', 'doctor.commandCopied': '防火墙命令已复制,请确认网络范围后手动执行', 'doctor.addressCopied': '服务器地址已复制',
|
|
563
|
-
'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔',
|
|
560
|
+
'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔', 'theme.mono': '黑曜白',
|
|
564
561
|
'theme.panelTitle': '选择配色', 'theme.close': '关闭',
|
|
565
562
|
'repo': '仓库',
|
|
566
563
|
'donateTitle': '赞赏支持', 'donateThanks': '感谢你的支持 ☕',
|
|
@@ -670,7 +667,7 @@
|
|
|
670
667
|
'doctor.realtime': 'Realtime channels', 'doctor.realtimePass': 'Both mux and host channels are connected', 'doctor.realtimeFail': 'Realtime channels are incomplete. Polling remains available and recovery is automatic.',
|
|
671
668
|
'doctor.start': 'Start', 'doctor.showQr': 'Show QR', 'doctor.copyAddress': 'Copy address', 'doctor.copyCommand': 'Copy command',
|
|
672
669
|
'doctor.reportCopied': 'Diagnostic report copied', 'doctor.commandCopied': 'Firewall command copied. Review the network scope before running it.', 'doctor.addressCopied': 'Server address copied',
|
|
673
|
-
'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower',
|
|
670
|
+
'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower', 'theme.mono': 'Monochrome',
|
|
674
671
|
'theme.panelTitle': 'Choose theme', 'theme.close': 'Close',
|
|
675
672
|
'repo': 'Repo',
|
|
676
673
|
'donateTitle': 'Support', 'donateThanks': 'Thanks for your support ☕',
|
|
@@ -767,6 +764,8 @@
|
|
|
767
764
|
<script src="../qrcode.min.js"></script>
|
|
768
765
|
<script src="../i18n.js"></script>
|
|
769
766
|
<script src="../theme.js"></script>
|
|
767
|
+
<script src="../vendor/gsap/gsap.min.js"></script>
|
|
768
|
+
<script src="../motion.js"></script>
|
|
770
769
|
<script src="../admin.js"></script>
|
|
771
770
|
</body>
|
|
772
771
|
</html>
|
package/public/admin.js
CHANGED
|
@@ -935,10 +935,11 @@ function renderLangBtn() {
|
|
|
935
935
|
}
|
|
936
936
|
|
|
937
937
|
const THEME_META = [
|
|
938
|
-
{ id: 'default', sw: ['#
|
|
939
|
-
{ id: 'dark', sw: ['#
|
|
940
|
-
{ id: 'light', sw: ['#
|
|
941
|
-
{ id: 'neutral', sw: ['#
|
|
938
|
+
{ id: 'default', sw: ['#0D1117', '#21262D', '#58A6FF'] },
|
|
939
|
+
{ id: 'dark', sw: ['#161316', '#302323', '#FFB86B'] },
|
|
940
|
+
{ id: 'light', sw: ['#F5F7F8', '#FFFFFF', '#176B87'] },
|
|
941
|
+
{ id: 'neutral', sw: ['#EEF1E8', '#FAFBF6', '#47643C'] },
|
|
942
|
+
{ id: 'mono', sw: ['#050505', '#333333', '#F5F5F5'] }
|
|
942
943
|
]
|
|
943
944
|
|
|
944
945
|
function renderThemeBtn() {
|
|
@@ -957,7 +958,7 @@ function renderThemeOptions() {
|
|
|
957
958
|
if (!box) return
|
|
958
959
|
const cur = window.DSHTheme.get()
|
|
959
960
|
box.innerHTML = THEME_META.map(m => `
|
|
960
|
-
<button class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" title="${t('theme.' + m.id)}">
|
|
961
|
+
<button type="button" class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" aria-pressed="${m.id === cur}" title="${t('theme.' + m.id)}">
|
|
961
962
|
<span class="theme-swatches">${m.sw.map(c => `<i style="background:${c}"></i>`).join('')}</span>
|
|
962
963
|
<span class="theme-name">${t('theme.' + m.id)}</span>
|
|
963
964
|
<span class="theme-check">${m.id === cur ? '✓' : ''}</span>
|
package/public/app.js
CHANGED
|
@@ -1673,6 +1673,24 @@ function goalOf(s) {
|
|
|
1673
1673
|
if (!p) return null
|
|
1674
1674
|
return p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1675
1675
|
}
|
|
1676
|
+
const COLLAPSED_GOALS_KEY = 'dshCollapsedGoalsV1'
|
|
1677
|
+
function collapsedGoals() {
|
|
1678
|
+
try {
|
|
1679
|
+
const value = JSON.parse(LS.get(COLLAPSED_GOALS_KEY, '[]'))
|
|
1680
|
+
return Array.isArray(value) ? value.filter(item => item && typeof item.sessionId === 'string' && typeof item.goalId === 'string') : []
|
|
1681
|
+
} catch { return [] }
|
|
1682
|
+
}
|
|
1683
|
+
function goalDisplayId(goal) { return String(goal?.id || '__current__') }
|
|
1684
|
+
function isGoalCollapsed(sessionId, goal) {
|
|
1685
|
+
const goalId = goalDisplayId(goal)
|
|
1686
|
+
return collapsedGoals().some(item => item.sessionId === sessionId && item.goalId === goalId)
|
|
1687
|
+
}
|
|
1688
|
+
function setGoalCollapsed(sessionId, goal, collapsed) {
|
|
1689
|
+
const goalId = goalDisplayId(goal)
|
|
1690
|
+
const next = collapsedGoals().filter(item => item.sessionId !== sessionId)
|
|
1691
|
+
if (collapsed) next.push({ sessionId, goalId })
|
|
1692
|
+
LS.set(COLLAPSED_GOALS_KEY, JSON.stringify(next.slice(-100)))
|
|
1693
|
+
}
|
|
1676
1694
|
|
|
1677
1695
|
function updatePendingBadge() {
|
|
1678
1696
|
const pending = state.approvals.length + state.questions.length
|
|
@@ -2090,6 +2108,7 @@ async function openSession(id) {
|
|
|
2090
2108
|
renderSessionTitle(); renderSessionSub(); updateCancelBtn(); updateSessionStatus()
|
|
2091
2109
|
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
2092
2110
|
renderQueue()
|
|
2111
|
+
renderSessionPending()
|
|
2093
2112
|
restoreCachedHistory()
|
|
2094
2113
|
await loadHistory(true)
|
|
2095
2114
|
renderSessionCards()
|
|
@@ -2122,6 +2141,7 @@ async function closeSession() {
|
|
|
2122
2141
|
const discard = await shouldDiscardEmptySession(sessionId)
|
|
2123
2142
|
if (state.current !== sessionId) return
|
|
2124
2143
|
state.current = null
|
|
2144
|
+
renderSessionPending()
|
|
2125
2145
|
if (discard) removeLocalSessionRecord(sessionId)
|
|
2126
2146
|
setComposerFullscreen(false)
|
|
2127
2147
|
clearComposerImages()
|
|
@@ -2684,15 +2704,23 @@ async function renderSessionCards() {
|
|
|
2684
2704
|
let html = ''
|
|
2685
2705
|
|
|
2686
2706
|
if (goal && !isGoalTerminal(goal)) {
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
<div
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
<
|
|
2694
|
-
<
|
|
2695
|
-
|
|
2707
|
+
const collapsed = isGoalCollapsed(sessionId, goal)
|
|
2708
|
+
html += `<div class="goal-disclosure${collapsed ? ' is-collapsed' : ''}">
|
|
2709
|
+
<div id="goal-panel-mobile" class="card goal-card${collapsed ? ' hidden' : ''}">
|
|
2710
|
+
<div class="goal-card-head"><div class="card-title">${t('goal.title')}</div>
|
|
2711
|
+
<button type="button" class="goal-collapse-btn" data-goal-collapse="1" aria-expanded="true" aria-controls="goal-panel-mobile" title="${esc(t('goal.collapse'))}"><span aria-hidden="true">›</span>${t('goal.collapseShort')}</button>
|
|
2712
|
+
</div>
|
|
2713
|
+
<div class="goal-obj">${esc(goal.objective || '')}</div>
|
|
2714
|
+
<div class="goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
|
|
2715
|
+
<div class="goal-actions">
|
|
2716
|
+
${goal.phase === 'active' ? '<button class="mini-btn" data-goal="pause">' + t('goal.pause') + '</button>' : '<button class="mini-btn" data-goal="resume">' + t('goal.resume') + '</button>'}
|
|
2717
|
+
<button class="mini-btn" data-goal="complete">${t('goal.complete')}</button>
|
|
2718
|
+
<button class="mini-btn" data-goal="edit">${t('goal.edit')}</button>
|
|
2719
|
+
<button class="mini-btn" data-goal="clear">${t('goal.clear')}</button>
|
|
2720
|
+
</div>
|
|
2721
|
+
</div>
|
|
2722
|
+
<button type="button" class="goal-side-tab${collapsed ? '' : ' hidden'}" data-goal-collapse="0" aria-expanded="false" aria-controls="goal-panel-mobile" aria-label="${esc(t('goal.expand'))}" title="${esc(t('goal.expand'))}"><span aria-hidden="true">‹</span><span>${t('goal.title')}</span><small>${t('goal.collapsedHint')}</small></button>
|
|
2723
|
+
</div>`
|
|
2696
2724
|
}
|
|
2697
2725
|
if (todos?.items?.length) {
|
|
2698
2726
|
html += `<div class="card"><div class="card-title">${t('todos.title')}</div>${todos.items.map(t =>
|
|
@@ -2702,6 +2730,15 @@ async function renderSessionCards() {
|
|
|
2702
2730
|
box.innerHTML = html
|
|
2703
2731
|
box.querySelectorAll('[data-goal]').forEach(btn =>
|
|
2704
2732
|
btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
|
|
2733
|
+
box.querySelectorAll('[data-goal-collapse]').forEach(btn =>
|
|
2734
|
+
btn.addEventListener('click', () => {
|
|
2735
|
+
const collapse = btn.dataset.goalCollapse === '1'
|
|
2736
|
+
const currentGoal = goalOf(state.byId.get(state.current))
|
|
2737
|
+
if (!currentGoal) return
|
|
2738
|
+
setGoalCollapsed(state.current, currentGoal, collapse)
|
|
2739
|
+
void renderSessionCards()
|
|
2740
|
+
requestAnimationFrame(() => box.querySelector(`[data-goal-collapse="${collapse ? '0' : '1'}"]`)?.focus())
|
|
2741
|
+
}))
|
|
2705
2742
|
|
|
2706
2743
|
// 子代理
|
|
2707
2744
|
const sub = await safeRpc('subagent.list', { parentSessionId: sessionId })
|
|
@@ -3294,7 +3331,7 @@ function renderOverview() {
|
|
|
3294
3331
|
$('overview-attention-list').innerHTML = pending.length ? pending.slice(0, 3).map(({ kind, item }) => {
|
|
3295
3332
|
const title = titleOf(state.byId.get(item.sessionId))
|
|
3296
3333
|
if (kind === 'approval') return `<div class="overview-attention-item" data-overview-approval="${esc(item.approvalId)}">
|
|
3297
|
-
<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
|
|
3334
|
+
<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>
|
|
3298
3335
|
<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>
|
|
3299
3336
|
</div>`
|
|
3300
3337
|
return `<button type="button" class="overview-attention-item question" data-overview-question="${esc(item.rpcId)}">
|
|
@@ -3359,7 +3396,7 @@ function renderPending() {
|
|
|
3359
3396
|
const title = titleOf(state.byId.get(a.sessionId))
|
|
3360
3397
|
return `<div class="pending-card approval" data-approval="${esc(a.approvalId)}">
|
|
3361
3398
|
<div class="pc-title">${esc(t('pending.approvalTitle', { tool: a.toolName || t('tool.default') }))}</div>
|
|
3362
|
-
<div class="pc-desc">${esc(a
|
|
3399
|
+
<div class="pc-desc">${esc(approvalDetail(a))}</div>
|
|
3363
3400
|
<div class="pc-session">${esc(title)}</div>
|
|
3364
3401
|
<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>
|
|
3365
3402
|
</div>`
|
|
@@ -3379,10 +3416,48 @@ function renderPending() {
|
|
|
3379
3416
|
})
|
|
3380
3417
|
list.querySelectorAll('[data-question]').forEach(btn =>
|
|
3381
3418
|
btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.question))))
|
|
3419
|
+
renderSessionPending()
|
|
3382
3420
|
updatePendingBadge()
|
|
3383
3421
|
renderOverview()
|
|
3384
3422
|
}
|
|
3385
3423
|
|
|
3424
|
+
function approvalDetail(a) {
|
|
3425
|
+
const lines = []
|
|
3426
|
+
if (a?.reason) lines.push(String(a.reason))
|
|
3427
|
+
if (a?.arguments !== undefined) lines.push(safeJson(a.arguments))
|
|
3428
|
+
if (a?.callId) lines.push(`callId: ${a.callId}`)
|
|
3429
|
+
return lines.join('\n') || t('pending.noReason')
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
function renderSessionPending() {
|
|
3433
|
+
const box = $('session-pending')
|
|
3434
|
+
if (!box) return
|
|
3435
|
+
const sessionId = state.current
|
|
3436
|
+
const items = [
|
|
3437
|
+
...state.approvals.filter(a => a.sessionId === sessionId).map(a => ({ kind: 'approval', item: a })),
|
|
3438
|
+
...state.questions.filter(q => q.sessionId === sessionId).map(q => ({ kind: 'question', item: q }))
|
|
3439
|
+
]
|
|
3440
|
+
box.classList.toggle('hidden', !sessionId || !items.length)
|
|
3441
|
+
if (!sessionId || !items.length) { box.innerHTML = ''; return }
|
|
3442
|
+
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 }) => {
|
|
3443
|
+
if (kind === 'approval') return `<article class="pending-card approval" data-session-approval="${esc(item.approvalId)}">
|
|
3444
|
+
<div class="pc-title">${esc(item.toolName || t('tool.default'))}</div>
|
|
3445
|
+
<div class="pc-desc">${esc(approvalDetail(item))}</div>
|
|
3446
|
+
<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>
|
|
3447
|
+
</article>`
|
|
3448
|
+
return `<article class="pending-card question" data-session-question="${esc(item.rpcId)}">
|
|
3449
|
+
<div class="pc-title">❓ ${esc((item.questions || []).map(question => question.question).filter(Boolean).join(' / ') || t('notify.questionTitle'))}</div>
|
|
3450
|
+
<div class="goal-actions"><button type="button" class="mini-btn" data-session-answer>${t('pending.answer')}</button></div>
|
|
3451
|
+
</article>`
|
|
3452
|
+
}).join('')}</div>`
|
|
3453
|
+
box.querySelectorAll('[data-session-approve]').forEach(button => {
|
|
3454
|
+
button.addEventListener('click', () => approveApproval(button.closest('[data-session-approval]')?.dataset.sessionApproval || '', button.dataset.sessionApprove === '1'))
|
|
3455
|
+
})
|
|
3456
|
+
box.querySelectorAll('[data-session-answer]').forEach(button => {
|
|
3457
|
+
button.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === button.closest('[data-session-question]')?.dataset.sessionQuestion)))
|
|
3458
|
+
})
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3386
3461
|
async function approveApproval(id, allow) {
|
|
3387
3462
|
const a = state.approvals.find(x => x.approvalId === id)
|
|
3388
3463
|
if (!a) return
|
|
@@ -5728,6 +5803,8 @@ function showView(id) {
|
|
|
5728
5803
|
// 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
|
|
5729
5804
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
5730
5805
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
5806
|
+
// 主页已有上下文明确的刷新按钮,避免顶栏出现第二个同义入口。
|
|
5807
|
+
$('btn-refresh')?.classList.toggle('hidden', id === 'view-activity')
|
|
5731
5808
|
window.DshMotion?.view($(id))
|
|
5732
5809
|
window.scrollTo(0, 0)
|
|
5733
5810
|
if (id === 'view-files' && !state.fs.loaded) {
|
|
@@ -6411,10 +6488,11 @@ function renderLangBtn() {
|
|
|
6411
6488
|
}
|
|
6412
6489
|
|
|
6413
6490
|
const THEME_META = [
|
|
6414
|
-
{ id: 'default', sw: ['#
|
|
6415
|
-
{ id: 'dark', sw: ['#
|
|
6416
|
-
{ id: 'light', sw: ['#
|
|
6417
|
-
{ id: 'neutral', sw: ['#
|
|
6491
|
+
{ id: 'default', sw: ['#0D1117', '#21262D', '#58A6FF'] },
|
|
6492
|
+
{ id: 'dark', sw: ['#161316', '#302323', '#FFB86B'] },
|
|
6493
|
+
{ id: 'light', sw: ['#F5F7F8', '#FFFFFF', '#176B87'] },
|
|
6494
|
+
{ id: 'neutral', sw: ['#EEF1E8', '#FAFBF6', '#47643C'] },
|
|
6495
|
+
{ id: 'mono', sw: ['#050505', '#333333', '#F5F5F5'] }
|
|
6418
6496
|
]
|
|
6419
6497
|
|
|
6420
6498
|
function renderThemeBtn() {
|
|
@@ -6427,7 +6505,7 @@ function renderThemeOptions() {
|
|
|
6427
6505
|
if (!box) return
|
|
6428
6506
|
const cur = window.DSHTheme.get()
|
|
6429
6507
|
box.innerHTML = THEME_META.map(m => `
|
|
6430
|
-
<button class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" title="${t('theme.' + m.id)}">
|
|
6508
|
+
<button type="button" class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" aria-pressed="${m.id === cur}" title="${t('theme.' + m.id)}">
|
|
6431
6509
|
<span class="theme-swatches">${m.sw.map(c => `<i style="background:${c}"></i>`).join('')}</span>
|
|
6432
6510
|
<span class="theme-name">${t('theme.' + m.id)}</span>
|
|
6433
6511
|
<span class="theme-check">${m.id === cur ? '✓' : ''}</span>
|
|
@@ -8,6 +8,7 @@ html, body {
|
|
|
8
8
|
.hidden { display: none !important; }
|
|
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
|
+
.ds-sidebar-backdrop { display: none; }
|
|
11
12
|
.ds-brand { display: flex; align-items: center; gap: 8px; padding: 17px 18px 12px; font-weight: 750; font-size: 15px; letter-spacing: .1px; }
|
|
12
13
|
.ds-logo { color: var(--dsr-accent-strong); }
|
|
13
14
|
.ds-brand-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -224,15 +225,15 @@ html.reorder-scroll-lock body { overscroll-behavior: none; }
|
|
|
224
225
|
.ds-overview-attention-list, .ds-overview-session-list { display:flex; flex-direction:column; gap:8px; }
|
|
225
226
|
.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; }
|
|
226
227
|
button.ds-overview-attention-item, button.ds-overview-session-item { cursor:pointer; font:inherit; }
|
|
227
|
-
.ds-overview-attention-item { border-left:3px solid var(--dsr-warning); }
|
|
228
|
+
.ds-overview-attention-item { align-items:flex-start; border-left:3px solid var(--dsr-warning); }
|
|
228
229
|
.ds-overview-attention-item.question { border-left-color:var(--dsr-accent-2); }
|
|
229
230
|
.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; }
|
|
230
231
|
.ds-overview-attention-item.question .ds-overview-mark { background:var(--dsr-accent-2-soft); color:var(--dsr-accent-2); }
|
|
231
232
|
.ds-overview-copy { flex:1; min-width:0; }
|
|
232
|
-
.ds-overview-item-title, .ds-overview-item-desc { display:block;
|
|
233
|
+
.ds-overview-item-title, .ds-overview-item-desc { display:block; line-height:1.45; overflow-wrap:anywhere; word-break:break-word; }
|
|
233
234
|
.ds-overview-item-title { font-size:12px; font-weight:650; }
|
|
234
|
-
.ds-overview-item-desc { margin-top:3px; color:var(--dsr-muted); font-size:11px; }
|
|
235
|
-
.ds-overview-actions { flex:0 0 auto; display:flex; gap:4px; }
|
|
235
|
+
.ds-overview-item-desc { margin-top:3px; color:var(--dsr-muted); font-size:11px; white-space:normal; }
|
|
236
|
+
.ds-overview-actions { flex:0 0 auto; display:flex; flex-wrap:wrap; justify-content:flex-end; gap:4px; }
|
|
236
237
|
.ds-overview-actions .ds-btn { min-height:27px; padding:2px 8px; font-size:11px; }
|
|
237
238
|
.ds-overview-actions .allow { background:var(--dsr-success-soft); border-color:var(--dsr-success-line); color:var(--dsr-success); }
|
|
238
239
|
.ds-overview-actions .reject { background:var(--dsr-warning-soft); border-color:var(--dsr-warning-line); color:var(--dsr-warning); }
|
|
@@ -286,9 +287,37 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
286
287
|
.ds-presets-guide { padding: 2px 8px 10px; font-size: 11px; line-height: 1.5; }
|
|
287
288
|
.ds-session-cards { flex: none; max-height: 220px; overflow-y: auto; padding: 10px 22px 0; display: flex; flex-direction: column; gap: 8px; }
|
|
288
289
|
.ds-session-cards:empty { display: none; }
|
|
290
|
+
.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); }
|
|
291
|
+
.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; }
|
|
292
|
+
.ds-session-pending-list { display:flex; flex-direction:column; gap:8px; }
|
|
293
|
+
.ds-session-pending .ds-notif-card { min-width:0; box-shadow:none; animation:none; }
|
|
294
|
+
.ds-session-pending .ds-notif-title, .ds-session-pending .ds-notif-body { overflow-wrap:anywhere; word-break:break-word; }
|
|
289
295
|
.ds-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 12px; }
|
|
290
296
|
.ds-card-title { font-size: 11px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; margin-bottom: 6px; text-transform: uppercase; }
|
|
291
|
-
.ds-goal-
|
|
297
|
+
.ds-goal-disclosure { width: 100%; min-width: 0; display: flex; justify-content: flex-end; }
|
|
298
|
+
.ds-goal-disclosure .ds-goal-card { width: 100%; min-width: 0; }
|
|
299
|
+
.ds-goal-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
|
|
300
|
+
.ds-goal-card-head .ds-card-title { min-width: 0; margin-bottom: 0; }
|
|
301
|
+
.ds-goal-collapse-btn,
|
|
302
|
+
.ds-goal-side-tab {
|
|
303
|
+
touch-action: manipulation; border: 1px solid var(--dsr-line); background: var(--dsr-bg-2); color: var(--dsr-muted);
|
|
304
|
+
font: inherit; cursor: pointer;
|
|
305
|
+
}
|
|
306
|
+
.ds-goal-collapse-btn { min-height: 32px; display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border-radius: 8px; font-size: 11.5px; }
|
|
307
|
+
.ds-goal-collapse-btn > span { color: var(--dsr-accent-strong); font-size: 17px; line-height: 1; }
|
|
308
|
+
.ds-goal-side-tab {
|
|
309
|
+
min-height: 42px; max-width: min(100%, 210px); display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px 6px 10px;
|
|
310
|
+
border-right: 0; border-radius: 11px 0 0 11px; box-shadow: -5px 6px 18px var(--dsr-shadow); color: var(--dsr-text); font-size: 12px; font-weight: 700;
|
|
311
|
+
}
|
|
312
|
+
.ds-goal-side-tab > span:first-child { color: var(--dsr-accent-strong); font-size: 19px; line-height: 1; }
|
|
313
|
+
.ds-goal-side-tab small { min-width: 0; color: var(--dsr-muted); font-size: 10.5px; font-weight: 500; white-space: nowrap; }
|
|
314
|
+
.ds-goal-collapse-btn:hover,
|
|
315
|
+
.ds-goal-side-tab:hover { background: var(--dsr-panel-2); color: var(--dsr-text); }
|
|
316
|
+
.ds-goal-collapse-btn:active,
|
|
317
|
+
.ds-goal-side-tab:active { transform: translateY(1px); }
|
|
318
|
+
.ds-goal-collapse-btn:focus-visible,
|
|
319
|
+
.ds-goal-side-tab:focus-visible { outline: 2px solid var(--dsr-accent-strong); outline-offset: 2px; }
|
|
320
|
+
.ds-goal-obj { font-size: 13px; line-height: 1.55; word-break: break-word; overflow-wrap: anywhere; }
|
|
292
321
|
.ds-goal-phase { font-size: 11px; color: var(--dsr-muted); margin-top: 3px; }
|
|
293
322
|
.ds-goal-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
294
323
|
.ds-mini-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 26px; padding: 2px 9px; border-radius: 7px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); color: var(--dsr-text); font: inherit; font-size: 11.5px; cursor: pointer; }
|
|
@@ -350,6 +379,10 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
350
379
|
.ds-settings { max-width: 720px; display: flex; flex-direction: column; gap: 10px; }
|
|
351
380
|
.ds-setting-row { display: flex; align-items: center; gap: 10px; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 14px; }
|
|
352
381
|
.ds-setting-row > div:first-child { flex: 1; min-width: 0; }
|
|
382
|
+
.ds-setting-link { width: 100%; color: var(--dsr-text); font: inherit; text-align: left; cursor: pointer; transition: background-color .18s ease, border-color .18s ease; }
|
|
383
|
+
.ds-setting-link:hover, .ds-setting-link:focus-visible { background: var(--dsr-bg-2); border-color: var(--dsr-accent-line); }
|
|
384
|
+
.ds-setting-link .ds-setting-arrow { transition: transform .18s ease, color .18s ease; }
|
|
385
|
+
.ds-setting-link:hover .ds-setting-arrow, .ds-setting-link:focus-visible .ds-setting-arrow { color: var(--dsr-accent-strong); transform: translateX(2px); }
|
|
353
386
|
.ds-setting-arrow,
|
|
354
387
|
#settings-home .ds-setting-row .ds-btn {
|
|
355
388
|
display: inline-flex; align-items: center; justify-content: center;
|
|
@@ -397,8 +430,8 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
397
430
|
.ds-mini:hover { color: var(--dsr-text); }
|
|
398
431
|
.ds-switch { position: relative; display: inline-block; width: 36px; height: 21px; flex-shrink: 0; }
|
|
399
432
|
.ds-switch input { opacity: 0; width: 0; height: 0; }
|
|
400
|
-
.ds-switch .ds-slider { position: absolute; inset: 0; border-radius: 999px; background: var(--dsr-panel-2); border: 1px solid var(--dsr-line); transition: .2s; }
|
|
401
|
-
.ds-switch .ds-slider::before { content: ''; position: absolute; width: 15px; height: 15px; border-radius: 50%; left: 2px; top: 2px; background: var(--dsr-muted); transition: .2s; }
|
|
433
|
+
.ds-switch .ds-slider { position: absolute; inset: 0; border-radius: 999px; background: var(--dsr-panel-2); border: 1px solid var(--dsr-line); transition: background-color .2s ease, border-color .2s ease; }
|
|
434
|
+
.ds-switch .ds-slider::before { content: ''; position: absolute; width: 15px; height: 15px; border-radius: 50%; left: 2px; top: 2px; background: var(--dsr-muted); transition: transform .2s ease, background-color .2s ease; }
|
|
402
435
|
.ds-switch input:checked + .ds-slider { background: var(--dsr-accent-soft); border-color: var(--dsr-accent); }
|
|
403
436
|
.ds-switch input:checked + .ds-slider::before { transform: translateX(15px); background: var(--dsr-accent); }
|
|
404
437
|
|
|
@@ -429,7 +462,7 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
429
462
|
.ds-stats-note { font-size: 11px; color: var(--dsr-muted); line-height: 1.6; margin-top: 8px; }
|
|
430
463
|
|
|
431
464
|
/* 统计抽屉(右侧滑出) */
|
|
432
|
-
.ds-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 110; width: min(540px, 100vw); background: var(--dsr-panel); border-left: 1px solid var(--dsr-line); box-shadow: -12px 0 32px rgba(0,0,0,.35); display: flex; flex-direction: column; }
|
|
465
|
+
.ds-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 110; width: min(540px, 100vw); background: var(--dsr-panel); border-left: 1px solid var(--dsr-line); box-shadow: -12px 0 32px rgba(0,0,0,.35); display: flex; flex-direction: column; overscroll-behavior: contain; }
|
|
433
466
|
.ds-drawer.hidden { display: none; }
|
|
434
467
|
.ds-drawer-head { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 13px 15px; border-bottom: 1px solid var(--dsr-line); font-weight: 700; font-size: 15px; }
|
|
435
468
|
.ds-drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 15px; scrollbar-width: thin; scrollbar-color: var(--dsr-line) transparent; }
|
|
@@ -453,7 +486,7 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
453
486
|
/* 模态 */
|
|
454
487
|
.ds-modal { position: fixed; inset: 0; z-index: 120; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; padding: 16px; }
|
|
455
488
|
.ds-modal.hidden { display: none; }
|
|
456
|
-
.ds-modal-card { width: min(520px, 100%); background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius:
|
|
489
|
+
.ds-modal-card { width: min(520px, 100%); background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 18px; padding: 18px; box-shadow: 0 22px 60px var(--dsr-shadow); overscroll-behavior: contain; }
|
|
457
490
|
.ds-modal-title { font-weight: 700; margin-bottom: 10px; }
|
|
458
491
|
.ds-modal-body { display: flex; flex-direction: column; gap: 10px; max-height: 60vh; overflow-y: auto; }
|
|
459
492
|
.ds-workspace-create-desc { margin: 0; color: var(--dsr-muted); line-height: 1.55; }
|
|
@@ -529,9 +562,22 @@ input:focus-visible, select:focus-visible, textarea:focus-visible {
|
|
|
529
562
|
.ds-btn:hover { filter: brightness(1.08); }
|
|
530
563
|
}
|
|
531
564
|
|
|
532
|
-
/* 窄屏回退:
|
|
565
|
+
/* 窄屏回退: 侧栏变为可关闭抽屉,保留完整导航与会话上下文。 */
|
|
533
566
|
@media (max-width: 1023px) {
|
|
534
|
-
.ds-sidebar {
|
|
567
|
+
.ds-sidebar {
|
|
568
|
+
position: fixed; inset: 0 auto 0 0; z-index: 90; display: flex;
|
|
569
|
+
width: min(320px, 86vw); max-width: 100%;
|
|
570
|
+
opacity: 0; pointer-events: none; transform: translateX(-104%);
|
|
571
|
+
transition: transform .24s ease, opacity .18s ease;
|
|
572
|
+
box-shadow: 18px 0 48px rgba(0,0,0,.38);
|
|
573
|
+
}
|
|
574
|
+
.ds-sidebar.mobile-open { opacity: 1; pointer-events: auto; transform: translateX(0); }
|
|
575
|
+
.ds-sidebar-backdrop {
|
|
576
|
+
position: fixed; inset: 0; z-index: 80; display: block;
|
|
577
|
+
border: 0; background: var(--dsr-overlay); cursor: default;
|
|
578
|
+
}
|
|
579
|
+
.ds-sidebar-backdrop.hidden { display: none; }
|
|
580
|
+
.ds-app.mobile-nav-open { overflow: hidden; }
|
|
535
581
|
#btn-mobile-nav { display: inline-flex; }
|
|
536
582
|
.ds-content { flex-direction: column; }
|
|
537
583
|
.ds-view { padding: 10px; overflow-y: auto; }
|