dsh-remote-plugin 0.6.6 → 0.6.7
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 +119 -1
- package/index.mjs +10 -1
- package/package.json +1 -1
- package/public/app.js +419 -39
- package/public/desktop/desktop.css +45 -2
- package/public/desktop/desktop.html +40 -0
- package/public/desktop/desktop.js +283 -17
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +58 -2
- package/public/styles.css +44 -0
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -52,6 +52,7 @@ const state = {
|
|
|
52
52
|
hostInfo: null,
|
|
53
53
|
localVersion: '',
|
|
54
54
|
updateInfo: null,
|
|
55
|
+
announcement: null,
|
|
55
56
|
approvals: [], // 待处理审批
|
|
56
57
|
questions: [], // 待处理提问
|
|
57
58
|
queues: {}, // sessionId -> queue items
|
|
@@ -62,7 +63,12 @@ const state = {
|
|
|
62
63
|
pollSeq: { mux: 0, host: 0 },
|
|
63
64
|
refreshTimer: null,
|
|
64
65
|
fs: { path: null, initial: null, loaded: false, upload: null },
|
|
65
|
-
models: { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
66
|
+
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
67
|
+
wb: null,
|
|
68
|
+
wbProjects: [],
|
|
69
|
+
wbArchived: [],
|
|
70
|
+
wbOpen: false,
|
|
71
|
+
wbOpenProjects: {}
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const $ = (id) => document.getElementById(id)
|
|
@@ -1090,6 +1096,7 @@ async function refreshSessions() {
|
|
|
1090
1096
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1091
1097
|
cacheWrite(CACHE.sessions, state.sessions.slice(0, 80))
|
|
1092
1098
|
renderSessions()
|
|
1099
|
+
refreshWorkbench()
|
|
1093
1100
|
}
|
|
1094
1101
|
|
|
1095
1102
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
@@ -1123,6 +1130,96 @@ function updatePendingBadge() {
|
|
|
1123
1130
|
if (pending) $('nav-pending').textContent = pending
|
|
1124
1131
|
}
|
|
1125
1132
|
|
|
1133
|
+
/* ---------------- 工作台与归档会话 ---------------- */
|
|
1134
|
+
function wbPathKey(p) {
|
|
1135
|
+
let value = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1136
|
+
if (value.length > 1) value = value.replace(/\/+$/, '')
|
|
1137
|
+
const windows = /^[A-Za-z]:\//.test(value) || /Windows/i.test(navigator.platform || navigator.userAgent || '')
|
|
1138
|
+
return windows ? value.toLowerCase() : value
|
|
1139
|
+
}
|
|
1140
|
+
function wbBaseName(p) {
|
|
1141
|
+
const value = String(p || '').replace(/[\\/]+$/, '')
|
|
1142
|
+
return value.split(/[\\/]/).pop() || value
|
|
1143
|
+
}
|
|
1144
|
+
function wbStrictInside(pathValue, rootValue) {
|
|
1145
|
+
const pathKey = wbPathKey(pathValue)
|
|
1146
|
+
const rootKey = wbPathKey(rootValue)
|
|
1147
|
+
if (!pathKey || !rootKey || pathKey === rootKey) return false
|
|
1148
|
+
return pathKey.startsWith(rootKey.endsWith('/') ? rootKey : rootKey + '/')
|
|
1149
|
+
}
|
|
1150
|
+
function wbJoin(root, name) {
|
|
1151
|
+
const raw = String(root || '')
|
|
1152
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1153
|
+
return raw.replace(/[\\/]+$/, '') + separator + String(name || '')
|
|
1154
|
+
}
|
|
1155
|
+
function workbenchRoot() {
|
|
1156
|
+
return state.wb?.bound && state.wb.path ? state.wb.path : ''
|
|
1157
|
+
}
|
|
1158
|
+
async function refreshWorkbench() {
|
|
1159
|
+
if (!state.token) return
|
|
1160
|
+
try {
|
|
1161
|
+
const res = await fetch(apiUrl('/workbench'), {
|
|
1162
|
+
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' }
|
|
1163
|
+
})
|
|
1164
|
+
if (res.ok) {
|
|
1165
|
+
const value = await res.json().catch(() => null)
|
|
1166
|
+
if (value && typeof value.bound === 'boolean') state.wb = value
|
|
1167
|
+
}
|
|
1168
|
+
} catch {}
|
|
1169
|
+
try {
|
|
1170
|
+
const value = await rpc('workspace.list', {})
|
|
1171
|
+
state.wbProjects = Array.isArray(value?.items) ? value.items : []
|
|
1172
|
+
state.wbArchived = Array.isArray(value?.archivedSessionIds) ? value.archivedSessionIds : []
|
|
1173
|
+
} catch {
|
|
1174
|
+
state.wbProjects = []
|
|
1175
|
+
state.wbArchived = []
|
|
1176
|
+
}
|
|
1177
|
+
renderWorkbench()
|
|
1178
|
+
renderSessions()
|
|
1179
|
+
}
|
|
1180
|
+
function renderWorkbench() {
|
|
1181
|
+
const bar = $('workbench-bar')
|
|
1182
|
+
if (!bar) return
|
|
1183
|
+
const bound = !!state.wb?.bound && !!state.wb.path
|
|
1184
|
+
const toggle = $('wb-toggle')
|
|
1185
|
+
const panel = $('wb-panel')
|
|
1186
|
+
bar.classList.toggle('bound', bound)
|
|
1187
|
+
bar.classList.toggle('unbound', !bound)
|
|
1188
|
+
if (!bound) {
|
|
1189
|
+
$('wb-label').textContent = t('wb.unbound')
|
|
1190
|
+
toggle.setAttribute('aria-expanded', 'false')
|
|
1191
|
+
panel.classList.add('hidden')
|
|
1192
|
+
panel.innerHTML = ''
|
|
1193
|
+
return
|
|
1194
|
+
}
|
|
1195
|
+
$('wb-label').textContent = t('wb.bound', { title: state.wb.title || wbBaseName(state.wb.path) })
|
|
1196
|
+
toggle.setAttribute('aria-expanded', state.wbOpen ? 'true' : 'false')
|
|
1197
|
+
panel.classList.toggle('hidden', !state.wbOpen)
|
|
1198
|
+
if (!state.wbOpen) { panel.innerHTML = ''; return }
|
|
1199
|
+
const projects = state.wbProjects.filter(w => wbStrictInside(w.path, workbenchRoot()))
|
|
1200
|
+
if (!projects.length) {
|
|
1201
|
+
panel.innerHTML = `<div class="wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1202
|
+
return
|
|
1203
|
+
}
|
|
1204
|
+
panel.innerHTML = projects.map(w => {
|
|
1205
|
+
const id = String(w.workspaceId || '')
|
|
1206
|
+
const open = !!state.wbOpenProjects[id]
|
|
1207
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean)
|
|
1208
|
+
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
1209
|
+
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}">
|
|
1210
|
+
<span class="wb-session-title">${esc(titleOf(s))}</span>
|
|
1211
|
+
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(s.updatedAt))}</span>
|
|
1212
|
+
</button>`).join('') : `<div class="wb-empty">${esc(t('wb.noSessions'))}</div>`}</div>` : ''
|
|
1213
|
+
return `<div class="wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}">
|
|
1214
|
+
<div class="wb-project-head">
|
|
1215
|
+
<span class="wb-chevron" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1216
|
+
<span class="wb-project-title">${esc(w.title || wbBaseName(w.path) || w.path)}</span>
|
|
1217
|
+
<button class="mini-btn wb-new" type="button" data-wb-new="${esc(id)}">${esc(t('wb.newSession'))}</button>
|
|
1218
|
+
</div>${body}
|
|
1219
|
+
</div>`
|
|
1220
|
+
}).join('')
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1126
1223
|
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1127
1224
|
function sessionWorkspaceLabel(s) {
|
|
1128
1225
|
const cwd = sessionCwd(s)
|
|
@@ -1149,48 +1246,66 @@ function sortedSessions() {
|
|
|
1149
1246
|
}
|
|
1150
1247
|
function renderSessions() {
|
|
1151
1248
|
const list = $('session-list')
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1249
|
+
const allItems = sortedSessions()
|
|
1250
|
+
const wbIds = new Set()
|
|
1251
|
+
if (state.wb?.bound) for (const w of state.wbProjects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1252
|
+
const root = workbenchRoot()
|
|
1253
|
+
const visible = allItems.filter(s => !(state.wb?.bound && (wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))))
|
|
1254
|
+
const archivedSet = new Set(state.wbArchived || [])
|
|
1255
|
+
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1256
|
+
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1257
|
+
const showArchived = LS.get('showArchivedV1', '0') === '1'
|
|
1258
|
+
const renderItems = (items) => {
|
|
1259
|
+
let lastWorkspace = null
|
|
1260
|
+
const rows = []
|
|
1261
|
+
for (const s of items) {
|
|
1262
|
+
const workspace = sessionWorkspaceLabel(s)
|
|
1263
|
+
const workspaceName = workspaceDisplayName(workspace)
|
|
1264
|
+
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1265
|
+
rows.push(`<div class="session-group-label" title="${esc(workspace)}"><span class="session-group-icon" aria-hidden="true">⌂</span><span class="session-group-name">${esc(workspaceName)}</span></div>`)
|
|
1266
|
+
lastWorkspace = workspace
|
|
1267
|
+
}
|
|
1268
|
+
const title = titleOf(s)
|
|
1269
|
+
const goal = goalOf(s)
|
|
1270
|
+
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
1271
|
+
const queueN = (state.queues[s.sessionId] || []).filter(i => i.placement === 'queued').length
|
|
1272
|
+
const dots = []
|
|
1273
|
+
if (s.running) dots.push('running')
|
|
1274
|
+
if (pending) dots.push('pending')
|
|
1275
|
+
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}">${esc(t('sessions.goalBadge', { phase: goal.phase || '?' }))}</span>` : ''
|
|
1276
|
+
const queueBadge = queueN ? `<span class="sc-badge">${esc(t('sessions.queueBadge', { n: queueN }))}</span>` : ''
|
|
1277
|
+
const archiveButton = archivedSet.has(s.sessionId) ? '' : `<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>`
|
|
1278
|
+
rows.push(`<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1279
|
+
<div class="session-card ${state.current === s.sessionId ? 'current' : ''}">
|
|
1280
|
+
<div class="sc-title">${esc(title)}</div>
|
|
1281
|
+
<div class="sc-meta">
|
|
1282
|
+
<span class="sc-dot ${dots.join(' ')}"></span>
|
|
1283
|
+
<span>${fmtTime(s.updatedAt)}</span>
|
|
1284
|
+
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
1285
|
+
${badge}${queueBadge}
|
|
1286
|
+
</div>
|
|
1287
|
+
<div class="sc-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</div>
|
|
1288
|
+
<span class="sc-arrow">›</span>
|
|
1289
|
+
</div>
|
|
1290
|
+
${archiveButton}
|
|
1291
|
+
</div>`)
|
|
1161
1292
|
}
|
|
1162
|
-
|
|
1163
|
-
const goal = goalOf(s)
|
|
1164
|
-
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
1165
|
-
const queueN = (state.queues[s.sessionId] || []).filter(i => i.placement === 'queued').length
|
|
1166
|
-
const dots = []
|
|
1167
|
-
if (s.running) dots.push('running')
|
|
1168
|
-
if (pending) dots.push('pending')
|
|
1169
|
-
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}">${esc(t('sessions.goalBadge', { phase: goal.phase || '?' }))}</span>` : ''
|
|
1170
|
-
const queueBadge = queueN ? `<span class="sc-badge">${esc(t('sessions.queueBadge', { n: queueN }))}</span>` : ''
|
|
1171
|
-
rows.push(`<div class="session-card ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1172
|
-
<div class="sc-title">${esc(title)}</div>
|
|
1173
|
-
<div class="sc-meta">
|
|
1174
|
-
<span class="sc-dot ${dots.join(' ')}"></span>
|
|
1175
|
-
<span>${fmtTime(s.updatedAt)}</span>
|
|
1176
|
-
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
1177
|
-
${badge}${queueBadge}
|
|
1178
|
-
</div>
|
|
1179
|
-
<div class="sc-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</div>
|
|
1180
|
-
<span class="sc-arrow">›</span>
|
|
1181
|
-
</div>`)
|
|
1293
|
+
return rows.join('')
|
|
1182
1294
|
}
|
|
1183
|
-
|
|
1295
|
+
const divider = archived.length ? `<button class="archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1296
|
+
const rows = renderItems(main) + divider + (showArchived ? renderItems(archived) : '')
|
|
1297
|
+
const hiddenByWorkbench = allItems.length - visible.length
|
|
1298
|
+
list.innerHTML = rows || `<div class="empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('home.empty'))}</div>`
|
|
1184
1299
|
list.classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1185
1300
|
const sort = $('session-sort')
|
|
1186
1301
|
if (sort) sort.value = state.sessionSort
|
|
1187
|
-
$('home-empty').classList.toggle('hidden',
|
|
1302
|
+
$('home-empty').classList.toggle('hidden', visible.length > 0)
|
|
1188
1303
|
const running = state.sessions.filter(s => s.running).length
|
|
1189
1304
|
const pending = state.approvals.length + state.questions.length
|
|
1190
1305
|
$('stat-strip').innerHTML = `
|
|
1191
1306
|
<div class="stat running"><div class="v">${running}</div><div class="k">${t('sessions.statRunning')}</div></div>
|
|
1192
1307
|
<div class="stat pending"><div class="v">${pending}</div><div class="k">${t('sessions.statPending')}</div></div>
|
|
1193
|
-
<div class="stat ctx"><div class="v">${
|
|
1308
|
+
<div class="stat ctx"><div class="v">${visible.length}</div><div class="k">${t('sessions.statTotal')}</div></div>`
|
|
1194
1309
|
updatePendingBadge()
|
|
1195
1310
|
}
|
|
1196
1311
|
|
|
@@ -1223,7 +1338,7 @@ function bindNativeBack() {
|
|
|
1223
1338
|
try {
|
|
1224
1339
|
CAP.Plugins?.App?.addListener?.('backButton', () => {
|
|
1225
1340
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
1226
|
-
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1341
|
+
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1227
1342
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
1228
1343
|
if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
|
|
1229
1344
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
|
|
@@ -1752,6 +1867,7 @@ async function sendMessage() {
|
|
|
1752
1867
|
function hideComposerMenu() {
|
|
1753
1868
|
$('composer-menu').classList.add('hidden')
|
|
1754
1869
|
$('btn-plus').classList.remove('active')
|
|
1870
|
+
$('permission-submenu')?.classList.add('hidden')
|
|
1755
1871
|
}
|
|
1756
1872
|
|
|
1757
1873
|
function toggleComposerMenu() {
|
|
@@ -1762,6 +1878,13 @@ function toggleComposerMenu() {
|
|
|
1762
1878
|
if (show && !state.models.loaded && !state.models.loading) loadSessionModels()
|
|
1763
1879
|
}
|
|
1764
1880
|
|
|
1881
|
+
function isMobileDevice() {
|
|
1882
|
+
return !!CAP?.isNativePlatform?.() || /Android|iPhone|iPad|iPod|Mobile|Windows Phone/i.test(navigator.userAgent || '')
|
|
1883
|
+
}
|
|
1884
|
+
function mobileEnterAction() {
|
|
1885
|
+
return LS.get('mobileEnterAction', 'newline') === 'send' ? 'send' : 'newline'
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1765
1888
|
async function loadSessionModels() {
|
|
1766
1889
|
if (!state.current || state.models.loading) return
|
|
1767
1890
|
state.models.loading = true
|
|
@@ -1876,6 +1999,85 @@ async function newSession() {
|
|
|
1876
1999
|
openSession(v.sessionId)
|
|
1877
2000
|
}
|
|
1878
2001
|
|
|
2002
|
+
let archivePendingSessionId = null
|
|
2003
|
+
function archiveSession(sessionId) {
|
|
2004
|
+
const session = state.byId.get(sessionId)
|
|
2005
|
+
if (!session) return
|
|
2006
|
+
archivePendingSessionId = sessionId
|
|
2007
|
+
$('archive-session-title').textContent = titleOf(session)
|
|
2008
|
+
$('archive-session-workspace').textContent = sessionWorkspaceLabel(session)
|
|
2009
|
+
$('modal-archive').classList.remove('hidden')
|
|
2010
|
+
}
|
|
2011
|
+
function closeArchiveConfirm() {
|
|
2012
|
+
archivePendingSessionId = null
|
|
2013
|
+
$('modal-archive').classList.add('hidden')
|
|
2014
|
+
}
|
|
2015
|
+
async function confirmArchiveSession() {
|
|
2016
|
+
const sessionId = archivePendingSessionId
|
|
2017
|
+
if (!sessionId) return
|
|
2018
|
+
const button = $('archive-confirm')
|
|
2019
|
+
button.disabled = true
|
|
2020
|
+
try {
|
|
2021
|
+
const value = await safeRpc('workspace.archiveSession', { sessionId }, t('session.archiveFailed', { msg: '' }))
|
|
2022
|
+
if (value == null) return
|
|
2023
|
+
closeArchiveConfirm()
|
|
2024
|
+
toast(t('session.archived'), 'ok')
|
|
2025
|
+
await refreshSessions()
|
|
2026
|
+
} finally {
|
|
2027
|
+
button.disabled = false
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
let swipeTracking = null
|
|
2032
|
+
let swipeSuppressClickUntil = 0
|
|
2033
|
+
function closeRevealedSwipes(except = null) {
|
|
2034
|
+
document.querySelectorAll('#session-list .session-swipe.revealed').forEach(row => {
|
|
2035
|
+
if (row !== except) row.classList.remove('revealed')
|
|
2036
|
+
})
|
|
2037
|
+
}
|
|
2038
|
+
function bindSessionSwipe() {
|
|
2039
|
+
const list = $('session-list')
|
|
2040
|
+
list.addEventListener('touchstart', e => {
|
|
2041
|
+
if (e.touches.length !== 1) return
|
|
2042
|
+
const row = e.target.closest('[data-session-swipe]')
|
|
2043
|
+
if (!row) return
|
|
2044
|
+
closeRevealedSwipes(row)
|
|
2045
|
+
const touch = e.touches[0]
|
|
2046
|
+
swipeTracking = {
|
|
2047
|
+
row,
|
|
2048
|
+
startX: touch.clientX,
|
|
2049
|
+
startY: touch.clientY,
|
|
2050
|
+
offset: row.classList.contains('revealed') ? -92 : 0,
|
|
2051
|
+
axis: null
|
|
2052
|
+
}
|
|
2053
|
+
}, { passive: true })
|
|
2054
|
+
list.addEventListener('touchmove', e => {
|
|
2055
|
+
if (!swipeTracking || e.touches.length !== 1) return
|
|
2056
|
+
const touch = e.touches[0]
|
|
2057
|
+
const dx = touch.clientX - swipeTracking.startX
|
|
2058
|
+
const dy = touch.clientY - swipeTracking.startY
|
|
2059
|
+
if (!swipeTracking.axis && Math.max(Math.abs(dx), Math.abs(dy)) >= 8) {
|
|
2060
|
+
swipeTracking.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y'
|
|
2061
|
+
}
|
|
2062
|
+
if (swipeTracking.axis !== 'x') return
|
|
2063
|
+
e.preventDefault()
|
|
2064
|
+
const offset = Math.max(-92, Math.min(0, swipeTracking.offset + dx))
|
|
2065
|
+
swipeTracking.row.style.setProperty('--swipe-x', offset + 'px')
|
|
2066
|
+
}, { passive: false })
|
|
2067
|
+
list.addEventListener('touchend', () => {
|
|
2068
|
+
if (!swipeTracking) return
|
|
2069
|
+
if (swipeTracking.axis === 'x') {
|
|
2070
|
+
const row = swipeTracking.row
|
|
2071
|
+
const offset = parseFloat(row.style.getPropertyValue('--swipe-x') || swipeTracking.offset)
|
|
2072
|
+
row.classList.toggle('revealed', offset <= -46)
|
|
2073
|
+
row.style.removeProperty('--swipe-x')
|
|
2074
|
+
swipeSuppressClickUntil = Date.now() + 350
|
|
2075
|
+
}
|
|
2076
|
+
swipeTracking = null
|
|
2077
|
+
}, { passive: true })
|
|
2078
|
+
list.addEventListener('touchcancel', () => { swipeTracking = null }, { passive: true })
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1879
2081
|
/* ---------------- 待办 ---------------- */
|
|
1880
2082
|
function renderPending() {
|
|
1881
2083
|
const list = $('pending-list')
|
|
@@ -2537,6 +2739,105 @@ async function loadLocalVersion() {
|
|
|
2537
2739
|
$('update-desc').textContent = state.localVersion ? t('update.currentV', { version: state.localVersion }) : t('update.noVersion')
|
|
2538
2740
|
}
|
|
2539
2741
|
|
|
2742
|
+
/* ---------------- 远程公告 ----------------
|
|
2743
|
+
* 与 update.json 放在同一台服务器上,格式见 README/发布说明。
|
|
2744
|
+
* 公告只读取文本并用 textContent/转义后的换行渲染,不执行服务端下发的 HTML/脚本。
|
|
2745
|
+
*/
|
|
2746
|
+
const ANNOUNCEMENTS_KEY = 'seenAnnouncementsV1'
|
|
2747
|
+
function readSeenAnnouncements() {
|
|
2748
|
+
try {
|
|
2749
|
+
const value = JSON.parse(LS.get(ANNOUNCEMENTS_KEY, '{}'))
|
|
2750
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
2751
|
+
} catch { return {} }
|
|
2752
|
+
}
|
|
2753
|
+
function markAnnouncementSeen(id) {
|
|
2754
|
+
if (!id) return
|
|
2755
|
+
const seen = readSeenAnnouncements()
|
|
2756
|
+
seen[id] = Date.now()
|
|
2757
|
+
const keys = Object.keys(seen)
|
|
2758
|
+
if (keys.length > 100) {
|
|
2759
|
+
keys.sort((a, b) => Number(seen[a]) - Number(seen[b]))
|
|
2760
|
+
for (const key of keys.slice(0, keys.length - 100)) delete seen[key]
|
|
2761
|
+
}
|
|
2762
|
+
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
2763
|
+
}
|
|
2764
|
+
function announcementVersionMatch(item) {
|
|
2765
|
+
const min = String(item.minVersion || item.minAppVersion || '').trim()
|
|
2766
|
+
const max = String(item.maxVersion || item.maxAppVersion || '').trim()
|
|
2767
|
+
if (!state.localVersion) return false
|
|
2768
|
+
if (min && cmpVersion(state.localVersion, min) < 0) return false
|
|
2769
|
+
if (max && cmpVersion(state.localVersion, max) > 0) return false
|
|
2770
|
+
return true
|
|
2771
|
+
}
|
|
2772
|
+
function normalizeAnnouncement(item, base) {
|
|
2773
|
+
if (!item || typeof item !== 'object') return null
|
|
2774
|
+
const id = String(item.id || '').trim().slice(0, 120)
|
|
2775
|
+
const title = String(item.title || '').trim().slice(0, 160)
|
|
2776
|
+
const content = String(item.content ?? item.body ?? '').trim().slice(0, 20000)
|
|
2777
|
+
if (!id || !title || !content || !announcementVersionMatch(item)) return null
|
|
2778
|
+
const now = Date.now()
|
|
2779
|
+
const startsAt = Date.parse(item.publishedAt || item.startsAt || '')
|
|
2780
|
+
const expiresAt = Date.parse(item.expiresAt || '')
|
|
2781
|
+
if (Number.isFinite(startsAt) && startsAt > now) return null
|
|
2782
|
+
if (Number.isFinite(expiresAt) && expiresAt <= now) return null
|
|
2783
|
+
let actionUrl = String(item.actionUrl || item.url || '').trim()
|
|
2784
|
+
if (actionUrl) {
|
|
2785
|
+
try {
|
|
2786
|
+
const parsed = new URL(actionUrl, base + '/')
|
|
2787
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) actionUrl = ''
|
|
2788
|
+
else actionUrl = parsed.href
|
|
2789
|
+
} catch { actionUrl = '' }
|
|
2790
|
+
}
|
|
2791
|
+
return {
|
|
2792
|
+
id, title, content, actionUrl,
|
|
2793
|
+
actionText: String(item.actionText || '').trim().slice(0, 80),
|
|
2794
|
+
publishedAt: Number.isFinite(startsAt) ? startsAt : 0,
|
|
2795
|
+
force: item.force === true
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
function openAnnouncementModal(item) {
|
|
2799
|
+
state.announcement = item
|
|
2800
|
+
$('announcement-title').textContent = item.title
|
|
2801
|
+
$('announcement-content').innerHTML = esc(item.content).replace(/\r?\n/g, '<br>')
|
|
2802
|
+
const action = $('announcement-action')
|
|
2803
|
+
if (item.actionUrl) {
|
|
2804
|
+
action.href = item.actionUrl
|
|
2805
|
+
action.textContent = item.actionText || t('announcement.open')
|
|
2806
|
+
action.classList.remove('hidden')
|
|
2807
|
+
} else {
|
|
2808
|
+
action.removeAttribute('href')
|
|
2809
|
+
action.textContent = ''
|
|
2810
|
+
action.classList.add('hidden')
|
|
2811
|
+
}
|
|
2812
|
+
$('announcement-later').classList.toggle('hidden', item.force)
|
|
2813
|
+
$('modal-announcement').classList.remove('hidden')
|
|
2814
|
+
}
|
|
2815
|
+
function closeAnnouncement(markSeen) {
|
|
2816
|
+
if (markSeen && state.announcement) markAnnouncementSeen(state.announcement.id)
|
|
2817
|
+
state.announcement = null
|
|
2818
|
+
$('modal-announcement').classList.add('hidden')
|
|
2819
|
+
}
|
|
2820
|
+
async function checkAnnouncements() {
|
|
2821
|
+
const base = updateBase()
|
|
2822
|
+
if (!base || !state.localVersion) return false
|
|
2823
|
+
try {
|
|
2824
|
+
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
|
|
2825
|
+
const url = base + '/announcements.json?t=' + Date.now()
|
|
2826
|
+
const res = signal ? await fetch(url, { cache: 'no-store', signal }) : await fetch(url, { cache: 'no-store' })
|
|
2827
|
+
if (!res.ok) return false
|
|
2828
|
+
const raw = await res.text()
|
|
2829
|
+
if (raw.length > 512 * 1024) return false
|
|
2830
|
+
const data = JSON.parse(raw)
|
|
2831
|
+
const source = Array.isArray(data) ? data : (Array.isArray(data?.items) ? data.items : [data])
|
|
2832
|
+
const seen = readSeenAnnouncements()
|
|
2833
|
+
const items = source.map(item => normalizeAnnouncement(item, base)).filter(item => item && !seen[item.id])
|
|
2834
|
+
.sort((a, b) => b.publishedAt - a.publishedAt)
|
|
2835
|
+
if (!items.length) return false
|
|
2836
|
+
openAnnouncementModal(items[0])
|
|
2837
|
+
return true
|
|
2838
|
+
} catch { return false }
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2540
2841
|
/* ---------------- 更新内容弹窗 ---------------- */
|
|
2541
2842
|
const NOTES_KEY = 'seenNotesVersion'
|
|
2542
2843
|
let notesVersion = ''
|
|
@@ -3220,6 +3521,7 @@ function bindUi() {
|
|
|
3220
3521
|
renderUpdateExpandBtn()
|
|
3221
3522
|
renderServers()
|
|
3222
3523
|
renderSessions()
|
|
3524
|
+
renderWorkbench()
|
|
3223
3525
|
renderPending(); renderQueue(); renderJobs()
|
|
3224
3526
|
updateConn()
|
|
3225
3527
|
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
@@ -3247,9 +3549,53 @@ function bindUi() {
|
|
|
3247
3549
|
b.addEventListener('click', () => showView(b.dataset.view)))
|
|
3248
3550
|
// 会话列表点击
|
|
3249
3551
|
$('session-list').addEventListener('click', (e) => {
|
|
3552
|
+
if (swipeSuppressClickUntil > Date.now()) { swipeSuppressClickUntil = 0; return }
|
|
3553
|
+
if (e.target.closest('[data-archived-toggle]')) {
|
|
3554
|
+
LS.set('showArchivedV1', LS.get('showArchivedV1', '0') === '1' ? '0' : '1')
|
|
3555
|
+
renderSessions()
|
|
3556
|
+
return
|
|
3557
|
+
}
|
|
3558
|
+
const archive = e.target.closest('[data-archive-session]')
|
|
3559
|
+
if (archive) {
|
|
3560
|
+
e.stopPropagation()
|
|
3561
|
+
archiveSession(archive.dataset.archiveSession)
|
|
3562
|
+
return
|
|
3563
|
+
}
|
|
3564
|
+
const swipeRow = e.target.closest('[data-session-swipe]')
|
|
3565
|
+
if (swipeRow?.classList.contains('revealed')) {
|
|
3566
|
+
swipeRow.classList.remove('revealed')
|
|
3567
|
+
return
|
|
3568
|
+
}
|
|
3250
3569
|
const card = e.target.closest('[data-id]')
|
|
3251
3570
|
if (card) openSession(card.dataset.id)
|
|
3252
3571
|
})
|
|
3572
|
+
bindSessionSwipe()
|
|
3573
|
+
$('wb-toggle').addEventListener('click', () => {
|
|
3574
|
+
if (!state.wb?.bound) return
|
|
3575
|
+
state.wbOpen = !state.wbOpen
|
|
3576
|
+
renderWorkbench()
|
|
3577
|
+
})
|
|
3578
|
+
$('wb-panel').addEventListener('click', (e) => {
|
|
3579
|
+
const newButton = e.target.closest('[data-wb-new]')
|
|
3580
|
+
if (newButton) {
|
|
3581
|
+
safeRpc('session.create', { workspaceId: newButton.dataset.wbNew }, t('home.createFailed')).then(async v => {
|
|
3582
|
+
if (!v?.sessionId) return
|
|
3583
|
+
toast(t('home.created'), 'ok')
|
|
3584
|
+
await refreshSessions()
|
|
3585
|
+
openSession(v.sessionId)
|
|
3586
|
+
})
|
|
3587
|
+
return
|
|
3588
|
+
}
|
|
3589
|
+
const head = e.target.closest('.wb-project-head')
|
|
3590
|
+
if (head) {
|
|
3591
|
+
const project = head.closest('[data-wb-project]')
|
|
3592
|
+
const id = project?.dataset.wbProject
|
|
3593
|
+
if (id) { state.wbOpenProjects[id] = !state.wbOpenProjects[id]; renderWorkbench() }
|
|
3594
|
+
return
|
|
3595
|
+
}
|
|
3596
|
+
const session = e.target.closest('[data-wb-session]')
|
|
3597
|
+
if (session) openSession(session.dataset.wbSession)
|
|
3598
|
+
})
|
|
3253
3599
|
$('btn-back').addEventListener('click', closeSession)
|
|
3254
3600
|
$('btn-stats').addEventListener('click', () => { renderSessionCards(); $('modal-stats').classList.remove('hidden') })
|
|
3255
3601
|
$('stats-close').addEventListener('click', () => $('modal-stats').classList.add('hidden'))
|
|
@@ -3292,6 +3638,12 @@ function bindUi() {
|
|
|
3292
3638
|
$('composer-menu').addEventListener('click', async (e) => {
|
|
3293
3639
|
const chip = e.target.closest('[data-cmd]')
|
|
3294
3640
|
if (chip) {
|
|
3641
|
+
if (chip.dataset.cmd === '/permission') {
|
|
3642
|
+
const submenu = $('permission-submenu')
|
|
3643
|
+
submenu?.classList.toggle('hidden')
|
|
3644
|
+
return
|
|
3645
|
+
}
|
|
3646
|
+
$('permission-submenu')?.classList.add('hidden')
|
|
3295
3647
|
const input = $('composer-input')
|
|
3296
3648
|
input.value = chip.dataset.cmd + ' '
|
|
3297
3649
|
input.focus()
|
|
@@ -3299,6 +3651,15 @@ function bindUi() {
|
|
|
3299
3651
|
hideComposerMenu()
|
|
3300
3652
|
return
|
|
3301
3653
|
}
|
|
3654
|
+
const perm = e.target.closest('[data-perm]')
|
|
3655
|
+
if (perm) {
|
|
3656
|
+
const input = $('composer-input')
|
|
3657
|
+
input.value = '/permission ' + perm.dataset.perm + ' '
|
|
3658
|
+
input.focus()
|
|
3659
|
+
autosize(input)
|
|
3660
|
+
hideComposerMenu()
|
|
3661
|
+
return
|
|
3662
|
+
}
|
|
3302
3663
|
const preset = e.target.closest('[data-preset]')
|
|
3303
3664
|
if (preset) {
|
|
3304
3665
|
const found = readPresets().find(x => x.id === preset.dataset.preset)
|
|
@@ -3315,7 +3676,9 @@ function bindUi() {
|
|
|
3315
3676
|
const input = $('composer-input')
|
|
3316
3677
|
input.addEventListener('input', () => autosize(input))
|
|
3317
3678
|
input.addEventListener('keydown', (e) => {
|
|
3318
|
-
if (e.key
|
|
3679
|
+
if (e.key !== 'Enter' || e.isComposing) return
|
|
3680
|
+
if (isMobileDevice() && mobileEnterAction() !== 'send') return
|
|
3681
|
+
if (!e.shiftKey) { e.preventDefault(); sendMessage() }
|
|
3319
3682
|
})
|
|
3320
3683
|
|
|
3321
3684
|
// 审批
|
|
@@ -3337,6 +3700,14 @@ function bindUi() {
|
|
|
3337
3700
|
$('workspace-create').addEventListener('click', createWorkspace)
|
|
3338
3701
|
$('workspace-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createWorkspace() })
|
|
3339
3702
|
$('modal-workspace').addEventListener('click', (e) => { if (e.target === $('modal-workspace')) closeWorkspaceModal() })
|
|
3703
|
+
$('archive-cancel').addEventListener('click', closeArchiveConfirm)
|
|
3704
|
+
$('archive-confirm').addEventListener('click', confirmArchiveSession)
|
|
3705
|
+
$('modal-archive').addEventListener('click', (e) => { if (e.target === $('modal-archive')) closeArchiveConfirm() })
|
|
3706
|
+
$('announcement-later').addEventListener('click', () => closeAnnouncement(false))
|
|
3707
|
+
$('announcement-confirm').addEventListener('click', () => closeAnnouncement(true))
|
|
3708
|
+
$('modal-announcement').addEventListener('click', (e) => {
|
|
3709
|
+
if (e.target === $('modal-announcement') && !state.announcement?.force) closeAnnouncement(false)
|
|
3710
|
+
})
|
|
3340
3711
|
// 设置
|
|
3341
3712
|
$('view-settings').addEventListener('click', (e) => {
|
|
3342
3713
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -3373,10 +3744,16 @@ function bindUi() {
|
|
|
3373
3744
|
$('btn-update-expand').addEventListener('click', toggleUpdateExpand)
|
|
3374
3745
|
$('btn-reset').addEventListener('click', () => {
|
|
3375
3746
|
if (!confirm(t('settings.confirmReset'))) return
|
|
3376
|
-
LS.del('token'); LS.del('notify'); LS.del('server')
|
|
3747
|
+
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction')
|
|
3377
3748
|
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
3378
3749
|
location.reload()
|
|
3379
3750
|
})
|
|
3751
|
+
$('mobile-enter-action').value = mobileEnterAction()
|
|
3752
|
+
$('mobile-enter-action').addEventListener('change', (e) => {
|
|
3753
|
+
const action = e.target.value === 'send' ? 'send' : 'newline'
|
|
3754
|
+
LS.set('mobileEnterAction', action)
|
|
3755
|
+
toast(t(action === 'send' ? 'settings.mobileEnterSend' : 'settings.mobileEnterNewline'), 'ok')
|
|
3756
|
+
})
|
|
3380
3757
|
$('opt-notify').checked = LS.get('notify', '0') === '1'
|
|
3381
3758
|
$('opt-notify').addEventListener('change', async (e) => {
|
|
3382
3759
|
if (e.target.checked) {
|
|
@@ -3507,7 +3884,7 @@ async function boot() {
|
|
|
3507
3884
|
bindNativeLinks()
|
|
3508
3885
|
applyNativeInsets()
|
|
3509
3886
|
updateConn()
|
|
3510
|
-
loadLocalVersion()
|
|
3887
|
+
await loadLocalVersion()
|
|
3511
3888
|
if (!state.token) {
|
|
3512
3889
|
showView('view-settings')
|
|
3513
3890
|
$('token-desc').textContent = t('token.notSetHint')
|
|
@@ -3519,9 +3896,12 @@ async function boot() {
|
|
|
3519
3896
|
const host = await safeRpc('host.describe', {}, '')
|
|
3520
3897
|
if (host) { state.hostInfo = host; $('host-desc').textContent = t('settings.hostDesc', { version: host.version, cwd: host.cwd, n: host.attachedSessions }) }
|
|
3521
3898
|
loadDshControl()
|
|
3522
|
-
// 启动后自动检查一次更新(静默)
|
|
3523
|
-
setTimeout(() => checkUpdate(true), 4000)
|
|
3524
3899
|
}
|
|
3900
|
+
// 公告与更新共用当前服务器;公告优先,避免启动时两个弹窗重叠。
|
|
3901
|
+
setTimeout(async () => {
|
|
3902
|
+
const shown = await checkAnnouncements()
|
|
3903
|
+
if (!shown && state.token) checkUpdate(true)
|
|
3904
|
+
}, 4000)
|
|
3525
3905
|
renderPending()
|
|
3526
3906
|
}
|
|
3527
3907
|
|