dsh-remote-plugin 0.6.21 → 0.6.23
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 +271 -27
- package/index.mjs +99 -7
- package/package.json +1 -1
- package/public/announcements.json +8 -0
- package/public/app.js +247 -14
- package/public/desktop/desktop.css +3 -0
- package/public/desktop/desktop.html +6 -5
- package/public/desktop/desktop.js +169 -5
- package/public/index.html +13 -5
- package/public/styles.css +6 -0
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -75,11 +75,17 @@ const state = {
|
|
|
75
75
|
updateInfo: null,
|
|
76
76
|
warnedGatewayVersions: new Set(),
|
|
77
77
|
announcement: null,
|
|
78
|
+
announcementItems: [],
|
|
79
|
+
announcementIndex: 0,
|
|
78
80
|
announcements: [],
|
|
79
81
|
approvals: [], // 待处理审批
|
|
80
82
|
questions: [], // 待处理提问
|
|
81
83
|
queues: {}, // sessionId -> queue items
|
|
82
84
|
queueSteering: {}, // sessionId:itemId -> pending steer request
|
|
85
|
+
compactions: {}, // sessionId -> {active, phase, startedAt, message, source}
|
|
86
|
+
pendingCommands: {}, // sessionId -> command name; waits briefly before showing generic progress
|
|
87
|
+
compactionPollTimer: null,
|
|
88
|
+
compactionClockTimer: null,
|
|
83
89
|
sessionTurnTimes: {}, // sessionId -> 本轮开始/结束时间,避免中间事件推动排序
|
|
84
90
|
jobs: {}, // sessionId -> jobs
|
|
85
91
|
sessionActivity: new Set(), // 已发送消息或已执行命令的会话
|
|
@@ -287,6 +293,7 @@ function openFeedbackModal() {
|
|
|
287
293
|
document.querySelectorAll('#fb-chips .fb-chip').forEach(b => b.classList.toggle('current', b.dataset.fbType === 'bug'))
|
|
288
294
|
$('fb-msg').value = ''
|
|
289
295
|
$('fb-contact').value = ''
|
|
296
|
+
$('fb-include-diagnostics').checked = false
|
|
290
297
|
$('modal-feedback').classList.remove('hidden')
|
|
291
298
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
292
299
|
}
|
|
@@ -303,6 +310,7 @@ async function submitFeedback() {
|
|
|
303
310
|
const type = state.feedbackType || 'bug'
|
|
304
311
|
const message = $('fb-msg').value.trim()
|
|
305
312
|
const contact = $('fb-contact').value.trim()
|
|
313
|
+
const includeDiagnostics = $('fb-include-diagnostics').checked
|
|
306
314
|
if (!message) { toast(t('feedback.empty'), 'err'); return }
|
|
307
315
|
if (message.length > 2000) { toast(t('feedback.tooLong'), 'err'); return }
|
|
308
316
|
const btn = $('fb-submit')
|
|
@@ -313,7 +321,7 @@ async function submitFeedback() {
|
|
|
313
321
|
const res = await fetch(base + '/feedback', {
|
|
314
322
|
method: 'POST',
|
|
315
323
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
316
|
-
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion })
|
|
324
|
+
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion, includeDiagnostics })
|
|
317
325
|
})
|
|
318
326
|
let json = {}
|
|
319
327
|
try { json = await res.json() } catch {}
|
|
@@ -1583,6 +1591,11 @@ function onHostFrame(full) {
|
|
|
1583
1591
|
const f = full.payload
|
|
1584
1592
|
if (!f) return
|
|
1585
1593
|
if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) return scheduleRefresh()
|
|
1594
|
+
if (f.type === 'host/session-activity') {
|
|
1595
|
+
const session = state.byId.get(f.sessionId)
|
|
1596
|
+
if (session) { session.updatedAt = Number(f.updatedAt) || Date.now(); renderSessions(); renderOverview() }
|
|
1597
|
+
return
|
|
1598
|
+
}
|
|
1586
1599
|
if (f.type === 'host/session-status') {
|
|
1587
1600
|
const s = state.byId.get(f.sessionId)
|
|
1588
1601
|
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessionCards(); updateCancelBtn(); renderSessionSub(); updateSessionStatus() } }
|
|
@@ -1597,9 +1610,104 @@ function onHostFrame(full) {
|
|
|
1597
1610
|
if (f.type === 'host/remote-event') return scheduleRefresh()
|
|
1598
1611
|
}
|
|
1599
1612
|
|
|
1613
|
+
function activeCompaction(sessionId = state.current) {
|
|
1614
|
+
const compact = state.compactions[sessionId]
|
|
1615
|
+
return compact?.active === true ? compact : null
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function compactElapsed(startedAt) {
|
|
1619
|
+
const seconds = Math.max(0, Math.floor((Date.now() - Number(startedAt || Date.now())) / 1000))
|
|
1620
|
+
const minutes = Math.floor(seconds / 60)
|
|
1621
|
+
const remain = seconds % 60
|
|
1622
|
+
return minutes > 0 ? `${minutes}:${String(remain).padStart(2, '0')}` : `${remain}s`
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function setCompactionStatus(sessionId, next) {
|
|
1626
|
+
if (!sessionId) return
|
|
1627
|
+
const previous = state.compactions[sessionId]
|
|
1628
|
+
const active = next?.active === true
|
|
1629
|
+
if (active) {
|
|
1630
|
+
state.compactions[sessionId] = {
|
|
1631
|
+
active: true,
|
|
1632
|
+
phase: next.phase || previous?.phase || 'running',
|
|
1633
|
+
command: String(next.command || previous?.command || 'compact'),
|
|
1634
|
+
startedAt: Number(next.startedAt) || previous?.startedAt || Date.now(),
|
|
1635
|
+
message: String(next.message || ''),
|
|
1636
|
+
source: next.source || previous?.source || 'event',
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
delete state.compactions[sessionId]
|
|
1640
|
+
if (previous?.active) {
|
|
1641
|
+
const command = previous.command || 'compact'
|
|
1642
|
+
if (next?.phase === 'failed') toast(command === 'compact'
|
|
1643
|
+
? t('session.compactFailed', { msg: next.message || t('send.failed') })
|
|
1644
|
+
: t('session.commandFailed', { command, msg: next.message || t('send.failed') }), 'err')
|
|
1645
|
+
else toast(command === 'compact' ? t('session.compactComplete') : t('session.commandComplete', { command }), 'ok')
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
ensureCompactionMonitoring()
|
|
1649
|
+
if (state.current === sessionId) updateSessionStatus()
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
async function refreshCompactionStatus(sessionId = state.current) {
|
|
1653
|
+
if (!sessionId) return
|
|
1654
|
+
try {
|
|
1655
|
+
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
|
|
1656
|
+
const url = new URL(apiUrl('/remote/api/command-status'), location.href)
|
|
1657
|
+
url.searchParams.set('sessionId', sessionId)
|
|
1658
|
+
const res = await fetch(url, { headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }, ...(signal ? { signal } : {}) })
|
|
1659
|
+
if (res.status === 401) { authFailure(); return }
|
|
1660
|
+
if (!res.ok) return
|
|
1661
|
+
const body = await res.json().catch(() => null)
|
|
1662
|
+
const operation = body?.operation || body?.compact
|
|
1663
|
+
if (!operation) return
|
|
1664
|
+
const pending = state.pendingCommands[sessionId]
|
|
1665
|
+
if (operation.active) {
|
|
1666
|
+
delete state.pendingCommands[sessionId]
|
|
1667
|
+
setCompactionStatus(sessionId, { ...operation, source: 'status' })
|
|
1668
|
+
} else if (activeCompaction(sessionId) && activeCompaction(sessionId).source !== 'event') {
|
|
1669
|
+
setCompactionStatus(sessionId, operation)
|
|
1670
|
+
} else if (pending) {
|
|
1671
|
+
if (operation.phase === 'failed') toast(t('session.commandFailed', { command: pending, msg: operation.message || t('send.failed') }), 'err')
|
|
1672
|
+
delete state.pendingCommands[sessionId]
|
|
1673
|
+
}
|
|
1674
|
+
} catch {}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function ensureCompactionMonitoring() {
|
|
1678
|
+
const active = Object.values(state.compactions).some(compact => compact?.active)
|
|
1679
|
+
if (!active) {
|
|
1680
|
+
if (state.compactionPollTimer) clearInterval(state.compactionPollTimer)
|
|
1681
|
+
if (state.compactionClockTimer) clearInterval(state.compactionClockTimer)
|
|
1682
|
+
state.compactionPollTimer = null
|
|
1683
|
+
state.compactionClockTimer = null
|
|
1684
|
+
return
|
|
1685
|
+
}
|
|
1686
|
+
if (!state.compactionClockTimer) {
|
|
1687
|
+
state.compactionClockTimer = setInterval(() => {
|
|
1688
|
+
if (activeCompaction()) updateSessionStatus()
|
|
1689
|
+
}, 1000)
|
|
1690
|
+
}
|
|
1691
|
+
if (!state.compactionPollTimer) {
|
|
1692
|
+
state.compactionPollTimer = setInterval(() => {
|
|
1693
|
+
const compact = activeCompaction()
|
|
1694
|
+
if (compact && compact.source !== 'event') void refreshCompactionStatus()
|
|
1695
|
+
}, 3000)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function observeCompactionEvent(sessionId, event) {
|
|
1700
|
+
if (event?.type === 'compaction/start') {
|
|
1701
|
+
setCompactionStatus(sessionId, { active: true, phase: 'running', startedAt: activeCompaction(sessionId)?.startedAt || Date.now(), source: activeCompaction(sessionId)?.source || 'event' })
|
|
1702
|
+
} else if (event?.type === 'compaction/end' && activeCompaction(sessionId)?.source === 'event') {
|
|
1703
|
+
setCompactionStatus(sessionId, { active: false, phase: 'complete' })
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1600
1707
|
function onSessionEvent(sessionId, event) {
|
|
1601
1708
|
if (!event) return
|
|
1602
1709
|
const s = state.byId.get(sessionId)
|
|
1710
|
+
observeCompactionEvent(sessionId, event)
|
|
1603
1711
|
if (event.type === 'turn/start' || event.type === 'turn/end') {
|
|
1604
1712
|
noteSessionTurnTime(sessionId, event)
|
|
1605
1713
|
renderSessions()
|
|
@@ -2180,6 +2288,7 @@ async function openSession(id) {
|
|
|
2180
2288
|
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
2181
2289
|
renderQueue()
|
|
2182
2290
|
renderSessionPending()
|
|
2291
|
+
void refreshCompactionStatus(id)
|
|
2183
2292
|
restoreCachedHistory()
|
|
2184
2293
|
await loadHistory(true)
|
|
2185
2294
|
renderSessionCards()
|
|
@@ -2274,10 +2383,20 @@ function updateSessionStatus() {
|
|
|
2274
2383
|
const head = $('session-head')
|
|
2275
2384
|
if (!head) return
|
|
2276
2385
|
const composerStatus = $('composer-status')
|
|
2277
|
-
|
|
2386
|
+
const compact = activeCompaction()
|
|
2387
|
+
if (composerStatus) {
|
|
2388
|
+
composerStatus.classList.toggle('hidden', !s?.running && !compact)
|
|
2389
|
+
composerStatus.classList.toggle('compacting', !!compact)
|
|
2390
|
+
}
|
|
2391
|
+
const composerText = $('composer-status-text')
|
|
2392
|
+
if (composerText) composerText.textContent = compact
|
|
2393
|
+
? (compact.command === 'compact'
|
|
2394
|
+
? t('session.compacting', { elapsed: compactElapsed(compact.startedAt) })
|
|
2395
|
+
: t('session.commandRunning', { command: compact.command, elapsed: compactElapsed(compact.startedAt) }))
|
|
2396
|
+
: t('composer.running')
|
|
2278
2397
|
head.classList.remove('running', 'interrupted')
|
|
2279
2398
|
const queued = (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
2280
|
-
if (s?.running || queued) head.classList.add('running')
|
|
2399
|
+
if (s?.running || queued || compact) head.classList.add('running')
|
|
2281
2400
|
else if (s?.error) head.classList.add('interrupted')
|
|
2282
2401
|
}
|
|
2283
2402
|
|
|
@@ -2876,12 +2995,75 @@ async function interruptSubagent(childId) {
|
|
|
2876
2995
|
}
|
|
2877
2996
|
|
|
2878
2997
|
/* ---------------- 发送 / 取消 / 快捷菜单 ---------------- */
|
|
2998
|
+
const NO_FALLBACK_SLASH_COMMANDS = new Set(['compact', 'export'])
|
|
2999
|
+
const SLASH_COMMAND_TIMEOUT_MS = 20_000
|
|
3000
|
+
// 比插件端的 120 秒多留 5 秒,让服务端能返回确定的失败结果而非客户端先中断。
|
|
3001
|
+
const LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS = 125_000
|
|
3002
|
+
|
|
3003
|
+
function slashCommandName(text) {
|
|
3004
|
+
const match = /^\/+([^\s/]+)/.exec(String(text || '').trim())
|
|
3005
|
+
return match ? match[1].toLowerCase() : ''
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
function sessionLogFilename(sessionId) {
|
|
3009
|
+
return `dsh-session-${String(sessionId).replace(/[^A-Za-z0-9_-]/g, '_')}.zip`
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
function sessionLogExportUrl(sessionId, includeToken = false) {
|
|
3013
|
+
const url = new URL(apiUrl('/api/session.export'), location.href)
|
|
3014
|
+
url.searchParams.set('sessionId', sessionId)
|
|
3015
|
+
url.searchParams.set('includeDescendants', 'true')
|
|
3016
|
+
// 网关下载由浏览器/DownloadManager 发起,无法附加 Bearer 头时才使用短期既有 token
|
|
3017
|
+
// 查询参数兼容通道;同源 DSH 插件页仍只使用它自己的登录 Cookie。
|
|
3018
|
+
if (includeToken && state.token) url.searchParams.set('token', state.token)
|
|
3019
|
+
return url
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
async function downloadSessionExport(sessionId) {
|
|
3023
|
+
const nativePlatform = !!CAP?.isNativePlatform?.()
|
|
3024
|
+
const nativeDownload = !!(nativePlatform && window.NativeFile?.downloadToDownloads)
|
|
3025
|
+
if (nativePlatform && !nativeDownload) {
|
|
3026
|
+
toast(t('fs.downloadUnsupported'), 'err')
|
|
3027
|
+
return
|
|
3028
|
+
}
|
|
3029
|
+
const url = sessionLogExportUrl(sessionId, !nativeDownload && !!state.server)
|
|
3030
|
+
const headers = state.token ? { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() } : {}
|
|
3031
|
+
try {
|
|
3032
|
+
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
3033
|
+
? AbortSignal.timeout(LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS)
|
|
3034
|
+
: undefined
|
|
3035
|
+
const preflight = await fetch(url, { method: 'HEAD', headers, ...(signal ? { signal } : {}) })
|
|
3036
|
+
if (preflight.status === 401) { authFailure(); return }
|
|
3037
|
+
if (!preflight.ok) throw new Error('HTTP ' + preflight.status)
|
|
3038
|
+
const filename = sessionLogFilename(sessionId)
|
|
3039
|
+
if (nativeDownload) {
|
|
3040
|
+
window.NativeFile.downloadToDownloads(url.href, filename, state.token)
|
|
3041
|
+
toast(t('fs.downloadStarted'), 'ok')
|
|
3042
|
+
return
|
|
3043
|
+
}
|
|
3044
|
+
// 浏览器下载目的地由浏览器自身的下载设置决定;同源插件页无需暴露网关 token。
|
|
3045
|
+
const anchor = document.createElement('a')
|
|
3046
|
+
anchor.href = url.href
|
|
3047
|
+
anchor.download = filename
|
|
3048
|
+
document.body.appendChild(anchor)
|
|
3049
|
+
anchor.click()
|
|
3050
|
+
anchor.remove()
|
|
3051
|
+
toast(t('session.exportStarted'), 'ok')
|
|
3052
|
+
} catch (e) {
|
|
3053
|
+
console.error('session export download failed', e)
|
|
3054
|
+
toast(t('session.exportFailed', { msg: e?.message || '' }), 'err')
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2879
3058
|
async function runSlashCommand(text) {
|
|
2880
3059
|
const clean = String(text || '').trim()
|
|
2881
3060
|
if (!clean.startsWith('/') || !state.current) return false
|
|
3061
|
+
const command = slashCommandName(clean)
|
|
3062
|
+
const noFallback = NO_FALLBACK_SLASH_COMMANDS.has(command)
|
|
3063
|
+
const longRunning = command === 'export'
|
|
2882
3064
|
try {
|
|
2883
3065
|
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
2884
|
-
? AbortSignal.timeout(
|
|
3066
|
+
? AbortSignal.timeout(longRunning ? LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS : SLASH_COMMAND_TIMEOUT_MS)
|
|
2885
3067
|
: undefined
|
|
2886
3068
|
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
2887
3069
|
method: 'POST',
|
|
@@ -2890,12 +3072,31 @@ async function runSlashCommand(text) {
|
|
|
2890
3072
|
...(signal ? { signal } : {})
|
|
2891
3073
|
})
|
|
2892
3074
|
if (res.status === 401) { authFailure(); return true }
|
|
2893
|
-
if (!res.ok)
|
|
3075
|
+
if (!res.ok) {
|
|
3076
|
+
if (noFallback) toast(t('session.commandTimedOut'), 'err')
|
|
3077
|
+
return noFallback
|
|
3078
|
+
}
|
|
2894
3079
|
const data = await res.json().catch(() => null)
|
|
2895
3080
|
if (data?.ok === false) { toast(data.message || t('send.failed'), 'err'); return true }
|
|
2896
|
-
if (data?.ok && data.executed === true) {
|
|
3081
|
+
if (data?.ok && data.executed === true) {
|
|
3082
|
+
if (data.accepted) {
|
|
3083
|
+
if (command === 'compact') {
|
|
3084
|
+
setCompactionStatus(state.current, { ...(data.operation || data.compact), active: true, command, source: 'command' })
|
|
3085
|
+
} else {
|
|
3086
|
+
const sessionId = state.current
|
|
3087
|
+
state.pendingCommands[sessionId] = command
|
|
3088
|
+
setTimeout(() => { if (state.current === sessionId) void refreshCompactionStatus(sessionId) }, 600)
|
|
3089
|
+
}
|
|
3090
|
+
} else if (command === 'export') await downloadSessionExport(state.current)
|
|
3091
|
+
else toast(t('send.commandExecuted'), 'ok')
|
|
3092
|
+
return true
|
|
3093
|
+
}
|
|
2897
3094
|
} catch (e) {
|
|
2898
3095
|
console.error('slash command bridge failed', e)
|
|
3096
|
+
if (noFallback) {
|
|
3097
|
+
toast(t('session.commandTimedOut'), 'err')
|
|
3098
|
+
return true
|
|
3099
|
+
}
|
|
2899
3100
|
}
|
|
2900
3101
|
return false
|
|
2901
3102
|
}
|
|
@@ -4429,10 +4630,12 @@ function readSeenAnnouncements() {
|
|
|
4429
4630
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
4430
4631
|
} catch { return {} }
|
|
4431
4632
|
}
|
|
4432
|
-
function
|
|
4433
|
-
|
|
4633
|
+
function markAnnouncementsSeen(items) {
|
|
4634
|
+
const ids = [...new Set((items || []).map(item => typeof item === 'string' ? item : item?.id).filter(Boolean))]
|
|
4635
|
+
if (!ids.length) return
|
|
4434
4636
|
const seen = readSeenAnnouncements()
|
|
4435
|
-
|
|
4637
|
+
const now = Date.now()
|
|
4638
|
+
for (const id of ids) seen[id] = now
|
|
4436
4639
|
const keys = Object.keys(seen)
|
|
4437
4640
|
if (keys.length > 100) {
|
|
4438
4641
|
keys.sort((a, b) => Number(seen[a]) - Number(seen[b]))
|
|
@@ -4441,6 +4644,7 @@ function markAnnouncementSeen(id) {
|
|
|
4441
4644
|
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
4442
4645
|
renderAnnouncementBoard()
|
|
4443
4646
|
}
|
|
4647
|
+
function markAnnouncementSeen(id) { markAnnouncementsSeen([id]) }
|
|
4444
4648
|
function readAnnouncementVotes() {
|
|
4445
4649
|
try {
|
|
4446
4650
|
const value = JSON.parse(LS.get(ANNOUNCEMENT_VOTES_KEY, '{}'))
|
|
@@ -4606,7 +4810,18 @@ function renderAnnouncementPoll(item) {
|
|
|
4606
4810
|
submit.classList.toggle('hidden', !!vote)
|
|
4607
4811
|
submit.disabled = true
|
|
4608
4812
|
}
|
|
4609
|
-
function
|
|
4813
|
+
function renderAnnouncementPagination() {
|
|
4814
|
+
const total = state.announcementItems.length
|
|
4815
|
+
const nav = $('announcement-pagination')
|
|
4816
|
+
if (!nav) return
|
|
4817
|
+
nav.classList.toggle('hidden', total < 2)
|
|
4818
|
+
$('announcement-page').textContent = total > 1 ? t('announcement.page', { current: state.announcementIndex + 1, total }) : ''
|
|
4819
|
+
$('announcement-prev').disabled = state.announcementIndex <= 0
|
|
4820
|
+
$('announcement-next').disabled = state.announcementIndex >= total - 1
|
|
4821
|
+
}
|
|
4822
|
+
function renderAnnouncementModal() {
|
|
4823
|
+
const item = state.announcementItems[state.announcementIndex]
|
|
4824
|
+
if (!item) return
|
|
4610
4825
|
state.announcement = item
|
|
4611
4826
|
$('announcement-title').textContent = item.title
|
|
4612
4827
|
$('announcement-content').innerHTML = esc(item.content).replace(/\r?\n/g, '<br>')
|
|
@@ -4621,9 +4836,23 @@ function openAnnouncementModal(item) {
|
|
|
4621
4836
|
action.textContent = ''
|
|
4622
4837
|
action.classList.add('hidden')
|
|
4623
4838
|
}
|
|
4624
|
-
|
|
4839
|
+
// 同批存在强制公告时,不能借由切到普通公告而绕过“稍后再看”限制。
|
|
4840
|
+
$('announcement-later').classList.toggle('hidden', state.announcementItems.some(entry => entry.force))
|
|
4841
|
+
renderAnnouncementPagination()
|
|
4842
|
+
}
|
|
4843
|
+
function openAnnouncementModal(items, index = 0) {
|
|
4844
|
+
const list = (Array.isArray(items) ? items : [items]).filter(item => item?.id)
|
|
4845
|
+
if (!list.length) return
|
|
4846
|
+
state.announcementItems = list
|
|
4847
|
+
state.announcementIndex = Math.max(0, Math.min(Number(index) || 0, list.length - 1))
|
|
4848
|
+
renderAnnouncementModal()
|
|
4625
4849
|
$('modal-announcement').classList.remove('hidden')
|
|
4626
4850
|
}
|
|
4851
|
+
function showAnnouncementAt(index) {
|
|
4852
|
+
if (!state.announcementItems.length) return
|
|
4853
|
+
state.announcementIndex = Math.max(0, Math.min(index, state.announcementItems.length - 1))
|
|
4854
|
+
renderAnnouncementModal()
|
|
4855
|
+
}
|
|
4627
4856
|
async function submitAnnouncementVote() {
|
|
4628
4857
|
const item = state.announcement
|
|
4629
4858
|
const poll = item?.poll
|
|
@@ -4687,8 +4916,10 @@ async function submitAnnouncementVote() {
|
|
|
4687
4916
|
}
|
|
4688
4917
|
}
|
|
4689
4918
|
function closeAnnouncement(markSeen) {
|
|
4690
|
-
if (markSeen
|
|
4919
|
+
if (markSeen) markAnnouncementsSeen(state.announcementItems)
|
|
4691
4920
|
state.announcement = null
|
|
4921
|
+
state.announcementItems = []
|
|
4922
|
+
state.announcementIndex = 0
|
|
4692
4923
|
$('modal-announcement').classList.add('hidden')
|
|
4693
4924
|
}
|
|
4694
4925
|
async function fetchAnnouncements() {
|
|
@@ -4711,7 +4942,7 @@ async function fetchAnnouncements() {
|
|
|
4711
4942
|
const items = normalized.filter(item => !seen[item.id])
|
|
4712
4943
|
.sort((a, b) => b.publishedAt - a.publishedAt)
|
|
4713
4944
|
if (!items.length || state.announcement) return false
|
|
4714
|
-
openAnnouncementModal(items
|
|
4945
|
+
openAnnouncementModal(items)
|
|
4715
4946
|
return true
|
|
4716
4947
|
} catch { return false }
|
|
4717
4948
|
}
|
|
@@ -6897,12 +7128,14 @@ function bindUi() {
|
|
|
6897
7128
|
$('archive-cancel').addEventListener('click', closeArchiveConfirm)
|
|
6898
7129
|
$('archive-confirm').addEventListener('click', confirmArchiveSession)
|
|
6899
7130
|
$('modal-archive').addEventListener('click', (e) => { if (e.target === $('modal-archive')) closeArchiveConfirm() })
|
|
7131
|
+
$('announcement-prev').addEventListener('click', () => showAnnouncementAt(state.announcementIndex - 1))
|
|
7132
|
+
$('announcement-next').addEventListener('click', () => showAnnouncementAt(state.announcementIndex + 1))
|
|
6900
7133
|
$('announcement-later').addEventListener('click', () => closeAnnouncement(false))
|
|
6901
7134
|
$('announcement-confirm').addEventListener('click', () => closeAnnouncement(true))
|
|
6902
7135
|
$('announcement-poll-options').addEventListener('change', () => { $('announcement-poll-submit').disabled = false })
|
|
6903
7136
|
$('announcement-poll-submit').addEventListener('click', submitAnnouncementVote)
|
|
6904
7137
|
$('modal-announcement').addEventListener('click', (e) => {
|
|
6905
|
-
if (e.target === $('modal-announcement') && !state.
|
|
7138
|
+
if (e.target === $('modal-announcement') && !state.announcementItems.some(item => item.force)) closeAnnouncement(false)
|
|
6906
7139
|
})
|
|
6907
7140
|
$('announcement-history-close').addEventListener('click', closeAnnouncementHistory)
|
|
6908
7141
|
$('announcement-history-list').addEventListener('click', (e) => {
|
|
@@ -166,6 +166,9 @@ html.reorder-scroll-lock body { overscroll-behavior: none; }
|
|
|
166
166
|
}
|
|
167
167
|
.ds-fb-textarea:focus, .ds-fb-input:focus { border-color: var(--dsr-accent-line); }
|
|
168
168
|
.ds-fb-input { min-height: 42px; }
|
|
169
|
+
.ds-fb-diagnostics { display:grid; grid-template-columns:auto minmax(0,1fr); gap:5px 8px; align-items:start; margin:-1px 1px 4px; color:var(--dsr-text); font-size:13px; line-height:1.45; cursor:pointer; }
|
|
170
|
+
.ds-fb-diagnostics input { width:17px; height:17px; margin:1px 0 0; accent-color:var(--dsr-accent); }
|
|
171
|
+
.ds-fb-diagnostics small { grid-column:2; color:var(--dsr-muted); font-size:11px; line-height:1.45; }
|
|
169
172
|
|
|
170
173
|
.ds-main { flex: 1; min-width: 0; display: flex; flex-direction: column; background: radial-gradient(900px 560px at 96% -18%, var(--dsr-accent-soft), transparent 64%), linear-gradient(180deg, var(--dsr-bg), var(--dsr-bg-2)); }
|
|
171
174
|
.ds-topbar { height: 58px; flex: none; display: flex; align-items: center; gap: 10px; padding: 0 20px; border-bottom: 1px solid var(--dsr-divider); background: linear-gradient(180deg, var(--dsr-head-bg), transparent); backdrop-filter: blur(16px); }
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
<section id="session-pending" class="ds-session-pending hidden" aria-live="polite"></section>
|
|
170
170
|
<div id="history" class="ds-history" aria-live="polite"></div>
|
|
171
171
|
<div class="ds-composer">
|
|
172
|
-
<div id="composer-status" class="ds-composer-status hidden" role="status" aria-live="polite"><span class="ds-composer-status-dot" aria-hidden="true"></span><span data-i18n="ds.composerRunning">运行中…</span></div>
|
|
172
|
+
<div id="composer-status" class="ds-composer-status hidden" role="status" aria-live="polite"><span class="ds-composer-status-dot" aria-hidden="true"></span><span id="composer-status-text" data-i18n="ds.composerRunning">运行中…</span></div>
|
|
173
173
|
<textarea id="composer" rows="1" data-i18n-placeholder="ds.composerPlaceholder" placeholder="输入消息…"></textarea>
|
|
174
174
|
<div class="ds-composer-actions">
|
|
175
175
|
<div class="ds-composer-left">
|
|
@@ -368,6 +368,7 @@
|
|
|
368
368
|
</div>
|
|
369
369
|
<textarea id="fb-msg" class="ds-fb-textarea" rows="5" maxlength="2000" data-i18n-placeholder="ds.feedbackMessagePlaceholder" placeholder="请描述遇到的问题或建议(必填,≤2000 字)"></textarea>
|
|
370
370
|
<input id="fb-contact" class="ds-fb-input" maxlength="200" data-i18n-placeholder="ds.feedbackContactPlaceholder" placeholder="联系方式(可选):邮箱 / 微信 / B站 ID">
|
|
371
|
+
<label class="ds-fb-diagnostics"><input id="fb-include-diagnostics" type="checkbox"> <span data-i18n="ds.feedbackIncludeDiagnostics">附带兼容性诊断日志(可选)</span><small data-i18n="ds.feedbackDiagnosticsPrivacy">仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。</small></label>
|
|
371
372
|
</div>
|
|
372
373
|
<div class="ds-modal-actions">
|
|
373
374
|
<button id="fb-cancel" class="ds-btn" data-i18n="ds.feedbackCancel">取消</button>
|
|
@@ -477,7 +478,7 @@
|
|
|
477
478
|
'ds.files': '文件传输', 'ds.settings': '设置', 'ds.stats': '统计', 'ds.menu': '导航',
|
|
478
479
|
'ds.send': '发送', 'ds.composerPlaceholder': '输入消息,Enter 发送…', 'ds.composerRunning': '运行中…', 'ds.presets': '预设', 'ds.sessionRename': '重命名会话', 'ds.sessionRenameTitle': '重命名会话', 'ds.sessionRenamePlaceholder': '输入新的会话名称', 'ds.sessionRenameConfirm': '保存', 'ds.sessionRenameFailed': '重命名失败', 'ds.sessionRenameEmpty': '会话名称不能为空', 'ds.sessionRenamed': '会话名称已更新', 'ds.sessionArchive': '归档', 'ds.sessionArchiveConfirm': '归档这个会话?归档后可在“显示已归档”中打开。', 'ds.sessionArchived': '会话已归档', 'ds.sessionStop': '停止本轮', 'ds.sessionStopConfirm': '停止当前回合?排队中的消息不会被删除。', 'ds.sessionStopFailed': '停止失败', 'ds.sessionStopRequested': '已请求停止本轮', 'ds.sessionRecovering': '恢复会话中…', 'ds.sessionReady': '会话已恢复', 'ds.sessionRecoveryFailed': '会话恢复失败',
|
|
479
480
|
'ds.commands': '指令',
|
|
480
|
-
'ds.cmdCompact': '/compact 压缩对话历史', 'ds.cmdExport': '/export 导出会话日志 ZIP',
|
|
481
|
+
'ds.cmdCompact': '/compact 压缩对话历史', 'ds.cmdExport': '/export 导出会话日志 ZIP', 'ds.commandTimedOut': '命令执行超时,未作为普通消息发送', 'ds.exportStarted': '会话日志已开始下载,请在浏览器下载中查看', 'ds.exportFailed': '无法导出会话日志:{msg}', 'ds.compacting': '正在压缩对话 · 已用时 {elapsed}', 'ds.compactComplete': '对话压缩完成', 'ds.compactFailed': '对话压缩未完成:{msg}', 'ds.commandRunning': '正在执行 /{command} · 已用时 {elapsed}', 'ds.commandComplete': '/{command} 已完成', 'ds.commandFailed': '/{command} 未完成:{msg}',
|
|
481
482
|
'ds.cmdFeedback': '/feedback 反馈当前会话', 'ds.cmdGoal': '/goal 设置/查看任务目标',
|
|
482
483
|
'ds.cmdPermission': '/permission 切换权限预设', 'ds.cmdPlan': '/plan 进入/退出计划模式',
|
|
483
484
|
'ds.fsUp': '上级', 'ds.fsRoot': '允许根目录', 'ds.fsNewWorkspace': '新建工作区', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
|
|
@@ -497,7 +498,7 @@
|
|
|
497
498
|
'ds.feedback': '反馈', 'ds.feedbackGithubDesc': '反馈 bug / 提建议', 'ds.feedbackGiteeDesc': '国内镜像,无需代理',
|
|
498
499
|
'ds.feedbackBiliDesc': 'UP 动态页交流', 'ds.feedbackCopyLink': '复制项目链接', 'ds.feedbackCopyDesc': '手动分享给朋友',
|
|
499
500
|
'ds.feedbackCopied': '项目链接已复制', 'ds.feedbackCopyFailed': '复制失败,请手动复制',
|
|
500
|
-
'ds.feedbackWrite': '写反馈', 'ds.feedbackWriteDesc': 'App 内直接提交', 'ds.feedbackModalTitle': '写反馈',
|
|
501
|
+
'ds.feedbackWrite': '写反馈', 'ds.feedbackWriteDesc': 'App 内直接提交', 'ds.feedbackModalTitle': '写反馈', 'ds.feedbackIncludeDiagnostics': '附带兼容性诊断日志(可选)', 'ds.feedbackDiagnosticsPrivacy': '仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。',
|
|
501
502
|
'ds.feedbackTypeBug': 'Bug', 'ds.feedbackTypeSuggestion': '建议', 'ds.feedbackTypeOther': '其他',
|
|
502
503
|
'ds.feedbackMessagePlaceholder': '请描述遇到的问题或建议(必填,≤2000 字)',
|
|
503
504
|
'ds.feedbackContactPlaceholder': '联系方式(可选):邮箱 / 微信 / B站 ID',
|
|
@@ -582,7 +583,7 @@
|
|
|
582
583
|
'ds.files': 'Files', 'ds.settings': 'Settings', 'ds.stats': 'Stats', 'ds.menu': 'Menu',
|
|
583
584
|
'ds.send': 'Send', 'ds.composerPlaceholder': 'Type a message, Enter to send…', 'ds.composerRunning': 'Running…', 'ds.presets': 'Presets', 'ds.sessionRename': 'Rename session', 'ds.sessionRenameTitle': 'Rename session', 'ds.sessionRenamePlaceholder': 'Enter a new session name', 'ds.sessionRenameConfirm': 'Save', 'ds.sessionRenameFailed': 'Rename failed', 'ds.sessionRenameEmpty': 'Session name cannot be empty', 'ds.sessionRenamed': 'Session name updated', 'ds.sessionArchive': 'Archive', 'ds.sessionArchiveConfirm': 'Archive this session? You can open it from “Show archived”.', 'ds.sessionArchived': 'Session archived', 'ds.sessionStop': 'Stop turn', 'ds.sessionStopConfirm': 'Stop the current turn? Queued messages will be kept.', 'ds.sessionStopFailed': 'Stop failed', 'ds.sessionStopRequested': 'Stop requested', 'ds.sessionRecovering': 'Restoring session…', 'ds.sessionReady': 'Session ready', 'ds.sessionRecoveryFailed': 'Session restore failed',
|
|
584
585
|
'ds.commands': 'Commands',
|
|
585
|
-
'ds.cmdCompact': '/compact Compress conversation history', 'ds.cmdExport': '/export Export session log ZIP',
|
|
586
|
+
'ds.cmdCompact': '/compact Compress conversation history', 'ds.cmdExport': '/export Export session log ZIP', 'ds.commandTimedOut': 'Command timed out and was not sent as a chat message', 'ds.exportStarted': 'Session log download started; check your browser downloads', 'ds.exportFailed': 'Could not export session log: {msg}', 'ds.compacting': 'Compressing conversation · {elapsed} elapsed', 'ds.compactComplete': 'Conversation compression complete', 'ds.compactFailed': 'Conversation compression did not finish: {msg}', 'ds.commandRunning': 'Running /{command} · {elapsed} elapsed', 'ds.commandComplete': '/{command} complete', 'ds.commandFailed': '/{command} did not finish: {msg}',
|
|
586
587
|
'ds.cmdFeedback': '/feedback Feedback current session', 'ds.cmdGoal': '/goal Set/view task goal',
|
|
587
588
|
'ds.cmdPermission': '/permission Switch permission preset', 'ds.cmdPlan': '/plan Enter/exit plan mode',
|
|
588
589
|
'ds.fsUp': 'Up', 'ds.fsRoot': 'Allowed root', 'ds.fsNewWorkspace': 'New workspace', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
|
|
@@ -602,7 +603,7 @@
|
|
|
602
603
|
'ds.feedback': 'Feedback', 'ds.feedbackGithubDesc': 'Report bugs · suggest features', 'ds.feedbackGiteeDesc': 'Mirror in China, no proxy needed',
|
|
603
604
|
'ds.feedbackBiliDesc': 'Chat on the UP\'s Bilibili page', 'ds.feedbackCopyLink': 'Copy project link', 'ds.feedbackCopyDesc': 'Share it manually',
|
|
604
605
|
'ds.feedbackCopied': 'Project link copied', 'ds.feedbackCopyFailed': 'Copy failed, copy manually',
|
|
605
|
-
'ds.feedbackWrite': 'Write feedback', 'ds.feedbackWriteDesc': 'Submit from the app', 'ds.feedbackModalTitle': 'Write feedback',
|
|
606
|
+
'ds.feedbackWrite': 'Write feedback', 'ds.feedbackWriteDesc': 'Submit from the app', 'ds.feedbackModalTitle': 'Write feedback', 'ds.feedbackIncludeDiagnostics': 'Include compatibility diagnostics (optional)', 'ds.feedbackDiagnosticsPrivacy': 'Uploads only gateway/protocol versions, interface failure summaries, and realtime status; no token, cookie, conversation content, or file path.',
|
|
606
607
|
'ds.feedbackTypeBug': 'Bug', 'ds.feedbackTypeSuggestion': 'Suggestion', 'ds.feedbackTypeOther': 'Other',
|
|
607
608
|
'ds.feedbackMessagePlaceholder': 'Describe the bug or suggestion (required, ≤2000 chars)',
|
|
608
609
|
'ds.feedbackContactPlaceholder': 'Contact (optional): email / WeChat / Bilibili ID',
|