dsh-remote-plugin 0.6.7 → 0.6.8
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/README.en.md +31 -25
- package/README.md +30 -24
- package/apk/dsh-remote.apk +0 -0
- package/client.js +11 -9
- package/package.json +1 -1
- package/public/admin.html +103 -7
- package/public/admin.js +24 -0
- package/public/announcements.json +12 -0
- package/public/app.js +445 -32
- package/public/desktop/desktop.css +126 -19
- package/public/desktop/desktop.html +63 -14
- package/public/desktop/desktop.js +151 -21
- package/public/index.html +145 -41
- package/public/plugin.html +151 -0
- package/public/plugin.js +179 -0
- package/public/styles.css +176 -15
- package/public/theme-vars.css +9 -0
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -63,6 +63,7 @@ const state = {
|
|
|
63
63
|
pollSeq: { mux: 0, host: 0 },
|
|
64
64
|
refreshTimer: null,
|
|
65
65
|
fs: { path: null, initial: null, loaded: false, upload: null },
|
|
66
|
+
composerImages: [], // 当前草稿中的图片附件:只在发送成功后释放
|
|
66
67
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
67
68
|
wb: null,
|
|
68
69
|
wbProjects: [],
|
|
@@ -1174,6 +1175,29 @@ async function refreshWorkbench() {
|
|
|
1174
1175
|
state.wbProjects = []
|
|
1175
1176
|
state.wbArchived = []
|
|
1176
1177
|
}
|
|
1178
|
+
// 以磁盘实际目录为准同步工作台项目:删除目录后不再残留,新增子目录自动收纳。
|
|
1179
|
+
if (state.wb?.bound && state.wb.path) {
|
|
1180
|
+
try {
|
|
1181
|
+
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1182
|
+
if (listRes.ok) {
|
|
1183
|
+
const listData = await listRes.json().catch(() => ({}))
|
|
1184
|
+
if (Array.isArray(listData.entries)) {
|
|
1185
|
+
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
1186
|
+
state.wbProjects = state.wbProjects.filter(w => diskDirs.has(wbPathKey(w.path)))
|
|
1187
|
+
const have = new Set(state.wbProjects.map(w => wbPathKey(w.path)))
|
|
1188
|
+
for (const entry of listData.entries) {
|
|
1189
|
+
if (entry.type !== 'dir') continue
|
|
1190
|
+
const projectPath = wbJoin(state.wb.path, entry.name)
|
|
1191
|
+
if (have.has(wbPathKey(projectPath))) continue
|
|
1192
|
+
try {
|
|
1193
|
+
const created = await rpc('workspace.create', { path: projectPath })
|
|
1194
|
+
if (created?.workspace) { state.wbProjects.push(created.workspace); have.add(wbPathKey(projectPath)) }
|
|
1195
|
+
} catch {}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} catch {}
|
|
1200
|
+
}
|
|
1177
1201
|
renderWorkbench()
|
|
1178
1202
|
renderSessions()
|
|
1179
1203
|
}
|
|
@@ -1201,15 +1225,19 @@ function renderWorkbench() {
|
|
|
1201
1225
|
panel.innerHTML = `<div class="wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1202
1226
|
return
|
|
1203
1227
|
}
|
|
1228
|
+
const archivedSet = new Set(state.wbArchived || [])
|
|
1204
1229
|
panel.innerHTML = projects.map(w => {
|
|
1205
1230
|
const id = String(w.workspaceId || '')
|
|
1206
1231
|
const open = !!state.wbOpenProjects[id]
|
|
1207
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean)
|
|
1232
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).filter(s => !archivedSet.has(s.sessionId))
|
|
1208
1233
|
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
1209
|
-
<
|
|
1210
|
-
<
|
|
1211
|
-
|
|
1212
|
-
|
|
1234
|
+
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1235
|
+
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}">
|
|
1236
|
+
<span class="wb-session-title">${esc(titleOf(s))}</span>
|
|
1237
|
+
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(s.updatedAt))}</span>
|
|
1238
|
+
</button>
|
|
1239
|
+
<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>
|
|
1240
|
+
</div>`).join('') : `<div class="wb-empty">${esc(t('wb.noSessions'))}</div>`}</div>` : ''
|
|
1213
1241
|
return `<div class="wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}">
|
|
1214
1242
|
<div class="wb-project-head">
|
|
1215
1243
|
<span class="wb-chevron" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
@@ -1250,8 +1278,12 @@ function renderSessions() {
|
|
|
1250
1278
|
const wbIds = new Set()
|
|
1251
1279
|
if (state.wb?.bound) for (const w of state.wbProjects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1252
1280
|
const root = workbenchRoot()
|
|
1253
|
-
const visible = allItems.filter(s => !(state.wb?.bound && (wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))))
|
|
1254
1281
|
const archivedSet = new Set(state.wbArchived || [])
|
|
1282
|
+
const visible = allItems.filter(s => {
|
|
1283
|
+
if (!state.wb?.bound) return true
|
|
1284
|
+
if (archivedSet.has(s.sessionId)) return true
|
|
1285
|
+
return !(wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))
|
|
1286
|
+
})
|
|
1255
1287
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1256
1288
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1257
1289
|
const showArchived = LS.get('showArchivedV1', '0') === '1'
|
|
@@ -1325,6 +1357,8 @@ async function openSession(id) {
|
|
|
1325
1357
|
}
|
|
1326
1358
|
|
|
1327
1359
|
function closeSession() {
|
|
1360
|
+
setComposerFullscreen(false)
|
|
1361
|
+
clearComposerImages()
|
|
1328
1362
|
state.current = null
|
|
1329
1363
|
state.history = emptyHistory()
|
|
1330
1364
|
document.body.classList.remove('in-session')
|
|
@@ -1337,6 +1371,7 @@ function bindNativeBack() {
|
|
|
1337
1371
|
if (!CAP?.isNativePlatform?.()) return
|
|
1338
1372
|
try {
|
|
1339
1373
|
CAP.Plugins?.App?.addListener?.('backButton', () => {
|
|
1374
|
+
if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
|
|
1340
1375
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
1341
1376
|
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1342
1377
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
@@ -1663,7 +1698,7 @@ function eventHtml(entry, ctx = {}) {
|
|
|
1663
1698
|
const blocks = msg.content || data.content || []
|
|
1664
1699
|
const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
|
|
1665
1700
|
if (sysText) {
|
|
1666
|
-
inner = `<details class="event" data-seq="${seq}"><summary>${esc(t('event.systemReminder'))}</summary><pre>${esc(truncate(sysText,
|
|
1701
|
+
inner = `<details class="event event-detail" data-seq="${seq}"><summary>${esc(t('event.systemReminder'))}</summary><pre>${esc(truncate(sysText, 4000))}</pre></details>`
|
|
1667
1702
|
} else {
|
|
1668
1703
|
inner = `<div class="msg ${esc(role)}" data-seq="${seq}"><div class="role">${esc(role === 'user' ? t('role.me') : t('role.dsh'))}</div>${blocks.map(blockHtml).join('')}</div>`
|
|
1669
1704
|
}
|
|
@@ -1841,27 +1876,147 @@ async function runSlashCommand(text) {
|
|
|
1841
1876
|
return false
|
|
1842
1877
|
}
|
|
1843
1878
|
|
|
1879
|
+
function bytesToBase64(bytes) {
|
|
1880
|
+
let binary = ''
|
|
1881
|
+
const step = 0x8000
|
|
1882
|
+
for (let i = 0; i < bytes.length; i += step) {
|
|
1883
|
+
binary += String.fromCharCode(...bytes.subarray(i, Math.min(i + step, bytes.length)))
|
|
1884
|
+
}
|
|
1885
|
+
return btoa(binary)
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function imageTypeOk(type) {
|
|
1889
|
+
return ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(String(type || '').toLowerCase())
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function renderComposerImages() {
|
|
1893
|
+
const box = $('composer-attachments')
|
|
1894
|
+
if (!box) return
|
|
1895
|
+
document.body.classList.toggle('has-composer-images', state.composerImages.length > 0)
|
|
1896
|
+
box.classList.toggle('hidden', state.composerImages.length === 0)
|
|
1897
|
+
box.innerHTML = state.composerImages.map(item => `<div class="composer-attachment" title="${esc(item.file.name || t('block.image'))}">
|
|
1898
|
+
<img src="${esc(item.url)}" alt="${esc(item.file.name || t('block.image'))}">
|
|
1899
|
+
<button type="button" class="composer-attachment-remove" data-remove-image="${esc(item.id)}" aria-label="${esc(t('composer.removeImage'))}">×</button>
|
|
1900
|
+
</div>`).join('')
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function clearComposerImages() {
|
|
1904
|
+
state.composerImages.splice(0).forEach(item => { try { URL.revokeObjectURL(item.url) } catch {} })
|
|
1905
|
+
renderComposerImages()
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function removeComposerImage(id) {
|
|
1909
|
+
const index = state.composerImages.findIndex(item => item.id === id)
|
|
1910
|
+
if (index < 0) return
|
|
1911
|
+
const [item] = state.composerImages.splice(index, 1)
|
|
1912
|
+
try { URL.revokeObjectURL(item.url) } catch {}
|
|
1913
|
+
renderComposerImages()
|
|
1914
|
+
toast(t('composer.imageRemoved'), 'ok')
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function addComposerImages(files) {
|
|
1918
|
+
const incoming = Array.from(files || []).filter(Boolean)
|
|
1919
|
+
if (!incoming.length) return
|
|
1920
|
+
if (state.composerImages.length + incoming.length > 20) {
|
|
1921
|
+
toast(t('composer.imageLimit', { count: 20 }), 'err')
|
|
1922
|
+
return
|
|
1923
|
+
}
|
|
1924
|
+
for (const file of incoming) {
|
|
1925
|
+
if (!imageTypeOk(file.type)) { toast(t('composer.imageUnsupported'), 'err'); continue }
|
|
1926
|
+
if (file.size > 3.5 * 1024 * 1024) { toast(t('composer.imageTooLarge', { size: '3.5 MB' }), 'err'); continue }
|
|
1927
|
+
state.composerImages.push({ id: uuid(), file, url: URL.createObjectURL(file) })
|
|
1928
|
+
}
|
|
1929
|
+
renderComposerImages()
|
|
1930
|
+
if (incoming.length) toast(t('composer.imageAdded'), 'ok')
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function dataUrlToFile(dataUrl, name = 'photo.jpg') {
|
|
1934
|
+
const m = /^data:([^;,]+);base64,(.*)$/i.exec(String(dataUrl || ''))
|
|
1935
|
+
if (!m) return null
|
|
1936
|
+
const bytes = Uint8Array.from(atob(m[2]), c => c.charCodeAt(0))
|
|
1937
|
+
return new File([bytes], name, { type: m[1] })
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
async function captureComposerImage(source) {
|
|
1941
|
+
if (!CAP?.isNativePlatform?.()) {
|
|
1942
|
+
const input = $(source === 'CAMERA' ? 'composer-camera-input' : 'composer-gallery-input')
|
|
1943
|
+
input?.click()
|
|
1944
|
+
return
|
|
1945
|
+
}
|
|
1946
|
+
const camera = CAP.Plugins?.Camera
|
|
1947
|
+
if (!camera?.getPhoto) { toast(t('scan.unsupported'), 'err'); return }
|
|
1948
|
+
try {
|
|
1949
|
+
if (source === 'CAMERA') {
|
|
1950
|
+
const perm = await camera.requestPermissions?.({ permissions: ['camera'] })
|
|
1951
|
+
if (perm && perm.camera !== 'granted') { toast(t('scan.permissionDenied'), 'err'); return }
|
|
1952
|
+
}
|
|
1953
|
+
const photo = await camera.getPhoto({
|
|
1954
|
+
resultType: 'dataUrl', source: source === 'PHOTOS' ? 'PHOTOS' : 'CAMERA', quality: 85,
|
|
1955
|
+
correctOrientation: true, saveToGallery: false
|
|
1956
|
+
})
|
|
1957
|
+
const file = dataUrlToFile(photo?.dataUrl, `dsh-image-${Date.now()}.${photo?.format || 'jpg'}`)
|
|
1958
|
+
if (file) addComposerImages([file])
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
const msg = String(e?.message || e || '')
|
|
1961
|
+
if (!/cancel/i.test(msg)) toast(t('composer.imageReadFailed', { msg }), 'err')
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function toggleComposerImageMenu() {
|
|
1966
|
+
const menu = $('composer-image-menu')
|
|
1967
|
+
if (!menu) return
|
|
1968
|
+
const show = menu.classList.contains('hidden')
|
|
1969
|
+
menu.classList.toggle('hidden', !show)
|
|
1970
|
+
$('btn-image')?.classList.toggle('active', show)
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1844
1973
|
async function sendSessionText(text) {
|
|
1974
|
+
return sendSessionContent(text, [])
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
async function sendSessionContent(text, images) {
|
|
1845
1978
|
const clean = String(text || '').trim()
|
|
1846
|
-
if (!clean || !state.current) return false
|
|
1847
|
-
if (await runSlashCommand(clean)) return true
|
|
1848
|
-
$('btn-send').
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
content
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1979
|
+
if ((!clean && !images.length) || !state.current) return false
|
|
1980
|
+
if (images.length === 0 && clean && await runSlashCommand(clean)) return true
|
|
1981
|
+
const buttons = [$('btn-send'), $('btn-fs-send')].filter(Boolean)
|
|
1982
|
+
buttons.forEach(button => { button.disabled = true })
|
|
1983
|
+
try {
|
|
1984
|
+
const content = [...await encodeComposerImagesFor(images)]
|
|
1985
|
+
if (clean) content.push({ type: 'text', text: clean })
|
|
1986
|
+
const v = await safeRpc('session.prompt', {
|
|
1987
|
+
sessionId: state.current,
|
|
1988
|
+
mode: 'queue',
|
|
1989
|
+
content
|
|
1990
|
+
}, t('send.failed'))
|
|
1991
|
+
if (v?.accepted) { toast(images.length ? t('send.imageSent') : (clean.startsWith('/') ? t('send.commandSent') : t('send.sent')), 'ok'); return true }
|
|
1992
|
+
if (v?.command?.text) { toast(t('send.commandExecuted'), 'ok'); return true }
|
|
1993
|
+
return false
|
|
1994
|
+
} catch (e) {
|
|
1995
|
+
toast(t('composer.imageReadFailed', { msg: e?.message || e }), 'err')
|
|
1996
|
+
return false
|
|
1997
|
+
} finally {
|
|
1998
|
+
buttons.forEach(button => { button.disabled = false })
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
async function encodeComposerImagesFor(images) {
|
|
2003
|
+
return Promise.all(images.map(async item => ({
|
|
2004
|
+
type: 'image', mediaType: item.file.type, data: bytesToBase64(new Uint8Array(await item.file.arrayBuffer())),
|
|
2005
|
+
...(item.file.name ? { name: item.file.name } : {})
|
|
2006
|
+
})))
|
|
1858
2007
|
}
|
|
1859
2008
|
|
|
1860
2009
|
async function sendMessage() {
|
|
1861
2010
|
const input = $('composer-input')
|
|
1862
2011
|
const text = input.value.trim()
|
|
1863
|
-
|
|
1864
|
-
if (
|
|
2012
|
+
const images = state.composerImages.slice()
|
|
2013
|
+
if ((!text && !images.length) || !state.current) return
|
|
2014
|
+
if (images.length && text.startsWith('/')) { toast(t('composer.imageSlashUnsupported'), 'err'); return }
|
|
2015
|
+
if (await sendSessionContent(text, images)) {
|
|
2016
|
+
input.value = ''
|
|
2017
|
+
autosize(input)
|
|
2018
|
+
clearComposerImages()
|
|
2019
|
+
}
|
|
1865
2020
|
}
|
|
1866
2021
|
|
|
1867
2022
|
function hideComposerMenu() {
|
|
@@ -2031,13 +2186,13 @@ async function confirmArchiveSession() {
|
|
|
2031
2186
|
let swipeTracking = null
|
|
2032
2187
|
let swipeSuppressClickUntil = 0
|
|
2033
2188
|
function closeRevealedSwipes(except = null) {
|
|
2034
|
-
document.querySelectorAll('
|
|
2189
|
+
document.querySelectorAll('.session-swipe.revealed').forEach(row => {
|
|
2035
2190
|
if (row !== except) row.classList.remove('revealed')
|
|
2036
2191
|
})
|
|
2037
2192
|
}
|
|
2038
2193
|
function bindSessionSwipe() {
|
|
2039
|
-
const
|
|
2040
|
-
list.addEventListener('touchstart', e => {
|
|
2194
|
+
const containers = [$('session-list'), $('wb-panel')].filter(Boolean)
|
|
2195
|
+
for (const list of containers) list.addEventListener('touchstart', e => {
|
|
2041
2196
|
if (e.touches.length !== 1) return
|
|
2042
2197
|
const row = e.target.closest('[data-session-swipe]')
|
|
2043
2198
|
if (!row) return
|
|
@@ -2051,7 +2206,7 @@ function bindSessionSwipe() {
|
|
|
2051
2206
|
axis: null
|
|
2052
2207
|
}
|
|
2053
2208
|
}, { passive: true })
|
|
2054
|
-
list.addEventListener('touchmove', e => {
|
|
2209
|
+
for (const list of containers) list.addEventListener('touchmove', e => {
|
|
2055
2210
|
if (!swipeTracking || e.touches.length !== 1) return
|
|
2056
2211
|
const touch = e.touches[0]
|
|
2057
2212
|
const dx = touch.clientX - swipeTracking.startX
|
|
@@ -2064,7 +2219,7 @@ function bindSessionSwipe() {
|
|
|
2064
2219
|
const offset = Math.max(-92, Math.min(0, swipeTracking.offset + dx))
|
|
2065
2220
|
swipeTracking.row.style.setProperty('--swipe-x', offset + 'px')
|
|
2066
2221
|
}, { passive: false })
|
|
2067
|
-
list.addEventListener('touchend', () => {
|
|
2222
|
+
for (const list of containers) list.addEventListener('touchend', () => {
|
|
2068
2223
|
if (!swipeTracking) return
|
|
2069
2224
|
if (swipeTracking.axis === 'x') {
|
|
2070
2225
|
const row = swipeTracking.row
|
|
@@ -2075,10 +2230,99 @@ function bindSessionSwipe() {
|
|
|
2075
2230
|
}
|
|
2076
2231
|
swipeTracking = null
|
|
2077
2232
|
}, { passive: true })
|
|
2078
|
-
list.addEventListener('touchcancel', () => { swipeTracking = null }, { passive: true })
|
|
2233
|
+
for (const list of containers) list.addEventListener('touchcancel', () => { swipeTracking = null }, { passive: true })
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/* ---------------- 系统总览 / 待办 ---------------- */
|
|
2237
|
+
function renderOverview() {
|
|
2238
|
+
const ring = $('overview-pulse-ring')
|
|
2239
|
+
if (!ring) return
|
|
2240
|
+
const checks = {
|
|
2241
|
+
gateway: !!state.token && !!state.server,
|
|
2242
|
+
dsh: !!state.hostInfo,
|
|
2243
|
+
mux: !!state.streamsOk?.mux,
|
|
2244
|
+
host: !!state.streamsOk?.host
|
|
2245
|
+
}
|
|
2246
|
+
const online = Object.values(checks).filter(Boolean).length
|
|
2247
|
+
const status = online === 4 ? 'nominal' : online > 0 ? 'degraded' : 'offline'
|
|
2248
|
+
const pulseCard = document.querySelector('.overview-pulse-card')
|
|
2249
|
+
if (pulseCard) {
|
|
2250
|
+
pulseCard.classList.remove('status-nominal', 'status-degraded', 'status-offline')
|
|
2251
|
+
pulseCard.classList.add('status-' + status)
|
|
2252
|
+
}
|
|
2253
|
+
ring.style.setProperty('--pulse-pct', `${online / 4 * 100}%`)
|
|
2254
|
+
$('overview-health').textContent = online === 4 ? t('overview.live') : online ? `${online}/4` : t('overview.offlineCore')
|
|
2255
|
+
$('overview-health-caption').textContent = online === 4 ? t('overview.allLinked') : online ? t('overview.components', { n: online }) : t('overview.offlineShort')
|
|
2256
|
+
$('overview-status').textContent = t(`overview.${status}`)
|
|
2257
|
+
$('overview-status-desc').textContent = t('overview.components', { n: online })
|
|
2258
|
+
for (const [name, ok] of Object.entries(checks)) {
|
|
2259
|
+
const item = document.querySelector(`[data-overview-link="${name}"]`)
|
|
2260
|
+
if (!item) continue
|
|
2261
|
+
item.classList.toggle('ok', ok)
|
|
2262
|
+
item.classList.toggle('off', !ok)
|
|
2263
|
+
const value = item.querySelector('b')
|
|
2264
|
+
if (value) value.textContent = ok ? t('overview.online') : t('overview.offlineShort')
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
const pending = [
|
|
2268
|
+
...state.approvals.map(a => ({ kind: 'approval', item: a })),
|
|
2269
|
+
...state.questions.map(q => ({ kind: 'question', item: q }))
|
|
2270
|
+
]
|
|
2271
|
+
$('overview-attention-count').textContent = pending.length ? t('overview.pendingCount', { n: pending.length }) : '—'
|
|
2272
|
+
$('overview-attention-list').innerHTML = pending.length ? pending.slice(0, 3).map(({ kind, item }) => {
|
|
2273
|
+
const title = titleOf(state.byId.get(item.sessionId))
|
|
2274
|
+
if (kind === 'approval') return `<div class="overview-attention-item" data-overview-approval="${esc(item.approvalId)}">
|
|
2275
|
+
<span class="overview-item-mark">⌁</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.toolName || t('tool.default'))}</span><span class="overview-item-desc">${esc(item.reason || t('pending.noReason'))} · ${esc(title)}</span></span>
|
|
2276
|
+
<span class="overview-item-actions"><button type="button" class="mini-btn" data-overview-approve="1">${t('pending.allow')}</button><button type="button" class="mini-btn" data-overview-approve="0">${t('pending.reject')}</button></span>
|
|
2277
|
+
</div>`
|
|
2278
|
+
return `<button type="button" class="overview-attention-item question" data-overview-question="${esc(item.rpcId)}">
|
|
2279
|
+
<span class="overview-item-mark">?</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.questions?.[0]?.question || t('notify.questionTitle'))}</span><span class="overview-item-desc">${esc(title)}</span></span><span class="overview-item-arrow">›</span>
|
|
2280
|
+
</button>`
|
|
2281
|
+
}).join('') : `<div class="overview-empty">${t('pending.empty')}</div>`
|
|
2282
|
+
$('overview-attention-list').querySelectorAll('[data-overview-approve]').forEach(btn => {
|
|
2283
|
+
btn.addEventListener('click', () => approveApproval(btn.closest('[data-overview-approval]')?.dataset.overviewApproval || '', btn.dataset.overviewApprove === '1'))
|
|
2284
|
+
})
|
|
2285
|
+
$('overview-attention-list').querySelectorAll('[data-overview-question]').forEach(btn => {
|
|
2286
|
+
btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.overviewQuestion)))
|
|
2287
|
+
})
|
|
2288
|
+
|
|
2289
|
+
const running = state.sessions.filter(s => s.running).length
|
|
2290
|
+
const sessions = [...state.sessions].sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 4)
|
|
2291
|
+
const primary = $('overview-primary-action')
|
|
2292
|
+
if (primary) {
|
|
2293
|
+
let action = 'new'
|
|
2294
|
+
let label = t('overview.action.newSession')
|
|
2295
|
+
let sessionId = ''
|
|
2296
|
+
if (!state.token) {
|
|
2297
|
+
action = 'settings'
|
|
2298
|
+
label = t('overview.action.connect')
|
|
2299
|
+
} else if (online > 0 && online < 4) {
|
|
2300
|
+
action = 'refresh'
|
|
2301
|
+
label = t('overview.action.refresh')
|
|
2302
|
+
} else if (pending.length) {
|
|
2303
|
+
action = 'attention'
|
|
2304
|
+
label = t('overview.action.attention')
|
|
2305
|
+
} else if (sessions.length) {
|
|
2306
|
+
action = 'session'
|
|
2307
|
+
sessionId = sessions[0].sessionId
|
|
2308
|
+
label = t('overview.action.openSession')
|
|
2309
|
+
}
|
|
2310
|
+
primary.textContent = label
|
|
2311
|
+
primary.dataset.overviewAction = action
|
|
2312
|
+
primary.dataset.overviewSession = sessionId
|
|
2313
|
+
primary.disabled = status === 'offline' && action === 'refresh'
|
|
2314
|
+
}
|
|
2315
|
+
$('overview-dsh-version').textContent = state.hostInfo?.version || '—'
|
|
2316
|
+
$('overview-gateway-version').textContent = checks.gateway ? t('overview.online') : t('overview.offlineShort')
|
|
2317
|
+
$('overview-active-sessions').textContent = String(running)
|
|
2318
|
+
$('overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'overview.poll' : 'overview.ws') : '—'
|
|
2319
|
+
$('overview-active-count').textContent = running ? t('overview.activeCount', { n: running }) : ''
|
|
2320
|
+
$('overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="overview-session-item ${s.running ? 'running' : ''}" data-overview-session="${esc(s.sessionId)}">
|
|
2321
|
+
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(titleOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(s.updatedAt))}</span></span><span class="overview-item-arrow">›</span>
|
|
2322
|
+
</button>`).join('') : `<div class="overview-empty">${t('overview.noSession')}</div>`
|
|
2323
|
+
$('overview-session-list').querySelectorAll('[data-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.overviewSession)))
|
|
2079
2324
|
}
|
|
2080
2325
|
|
|
2081
|
-
/* ---------------- 待办 ---------------- */
|
|
2082
2326
|
function renderPending() {
|
|
2083
2327
|
const list = $('pending-list')
|
|
2084
2328
|
const items = [
|
|
@@ -2113,6 +2357,7 @@ function renderPending() {
|
|
|
2113
2357
|
list.querySelectorAll('[data-question]').forEach(btn =>
|
|
2114
2358
|
btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.question))))
|
|
2115
2359
|
updatePendingBadge()
|
|
2360
|
+
renderOverview()
|
|
2116
2361
|
}
|
|
2117
2362
|
|
|
2118
2363
|
async function approveApproval(id, allow) {
|
|
@@ -2306,7 +2551,7 @@ function renderFs(data) {
|
|
|
2306
2551
|
list.innerHTML = data.entries.map(e => {
|
|
2307
2552
|
const isDir = e.type === 'dir'
|
|
2308
2553
|
return `<div class="fs-row" data-name="${esc(e.name)}" data-type="${esc(e.type)}">
|
|
2309
|
-
<span class="fs-ico">${isDir
|
|
2554
|
+
<span class="fs-ico">${fsIconSvg(isDir)}</span>
|
|
2310
2555
|
<span class="fs-meta">
|
|
2311
2556
|
<span class="fs-name">${esc(e.name)}</span>
|
|
2312
2557
|
<span class="fs-sub">${isDir ? t('fs.dir') : fmtSize(e.size)} · ${fmtFullTime(e.mtimeMs)}</span>
|
|
@@ -2318,6 +2563,12 @@ function renderFs(data) {
|
|
|
2318
2563
|
row.addEventListener('click', () => fsOpenEntry(row.dataset.name, row.dataset.type)))
|
|
2319
2564
|
}
|
|
2320
2565
|
|
|
2566
|
+
function fsIconSvg(isDir) {
|
|
2567
|
+
return isDir
|
|
2568
|
+
? '<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="M3.5 6.5h6l2 2H20a1 1 0 0 1 1 1v8.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7.5a1 1 0 0 1 .5-1Z"/></svg>'
|
|
2569
|
+
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3.5h8l4 4V20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1Z"/><path d="M14 3.5v4h4M8 13h8M8 16h6"/></svg>'
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2321
2572
|
function fsOpenEntry(name, type) {
|
|
2322
2573
|
if (!name) return
|
|
2323
2574
|
const p = fsJoin(state.fs.path, name)
|
|
@@ -2744,6 +2995,7 @@ async function loadLocalVersion() {
|
|
|
2744
2995
|
* 公告只读取文本并用 textContent/转义后的换行渲染,不执行服务端下发的 HTML/脚本。
|
|
2745
2996
|
*/
|
|
2746
2997
|
const ANNOUNCEMENTS_KEY = 'seenAnnouncementsV1'
|
|
2998
|
+
const ANNOUNCEMENT_HISTORY_KEY = 'announcementHistoryV1'
|
|
2747
2999
|
function readSeenAnnouncements() {
|
|
2748
3000
|
try {
|
|
2749
3001
|
const value = JSON.parse(LS.get(ANNOUNCEMENTS_KEY, '{}'))
|
|
@@ -2761,6 +3013,40 @@ function markAnnouncementSeen(id) {
|
|
|
2761
3013
|
}
|
|
2762
3014
|
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
2763
3015
|
}
|
|
3016
|
+
function readAnnouncementHistory() {
|
|
3017
|
+
try {
|
|
3018
|
+
const value = JSON.parse(LS.get(ANNOUNCEMENT_HISTORY_KEY, '[]'))
|
|
3019
|
+
return Array.isArray(value) ? value.filter(item => item && typeof item.id === 'string') : []
|
|
3020
|
+
} catch { return [] }
|
|
3021
|
+
}
|
|
3022
|
+
function storeAnnouncementHistory(items) {
|
|
3023
|
+
const merged = new Map(readAnnouncementHistory().map(item => [item.id, item]))
|
|
3024
|
+
for (const item of items) if (item?.id) merged.set(item.id, item)
|
|
3025
|
+
const list = [...merged.values()].sort((a, b) => Number(b.publishedAt || 0) - Number(a.publishedAt || 0)).slice(0, 50)
|
|
3026
|
+
LS.set(ANNOUNCEMENT_HISTORY_KEY, JSON.stringify(list))
|
|
3027
|
+
return list
|
|
3028
|
+
}
|
|
3029
|
+
function renderAnnouncementHistory() {
|
|
3030
|
+
const box = $('announcement-history-list')
|
|
3031
|
+
if (!box) return
|
|
3032
|
+
const list = readAnnouncementHistory()
|
|
3033
|
+
if (!list.length) {
|
|
3034
|
+
box.innerHTML = `<div class="empty">${esc(t('announcement.historyEmpty'))}</div>`
|
|
3035
|
+
return
|
|
3036
|
+
}
|
|
3037
|
+
box.innerHTML = list.map(item => {
|
|
3038
|
+
const date = Number(item.publishedAt) > 0 ? fmtFullTime(item.publishedAt) : t('announcement.noDate')
|
|
3039
|
+
const action = item.actionUrl ? `<a class="announcement-action" href="${esc(item.actionUrl)}" target="_blank" rel="noopener">${esc(item.actionText || t('announcement.open'))}</a>` : ''
|
|
3040
|
+
return `<details class="announcement-history-item"><summary><span>${esc(item.title)}</span><small>${esc(date)}</small></summary><div class="announcement-history-content">${esc(item.content).replace(/\r?\n/g, '<br>')}${action}</div></details>`
|
|
3041
|
+
}).join('')
|
|
3042
|
+
}
|
|
3043
|
+
function openAnnouncementHistory() {
|
|
3044
|
+
renderAnnouncementHistory()
|
|
3045
|
+
$('modal-announcement-history')?.classList.remove('hidden')
|
|
3046
|
+
}
|
|
3047
|
+
function closeAnnouncementHistory() {
|
|
3048
|
+
$('modal-announcement-history')?.classList.add('hidden')
|
|
3049
|
+
}
|
|
2764
3050
|
function announcementVersionMatch(item) {
|
|
2765
3051
|
const min = String(item.minVersion || item.minAppVersion || '').trim()
|
|
2766
3052
|
const max = String(item.maxVersion || item.maxAppVersion || '').trim()
|
|
@@ -2829,8 +3115,10 @@ async function checkAnnouncements() {
|
|
|
2829
3115
|
if (raw.length > 512 * 1024) return false
|
|
2830
3116
|
const data = JSON.parse(raw)
|
|
2831
3117
|
const source = Array.isArray(data) ? data : (Array.isArray(data?.items) ? data.items : [data])
|
|
3118
|
+
const normalized = source.map(item => normalizeAnnouncement(item, base)).filter(Boolean)
|
|
3119
|
+
storeAnnouncementHistory(normalized)
|
|
2832
3120
|
const seen = readSeenAnnouncements()
|
|
2833
|
-
const items =
|
|
3121
|
+
const items = normalized.filter(item => !seen[item.id])
|
|
2834
3122
|
.sort((a, b) => b.publishedAt - a.publishedAt)
|
|
2835
3123
|
if (!items.length) return false
|
|
2836
3124
|
openAnnouncementModal(items[0])
|
|
@@ -3294,8 +3582,84 @@ function updateConn() {
|
|
|
3294
3582
|
}
|
|
3295
3583
|
|
|
3296
3584
|
function autosize(el) {
|
|
3585
|
+
// 全屏编辑时 textarea 由 flex 容器提供整块高度;普通的 120px 限高
|
|
3586
|
+
// 不能覆盖这里,否则输入超过约五行后会被重新压回小输入框。
|
|
3587
|
+
if (el?.id === 'composer-input' && $('composer-wrap')?.classList.contains('fs')) {
|
|
3588
|
+
el.style.height = '100%'
|
|
3589
|
+
updateComposerFullscreenButton()
|
|
3590
|
+
return
|
|
3591
|
+
}
|
|
3297
3592
|
el.style.height = 'auto'
|
|
3298
3593
|
el.style.height = Math.min(el.scrollHeight, 120) + 'px'
|
|
3594
|
+
if (el.id === 'composer-input') updateComposerFullscreenButton()
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
function updateComposerFullscreenButton() {
|
|
3598
|
+
const input = $('composer-input')
|
|
3599
|
+
const wrap = $('composer-wrap')
|
|
3600
|
+
const button = $('btn-fs-toggle')
|
|
3601
|
+
if (!input || !wrap || !button) return
|
|
3602
|
+
const active = wrap.classList.contains('fs')
|
|
3603
|
+
const shouldShow = active || input.scrollHeight > 120
|
|
3604
|
+
button.classList.toggle('hidden', !shouldShow)
|
|
3605
|
+
$('composer-input-wrap')?.classList.toggle('has-fs-btn', shouldShow)
|
|
3606
|
+
$('fs-ico-expand')?.classList.toggle('hidden', active)
|
|
3607
|
+
$('fs-ico-collapse')?.classList.toggle('hidden', !active)
|
|
3608
|
+
button.title = t(active ? 'composer.exitFullscreen' : 'composer.fullscreen')
|
|
3609
|
+
button.setAttribute('aria-label', t(active ? 'composer.exitFullscreen' : 'composer.fullscreen'))
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function setComposerFullscreen(on) {
|
|
3613
|
+
const wrap = $('composer-wrap')
|
|
3614
|
+
if (!wrap) return
|
|
3615
|
+
$('btn-stats')?.classList.toggle('hidden', !!on)
|
|
3616
|
+
$('btn-fs-send')?.classList.toggle('hidden', !on)
|
|
3617
|
+
if (on) {
|
|
3618
|
+
$('composer-image-menu')?.classList.add('hidden')
|
|
3619
|
+
$('btn-image')?.classList.remove('active')
|
|
3620
|
+
}
|
|
3621
|
+
wrap.classList.toggle('fs', !!on)
|
|
3622
|
+
document.body.classList.toggle('composer-fullscreen', !!on)
|
|
3623
|
+
if (on) {
|
|
3624
|
+
$('composer-input')?.style.removeProperty('height')
|
|
3625
|
+
} else {
|
|
3626
|
+
wrap.style.transform = ''
|
|
3627
|
+
wrap.classList.remove('dragging')
|
|
3628
|
+
autosize($('composer-input'))
|
|
3629
|
+
}
|
|
3630
|
+
updateComposerFullscreenButton()
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
function bindComposerFullscreenGesture() {
|
|
3634
|
+
const handle = $('composer-fs-handle')
|
|
3635
|
+
if (!handle) return
|
|
3636
|
+
let startY = 0
|
|
3637
|
+
let tracking = false
|
|
3638
|
+
handle.addEventListener('touchstart', e => {
|
|
3639
|
+
if (!$('composer-wrap').classList.contains('fs') || e.touches.length !== 1) return
|
|
3640
|
+
tracking = true
|
|
3641
|
+
startY = e.touches[0].clientY
|
|
3642
|
+
}, { passive: true })
|
|
3643
|
+
handle.addEventListener('touchmove', e => {
|
|
3644
|
+
if (!tracking || e.touches.length !== 1) return
|
|
3645
|
+
const dy = e.touches[0].clientY - startY
|
|
3646
|
+
if (dy <= 0) return
|
|
3647
|
+
e.preventDefault()
|
|
3648
|
+
$('composer-wrap').classList.add('dragging')
|
|
3649
|
+
$('composer-wrap').style.transform = `translateY(${Math.min(dy, 180)}px)`
|
|
3650
|
+
}, { passive: false })
|
|
3651
|
+
const finish = () => {
|
|
3652
|
+
if (!tracking) return
|
|
3653
|
+
const wrap = $('composer-wrap')
|
|
3654
|
+
const transform = wrap.style.transform.match(/translateY\(([-\d.]+)px\)/)
|
|
3655
|
+
const dy = transform ? Number(transform[1]) : 0
|
|
3656
|
+
tracking = false
|
|
3657
|
+
wrap.classList.remove('dragging')
|
|
3658
|
+
if (dy > 60) setComposerFullscreen(false)
|
|
3659
|
+
else wrap.style.transform = ''
|
|
3660
|
+
}
|
|
3661
|
+
handle.addEventListener('touchend', finish, { passive: true })
|
|
3662
|
+
handle.addEventListener('touchcancel', finish, { passive: true })
|
|
3299
3663
|
}
|
|
3300
3664
|
|
|
3301
3665
|
/* ---------------- 初始化 ---------------- */
|
|
@@ -3547,6 +3911,19 @@ function bindUi() {
|
|
|
3547
3911
|
// 底部导航
|
|
3548
3912
|
document.querySelectorAll('.nav-btn').forEach(b =>
|
|
3549
3913
|
b.addEventListener('click', () => showView(b.dataset.view)))
|
|
3914
|
+
$('overview-primary-action').addEventListener('click', () => {
|
|
3915
|
+
const button = $('overview-primary-action')
|
|
3916
|
+
const action = button.dataset.overviewAction
|
|
3917
|
+
if (action === 'session' && button.dataset.overviewSession) return openSession(button.dataset.overviewSession)
|
|
3918
|
+
if (action === 'new') return newSession()
|
|
3919
|
+
if (action === 'settings') return showView('view-settings')
|
|
3920
|
+
if (action === 'refresh') return openStreams()
|
|
3921
|
+
const first = document.querySelector('.overview-attention-item')
|
|
3922
|
+
if (first) {
|
|
3923
|
+
first.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
3924
|
+
if (first.matches('button')) first.focus({ preventScroll: true })
|
|
3925
|
+
}
|
|
3926
|
+
})
|
|
3550
3927
|
// 会话列表点击
|
|
3551
3928
|
$('session-list').addEventListener('click', (e) => {
|
|
3552
3929
|
if (swipeSuppressClickUntil > Date.now()) { swipeSuppressClickUntil = 0; return }
|
|
@@ -3576,6 +3953,12 @@ function bindUi() {
|
|
|
3576
3953
|
renderWorkbench()
|
|
3577
3954
|
})
|
|
3578
3955
|
$('wb-panel').addEventListener('click', (e) => {
|
|
3956
|
+
const archive = e.target.closest('[data-archive-session]')
|
|
3957
|
+
if (archive) {
|
|
3958
|
+
e.stopPropagation()
|
|
3959
|
+
archiveSession(archive.dataset.archiveSession)
|
|
3960
|
+
return
|
|
3961
|
+
}
|
|
3579
3962
|
const newButton = e.target.closest('[data-wb-new]')
|
|
3580
3963
|
if (newButton) {
|
|
3581
3964
|
safeRpc('session.create', { workspaceId: newButton.dataset.wbNew }, t('home.createFailed')).then(async v => {
|
|
@@ -3634,7 +4017,28 @@ function bindUi() {
|
|
|
3634
4017
|
})
|
|
3635
4018
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
3636
4019
|
$('btn-send').addEventListener('click', sendMessage)
|
|
4020
|
+
$('btn-fs-send').addEventListener('click', sendMessage)
|
|
3637
4021
|
$('btn-plus').addEventListener('click', toggleComposerMenu)
|
|
4022
|
+
$('btn-image').addEventListener('click', toggleComposerImageMenu)
|
|
4023
|
+
$('composer-image-menu').addEventListener('click', (e) => {
|
|
4024
|
+
const option = e.target.closest('[data-image-source]')
|
|
4025
|
+
if (!option) return
|
|
4026
|
+
$('composer-image-menu').classList.add('hidden')
|
|
4027
|
+
$('btn-image').classList.remove('active')
|
|
4028
|
+
captureComposerImage(option.dataset.imageSource)
|
|
4029
|
+
})
|
|
4030
|
+
$('composer-attachments').addEventListener('click', (e) => {
|
|
4031
|
+
const button = e.target.closest('[data-remove-image]')
|
|
4032
|
+
if (button) removeComposerImage(button.dataset.removeImage)
|
|
4033
|
+
})
|
|
4034
|
+
$('composer-camera-input').addEventListener('change', (e) => { addComposerImages(e.target.files); e.target.value = '' })
|
|
4035
|
+
$('composer-gallery-input').addEventListener('change', (e) => { addComposerImages(e.target.files); e.target.value = '' })
|
|
4036
|
+
document.addEventListener('click', (e) => {
|
|
4037
|
+
if (!e.target.closest('#composer-image-menu, #btn-image')) {
|
|
4038
|
+
$('composer-image-menu')?.classList.add('hidden')
|
|
4039
|
+
$('btn-image')?.classList.remove('active')
|
|
4040
|
+
}
|
|
4041
|
+
})
|
|
3638
4042
|
$('composer-menu').addEventListener('click', async (e) => {
|
|
3639
4043
|
const chip = e.target.closest('[data-cmd]')
|
|
3640
4044
|
if (chip) {
|
|
@@ -3675,6 +4079,9 @@ function bindUi() {
|
|
|
3675
4079
|
$('btn-model-refresh').addEventListener('click', loadSessionModels)
|
|
3676
4080
|
const input = $('composer-input')
|
|
3677
4081
|
input.addEventListener('input', () => autosize(input))
|
|
4082
|
+
$('btn-fs-toggle').addEventListener('click', () => setComposerFullscreen(!$('composer-wrap').classList.contains('fs')))
|
|
4083
|
+
bindComposerFullscreenGesture()
|
|
4084
|
+
updateComposerFullscreenButton()
|
|
3678
4085
|
input.addEventListener('keydown', (e) => {
|
|
3679
4086
|
if (e.key !== 'Enter' || e.isComposing) return
|
|
3680
4087
|
if (isMobileDevice() && mobileEnterAction() !== 'send') return
|
|
@@ -3708,6 +4115,10 @@ function bindUi() {
|
|
|
3708
4115
|
$('modal-announcement').addEventListener('click', (e) => {
|
|
3709
4116
|
if (e.target === $('modal-announcement') && !state.announcement?.force) closeAnnouncement(false)
|
|
3710
4117
|
})
|
|
4118
|
+
$('announcement-history-close').addEventListener('click', closeAnnouncementHistory)
|
|
4119
|
+
$('modal-announcement-history').addEventListener('click', (e) => {
|
|
4120
|
+
if (e.target === $('modal-announcement-history')) closeAnnouncementHistory()
|
|
4121
|
+
})
|
|
3711
4122
|
// 设置
|
|
3712
4123
|
$('view-settings').addEventListener('click', (e) => {
|
|
3713
4124
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -3744,7 +4155,7 @@ function bindUi() {
|
|
|
3744
4155
|
$('btn-update-expand').addEventListener('click', toggleUpdateExpand)
|
|
3745
4156
|
$('btn-reset').addEventListener('click', () => {
|
|
3746
4157
|
if (!confirm(t('settings.confirmReset'))) return
|
|
3747
|
-
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction')
|
|
4158
|
+
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY)
|
|
3748
4159
|
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
3749
4160
|
location.reload()
|
|
3750
4161
|
})
|
|
@@ -3788,6 +4199,7 @@ function bindUi() {
|
|
|
3788
4199
|
LS.set('peakRemind', e.target.checked ? '1' : '0')
|
|
3789
4200
|
})
|
|
3790
4201
|
$('btn-test-notify').addEventListener('click', sendTestNotification)
|
|
4202
|
+
$('btn-announcement-history').addEventListener('click', openAnnouncementHistory)
|
|
3791
4203
|
// 已开启则启动时重新调度, 防止系统清理后丢失
|
|
3792
4204
|
if (peakRemindOn() && CAP?.isNativePlatform?.()) schedulePeakReminders()
|
|
3793
4205
|
applyBgConfigFromNative()
|
|
@@ -3883,10 +4295,11 @@ async function boot() {
|
|
|
3883
4295
|
bindNativeBack()
|
|
3884
4296
|
bindNativeLinks()
|
|
3885
4297
|
applyNativeInsets()
|
|
4298
|
+
showView('view-activity')
|
|
3886
4299
|
updateConn()
|
|
3887
4300
|
await loadLocalVersion()
|
|
3888
4301
|
if (!state.token) {
|
|
3889
|
-
showView('view-
|
|
4302
|
+
showView('view-activity')
|
|
3890
4303
|
$('token-desc').textContent = t('token.notSetHint')
|
|
3891
4304
|
} else {
|
|
3892
4305
|
// 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
|