dsh-remote-plugin 0.6.1 → 0.6.3
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/gateway.cjs +12 -2
- package/index.mjs +90 -12
- package/package.json +1 -1
- package/public/app.js +30 -1
- package/public/desktop/desktop.css +62 -8
- package/public/desktop/desktop.html +88 -45
- package/public/desktop/desktop.js +350 -5
- package/public/index.html +2 -2
- package/public/update.json +14 -4
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/gateway.cjs
CHANGED
|
@@ -1653,10 +1653,20 @@ function proxyApi(req, res, url) {
|
|
|
1653
1653
|
}
|
|
1654
1654
|
|
|
1655
1655
|
// ---------- 其它 ----------
|
|
1656
|
-
function serveHealth(res) {
|
|
1656
|
+
async function serveHealth(res) {
|
|
1657
|
+
let upstreamOk = false
|
|
1658
|
+
try {
|
|
1659
|
+
const ctrl = new AbortController()
|
|
1660
|
+
const timer = setTimeout(() => ctrl.abort(), 2000)
|
|
1661
|
+
const probe = await fetch(UPSTREAM.origin + '/healthz', { signal: ctrl.signal, cache: 'no-store' })
|
|
1662
|
+
clearTimeout(timer)
|
|
1663
|
+
upstreamOk = probe.ok
|
|
1664
|
+
} catch {
|
|
1665
|
+
upstreamOk = false
|
|
1666
|
+
}
|
|
1657
1667
|
cors(res)
|
|
1658
1668
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
|
|
1659
|
-
res.end(JSON.stringify({ ok: true, service: 'dsh-remote', version: VERSION, upstream: UPSTREAM.origin }))
|
|
1669
|
+
res.end(JSON.stringify({ ok: true, service: 'dsh-remote', version: VERSION, pid: process.pid, upstream: UPSTREAM.origin, upstreamOk }))
|
|
1660
1670
|
}
|
|
1661
1671
|
|
|
1662
1672
|
function lanAddresses() {
|
package/index.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 网关不可用时回退到插件模式主机状态
|
|
6
6
|
* 浏览器侧入口由 client half 注册在 DSH 原生侧边栏(见 client.js)。
|
|
7
7
|
*/
|
|
8
|
-
import { spawn } from 'node:child_process'
|
|
9
|
-
import { createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
9
|
+
import { appendFileSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
10
|
import { stat } from 'node:fs/promises'
|
|
11
11
|
import { homedir, hostname, networkInterfaces } from 'node:os'
|
|
12
12
|
import { dirname, extname, normalize, resolve } from 'node:path'
|
|
@@ -128,9 +128,59 @@ function runExit(cmd, args) {
|
|
|
128
128
|
|
|
129
129
|
async function gatewayRunning() {
|
|
130
130
|
try {
|
|
131
|
-
const res = await fetch(`${GATEWAY_BASE}/health`, { signal: AbortSignal.timeout(
|
|
132
|
-
|
|
131
|
+
const res = await fetch(`${GATEWAY_BASE}/health`, { signal: AbortSignal.timeout(3000) })
|
|
132
|
+
if (!res.ok) return { running: false }
|
|
133
|
+
const data = await res.json().catch(() => ({}))
|
|
134
|
+
return {
|
|
135
|
+
running: true,
|
|
136
|
+
pid: Number(data.pid) || 0,
|
|
137
|
+
upstream: typeof data.upstream === 'string' ? data.upstream : '',
|
|
138
|
+
upstreamOk: data.upstreamOk === true,
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
return { running: false }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function gatewayPidFile() { return `${homedir()}/.dsh-remote/plugin-gateway.pid` }
|
|
146
|
+
|
|
147
|
+
function readGatewayPid() {
|
|
148
|
+
try {
|
|
149
|
+
const pid = Number(readFileSync(gatewayPidFile(), 'utf8').trim())
|
|
150
|
+
return Number.isFinite(pid) && pid > 0 ? pid : 0
|
|
133
151
|
} catch {
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function writeGatewayPid(pid) {
|
|
157
|
+
try {
|
|
158
|
+
mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
|
|
159
|
+
writeFileSync(gatewayPidFile(), String(pid) + '\n')
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function logGateway(msg) {
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(`${homedir()}/.dsh-remote`, { recursive: true })
|
|
166
|
+
appendFileSync(`${homedir()}/.dsh-remote/plugin-gateway.log`, `[${new Date().toISOString()}] ${msg}\n`)
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function killGateway(health) {
|
|
171
|
+
const pid = (health && Number(health.pid)) || readGatewayPid()
|
|
172
|
+
if (!pid) return false
|
|
173
|
+
try {
|
|
174
|
+
if (process.platform === 'win32') {
|
|
175
|
+
await runExit('taskkill', ['/F', '/PID', String(pid)])
|
|
176
|
+
} else {
|
|
177
|
+
process.kill(pid)
|
|
178
|
+
}
|
|
179
|
+
logGateway('已停止旧网关 PID=' + pid)
|
|
180
|
+
await sleep(300)
|
|
181
|
+
return true
|
|
182
|
+
} catch (e) {
|
|
183
|
+
logGateway('停止旧网关失败 PID=' + pid + ' ' + (e?.message || String(e)))
|
|
134
184
|
return false
|
|
135
185
|
}
|
|
136
186
|
}
|
|
@@ -157,7 +207,9 @@ function setGatewayEnabled(on) {
|
|
|
157
207
|
|
|
158
208
|
/** 启动随插件分发的 gateway.cjs; 已运行则直接返回。 */
|
|
159
209
|
async function startGateway() {
|
|
160
|
-
|
|
210
|
+
const upstream = `http://${dshListen.host}:${dshListen.port}`
|
|
211
|
+
const health = await gatewayRunning()
|
|
212
|
+
if (health.running) {
|
|
161
213
|
setGatewayEnabled(true)
|
|
162
214
|
return { ok: true, running: true, started: false }
|
|
163
215
|
}
|
|
@@ -166,6 +218,7 @@ async function startGateway() {
|
|
|
166
218
|
return { ok: false, running: false, error: '插件包缺少 gateway.cjs, 请升级插件' }
|
|
167
219
|
}
|
|
168
220
|
const port = process.env.DSH_REMOTE_GATEWAY_PORT || '8787'
|
|
221
|
+
logGateway('启动网关, 上游: ' + upstream)
|
|
169
222
|
|
|
170
223
|
// 首选 systemd-run: 网关成为独立 user 单元, DSH 重启/升级不会连带杀掉它
|
|
171
224
|
let sysd = false
|
|
@@ -173,10 +226,16 @@ async function startGateway() {
|
|
|
173
226
|
await runExit('systemctl', ['--user', 'reset-failed', 'dsh-remote-gateway'])
|
|
174
227
|
sysd = (await runExit('systemd-run', [
|
|
175
228
|
'--user', '--unit=dsh-remote-gateway', '--service-type=exec',
|
|
176
|
-
'--setenv=PORT=' + port, '--setenv=HOST=0.0.0.0',
|
|
229
|
+
'--setenv=PORT=' + port, '--setenv=HOST=0.0.0.0', '--setenv=DSH_UPSTREAM=' + upstream,
|
|
177
230
|
'--', process.execPath, script,
|
|
178
231
|
])) === 0
|
|
179
232
|
} catch {}
|
|
233
|
+
if (sysd) {
|
|
234
|
+
try {
|
|
235
|
+
const pid = Number(execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', 'dsh-remote-gateway'], { encoding: 'utf8' }).trim())
|
|
236
|
+
if (Number.isFinite(pid) && pid > 1) writeGatewayPid(pid)
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
180
239
|
|
|
181
240
|
// 无 systemd 的机器回退: detached 子进程
|
|
182
241
|
if (!sysd) {
|
|
@@ -188,14 +247,17 @@ async function startGateway() {
|
|
|
188
247
|
cwd: dirname(script),
|
|
189
248
|
detached: true,
|
|
190
249
|
stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'],
|
|
191
|
-
env: { ...process.env, PORT: port },
|
|
250
|
+
env: { ...process.env, PORT: port, DSH_UPSTREAM: upstream },
|
|
192
251
|
})
|
|
193
252
|
child.unref()
|
|
253
|
+
writeGatewayPid(child.pid)
|
|
194
254
|
}
|
|
195
255
|
// 最多等 4 秒; 超过可能是端口冲突或首次初始化, 前端稍后刷新即可
|
|
196
256
|
for (let i = 0; i < 16; i++) {
|
|
197
257
|
await sleep(250)
|
|
198
|
-
|
|
258
|
+
const h = await gatewayRunning()
|
|
259
|
+
if (h.running) {
|
|
260
|
+
if (h.pid) writeGatewayPid(h.pid)
|
|
199
261
|
setGatewayEnabled(true)
|
|
200
262
|
return { ok: true, running: true, started: true }
|
|
201
263
|
}
|
|
@@ -210,9 +272,24 @@ function ensureGateway() {
|
|
|
210
272
|
if (ensurePromise) return ensurePromise
|
|
211
273
|
ensurePromise = (async () => {
|
|
212
274
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
const health = await gatewayRunning()
|
|
276
|
+
if (!health.running) {
|
|
277
|
+
const out = await startGateway()
|
|
278
|
+
return !!out.running
|
|
279
|
+
}
|
|
280
|
+
const upstream = `http://${dshListen.host}:${dshListen.port}`
|
|
281
|
+
const oldUpstream = health.upstream || ''
|
|
282
|
+
if (health.upstreamOk === false || (oldUpstream && oldUpstream !== upstream) || (!oldUpstream && upstream)) {
|
|
283
|
+
logGateway(`网关上游需刷新: 旧=${oldUpstream || '?'} 新=${upstream}`)
|
|
284
|
+
await killGateway(health)
|
|
285
|
+
for (let i = 0; i < 10; i++) {
|
|
286
|
+
if (!(await gatewayRunning()).running) break
|
|
287
|
+
await sleep(200)
|
|
288
|
+
}
|
|
289
|
+
const out = await startGateway()
|
|
290
|
+
return !!out.running
|
|
291
|
+
}
|
|
292
|
+
return true
|
|
216
293
|
} finally {
|
|
217
294
|
setTimeout(() => { ensurePromise = null }, 4000)
|
|
218
295
|
}
|
|
@@ -417,7 +494,8 @@ async function serveStatic(req, res, ctx) {
|
|
|
417
494
|
// 本地网关开关(仅插件内嵌页使用): GET 状态 / POST {action:'start'|'stop'}
|
|
418
495
|
if (pathname === `${MOUNT}/admin/api/gateway`) {
|
|
419
496
|
if (req.method === 'GET') {
|
|
420
|
-
|
|
497
|
+
const h = await gatewayRunning()
|
|
498
|
+
sendJson(res, 200, { ok: true, running: h.running, upstream: h.upstream || '', upstreamOk: h.upstreamOk === true })
|
|
421
499
|
return
|
|
422
500
|
}
|
|
423
501
|
if (req.method === 'POST') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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
|
@@ -2324,6 +2324,21 @@ function renderNotesPages(items) {
|
|
|
2324
2324
|
box.scrollLeft = 0
|
|
2325
2325
|
updateNotesPage()
|
|
2326
2326
|
}
|
|
2327
|
+
function renderNotesVersionPages(entries) {
|
|
2328
|
+
const box = $('notes-pages')
|
|
2329
|
+
if (!box) return
|
|
2330
|
+
notesPages = entries
|
|
2331
|
+
notesPage = 0
|
|
2332
|
+
box.innerHTML = entries.map(entry => {
|
|
2333
|
+
const items = splitNotes(entry.notes)
|
|
2334
|
+
return `<div class="notes-page" style="flex:0 0 100%;scroll-snap-align:start;box-sizing:border-box;min-width:0;">
|
|
2335
|
+
<div class="notes-version-title" style="font-weight:700;margin-bottom:6px;opacity:.9;">v${esc(entry.version)}</div>
|
|
2336
|
+
${items.length ? items.map(item => `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(item)}</div>`).join('') : `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(entry.notes || '')}</div>`}
|
|
2337
|
+
</div>`
|
|
2338
|
+
}).join('')
|
|
2339
|
+
box.scrollLeft = 0
|
|
2340
|
+
updateNotesPage()
|
|
2341
|
+
}
|
|
2327
2342
|
function updateNotesPage() {
|
|
2328
2343
|
const box = $('notes-pages')
|
|
2329
2344
|
const pageEl = $('notes-page')
|
|
@@ -2338,7 +2353,21 @@ function scrollNotes(dir) {
|
|
|
2338
2353
|
if (box) box.scrollBy({ left: dir * box.clientWidth, behavior: 'smooth' })
|
|
2339
2354
|
}
|
|
2340
2355
|
function openNotesModal(info) {
|
|
2341
|
-
if (!info?.version
|
|
2356
|
+
if (!info?.version) return
|
|
2357
|
+
const history = Array.isArray(info.history) ? info.history.filter(h => h && typeof h.version === 'string' && typeof h.notes === 'string' && !String(h.version).includes('-rc')) : []
|
|
2358
|
+
const latestStable = history[0]?.version || info.version
|
|
2359
|
+
if (history.length) {
|
|
2360
|
+
const entries = history.filter(h => cmpVersion(h.version, state.localVersion) > 0)
|
|
2361
|
+
if (!entries.length) return
|
|
2362
|
+
if (LS.get(NOTES_KEY) === latestStable) return
|
|
2363
|
+
notesVersion = latestStable
|
|
2364
|
+
const vEl = $('notes-version')
|
|
2365
|
+
if (vEl) vEl.textContent = 'v' + latestStable
|
|
2366
|
+
renderNotesVersionPages(entries.reverse())
|
|
2367
|
+
$('modal-notes').classList.remove('hidden')
|
|
2368
|
+
return
|
|
2369
|
+
}
|
|
2370
|
+
if (String(info.version).includes('-rc')) return
|
|
2342
2371
|
if (LS.get(NOTES_KEY) === info.version) return
|
|
2343
2372
|
const items = splitNotes(info.notes)
|
|
2344
2373
|
if (!items.length) return
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* DSH Remote 桌面端专用样式 · 零依赖 · 只引用 --dsr-* 皮肤变量 */
|
|
2
2
|
html, body { height: 100%; margin: 0; overflow: hidden; overscroll-behavior: none; background: var(--dsr-bg); color: var(--dsr-text); }
|
|
3
|
+
.hidden { display: none !important; }
|
|
3
4
|
.ds-app { position: fixed; inset: 0; display: flex; overflow: hidden; }
|
|
4
5
|
.ds-sidebar { width: 280px; flex: none; display: flex; flex-direction: column; border-right: 1px solid var(--dsr-line); background: var(--dsr-panel); }
|
|
5
6
|
.ds-brand { display: flex; align-items: center; gap: 8px; padding: 14px 14px 10px; font-weight: 700; font-size: 15px; }
|
|
@@ -52,12 +53,10 @@ a.ds-btn { text-decoration: none; }
|
|
|
52
53
|
font: inherit; text-align: left; cursor: pointer; text-decoration: none;
|
|
53
54
|
}
|
|
54
55
|
.ds-feedback-item:hover, .ds-feedback-item:focus-visible { background: var(--dsr-bg); outline: none; }
|
|
55
|
-
.ds-feedback-item.primary { background: var(--dsr-accent-soft); border: 1px solid var(--dsr-accent-line); }
|
|
56
56
|
.ds-feedback-ico {
|
|
57
57
|
width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
|
|
58
58
|
display: grid; place-items: center; background: var(--dsr-panel); border: 1px solid var(--dsr-line); color: var(--dsr-accent-strong);
|
|
59
59
|
}
|
|
60
|
-
.ds-feedback-item.primary .ds-feedback-ico { background: var(--dsr-accent-strong); border-color: var(--dsr-accent-strong); color: var(--dsr-on-accent); }
|
|
61
60
|
.ds-feedback-ico svg { width: 16px; height: 16px; fill: currentColor; }
|
|
62
61
|
.ds-feedback-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
63
62
|
.ds-feedback-name { font-size: 13px; font-weight: 600; }
|
|
@@ -100,16 +99,47 @@ a.ds-btn { text-decoration: none; }
|
|
|
100
99
|
/* 会话视图 */
|
|
101
100
|
#view-chat { padding: 0; }
|
|
102
101
|
.ds-history { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 18px 22px; display: flex; flex-direction: column; gap: 10px; }
|
|
103
|
-
.ds-msg { max-width: 78
|
|
102
|
+
.ds-msg { max-width: min(78%, 680px); width: fit-content; box-sizing: border-box; padding: 9px 12px; border-radius: 12px; font-size: 13.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
104
103
|
.ds-msg.user { align-self: flex-end; background: var(--dsr-accent-soft); border: 1px solid var(--dsr-accent-line); color: var(--dsr-accent-strong); }
|
|
105
104
|
.ds-msg.assistant { align-self: flex-start; background: var(--dsr-panel); border: 1px solid var(--dsr-line); color: var(--dsr-text); }
|
|
106
105
|
.ds-msg .role { font-size: 10px; color: var(--dsr-muted); margin-bottom: 3px; }
|
|
107
|
-
.ds-tool { align-self: flex-start; max-width: 78
|
|
106
|
+
.ds-tool { align-self: flex-start; max-width: min(78%, 680px); width: fit-content; box-sizing: border-box; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 10px; padding: 8px 11px; font-size: 12px; }
|
|
108
107
|
.ds-tool summary { cursor: pointer; color: var(--dsr-muted); }
|
|
109
108
|
.ds-tool pre { margin: 6px 0 0; white-space: pre-wrap; word-break: break-all; font-size: 11px; color: var(--dsr-text); }
|
|
110
109
|
.ds-empty { color: var(--dsr-muted); text-align: center; padding: 30px 0; font-size: 13px; }
|
|
111
|
-
.ds-
|
|
112
|
-
.ds-
|
|
110
|
+
.ds-presets-guide { padding: 2px 8px 10px; font-size: 11px; line-height: 1.5; }
|
|
111
|
+
.ds-session-cards { flex: none; max-height: 220px; overflow-y: auto; padding: 10px 22px 0; display: flex; flex-direction: column; gap: 8px; }
|
|
112
|
+
.ds-session-cards:empty { display: none; }
|
|
113
|
+
.ds-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 12px; }
|
|
114
|
+
.ds-card-title { font-size: 11px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; margin-bottom: 6px; text-transform: uppercase; }
|
|
115
|
+
.ds-goal-obj { font-size: 13px; line-height: 1.55; word-break: break-word; }
|
|
116
|
+
.ds-goal-phase { font-size: 11px; color: var(--dsr-muted); margin-top: 3px; }
|
|
117
|
+
.ds-goal-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
118
|
+
.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; }
|
|
119
|
+
.ds-mini-btn:hover { filter: brightness(1.08); }
|
|
120
|
+
.ds-todo-row { display: flex; align-items: flex-start; gap: 8px; font-size: 12.5px; line-height: 1.5; padding: 2px 0; }
|
|
121
|
+
.ds-pill { flex: none; font-size: 10.5px; line-height: 16px; padding: 0 7px; border-radius: 999px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); color: var(--dsr-muted); }
|
|
122
|
+
.ds-pill.active { background: var(--dsr-accent-soft); border-color: var(--dsr-accent-line); color: var(--dsr-accent-strong); }
|
|
123
|
+
.ds-pill.done { background: var(--dsr-success-soft); border-color: var(--dsr-success-line); color: var(--dsr-success); }
|
|
124
|
+
.ds-card-row { display: flex; align-items: center; gap: 10px; font-size: 12.5px; padding: 3px 0; }
|
|
125
|
+
.ds-card-k { flex: 1; min-width: 0; word-break: break-word; }
|
|
126
|
+
.ds-card-v { color: var(--dsr-muted); font-size: 11.5px; white-space: nowrap; }
|
|
127
|
+
.ds-model-head { font-size: 11px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; padding: 4px 6px 2px; }
|
|
128
|
+
.ds-model-group { padding: 2px 0 4px; }
|
|
129
|
+
.ds-model-provider { font-size: 11px; color: var(--dsr-muted); padding: 2px 6px; }
|
|
130
|
+
.ds-model-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 6px 4px; }
|
|
131
|
+
.ds-model-chip { display: inline-flex; align-items: center; min-height: 26px; padding: 2px 9px; border-radius: 999px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); color: var(--dsr-text); font: inherit; font-size: 11.5px; cursor: pointer; white-space: nowrap; }
|
|
132
|
+
.ds-model-chip:hover { filter: brightness(1.08); }
|
|
133
|
+
.ds-model-chip.current { background: var(--dsr-accent-soft); border-color: var(--dsr-accent-line); color: var(--dsr-accent-strong); font-weight: 600; }
|
|
134
|
+
.ds-model-effort-group { padding-top: 2px; border-top: 1px solid var(--dsr-divider); margin-top: 2px; }
|
|
135
|
+
.ds-model-effort-group.hidden { display: none; }
|
|
136
|
+
.ds-composer { flex: none; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--dsr-line); background: var(--dsr-panel); }
|
|
137
|
+
.ds-composer textarea { width: 100%; min-width: 0; resize: none; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 14px; font: inherit; font-size: 13.5px; line-height: 1.5; outline: none; min-height: 44px; max-height: 120px; box-sizing: border-box; }
|
|
138
|
+
.ds-composer-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
139
|
+
.ds-composer-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
140
|
+
.ds-composer-right { display: flex; align-items: center; flex: none; }
|
|
141
|
+
.ds-composer .ds-btn { min-height: 34px; box-sizing: border-box; }
|
|
142
|
+
.ds-send-btn { width: 36px; height: 36px; min-height: 36px; padding: 0; border-radius: 50%; background: var(--dsr-accent-strong); border-color: var(--dsr-accent-strong); color: var(--dsr-on-accent); font-size: 18px; line-height: 1; justify-content: center; }
|
|
113
143
|
|
|
114
144
|
/* 文件传输 */
|
|
115
145
|
.ds-fs-bar { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
|
|
@@ -122,12 +152,26 @@ a.ds-btn { text-decoration: none; }
|
|
|
122
152
|
.ds-fs-size { color: var(--dsr-muted); font-size: 12px; white-space: nowrap; }
|
|
123
153
|
|
|
124
154
|
/* 设置 */
|
|
155
|
+
#view-settings { overflow-y: auto; }
|
|
125
156
|
.ds-settings { max-width: 720px; display: flex; flex-direction: column; gap: 10px; }
|
|
126
|
-
.ds-setting-row { display: flex; align-items: center; gap:
|
|
157
|
+
.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; }
|
|
127
158
|
.ds-setting-row > div:first-child { flex: 1; min-width: 0; }
|
|
159
|
+
.ds-setting-arrow,
|
|
160
|
+
#settings-home .ds-setting-row .ds-btn {
|
|
161
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
162
|
+
min-height: 32px; padding: 0 10px; border-radius: 8px;
|
|
163
|
+
background: transparent; border: 1px solid transparent; color: var(--dsr-muted);
|
|
164
|
+
font-size: 12.5px; cursor: pointer; white-space: nowrap; text-decoration: none;
|
|
165
|
+
}
|
|
166
|
+
.ds-setting-arrow { font-size: 16px; padding: 0 12px; }
|
|
167
|
+
.ds-setting-arrow:hover,
|
|
168
|
+
#settings-home .ds-setting-row .ds-btn:hover,
|
|
169
|
+
#settings-home .ds-setting-row .ds-btn:focus-visible {
|
|
170
|
+
background: var(--dsr-bg-2); border-color: var(--dsr-line); color: var(--dsr-text);
|
|
171
|
+
}
|
|
128
172
|
.ds-setting-name { font-size: 13.5px; font-weight: 600; }
|
|
129
173
|
.ds-setting-desc { font-size: 12px; color: var(--dsr-muted); margin-top: 2px; word-break: break-all; }
|
|
130
|
-
|
|
174
|
+
#token-desc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
131
175
|
.ds-group-bar { display: flex; align-items: center; gap: 10px; padding: 2px 0; }
|
|
132
176
|
.ds-group-bar > label { font-size: 12px; color: var(--dsr-muted); white-space: nowrap; }
|
|
133
177
|
.ds-group-select { flex: 1; min-width: 0; position: relative; }
|
|
@@ -222,6 +266,16 @@ a.ds-btn { text-decoration: none; }
|
|
|
222
266
|
.ds-q-option .muted { font-size: 11px; color: var(--dsr-muted); }
|
|
223
267
|
.ds-modal-body textarea { width: 100%; box-sizing: border-box; margin-top: 6px; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 7px 9px; font: inherit; font-size: 13px; outline: none; }
|
|
224
268
|
|
|
269
|
+
/* 预设提示词管理 */
|
|
270
|
+
.ds-preset-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
|
|
271
|
+
.ds-preset-count { font-size: 12px; color: var(--dsr-muted); }
|
|
272
|
+
.ds-preset-list { display: flex; flex-direction: column; gap: 8px; }
|
|
273
|
+
.ds-preset-row { display: flex; align-items: center; gap: 8px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 10px; padding: 8px 10px; }
|
|
274
|
+
.ds-preset-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
275
|
+
.ds-preset-name { font-size: 13px; font-weight: 600; word-break: break-word; }
|
|
276
|
+
.ds-preset-preview { font-size: 11.5px; color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
277
|
+
.ds-preset-empty { font-size: 12.5px; color: var(--dsr-muted); text-align: center; padding: 18px 0; }
|
|
278
|
+
|
|
225
279
|
.ds-toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 150; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 10px; padding: 9px 15px; font-size: 13px; box-shadow: 0 8px 24px rgba(0,0,0,.3); }
|
|
226
280
|
.ds-toast.hidden { display: none; }
|
|
227
281
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<span class="ds-feedback-ico"><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="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></span>
|
|
36
36
|
<span class="ds-feedback-body"><span class="ds-feedback-name" data-i18n="ds.feedbackWrite">写反馈</span><span class="ds-feedback-desc" data-i18n="ds.feedbackWriteDesc">App 内直接提交</span></span>
|
|
37
37
|
</button>
|
|
38
|
-
<a class="ds-feedback-item
|
|
38
|
+
<a class="ds-feedback-item" href="https://github.com/Blank-not-black/dsh-Remote/issues" target="_blank" rel="noopener" role="menuitem">
|
|
39
39
|
<span class="ds-feedback-ico"><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></span>
|
|
40
40
|
<span class="ds-feedback-body"><span class="ds-feedback-name">GitHub Issues</span><span class="ds-feedback-desc" data-i18n="ds.feedbackGithubDesc">反馈 bug / 提建议</span></span>
|
|
41
41
|
</a>
|
|
@@ -77,25 +77,36 @@
|
|
|
77
77
|
|
|
78
78
|
<!-- 会话详情 -->
|
|
79
79
|
<section id="view-chat" class="ds-view">
|
|
80
|
+
<div id="session-cards" class="ds-session-cards"></div>
|
|
80
81
|
<div id="history" class="ds-history" aria-live="polite"></div>
|
|
81
82
|
<div class="ds-composer">
|
|
82
83
|
<textarea id="composer" rows="1" data-i18n-placeholder="ds.composerPlaceholder" placeholder="输入消息…"></textarea>
|
|
83
|
-
<div
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
<div class="ds-composer-actions">
|
|
85
|
+
<div class="ds-composer-left">
|
|
86
|
+
<div style="position:relative">
|
|
87
|
+
<button id="btn-cmd" class="ds-btn" data-i18n="ds.commands">指令</button>
|
|
88
|
+
<div id="cmd-menu" class="ds-group-menu hidden" style="width:280px; left:0; right:auto; top:auto; bottom:calc(100% + 8px)">
|
|
89
|
+
<button class="ds-feedback-item" data-ds-cmd="/compact" data-i18n="ds.cmdCompact">/compact 压缩对话历史</button>
|
|
90
|
+
<button class="ds-feedback-item" data-ds-cmd="/export" data-i18n="ds.cmdExport">/export 导出会话日志 ZIP</button>
|
|
91
|
+
<button class="ds-feedback-item" data-ds-cmd="/feedback" data-i18n="ds.cmdFeedback">/feedback 反馈当前会话</button>
|
|
92
|
+
<button class="ds-feedback-item" data-ds-cmd="/goal" data-i18n="ds.cmdGoal">/goal 设置/查看任务目标</button>
|
|
93
|
+
<button class="ds-feedback-item" data-ds-cmd="/permission" data-i18n="ds.cmdPermission">/permission 切换权限预设</button>
|
|
94
|
+
<button class="ds-feedback-item" data-ds-cmd="/plan" data-i18n="ds.cmdPlan">/plan 进入/退出计划模式</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div style="position:relative">
|
|
98
|
+
<button id="btn-preset" class="ds-btn" data-i18n="ds.presets">预设</button>
|
|
99
|
+
<div id="preset-menu" class="ds-group-menu hidden" style="width:260px; left:0; right:auto; top:auto; bottom:calc(100% + 8px)"></div>
|
|
100
|
+
</div>
|
|
101
|
+
<div style="position:relative">
|
|
102
|
+
<button id="btn-model" class="ds-btn" data-i18n="ds.models">模型</button>
|
|
103
|
+
<div id="model-menu" class="ds-group-menu hidden" style="width:360px; left:0; right:auto; top:auto; bottom:calc(100% + 8px)"></div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="ds-composer-right">
|
|
107
|
+
<button id="btn-send" class="ds-btn primary ds-send-btn" data-i18n-aria="ds.send" data-i18n-title="ds.send" aria-label="发送">↑</button>
|
|
92
108
|
</div>
|
|
93
109
|
</div>
|
|
94
|
-
<div style="position:relative">
|
|
95
|
-
<button id="btn-preset" class="ds-btn" data-i18n="ds.presets">预设</button>
|
|
96
|
-
<div id="preset-menu" class="ds-group-menu hidden" style="width:260px; left:auto; right:0; top:auto; bottom:calc(100% + 8px)"></div>
|
|
97
|
-
</div>
|
|
98
|
-
<button id="btn-send" class="ds-btn primary" data-i18n="ds.send">发送</button>
|
|
99
110
|
</div>
|
|
100
111
|
</section>
|
|
101
112
|
|
|
@@ -117,23 +128,19 @@
|
|
|
117
128
|
<div class="ds-settings">
|
|
118
129
|
<div class="ds-setting-row" data-settings-group="general" style="cursor:pointer">
|
|
119
130
|
<div><div class="ds-setting-name" data-i18n="ds.groupGeneral">通用</div><div class="ds-setting-desc" data-i18n="ds.groupGeneralDesc">工具调用、预设提示词</div></div>
|
|
120
|
-
<span class="ds-
|
|
131
|
+
<span class="ds-setting-arrow" aria-hidden="true">›</span>
|
|
121
132
|
</div>
|
|
122
133
|
<div class="ds-setting-row" data-settings-group="servers" style="cursor:pointer">
|
|
123
134
|
<div><div class="ds-setting-name" data-i18n="ds.groupServers">服务器</div><div class="ds-setting-desc" data-i18n="ds.groupServersDesc">服务器地址</div></div>
|
|
124
|
-
<span class="ds-
|
|
125
|
-
</div>
|
|
126
|
-
<div class="ds-setting-row" data-settings-group="notify" style="cursor:pointer">
|
|
127
|
-
<div><div class="ds-setting-name" data-i18n="ds.groupNotify">通知</div><div class="ds-setting-desc" data-i18n="ds.groupNotifyDesc">通知与提醒</div></div>
|
|
128
|
-
<span class="ds-feedback-item" style="font-size:18px">›</span>
|
|
135
|
+
<span class="ds-setting-arrow" aria-hidden="true">›</span>
|
|
129
136
|
</div>
|
|
130
137
|
<div class="ds-setting-row" data-settings-group="theme" style="cursor:pointer">
|
|
131
138
|
<div><div class="ds-setting-name" data-i18n="ds.groupTheme">皮肤</div><div class="ds-setting-desc" data-i18n="ds.groupThemeDesc">配色主题</div></div>
|
|
132
|
-
<span class="ds-
|
|
139
|
+
<span class="ds-setting-arrow" aria-hidden="true">›</span>
|
|
133
140
|
</div>
|
|
134
141
|
<div class="ds-setting-row" data-settings-group="about" style="cursor:pointer">
|
|
135
142
|
<div><div class="ds-setting-name" data-i18n="ds.groupAbout">关于</div><div class="ds-setting-desc" data-i18n="ds.groupAboutDesc">版本信息</div></div>
|
|
136
|
-
<span class="ds-
|
|
143
|
+
<span class="ds-setting-arrow" aria-hidden="true">›</span>
|
|
137
144
|
</div>
|
|
138
145
|
</div>
|
|
139
146
|
<div class="ds-settings">
|
|
@@ -145,14 +152,6 @@
|
|
|
145
152
|
<div><div class="ds-setting-name" data-i18n="ds.langTitle">语言 / Language</div></div>
|
|
146
153
|
<button id="btn-lang" class="ds-btn">EN</button>
|
|
147
154
|
</div>
|
|
148
|
-
<div class="ds-setting-row">
|
|
149
|
-
<div><div class="ds-setting-name" data-i18n="ds.feedbackTitle">反馈渠道</div><div class="ds-setting-desc" data-i18n="ds.feedbackDesc">GitHub / Gitee / B站:反馈 bug、提建议、唠嗑</div></div>
|
|
150
|
-
</div>
|
|
151
|
-
<div class="ds-feedback-links">
|
|
152
|
-
<a class="ds-btn" href="https://github.com/Blank-not-black/dsh-Remote" target="_blank" rel="noopener">GitHub</a>
|
|
153
|
-
<a class="ds-btn" href="https://gitee.com/Blankneverfails/dsh-Remote" target="_blank" rel="noopener">Gitee</a>
|
|
154
|
-
<a class="ds-btn" href="https://space.bilibili.com/419009275/dynamic" target="_blank" rel="noopener">B站</a>
|
|
155
|
-
</div>
|
|
156
155
|
</div>
|
|
157
156
|
</div>
|
|
158
157
|
|
|
@@ -162,7 +161,8 @@
|
|
|
162
161
|
<div class="ds-setting-name" data-i18n="ds.groupGeneral">通用</div>
|
|
163
162
|
</div>
|
|
164
163
|
<div class="ds-setting-row">
|
|
165
|
-
<div><div class="ds-setting-name" data-i18n="ds.
|
|
164
|
+
<div><div class="ds-setting-name" data-i18n="presets.title">预设提示词</div><div class="ds-setting-desc"><span data-i18n="presets.desc">常用提示词,对话界面一键插入(最多 20 条)</span> <span id="preset-count-home"></span></div></div>
|
|
165
|
+
<button id="btn-manage-presets" class="ds-btn" data-i18n="presets.manage">管理</button>
|
|
166
166
|
</div>
|
|
167
167
|
</div>
|
|
168
168
|
|
|
@@ -190,16 +190,6 @@
|
|
|
190
190
|
</div>
|
|
191
191
|
</div>
|
|
192
192
|
|
|
193
|
-
<div id="settings-page-notify" class="ds-settings hidden">
|
|
194
|
-
<div class="ds-setting-row" data-settings-back style="cursor:pointer">
|
|
195
|
-
<button class="ds-btn" data-i18n="ds.back">‹ 返回</button>
|
|
196
|
-
<div class="ds-setting-name" data-i18n="ds.groupNotify">通知</div>
|
|
197
|
-
</div>
|
|
198
|
-
<div class="ds-setting-row">
|
|
199
|
-
<div><div class="ds-setting-name" data-i18n="ds.emptyGroup">暂无设置项</div></div>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
193
|
<div id="settings-page-theme" class="ds-settings hidden">
|
|
204
194
|
<div class="ds-setting-row" data-settings-back style="cursor:pointer">
|
|
205
195
|
<button class="ds-btn" data-i18n="ds.back">‹ 返回</button>
|
|
@@ -218,7 +208,7 @@
|
|
|
218
208
|
</div>
|
|
219
209
|
<div class="ds-setting-row" id="btn-donate-about" style="cursor:pointer">
|
|
220
210
|
<div><div class="ds-setting-name" data-i18n="ds.donate">赞赏支持</div><div class="ds-setting-desc" data-i18n="ds.donateDesc">如果 dsh-remote 帮到了你,欢迎赞赏支持开发 ☕</div></div>
|
|
221
|
-
<span class="ds-
|
|
211
|
+
<span class="ds-setting-arrow" aria-hidden="true">›</span>
|
|
222
212
|
</div>
|
|
223
213
|
</div>
|
|
224
214
|
</section>
|
|
@@ -276,6 +266,23 @@
|
|
|
276
266
|
</div>
|
|
277
267
|
</div>
|
|
278
268
|
|
|
269
|
+
<!-- 预设提示词管理模态 -->
|
|
270
|
+
<div id="modal-presets" class="ds-modal hidden">
|
|
271
|
+
<div class="ds-modal-card">
|
|
272
|
+
<div class="ds-modal-title" data-i18n="presets.title">预设提示词</div>
|
|
273
|
+
<div class="ds-modal-body">
|
|
274
|
+
<div class="ds-preset-toolbar">
|
|
275
|
+
<span id="preset-count" class="ds-preset-count"></span>
|
|
276
|
+
<button id="btn-preset-add" class="ds-btn" data-i18n="presets.add">新增</button>
|
|
277
|
+
</div>
|
|
278
|
+
<div id="preset-list" class="ds-preset-list"></div>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="ds-modal-actions">
|
|
281
|
+
<button id="btn-presets-close" class="ds-btn" data-i18n="ds.close">关闭</button>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
279
286
|
<!-- 赞赏支持模态 -->
|
|
280
287
|
<div id="modal-donate" class="ds-modal hidden">
|
|
281
288
|
<div class="ds-modal-card">
|
|
@@ -328,7 +335,7 @@
|
|
|
328
335
|
'ds.groupTheme': '皮肤', 'ds.groupThemeDesc': '配色主题',
|
|
329
336
|
'ds.groupAbout': '关于', 'ds.groupAboutDesc': '版本信息',
|
|
330
337
|
'ds.donate': '赞赏支持', 'ds.donateDesc': '如果 dsh-remote 帮到了你,欢迎赞赏支持开发 ☕', 'ds.donateThanks': '感谢你的支持 ☕', 'ds.donateClose': '关闭',
|
|
331
|
-
'ds.notesTitle': '更新内容', 'ds.notesClose': '关闭', 'ds.notesPage': '{current}/{total}',
|
|
338
|
+
'ds.notesTitle': '更新内容', 'ds.notesClose': '关闭', 'ds.notesPage': '版本号 {current}/{total}',
|
|
332
339
|
'ds.back': '‹ 返回', 'ds.emptyGroup': '暂无设置项',
|
|
333
340
|
'ds.serversTitle': '服务器(分组)', 'ds.speedTest': '测速', 'ds.currentGroup': '当前组',
|
|
334
341
|
'ds.addGroup': '+ 组', 'ds.addServer': '添加', 'ds.tokenTitle': '访问令牌', 'ds.copyToken': '复制',
|
|
@@ -388,6 +395,24 @@
|
|
|
388
395
|
'ds.questionOptions': '选项', 'ds.questionCustom': '自定义回答',
|
|
389
396
|
'ds.questionNeedAnswer': '请选择一个选项或填写自定义回答', 'ds.questionSubmitted': '已提交回答',
|
|
390
397
|
'ds.questionFrom': '来自 {server}',
|
|
398
|
+
'ds.models': '模型', 'ds.close': '关闭',
|
|
399
|
+
'ds.presetsEmpty': '暂时未设置快捷提示词', 'ds.presetsGuide': '去设置 → 通用 → 预设提示词 管理添加',
|
|
400
|
+
'presets.title': '预设提示词', 'presets.desc': '常用提示词,对话界面一键插入(最多 20 条)', 'presets.manage': '管理',
|
|
401
|
+
'presets.add': '新增', 'presets.edit': '编辑', 'presets.delete': '删除', 'presets.empty': '暂无预设',
|
|
402
|
+
'presets.menuTitle': '预设', 'presets.namePrompt': '预设名称(≤20 字):', 'presets.textPrompt': '预设内容(≤2000 字):',
|
|
403
|
+
'presets.nameEmpty': '名称不能为空', 'presets.nameTooLong': '名称不能超过 20 字', 'presets.textTooLong': '内容不能超过 2000 字', 'presets.limit': '最多 20 条预设', 'presets.added': '预设已添加',
|
|
404
|
+
'presets.confirmDelete': '删除预设「{name}」?', 'presets.saved': '预设已保存', 'presets.deleted': '预设已删除',
|
|
405
|
+
'goal.title': '目标', 'goal.pause': '暂停', 'goal.resume': '继续', 'goal.complete': '完成', 'goal.edit': '改目标', 'goal.clear': '清除',
|
|
406
|
+
'goal.none': '当前会话没有目标', 'goal.confirmClear': '清除当前目标?(不会删除会话)', 'goal.confirmComplete': '将目标标记为完成?',
|
|
407
|
+
'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
|
|
408
|
+
'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新', 'goal.editPrompt': '修改目标:',
|
|
409
|
+
'todos.title': '任务清单',
|
|
410
|
+
'subagent.title': '子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
|
|
411
|
+
'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
|
|
412
|
+
'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
|
|
413
|
+
'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
|
|
414
|
+
'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}',
|
|
415
|
+
'menu.modelTitle': '模型切换', 'menu.effortTitle': '思考深度',
|
|
391
416
|
},
|
|
392
417
|
en: {
|
|
393
418
|
'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.sessions': 'Sessions',
|
|
@@ -404,7 +429,7 @@
|
|
|
404
429
|
'ds.groupTheme': 'Theme', 'ds.groupThemeDesc': 'Color themes',
|
|
405
430
|
'ds.groupAbout': 'About', 'ds.groupAboutDesc': 'Version info',
|
|
406
431
|
'ds.donate': 'Support', 'ds.donateDesc': 'If dsh-remote has been helpful to you, feel free to support development with a coffee ☕', 'ds.donateThanks': 'Thanks for your support ☕', 'ds.donateClose': 'Close',
|
|
407
|
-
'ds.notesTitle': 'What\'s New', 'ds.notesClose': 'Close', 'ds.notesPage': '{current}/{total}',
|
|
432
|
+
'ds.notesTitle': 'What\'s New', 'ds.notesClose': 'Close', 'ds.notesPage': 'Version {current}/{total}',
|
|
408
433
|
'ds.back': '‹ Back', 'ds.emptyGroup': 'No settings here',
|
|
409
434
|
'ds.serversTitle': 'Servers (groups)', 'ds.speedTest': 'Test', 'ds.currentGroup': 'Current group',
|
|
410
435
|
'ds.addGroup': '+ Group', 'ds.addServer': 'Add', 'ds.tokenTitle': 'Access token', 'ds.copyToken': 'Copy',
|
|
@@ -464,6 +489,24 @@
|
|
|
464
489
|
'ds.questionOptions': 'Options', 'ds.questionCustom': 'Custom answer',
|
|
465
490
|
'ds.questionNeedAnswer': 'Choose an option or write a custom answer', 'ds.questionSubmitted': 'Answer submitted',
|
|
466
491
|
'ds.questionFrom': 'From {server}',
|
|
492
|
+
'ds.models': 'Models', 'ds.close': 'Close',
|
|
493
|
+
'ds.presetsEmpty': 'No quick prompts yet', 'ds.presetsGuide': 'Go to Settings → General → Prompt presets to add one',
|
|
494
|
+
'presets.title': 'Prompt presets', 'presets.desc': 'Common prompts, insert into the composer in one tap (max 20)', 'presets.manage': 'Manage',
|
|
495
|
+
'presets.add': 'Add', 'presets.edit': 'Edit', 'presets.delete': 'Delete', 'presets.empty': 'No presets yet',
|
|
496
|
+
'presets.menuTitle': 'Presets', 'presets.namePrompt': 'Preset name (≤20 chars):', 'presets.textPrompt': 'Preset content (≤2000 chars):',
|
|
497
|
+
'presets.nameEmpty': 'Name cannot be empty', 'presets.nameTooLong': 'Name must be ≤20 characters', 'presets.textTooLong': 'Content must be ≤2000 characters', 'presets.limit': 'At most 20 presets', 'presets.added': 'Preset added',
|
|
498
|
+
'presets.confirmDelete': 'Delete preset "{name}"?', 'presets.saved': 'Preset saved', 'presets.deleted': 'Preset deleted',
|
|
499
|
+
'goal.title': 'Goal', 'goal.pause': 'Pause', 'goal.resume': 'Resume', 'goal.complete': 'Complete', 'goal.edit': 'Edit goal', 'goal.clear': 'Clear',
|
|
500
|
+
'goal.none': 'This session has no goal', 'goal.confirmClear': 'Clear the current goal? (the session stays)', 'goal.confirmComplete': 'Mark the goal as completed?',
|
|
501
|
+
'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
|
|
502
|
+
'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated', 'goal.editPrompt': 'Edit goal:',
|
|
503
|
+
'todos.title': 'Todos',
|
|
504
|
+
'subagent.title': 'Subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
|
|
505
|
+
'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
|
|
506
|
+
'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
|
|
507
|
+
'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
|
|
508
|
+
'models.effortFailed': 'Failed to switch reasoning effort', 'models.effortSwitched': 'Reasoning effort: {effort}',
|
|
509
|
+
'menu.modelTitle': 'Model', 'menu.effortTitle': 'Reasoning effort',
|
|
467
510
|
}
|
|
468
511
|
}
|
|
469
512
|
</script>
|
|
@@ -60,6 +60,7 @@ const state = {
|
|
|
60
60
|
streamMode: 'ws', // 'ws' | 'poll'
|
|
61
61
|
pollSeq: { mux: 0, host: 0 },
|
|
62
62
|
fs: { path: null, initial: null, loaded: false },
|
|
63
|
+
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
63
64
|
view: 'sessions'
|
|
64
65
|
}
|
|
65
66
|
const streams = {}
|
|
@@ -109,14 +110,90 @@ function renderPresetMenuDesktop() {
|
|
|
109
110
|
const menu = $('preset-menu')
|
|
110
111
|
if (!btn || !menu) return
|
|
111
112
|
const list = readPresets()
|
|
112
|
-
btn.style.display =
|
|
113
|
+
btn.style.display = ''
|
|
113
114
|
menu.classList.add('hidden')
|
|
115
|
+
if (!list.length) {
|
|
116
|
+
menu.innerHTML = `<div class="ds-empty">${esc(t('ds.presetsEmpty'))}</div><div class="ds-empty ds-presets-guide">${esc(t('ds.presetsGuide'))}</div>`
|
|
117
|
+
return
|
|
118
|
+
}
|
|
114
119
|
menu.innerHTML = list.map(p => `<button class="ds-feedback-item" data-ds-preset="${esc(p.id)}">${esc(p.name)}</button>`).join('')
|
|
115
120
|
}
|
|
121
|
+
const PRESET_NAME_MAX = 20
|
|
122
|
+
const PRESET_TEXT_MAX = 2000
|
|
123
|
+
const PRESET_LIMIT = 20
|
|
124
|
+
function renderPresetSummary() {
|
|
125
|
+
const home = $('preset-count-home')
|
|
126
|
+
const modal = $('preset-count')
|
|
127
|
+
const n = readPresets().length
|
|
128
|
+
if (home) home.textContent = `· ${n}/${PRESET_LIMIT}`
|
|
129
|
+
if (modal) modal.textContent = `${n}/${PRESET_LIMIT}`
|
|
130
|
+
}
|
|
131
|
+
function writePresets(list) {
|
|
132
|
+
LS.set(PRESETS_KEY, JSON.stringify(list))
|
|
133
|
+
renderPresetSummary()
|
|
134
|
+
renderPresets()
|
|
135
|
+
renderPresetMenuDesktop()
|
|
136
|
+
}
|
|
137
|
+
function renderPresets() {
|
|
138
|
+
const box = $('preset-list')
|
|
139
|
+
if (!box) return
|
|
140
|
+
const list = readPresets()
|
|
141
|
+
renderPresetSummary()
|
|
142
|
+
if (!list.length) {
|
|
143
|
+
box.innerHTML = `<div class="ds-preset-empty">${esc(t('presets.empty'))}</div>`
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
box.innerHTML = list.map(p => `<div class="ds-preset-row">
|
|
147
|
+
<div class="ds-preset-main"><div class="ds-preset-name">${esc(p.name)}</div><div class="ds-preset-preview">${esc((p.text || '').slice(0, 60))}</div></div>
|
|
148
|
+
<button class="ds-mini-btn" data-preset-edit="${esc(p.id)}">${t('presets.edit')}</button>
|
|
149
|
+
<button class="ds-mini-btn" data-preset-del="${esc(p.id)}">${t('presets.delete')}</button>
|
|
150
|
+
</div>`).join('')
|
|
151
|
+
box.querySelectorAll('[data-preset-edit]').forEach(b => b.addEventListener('click', () => editPreset(b.dataset.presetEdit)))
|
|
152
|
+
box.querySelectorAll('[data-preset-del]').forEach(b => b.addEventListener('click', () => deletePreset(b.dataset.presetDel)))
|
|
153
|
+
}
|
|
154
|
+
function promptPreset(id) {
|
|
155
|
+
const list = readPresets()
|
|
156
|
+
const existing = id ? list.find(p => p.id === id) : null
|
|
157
|
+
const name = prompt(t('presets.namePrompt'), existing?.name || '')
|
|
158
|
+
if (name == null) return
|
|
159
|
+
const text = prompt(t('presets.textPrompt'), existing?.text || '')
|
|
160
|
+
if (text == null) return
|
|
161
|
+
const n = (name || '').trim()
|
|
162
|
+
if (!n) return toast(t('presets.nameEmpty'), 'err')
|
|
163
|
+
if (n.length > PRESET_NAME_MAX) return toast(t('presets.nameTooLong'), 'err')
|
|
164
|
+
if (text.length > PRESET_TEXT_MAX) return toast(t('presets.textTooLong'), 'err')
|
|
165
|
+
if (existing) {
|
|
166
|
+
existing.name = n
|
|
167
|
+
existing.text = text
|
|
168
|
+
} else {
|
|
169
|
+
if (list.length >= PRESET_LIMIT) return toast(t('presets.limit'), 'err')
|
|
170
|
+
list.push({ id: 'p' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name: n, text })
|
|
171
|
+
}
|
|
172
|
+
writePresets(list)
|
|
173
|
+
toast(existing ? t('presets.saved') : t('presets.added'), 'ok')
|
|
174
|
+
}
|
|
175
|
+
function addPreset() { promptPreset(null) }
|
|
176
|
+
function editPreset(id) { promptPreset(id) }
|
|
177
|
+
function deletePreset(id) {
|
|
178
|
+
const list = readPresets()
|
|
179
|
+
const p = list.find(x => x.id === id)
|
|
180
|
+
if (!p) return
|
|
181
|
+
if (!confirm(t('presets.confirmDelete', { name: p.name }))) return
|
|
182
|
+
writePresets(list.filter(x => x.id !== id))
|
|
183
|
+
toast(t('presets.deleted'), 'ok')
|
|
184
|
+
}
|
|
185
|
+
function openPresetModal() {
|
|
186
|
+
renderPresets()
|
|
187
|
+
const modal = $('modal-presets')
|
|
188
|
+
if (modal) modal.classList.remove('hidden')
|
|
189
|
+
}
|
|
190
|
+
function closePresetModal() {
|
|
191
|
+
const modal = $('modal-presets')
|
|
192
|
+
if (modal) modal.classList.add('hidden')
|
|
193
|
+
}
|
|
116
194
|
function togglePresetMenuDesktop() {
|
|
117
195
|
const menu = $('preset-menu')
|
|
118
196
|
if (!menu) return
|
|
119
|
-
if (!readPresets().length) return
|
|
120
197
|
menu.classList.toggle('hidden')
|
|
121
198
|
}
|
|
122
199
|
function toggleCmdMenuDesktop() {
|
|
@@ -125,6 +202,101 @@ function toggleCmdMenuDesktop() {
|
|
|
125
202
|
menu.classList.toggle('hidden')
|
|
126
203
|
}
|
|
127
204
|
|
|
205
|
+
/* ---------------- 模型 / 思考深度 ---------------- */
|
|
206
|
+
function toggleModelMenuDesktop() {
|
|
207
|
+
const menu = $('model-menu')
|
|
208
|
+
if (!menu) return
|
|
209
|
+
const willOpen = menu.classList.contains('hidden')
|
|
210
|
+
menu.classList.toggle('hidden', !willOpen)
|
|
211
|
+
if (willOpen && !state.models.loaded && !state.models.loading) loadSessionModels()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function loadSessionModels() {
|
|
215
|
+
if (!state.current || state.models.loading) return
|
|
216
|
+
state.models.loading = true
|
|
217
|
+
renderModelMenu()
|
|
218
|
+
try {
|
|
219
|
+
const v = await rpc('session.models', { sessionId: state.current })
|
|
220
|
+
state.models.groups = v.groups || []
|
|
221
|
+
state.models.current = v.current || null
|
|
222
|
+
state.models.failures = v.failures || []
|
|
223
|
+
state.models.loaded = true
|
|
224
|
+
} catch (e) {
|
|
225
|
+
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); state.models.loading = false; renderModelMenu(); return }
|
|
226
|
+
toast(t('models.loadFailed', { msg: e.message }), 'err')
|
|
227
|
+
}
|
|
228
|
+
state.models.loading = false
|
|
229
|
+
renderModelMenu()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderModelMenu() {
|
|
233
|
+
const box = $('model-menu')
|
|
234
|
+
if (!box) return
|
|
235
|
+
if (state.models.loading) { box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${t('models.loading')}</span>`; return }
|
|
236
|
+
const groups = state.models.groups || []
|
|
237
|
+
if (!groups.length) {
|
|
238
|
+
box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${(state.models.failures || []).map(f => f.name + ' ' + t('models.unavailable')).join(';') || t('models.none')}</span>`
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const cur = state.models.current
|
|
242
|
+
box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div>` + groups.map(g => `
|
|
243
|
+
<div class="ds-model-group">
|
|
244
|
+
<div class="ds-model-provider">${esc(g.name || g.id)}</div>
|
|
245
|
+
<div class="ds-model-chips">${(g.models || []).map(m => {
|
|
246
|
+
const isCur = cur && cur.provider === g.id && cur.model === m.id
|
|
247
|
+
return `<button class="ds-model-chip ${isCur ? 'current' : ''}" data-model="${esc(m.id)}" data-provider="${esc(g.id)}">${esc(m.name || m.id)}</button>`
|
|
248
|
+
}).join('')}</div>
|
|
249
|
+
</div>`).join('') + `<div class="ds-model-effort-group" id="model-effort-group"><div class="ds-model-provider">${t('menu.effortTitle')}</div><div class="ds-model-chips" id="model-efforts"></div></div>`
|
|
250
|
+
box.querySelectorAll('[data-model]').forEach(btn =>
|
|
251
|
+
btn.addEventListener('click', () => selectSessionModel(btn.dataset.provider, btn.dataset.model)))
|
|
252
|
+
renderEffortMenu()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderEffortMenu() {
|
|
256
|
+
const group = $('model-effort-group')
|
|
257
|
+
const box = $('model-efforts')
|
|
258
|
+
if (!group || !box) return
|
|
259
|
+
const cur = state.models.current
|
|
260
|
+
const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
|
|
261
|
+
const model = (provider?.models || []).find(m => m.id === cur?.model)
|
|
262
|
+
const efforts = model?.reasoning?.efforts || []
|
|
263
|
+
group.classList.toggle('hidden', !efforts.length)
|
|
264
|
+
box.innerHTML = efforts.map(e => {
|
|
265
|
+
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === model.reasoning.defaultEffort)
|
|
266
|
+
return `<button class="ds-model-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
|
|
267
|
+
}).join('')
|
|
268
|
+
box.querySelectorAll('[data-effort]').forEach(btn =>
|
|
269
|
+
btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function selectSessionEffort(effortId) {
|
|
273
|
+
const cur = state.models.current
|
|
274
|
+
if (!state.current || !cur) return
|
|
275
|
+
const v = await safeRpc('session.selectModel', {
|
|
276
|
+
sessionId: state.current, provider: cur.provider, model: cur.model, reasoningEffort: effortId
|
|
277
|
+
}, t('models.effortFailed'))
|
|
278
|
+
if (v?.selected) {
|
|
279
|
+
state.models.current = v.selected
|
|
280
|
+
renderEffortMenu()
|
|
281
|
+
toast(t('models.effortSwitched', { effort: effortId }), 'ok')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function selectSessionModel(provider, modelId) {
|
|
286
|
+
if (!state.current) return
|
|
287
|
+
const group = (state.models.groups || []).find(g => g.id === provider)
|
|
288
|
+
const model = (group?.models || []).find(m => m.id === modelId)
|
|
289
|
+
const payload = { sessionId: state.current, provider, model: modelId }
|
|
290
|
+
const effort = model?.reasoning?.defaultEffort || model?.reasoning?.efforts?.[0]?.id
|
|
291
|
+
if (effort) payload.reasoningEffort = effort
|
|
292
|
+
const v = await safeRpc('session.selectModel', payload, t('models.switchFailed'))
|
|
293
|
+
if (v?.selected) {
|
|
294
|
+
state.models.current = v.selected
|
|
295
|
+
renderModelMenu()
|
|
296
|
+
toast(t('models.switched', { model: v.selected.model }), 'ok')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
128
300
|
/* ---------------- 反馈 ---------------- */
|
|
129
301
|
const FEEDBACK_LINKS = {
|
|
130
302
|
githubIssues: 'https://github.com/Blank-not-black/dsh-Remote/issues',
|
|
@@ -172,6 +344,31 @@ const NOTES_KEY = 'seenNotesVersion'
|
|
|
172
344
|
let notesVersion = ''
|
|
173
345
|
let notesPages = []
|
|
174
346
|
let notesPage = 0
|
|
347
|
+
function parseVersion(v) {
|
|
348
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(v || '').trim())
|
|
349
|
+
if (!m) return { core: [0, 0, 0], pre: null }
|
|
350
|
+
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] || null }
|
|
351
|
+
}
|
|
352
|
+
function cmpVersion(a, b) {
|
|
353
|
+
const pa = parseVersion(a), pb = parseVersion(b)
|
|
354
|
+
for (let i = 0; i < 3; i++) {
|
|
355
|
+
const d = pa.core[i] - pb.core[i]
|
|
356
|
+
if (d) return d
|
|
357
|
+
}
|
|
358
|
+
if (!pa.pre && !pb.pre) return 0
|
|
359
|
+
if (!pa.pre) return 1
|
|
360
|
+
if (!pb.pre) return -1
|
|
361
|
+
const sa = String(pa.pre).split('.'), sb = String(pb.pre).split('.')
|
|
362
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
363
|
+
const x = sa[i] ?? '', y = sb[i] ?? ''
|
|
364
|
+
if (x === y) continue
|
|
365
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y)
|
|
366
|
+
if (nx && ny) { const d = Number(x) - Number(y); if (d) return d }
|
|
367
|
+
else if (nx !== ny) return nx ? -1 : 1
|
|
368
|
+
else { const d = x.localeCompare(y); if (d) return d }
|
|
369
|
+
}
|
|
370
|
+
return 0
|
|
371
|
+
}
|
|
175
372
|
function splitNotes(notes) {
|
|
176
373
|
return String(notes || '').split(/[;;]/).map(s => s.trim()).filter(Boolean)
|
|
177
374
|
}
|
|
@@ -186,6 +383,21 @@ function renderNotesPages(items) {
|
|
|
186
383
|
box.scrollLeft = 0
|
|
187
384
|
updateNotesPage()
|
|
188
385
|
}
|
|
386
|
+
function renderNotesVersionPages(entries) {
|
|
387
|
+
const box = $('notes-pages')
|
|
388
|
+
if (!box) return
|
|
389
|
+
notesPages = entries
|
|
390
|
+
notesPage = 0
|
|
391
|
+
box.innerHTML = entries.map(entry => {
|
|
392
|
+
const items = splitNotes(entry.notes)
|
|
393
|
+
return `<div class="notes-page" style="flex:0 0 100%;scroll-snap-align:start;box-sizing:border-box;min-width:0;">
|
|
394
|
+
<div class="notes-version-title" style="font-weight:700;margin-bottom:6px;opacity:.9;">v${esc(entry.version)}</div>
|
|
395
|
+
${items.length ? items.map(item => `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(item)}</div>`).join('') : `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(entry.notes || '')}</div>`}
|
|
396
|
+
</div>`
|
|
397
|
+
}).join('')
|
|
398
|
+
box.scrollLeft = 0
|
|
399
|
+
updateNotesPage()
|
|
400
|
+
}
|
|
189
401
|
function updateNotesPage() {
|
|
190
402
|
const box = $('notes-pages')
|
|
191
403
|
const pageEl = $('notes-page')
|
|
@@ -200,7 +412,23 @@ function scrollNotes(dir) {
|
|
|
200
412
|
if (box) box.scrollBy({ left: dir * box.clientWidth, behavior: 'smooth' })
|
|
201
413
|
}
|
|
202
414
|
function openNotesModal(info) {
|
|
203
|
-
if (!info?.version
|
|
415
|
+
if (!info?.version) return
|
|
416
|
+
const history = Array.isArray(info.history) ? info.history.filter(h => h && typeof h.version === 'string' && typeof h.notes === 'string' && !String(h.version).includes('-rc')) : []
|
|
417
|
+
const latestStable = history[0]?.version || info.version
|
|
418
|
+
if (history.length) {
|
|
419
|
+
const seen = LS.get(NOTES_KEY, '')
|
|
420
|
+
let entries
|
|
421
|
+
if (seen) entries = history.filter(h => cmpVersion(h.version, seen) > 0)
|
|
422
|
+
else entries = history.slice(0, 3)
|
|
423
|
+
if (!entries.length) return
|
|
424
|
+
notesVersion = latestStable
|
|
425
|
+
const vEl = $('notes-version')
|
|
426
|
+
if (vEl) vEl.textContent = 'v' + latestStable
|
|
427
|
+
renderNotesVersionPages(entries.slice().reverse())
|
|
428
|
+
$('modal-notes').classList.remove('hidden')
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
if (String(info.version).includes('-rc')) return
|
|
204
432
|
if (LS.get(NOTES_KEY) === info.version) return
|
|
205
433
|
const items = splitNotes(info.notes)
|
|
206
434
|
if (!items.length) return
|
|
@@ -724,11 +952,23 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
724
952
|
s.projections.values[key] = value
|
|
725
953
|
s.projections.asOfSeq = Math.max(s.projections.asOfSeq || 0, seq || 0)
|
|
726
954
|
}
|
|
727
|
-
if (state.current === sessionId)
|
|
955
|
+
if (state.current === sessionId) {
|
|
956
|
+
renderSessions()
|
|
957
|
+
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
958
|
+
}
|
|
728
959
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
729
960
|
}
|
|
730
961
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
731
962
|
function titleOf(s) { return proj(s, 'title') || short(s.sessionId) }
|
|
963
|
+
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
964
|
+
function isGoalTerminal(goal) {
|
|
965
|
+
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
966
|
+
}
|
|
967
|
+
function goalOf(s) {
|
|
968
|
+
const p = proj(s, 'goal')
|
|
969
|
+
if (!p) return null
|
|
970
|
+
return p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
971
|
+
}
|
|
732
972
|
function onSessionEvent(sessionId, event) {
|
|
733
973
|
if (state.current === sessionId && event) {
|
|
734
974
|
const h = state.history
|
|
@@ -739,6 +979,7 @@ function onSessionEvent(sessionId, event) {
|
|
|
739
979
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
740
980
|
renderHistory()
|
|
741
981
|
}
|
|
982
|
+
if (String(event.type || '').startsWith('goal/') || String(event.type || '').startsWith('todo/')) renderSessionCards()
|
|
742
983
|
}
|
|
743
984
|
}
|
|
744
985
|
|
|
@@ -767,15 +1008,19 @@ function renderSessions() {
|
|
|
767
1008
|
async function openSession(id) {
|
|
768
1009
|
state.current = id
|
|
769
1010
|
state.history = { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity }
|
|
1011
|
+
state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
770
1012
|
showView('view-chat')
|
|
771
1013
|
$('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
|
|
772
1014
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
773
1015
|
renderSessions()
|
|
1016
|
+
renderSessionCards()
|
|
774
1017
|
await loadHistory()
|
|
775
1018
|
}
|
|
776
1019
|
function closeSession() {
|
|
777
1020
|
state.current = null
|
|
778
1021
|
state.history = { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity }
|
|
1022
|
+
const cards = $('session-cards')
|
|
1023
|
+
if (cards) cards.innerHTML = ''
|
|
779
1024
|
showView('view-sessions')
|
|
780
1025
|
}
|
|
781
1026
|
async function loadHistory() {
|
|
@@ -866,6 +1111,95 @@ function renderHistory() {
|
|
|
866
1111
|
box.innerHTML = items.map(eventHtml).join('') || `<div class="ds-empty">${t('ds.historyEmpty')}</div>`
|
|
867
1112
|
box.scrollTop = box.scrollHeight
|
|
868
1113
|
}
|
|
1114
|
+
|
|
1115
|
+
/* ---------------- 会话信息卡(goal / todo / 子代理) ---------------- */
|
|
1116
|
+
async function renderSessionCards() {
|
|
1117
|
+
const box = $('session-cards')
|
|
1118
|
+
const s = state.byId.get(state.current)
|
|
1119
|
+
if (!box) return
|
|
1120
|
+
if (!s) { box.innerHTML = ''; return }
|
|
1121
|
+
const goal = goalOf(s)
|
|
1122
|
+
const todos = proj(s, 'todos')
|
|
1123
|
+
let html = ''
|
|
1124
|
+
if (goal && !isGoalTerminal(goal)) {
|
|
1125
|
+
html += `<div class="ds-card ds-goal-card"><div class="ds-card-title">${t('goal.title')}</div>
|
|
1126
|
+
<div class="ds-goal-obj">${esc(goal.objective || '')}</div>
|
|
1127
|
+
<div class="ds-goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
|
|
1128
|
+
<div class="ds-goal-actions">
|
|
1129
|
+
${goal.phase === 'active' ? `<button class="ds-mini-btn" data-goal="pause">${t('goal.pause')}</button>` : `<button class="ds-mini-btn" data-goal="resume">${t('goal.resume')}</button>`}
|
|
1130
|
+
<button class="ds-mini-btn" data-goal="complete">${t('goal.complete')}</button>
|
|
1131
|
+
<button class="ds-mini-btn" data-goal="edit">${t('goal.edit')}</button>
|
|
1132
|
+
<button class="ds-mini-btn" data-goal="clear">${t('goal.clear')}</button>
|
|
1133
|
+
</div></div>`
|
|
1134
|
+
}
|
|
1135
|
+
if (todos?.items?.length) {
|
|
1136
|
+
html += `<div class="ds-card"><div class="ds-card-title">${t('todos.title')}</div>${todos.items.map(item =>
|
|
1137
|
+
`<div class="ds-todo-row"><span class="ds-pill ${item.status === 'completed' ? 'done' : item.status === 'in_progress' ? 'active' : ''}">${esc(item.status || 'pending')}</span><span>${esc(item.content || '')}</span></div>`
|
|
1138
|
+
).join('')}</div>`
|
|
1139
|
+
}
|
|
1140
|
+
box.innerHTML = html
|
|
1141
|
+
box.querySelectorAll('[data-goal]').forEach(btn =>
|
|
1142
|
+
btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
|
|
1143
|
+
|
|
1144
|
+
const sub = await safeRpc('subagent.list', { parentSessionId: state.current }, '')
|
|
1145
|
+
if (sub?.entries?.length) {
|
|
1146
|
+
const rows = sub.entries.map(e => {
|
|
1147
|
+
if (e.kind === 'diagnostic') return `<div class="ds-card-row"><span class="ds-card-k">${t('subagent.diagnostic')}</span><span class="ds-card-v">${esc(e.reason)}</span></div>`
|
|
1148
|
+
const label = e.label || short(e.id)
|
|
1149
|
+
const running = e.activity === 'running'
|
|
1150
|
+
return `<div class="ds-card-row"><span class="ds-card-k">${running ? '▶ ' : ''}${esc(label)}</span><span class="ds-card-v">${esc(e.mode)} ${running ? t('subagent.running') : ''}${e.mode === 'continuable' && running ? ` <button class="ds-mini-btn" data-sub-interrupt="${esc(e.id)}">${t('subagent.interrupt')}</button>` : ''}</span></div>`
|
|
1151
|
+
}).join('')
|
|
1152
|
+
box.insertAdjacentHTML('beforeend', `<div class="ds-card"><div class="ds-card-title">${t('subagent.title')}</div>${rows}</div>`)
|
|
1153
|
+
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
1154
|
+
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function setGoalPhaseLocal(phase) {
|
|
1159
|
+
const s = state.byId.get(state.current)
|
|
1160
|
+
const p = s && proj(s, 'goal')
|
|
1161
|
+
const goal = p && typeof p === 'object' && p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1162
|
+
if (!goal) return
|
|
1163
|
+
goal.phase = phase
|
|
1164
|
+
renderSessions()
|
|
1165
|
+
renderSessionCards()
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function goalAction(kind) {
|
|
1169
|
+
const s = state.byId.get(state.current)
|
|
1170
|
+
const goal = goalOf(s)
|
|
1171
|
+
if (!goal) return toast(t('goal.none'), 'err')
|
|
1172
|
+
const ref = { id: goal.id, revision: goal.revision }
|
|
1173
|
+
if (kind === 'edit') {
|
|
1174
|
+
const objective = prompt(t('goal.editPrompt'), goal.objective || '')
|
|
1175
|
+
if (objective === null) return
|
|
1176
|
+
if (!objective.trim()) return toast(t('goal.cannotEmpty'), 'err')
|
|
1177
|
+
await safeRpc('goal.edit', { sessionId: state.current, ref, objective: objective.trim() }, t('goal.updateFailed'))
|
|
1178
|
+
toast(t('goal.updated'), 'ok')
|
|
1179
|
+
refreshSessions()
|
|
1180
|
+
renderSessionCards()
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
const map = { pause: 'goal.pause', resume: 'goal.resume', complete: 'goal.complete', clear: 'goal.clear' }
|
|
1184
|
+
const method = map[kind]
|
|
1185
|
+
if (!method) return
|
|
1186
|
+
if (kind === 'clear' && !confirm(t('goal.confirmClear'))) return
|
|
1187
|
+
if (kind === 'complete' && !confirm(t('goal.confirmComplete'))) return
|
|
1188
|
+
await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1189
|
+
if (kind === 'complete') setGoalPhaseLocal('complete')
|
|
1190
|
+
if (kind === 'clear') setGoalPhaseLocal('cleared')
|
|
1191
|
+
toast(t('goal.actionSubmitted'), 'ok')
|
|
1192
|
+
refreshSessions()
|
|
1193
|
+
renderSessionCards()
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function interruptSubagent(childId) {
|
|
1197
|
+
if (!confirm(t('subagent.confirmInterrupt'))) return
|
|
1198
|
+
await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
1199
|
+
toast(t('subagent.interruptSubmitted'), 'ok')
|
|
1200
|
+
setTimeout(renderSessionCards, 600)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
869
1203
|
async function runSlashCommand(text) {
|
|
870
1204
|
const clean = String(text || '').trim()
|
|
871
1205
|
if (!clean.startsWith('/') || !state.current) return false
|
|
@@ -1120,7 +1454,7 @@ function showView(id) {
|
|
|
1120
1454
|
if (id === 'view-settings') showSettingsHome()
|
|
1121
1455
|
}
|
|
1122
1456
|
|
|
1123
|
-
const SETTINGS_GROUPS = ['general', 'servers', '
|
|
1457
|
+
const SETTINGS_GROUPS = ['general', 'servers', 'theme', 'about']
|
|
1124
1458
|
function showSettingsHome() {
|
|
1125
1459
|
const home = $('settings-home')
|
|
1126
1460
|
if (!home) return
|
|
@@ -1132,6 +1466,7 @@ function showSettingsPage(name) {
|
|
|
1132
1466
|
if (!home || !SETTINGS_GROUPS.includes(name)) return
|
|
1133
1467
|
home.classList.add('hidden')
|
|
1134
1468
|
for (const g of SETTINGS_GROUPS) $('settings-page-' + g)?.classList.toggle('hidden', g !== name)
|
|
1469
|
+
if (name === 'general') renderPresetSummary()
|
|
1135
1470
|
}
|
|
1136
1471
|
function updateConn() {
|
|
1137
1472
|
const el = $('conn-badge')
|
|
@@ -1173,6 +1508,7 @@ function bindUi() {
|
|
|
1173
1508
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
1174
1509
|
})
|
|
1175
1510
|
renderPresetMenuDesktop()
|
|
1511
|
+
renderPresetSummary()
|
|
1176
1512
|
$('btn-cmd').addEventListener('click', (e) => { e.stopPropagation(); toggleCmdMenuDesktop() })
|
|
1177
1513
|
$('cmd-menu').addEventListener('click', (e) => {
|
|
1178
1514
|
const item = e.target.closest('[data-ds-cmd]')
|
|
@@ -1196,9 +1532,14 @@ function bindUi() {
|
|
|
1196
1532
|
$('preset-menu').classList.add('hidden')
|
|
1197
1533
|
}
|
|
1198
1534
|
})
|
|
1535
|
+
$('btn-model').addEventListener('click', (e) => { e.stopPropagation(); toggleModelMenuDesktop() })
|
|
1536
|
+
$('model-menu').addEventListener('click', (e) => {
|
|
1537
|
+
if (e.target.closest('[data-model]') || e.target.closest('[data-effort]')) e.stopPropagation()
|
|
1538
|
+
})
|
|
1199
1539
|
document.addEventListener('click', (e) => {
|
|
1200
1540
|
if (!e.target.closest('#preset-menu') && !e.target.closest('#btn-preset')) $('preset-menu')?.classList.add('hidden')
|
|
1201
1541
|
if (!e.target.closest('#cmd-menu') && !e.target.closest('#btn-cmd')) $('cmd-menu')?.classList.add('hidden')
|
|
1542
|
+
if (!e.target.closest('#model-menu') && !e.target.closest('#btn-model')) $('model-menu')?.classList.add('hidden')
|
|
1202
1543
|
})
|
|
1203
1544
|
$('btn-stats-top').addEventListener('click', toggleStatsDrawer)
|
|
1204
1545
|
$('stats-drawer-close').addEventListener('click', toggleStatsDrawer)
|
|
@@ -1247,6 +1588,10 @@ function bindUi() {
|
|
|
1247
1588
|
if (group) { showSettingsPage(group.dataset.settingsGroup); return }
|
|
1248
1589
|
if (e.target.closest('[data-settings-back]')) { showSettingsHome(); return }
|
|
1249
1590
|
})
|
|
1591
|
+
$('btn-manage-presets').addEventListener('click', openPresetModal)
|
|
1592
|
+
$('btn-preset-add').addEventListener('click', addPreset)
|
|
1593
|
+
$('btn-presets-close').addEventListener('click', closePresetModal)
|
|
1594
|
+
$('modal-presets').addEventListener('click', (e) => { if (e.target === $('modal-presets')) closePresetModal() })
|
|
1250
1595
|
$('btn-server-speed').addEventListener('click', () => selectFastestServer({ silent: false }))
|
|
1251
1596
|
$('btn-server-add').addEventListener('click', addServer)
|
|
1252
1597
|
$('btn-group-add').addEventListener('click', addGroup)
|
package/public/index.html
CHANGED
|
@@ -592,7 +592,7 @@
|
|
|
592
592
|
'update.corrupted': '下载文件损坏,请重试', 'update.serverFileMissing': '服务器上还没有对应版本的文件,请稍后再试',
|
|
593
593
|
'update.installUnsupported': '当前版本不支持 App 内安装,已转浏览器下载',
|
|
594
594
|
'update.expand': '展开', 'update.collapse': '收起',
|
|
595
|
-
'notes.title': '更新内容', 'notes.close': '关闭', 'notes.page': '{current}/{total}',
|
|
595
|
+
'notes.title': '更新内容', 'notes.close': '关闭', 'notes.page': '版本号 {current}/{total}',
|
|
596
596
|
'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码',
|
|
597
597
|
'scan.browserHint': '浏览器请打开主机管理页,用手机相机扫码', 'scan.unsupported': '当前 App 版本不支持拍照扫码,请先更新 App',
|
|
598
598
|
'scan.permissionDenied': '未授予相机权限',
|
|
@@ -771,7 +771,7 @@
|
|
|
771
771
|
'update.corrupted': 'Downloaded file is corrupted, please retry', 'update.serverFileMissing': 'The file for this version is not on the server yet, please try again later',
|
|
772
772
|
'update.installUnsupported': 'This version cannot install in-app, opening browser download',
|
|
773
773
|
'update.expand': 'Expand', 'update.collapse': 'Collapse',
|
|
774
|
-
'notes.title': 'What\'s New', 'notes.close': 'Close', 'notes.page': '{current}/{total}',
|
|
774
|
+
'notes.title': 'What\'s New', 'notes.close': 'Close', 'notes.page': 'Version {current}/{total}',
|
|
775
775
|
'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images',
|
|
776
776
|
'scan.browserHint': 'In a browser, open the host admin page and scan with your phone camera', 'scan.unsupported': 'This app version cannot scan, please update first',
|
|
777
777
|
'scan.permissionDenied': 'Camera permission not granted',
|
package/public/update.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.6.
|
|
2
|
+
"version": "0.6.3",
|
|
3
3
|
"apkUrl": "dsh-remote.apk",
|
|
4
|
-
"sha256": "
|
|
5
|
-
"releasedAt": "2026-08-
|
|
6
|
-
"notes": "
|
|
4
|
+
"sha256": "e658387db7cee1e0b5392fad3719056b463d40ce55b5aac0d313d797c07438f2",
|
|
5
|
+
"releasedAt": "2026-08-19T03:20:39.386Z",
|
|
6
|
+
"notes": "修复 DSH 重启后网关 upstream 端口不刷新(Issue #1):/health 增加 upstream/upstreamOk/pid 探测;插件 ensureGateway 检测上游变化或不可达时自动杀旧网关并按新 DSH_UPSTREAM 重启;启动/刷新写插件日志与 PID 文件。",
|
|
7
|
+
"history": [
|
|
8
|
+
{
|
|
9
|
+
"version": "0.6.3",
|
|
10
|
+
"notes": "修复 DSH 重启后网关 upstream 端口不刷新(Issue #1):/health 增加 upstream/upstreamOk/pid 探测;插件 ensureGateway 检测上游变化或不可达时自动杀旧网关并按新 DSH_UPSTREAM 重启;启动/刷新写插件日志与 PID 文件。"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.6.2",
|
|
14
|
+
"notes": "桌面端 WebUI:设置页清理与输入区饱满化;新增会话功能(goal/todo/subagent/模型切换/思考深度);预设提示词管理与空状态引导;更新弹窗改为按正式版逐版展示历史(App/桌面端);移除通知分组;聊天宽度约 680px、输入框按钮对齐。"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
7
17
|
}
|
package/public/version.json
CHANGED