dsh-remote-plugin 0.6.6 → 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/gateway.cjs +119 -1
- package/index.mjs +10 -1
- 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 +850 -57
- package/public/desktop/desktop.css +170 -20
- package/public/desktop/desktop.html +102 -13
- package/public/desktop/desktop.js +422 -26
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +200 -40
- package/public/plugin.html +151 -0
- package/public/plugin.js +179 -0
- package/public/styles.css +220 -15
- package/public/theme-vars.css +9 -0
- package/public/update.json +12 -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,13 @@ 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
|
-
|
|
66
|
+
composerImages: [], // 当前草稿中的图片附件:只在发送成功后释放
|
|
67
|
+
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
68
|
+
wb: null,
|
|
69
|
+
wbProjects: [],
|
|
70
|
+
wbArchived: [],
|
|
71
|
+
wbOpen: false,
|
|
72
|
+
wbOpenProjects: {}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
const $ = (id) => document.getElementById(id)
|
|
@@ -1090,6 +1097,7 @@ async function refreshSessions() {
|
|
|
1090
1097
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1091
1098
|
cacheWrite(CACHE.sessions, state.sessions.slice(0, 80))
|
|
1092
1099
|
renderSessions()
|
|
1100
|
+
refreshWorkbench()
|
|
1093
1101
|
}
|
|
1094
1102
|
|
|
1095
1103
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
@@ -1123,6 +1131,123 @@ function updatePendingBadge() {
|
|
|
1123
1131
|
if (pending) $('nav-pending').textContent = pending
|
|
1124
1132
|
}
|
|
1125
1133
|
|
|
1134
|
+
/* ---------------- 工作台与归档会话 ---------------- */
|
|
1135
|
+
function wbPathKey(p) {
|
|
1136
|
+
let value = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1137
|
+
if (value.length > 1) value = value.replace(/\/+$/, '')
|
|
1138
|
+
const windows = /^[A-Za-z]:\//.test(value) || /Windows/i.test(navigator.platform || navigator.userAgent || '')
|
|
1139
|
+
return windows ? value.toLowerCase() : value
|
|
1140
|
+
}
|
|
1141
|
+
function wbBaseName(p) {
|
|
1142
|
+
const value = String(p || '').replace(/[\\/]+$/, '')
|
|
1143
|
+
return value.split(/[\\/]/).pop() || value
|
|
1144
|
+
}
|
|
1145
|
+
function wbStrictInside(pathValue, rootValue) {
|
|
1146
|
+
const pathKey = wbPathKey(pathValue)
|
|
1147
|
+
const rootKey = wbPathKey(rootValue)
|
|
1148
|
+
if (!pathKey || !rootKey || pathKey === rootKey) return false
|
|
1149
|
+
return pathKey.startsWith(rootKey.endsWith('/') ? rootKey : rootKey + '/')
|
|
1150
|
+
}
|
|
1151
|
+
function wbJoin(root, name) {
|
|
1152
|
+
const raw = String(root || '')
|
|
1153
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1154
|
+
return raw.replace(/[\\/]+$/, '') + separator + String(name || '')
|
|
1155
|
+
}
|
|
1156
|
+
function workbenchRoot() {
|
|
1157
|
+
return state.wb?.bound && state.wb.path ? state.wb.path : ''
|
|
1158
|
+
}
|
|
1159
|
+
async function refreshWorkbench() {
|
|
1160
|
+
if (!state.token) return
|
|
1161
|
+
try {
|
|
1162
|
+
const res = await fetch(apiUrl('/workbench'), {
|
|
1163
|
+
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' }
|
|
1164
|
+
})
|
|
1165
|
+
if (res.ok) {
|
|
1166
|
+
const value = await res.json().catch(() => null)
|
|
1167
|
+
if (value && typeof value.bound === 'boolean') state.wb = value
|
|
1168
|
+
}
|
|
1169
|
+
} catch {}
|
|
1170
|
+
try {
|
|
1171
|
+
const value = await rpc('workspace.list', {})
|
|
1172
|
+
state.wbProjects = Array.isArray(value?.items) ? value.items : []
|
|
1173
|
+
state.wbArchived = Array.isArray(value?.archivedSessionIds) ? value.archivedSessionIds : []
|
|
1174
|
+
} catch {
|
|
1175
|
+
state.wbProjects = []
|
|
1176
|
+
state.wbArchived = []
|
|
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
|
+
}
|
|
1201
|
+
renderWorkbench()
|
|
1202
|
+
renderSessions()
|
|
1203
|
+
}
|
|
1204
|
+
function renderWorkbench() {
|
|
1205
|
+
const bar = $('workbench-bar')
|
|
1206
|
+
if (!bar) return
|
|
1207
|
+
const bound = !!state.wb?.bound && !!state.wb.path
|
|
1208
|
+
const toggle = $('wb-toggle')
|
|
1209
|
+
const panel = $('wb-panel')
|
|
1210
|
+
bar.classList.toggle('bound', bound)
|
|
1211
|
+
bar.classList.toggle('unbound', !bound)
|
|
1212
|
+
if (!bound) {
|
|
1213
|
+
$('wb-label').textContent = t('wb.unbound')
|
|
1214
|
+
toggle.setAttribute('aria-expanded', 'false')
|
|
1215
|
+
panel.classList.add('hidden')
|
|
1216
|
+
panel.innerHTML = ''
|
|
1217
|
+
return
|
|
1218
|
+
}
|
|
1219
|
+
$('wb-label').textContent = t('wb.bound', { title: state.wb.title || wbBaseName(state.wb.path) })
|
|
1220
|
+
toggle.setAttribute('aria-expanded', state.wbOpen ? 'true' : 'false')
|
|
1221
|
+
panel.classList.toggle('hidden', !state.wbOpen)
|
|
1222
|
+
if (!state.wbOpen) { panel.innerHTML = ''; return }
|
|
1223
|
+
const projects = state.wbProjects.filter(w => wbStrictInside(w.path, workbenchRoot()))
|
|
1224
|
+
if (!projects.length) {
|
|
1225
|
+
panel.innerHTML = `<div class="wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
const archivedSet = new Set(state.wbArchived || [])
|
|
1229
|
+
panel.innerHTML = projects.map(w => {
|
|
1230
|
+
const id = String(w.workspaceId || '')
|
|
1231
|
+
const open = !!state.wbOpenProjects[id]
|
|
1232
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).filter(s => !archivedSet.has(s.sessionId))
|
|
1233
|
+
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
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>` : ''
|
|
1241
|
+
return `<div class="wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}">
|
|
1242
|
+
<div class="wb-project-head">
|
|
1243
|
+
<span class="wb-chevron" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1244
|
+
<span class="wb-project-title">${esc(w.title || wbBaseName(w.path) || w.path)}</span>
|
|
1245
|
+
<button class="mini-btn wb-new" type="button" data-wb-new="${esc(id)}">${esc(t('wb.newSession'))}</button>
|
|
1246
|
+
</div>${body}
|
|
1247
|
+
</div>`
|
|
1248
|
+
}).join('')
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1126
1251
|
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1127
1252
|
function sessionWorkspaceLabel(s) {
|
|
1128
1253
|
const cwd = sessionCwd(s)
|
|
@@ -1149,48 +1274,70 @@ function sortedSessions() {
|
|
|
1149
1274
|
}
|
|
1150
1275
|
function renderSessions() {
|
|
1151
1276
|
const list = $('session-list')
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (state.
|
|
1159
|
-
|
|
1160
|
-
|
|
1277
|
+
const allItems = sortedSessions()
|
|
1278
|
+
const wbIds = new Set()
|
|
1279
|
+
if (state.wb?.bound) for (const w of state.wbProjects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1280
|
+
const root = workbenchRoot()
|
|
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
|
+
})
|
|
1287
|
+
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1288
|
+
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1289
|
+
const showArchived = LS.get('showArchivedV1', '0') === '1'
|
|
1290
|
+
const renderItems = (items) => {
|
|
1291
|
+
let lastWorkspace = null
|
|
1292
|
+
const rows = []
|
|
1293
|
+
for (const s of items) {
|
|
1294
|
+
const workspace = sessionWorkspaceLabel(s)
|
|
1295
|
+
const workspaceName = workspaceDisplayName(workspace)
|
|
1296
|
+
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1297
|
+
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>`)
|
|
1298
|
+
lastWorkspace = workspace
|
|
1299
|
+
}
|
|
1300
|
+
const title = titleOf(s)
|
|
1301
|
+
const goal = goalOf(s)
|
|
1302
|
+
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
1303
|
+
const queueN = (state.queues[s.sessionId] || []).filter(i => i.placement === 'queued').length
|
|
1304
|
+
const dots = []
|
|
1305
|
+
if (s.running) dots.push('running')
|
|
1306
|
+
if (pending) dots.push('pending')
|
|
1307
|
+
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}">${esc(t('sessions.goalBadge', { phase: goal.phase || '?' }))}</span>` : ''
|
|
1308
|
+
const queueBadge = queueN ? `<span class="sc-badge">${esc(t('sessions.queueBadge', { n: queueN }))}</span>` : ''
|
|
1309
|
+
const archiveButton = archivedSet.has(s.sessionId) ? '' : `<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>`
|
|
1310
|
+
rows.push(`<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1311
|
+
<div class="session-card ${state.current === s.sessionId ? 'current' : ''}">
|
|
1312
|
+
<div class="sc-title">${esc(title)}</div>
|
|
1313
|
+
<div class="sc-meta">
|
|
1314
|
+
<span class="sc-dot ${dots.join(' ')}"></span>
|
|
1315
|
+
<span>${fmtTime(s.updatedAt)}</span>
|
|
1316
|
+
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
1317
|
+
${badge}${queueBadge}
|
|
1318
|
+
</div>
|
|
1319
|
+
<div class="sc-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</div>
|
|
1320
|
+
<span class="sc-arrow">›</span>
|
|
1321
|
+
</div>
|
|
1322
|
+
${archiveButton}
|
|
1323
|
+
</div>`)
|
|
1161
1324
|
}
|
|
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>`)
|
|
1325
|
+
return rows.join('')
|
|
1182
1326
|
}
|
|
1183
|
-
|
|
1327
|
+
const divider = archived.length ? `<button class="archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1328
|
+
const rows = renderItems(main) + divider + (showArchived ? renderItems(archived) : '')
|
|
1329
|
+
const hiddenByWorkbench = allItems.length - visible.length
|
|
1330
|
+
list.innerHTML = rows || `<div class="empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('home.empty'))}</div>`
|
|
1184
1331
|
list.classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1185
1332
|
const sort = $('session-sort')
|
|
1186
1333
|
if (sort) sort.value = state.sessionSort
|
|
1187
|
-
$('home-empty').classList.toggle('hidden',
|
|
1334
|
+
$('home-empty').classList.toggle('hidden', visible.length > 0)
|
|
1188
1335
|
const running = state.sessions.filter(s => s.running).length
|
|
1189
1336
|
const pending = state.approvals.length + state.questions.length
|
|
1190
1337
|
$('stat-strip').innerHTML = `
|
|
1191
1338
|
<div class="stat running"><div class="v">${running}</div><div class="k">${t('sessions.statRunning')}</div></div>
|
|
1192
1339
|
<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">${
|
|
1340
|
+
<div class="stat ctx"><div class="v">${visible.length}</div><div class="k">${t('sessions.statTotal')}</div></div>`
|
|
1194
1341
|
updatePendingBadge()
|
|
1195
1342
|
}
|
|
1196
1343
|
|
|
@@ -1210,6 +1357,8 @@ async function openSession(id) {
|
|
|
1210
1357
|
}
|
|
1211
1358
|
|
|
1212
1359
|
function closeSession() {
|
|
1360
|
+
setComposerFullscreen(false)
|
|
1361
|
+
clearComposerImages()
|
|
1213
1362
|
state.current = null
|
|
1214
1363
|
state.history = emptyHistory()
|
|
1215
1364
|
document.body.classList.remove('in-session')
|
|
@@ -1222,8 +1371,9 @@ function bindNativeBack() {
|
|
|
1222
1371
|
if (!CAP?.isNativePlatform?.()) return
|
|
1223
1372
|
try {
|
|
1224
1373
|
CAP.Plugins?.App?.addListener?.('backButton', () => {
|
|
1374
|
+
if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
|
|
1225
1375
|
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 } // 先关弹窗
|
|
1376
|
+
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1227
1377
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
1228
1378
|
if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
|
|
1229
1379
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
|
|
@@ -1548,7 +1698,7 @@ function eventHtml(entry, ctx = {}) {
|
|
|
1548
1698
|
const blocks = msg.content || data.content || []
|
|
1549
1699
|
const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
|
|
1550
1700
|
if (sysText) {
|
|
1551
|
-
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>`
|
|
1552
1702
|
} else {
|
|
1553
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>`
|
|
1554
1704
|
}
|
|
@@ -1726,32 +1876,153 @@ async function runSlashCommand(text) {
|
|
|
1726
1876
|
return false
|
|
1727
1877
|
}
|
|
1728
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
|
+
|
|
1729
1973
|
async function sendSessionText(text) {
|
|
1974
|
+
return sendSessionContent(text, [])
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
async function sendSessionContent(text, images) {
|
|
1730
1978
|
const clean = String(text || '').trim()
|
|
1731
|
-
if (!clean || !state.current) return false
|
|
1732
|
-
if (await runSlashCommand(clean)) return true
|
|
1733
|
-
$('btn-send').
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
content
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
+
})))
|
|
1743
2007
|
}
|
|
1744
2008
|
|
|
1745
2009
|
async function sendMessage() {
|
|
1746
2010
|
const input = $('composer-input')
|
|
1747
2011
|
const text = input.value.trim()
|
|
1748
|
-
|
|
1749
|
-
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
|
+
}
|
|
1750
2020
|
}
|
|
1751
2021
|
|
|
1752
2022
|
function hideComposerMenu() {
|
|
1753
2023
|
$('composer-menu').classList.add('hidden')
|
|
1754
2024
|
$('btn-plus').classList.remove('active')
|
|
2025
|
+
$('permission-submenu')?.classList.add('hidden')
|
|
1755
2026
|
}
|
|
1756
2027
|
|
|
1757
2028
|
function toggleComposerMenu() {
|
|
@@ -1762,6 +2033,13 @@ function toggleComposerMenu() {
|
|
|
1762
2033
|
if (show && !state.models.loaded && !state.models.loading) loadSessionModels()
|
|
1763
2034
|
}
|
|
1764
2035
|
|
|
2036
|
+
function isMobileDevice() {
|
|
2037
|
+
return !!CAP?.isNativePlatform?.() || /Android|iPhone|iPad|iPod|Mobile|Windows Phone/i.test(navigator.userAgent || '')
|
|
2038
|
+
}
|
|
2039
|
+
function mobileEnterAction() {
|
|
2040
|
+
return LS.get('mobileEnterAction', 'newline') === 'send' ? 'send' : 'newline'
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1765
2043
|
async function loadSessionModels() {
|
|
1766
2044
|
if (!state.current || state.models.loading) return
|
|
1767
2045
|
state.models.loading = true
|
|
@@ -1876,7 +2154,175 @@ async function newSession() {
|
|
|
1876
2154
|
openSession(v.sessionId)
|
|
1877
2155
|
}
|
|
1878
2156
|
|
|
1879
|
-
|
|
2157
|
+
let archivePendingSessionId = null
|
|
2158
|
+
function archiveSession(sessionId) {
|
|
2159
|
+
const session = state.byId.get(sessionId)
|
|
2160
|
+
if (!session) return
|
|
2161
|
+
archivePendingSessionId = sessionId
|
|
2162
|
+
$('archive-session-title').textContent = titleOf(session)
|
|
2163
|
+
$('archive-session-workspace').textContent = sessionWorkspaceLabel(session)
|
|
2164
|
+
$('modal-archive').classList.remove('hidden')
|
|
2165
|
+
}
|
|
2166
|
+
function closeArchiveConfirm() {
|
|
2167
|
+
archivePendingSessionId = null
|
|
2168
|
+
$('modal-archive').classList.add('hidden')
|
|
2169
|
+
}
|
|
2170
|
+
async function confirmArchiveSession() {
|
|
2171
|
+
const sessionId = archivePendingSessionId
|
|
2172
|
+
if (!sessionId) return
|
|
2173
|
+
const button = $('archive-confirm')
|
|
2174
|
+
button.disabled = true
|
|
2175
|
+
try {
|
|
2176
|
+
const value = await safeRpc('workspace.archiveSession', { sessionId }, t('session.archiveFailed', { msg: '' }))
|
|
2177
|
+
if (value == null) return
|
|
2178
|
+
closeArchiveConfirm()
|
|
2179
|
+
toast(t('session.archived'), 'ok')
|
|
2180
|
+
await refreshSessions()
|
|
2181
|
+
} finally {
|
|
2182
|
+
button.disabled = false
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
let swipeTracking = null
|
|
2187
|
+
let swipeSuppressClickUntil = 0
|
|
2188
|
+
function closeRevealedSwipes(except = null) {
|
|
2189
|
+
document.querySelectorAll('.session-swipe.revealed').forEach(row => {
|
|
2190
|
+
if (row !== except) row.classList.remove('revealed')
|
|
2191
|
+
})
|
|
2192
|
+
}
|
|
2193
|
+
function bindSessionSwipe() {
|
|
2194
|
+
const containers = [$('session-list'), $('wb-panel')].filter(Boolean)
|
|
2195
|
+
for (const list of containers) list.addEventListener('touchstart', e => {
|
|
2196
|
+
if (e.touches.length !== 1) return
|
|
2197
|
+
const row = e.target.closest('[data-session-swipe]')
|
|
2198
|
+
if (!row) return
|
|
2199
|
+
closeRevealedSwipes(row)
|
|
2200
|
+
const touch = e.touches[0]
|
|
2201
|
+
swipeTracking = {
|
|
2202
|
+
row,
|
|
2203
|
+
startX: touch.clientX,
|
|
2204
|
+
startY: touch.clientY,
|
|
2205
|
+
offset: row.classList.contains('revealed') ? -92 : 0,
|
|
2206
|
+
axis: null
|
|
2207
|
+
}
|
|
2208
|
+
}, { passive: true })
|
|
2209
|
+
for (const list of containers) list.addEventListener('touchmove', e => {
|
|
2210
|
+
if (!swipeTracking || e.touches.length !== 1) return
|
|
2211
|
+
const touch = e.touches[0]
|
|
2212
|
+
const dx = touch.clientX - swipeTracking.startX
|
|
2213
|
+
const dy = touch.clientY - swipeTracking.startY
|
|
2214
|
+
if (!swipeTracking.axis && Math.max(Math.abs(dx), Math.abs(dy)) >= 8) {
|
|
2215
|
+
swipeTracking.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y'
|
|
2216
|
+
}
|
|
2217
|
+
if (swipeTracking.axis !== 'x') return
|
|
2218
|
+
e.preventDefault()
|
|
2219
|
+
const offset = Math.max(-92, Math.min(0, swipeTracking.offset + dx))
|
|
2220
|
+
swipeTracking.row.style.setProperty('--swipe-x', offset + 'px')
|
|
2221
|
+
}, { passive: false })
|
|
2222
|
+
for (const list of containers) list.addEventListener('touchend', () => {
|
|
2223
|
+
if (!swipeTracking) return
|
|
2224
|
+
if (swipeTracking.axis === 'x') {
|
|
2225
|
+
const row = swipeTracking.row
|
|
2226
|
+
const offset = parseFloat(row.style.getPropertyValue('--swipe-x') || swipeTracking.offset)
|
|
2227
|
+
row.classList.toggle('revealed', offset <= -46)
|
|
2228
|
+
row.style.removeProperty('--swipe-x')
|
|
2229
|
+
swipeSuppressClickUntil = Date.now() + 350
|
|
2230
|
+
}
|
|
2231
|
+
swipeTracking = null
|
|
2232
|
+
}, { 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)))
|
|
2324
|
+
}
|
|
2325
|
+
|
|
1880
2326
|
function renderPending() {
|
|
1881
2327
|
const list = $('pending-list')
|
|
1882
2328
|
const items = [
|
|
@@ -1911,6 +2357,7 @@ function renderPending() {
|
|
|
1911
2357
|
list.querySelectorAll('[data-question]').forEach(btn =>
|
|
1912
2358
|
btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.question))))
|
|
1913
2359
|
updatePendingBadge()
|
|
2360
|
+
renderOverview()
|
|
1914
2361
|
}
|
|
1915
2362
|
|
|
1916
2363
|
async function approveApproval(id, allow) {
|
|
@@ -2104,7 +2551,7 @@ function renderFs(data) {
|
|
|
2104
2551
|
list.innerHTML = data.entries.map(e => {
|
|
2105
2552
|
const isDir = e.type === 'dir'
|
|
2106
2553
|
return `<div class="fs-row" data-name="${esc(e.name)}" data-type="${esc(e.type)}">
|
|
2107
|
-
<span class="fs-ico">${isDir
|
|
2554
|
+
<span class="fs-ico">${fsIconSvg(isDir)}</span>
|
|
2108
2555
|
<span class="fs-meta">
|
|
2109
2556
|
<span class="fs-name">${esc(e.name)}</span>
|
|
2110
2557
|
<span class="fs-sub">${isDir ? t('fs.dir') : fmtSize(e.size)} · ${fmtFullTime(e.mtimeMs)}</span>
|
|
@@ -2116,6 +2563,12 @@ function renderFs(data) {
|
|
|
2116
2563
|
row.addEventListener('click', () => fsOpenEntry(row.dataset.name, row.dataset.type)))
|
|
2117
2564
|
}
|
|
2118
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
|
+
|
|
2119
2572
|
function fsOpenEntry(name, type) {
|
|
2120
2573
|
if (!name) return
|
|
2121
2574
|
const p = fsJoin(state.fs.path, name)
|
|
@@ -2537,6 +2990,142 @@ async function loadLocalVersion() {
|
|
|
2537
2990
|
$('update-desc').textContent = state.localVersion ? t('update.currentV', { version: state.localVersion }) : t('update.noVersion')
|
|
2538
2991
|
}
|
|
2539
2992
|
|
|
2993
|
+
/* ---------------- 远程公告 ----------------
|
|
2994
|
+
* 与 update.json 放在同一台服务器上,格式见 README/发布说明。
|
|
2995
|
+
* 公告只读取文本并用 textContent/转义后的换行渲染,不执行服务端下发的 HTML/脚本。
|
|
2996
|
+
*/
|
|
2997
|
+
const ANNOUNCEMENTS_KEY = 'seenAnnouncementsV1'
|
|
2998
|
+
const ANNOUNCEMENT_HISTORY_KEY = 'announcementHistoryV1'
|
|
2999
|
+
function readSeenAnnouncements() {
|
|
3000
|
+
try {
|
|
3001
|
+
const value = JSON.parse(LS.get(ANNOUNCEMENTS_KEY, '{}'))
|
|
3002
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
3003
|
+
} catch { return {} }
|
|
3004
|
+
}
|
|
3005
|
+
function markAnnouncementSeen(id) {
|
|
3006
|
+
if (!id) return
|
|
3007
|
+
const seen = readSeenAnnouncements()
|
|
3008
|
+
seen[id] = Date.now()
|
|
3009
|
+
const keys = Object.keys(seen)
|
|
3010
|
+
if (keys.length > 100) {
|
|
3011
|
+
keys.sort((a, b) => Number(seen[a]) - Number(seen[b]))
|
|
3012
|
+
for (const key of keys.slice(0, keys.length - 100)) delete seen[key]
|
|
3013
|
+
}
|
|
3014
|
+
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
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
|
+
}
|
|
3050
|
+
function announcementVersionMatch(item) {
|
|
3051
|
+
const min = String(item.minVersion || item.minAppVersion || '').trim()
|
|
3052
|
+
const max = String(item.maxVersion || item.maxAppVersion || '').trim()
|
|
3053
|
+
if (!state.localVersion) return false
|
|
3054
|
+
if (min && cmpVersion(state.localVersion, min) < 0) return false
|
|
3055
|
+
if (max && cmpVersion(state.localVersion, max) > 0) return false
|
|
3056
|
+
return true
|
|
3057
|
+
}
|
|
3058
|
+
function normalizeAnnouncement(item, base) {
|
|
3059
|
+
if (!item || typeof item !== 'object') return null
|
|
3060
|
+
const id = String(item.id || '').trim().slice(0, 120)
|
|
3061
|
+
const title = String(item.title || '').trim().slice(0, 160)
|
|
3062
|
+
const content = String(item.content ?? item.body ?? '').trim().slice(0, 20000)
|
|
3063
|
+
if (!id || !title || !content || !announcementVersionMatch(item)) return null
|
|
3064
|
+
const now = Date.now()
|
|
3065
|
+
const startsAt = Date.parse(item.publishedAt || item.startsAt || '')
|
|
3066
|
+
const expiresAt = Date.parse(item.expiresAt || '')
|
|
3067
|
+
if (Number.isFinite(startsAt) && startsAt > now) return null
|
|
3068
|
+
if (Number.isFinite(expiresAt) && expiresAt <= now) return null
|
|
3069
|
+
let actionUrl = String(item.actionUrl || item.url || '').trim()
|
|
3070
|
+
if (actionUrl) {
|
|
3071
|
+
try {
|
|
3072
|
+
const parsed = new URL(actionUrl, base + '/')
|
|
3073
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) actionUrl = ''
|
|
3074
|
+
else actionUrl = parsed.href
|
|
3075
|
+
} catch { actionUrl = '' }
|
|
3076
|
+
}
|
|
3077
|
+
return {
|
|
3078
|
+
id, title, content, actionUrl,
|
|
3079
|
+
actionText: String(item.actionText || '').trim().slice(0, 80),
|
|
3080
|
+
publishedAt: Number.isFinite(startsAt) ? startsAt : 0,
|
|
3081
|
+
force: item.force === true
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function openAnnouncementModal(item) {
|
|
3085
|
+
state.announcement = item
|
|
3086
|
+
$('announcement-title').textContent = item.title
|
|
3087
|
+
$('announcement-content').innerHTML = esc(item.content).replace(/\r?\n/g, '<br>')
|
|
3088
|
+
const action = $('announcement-action')
|
|
3089
|
+
if (item.actionUrl) {
|
|
3090
|
+
action.href = item.actionUrl
|
|
3091
|
+
action.textContent = item.actionText || t('announcement.open')
|
|
3092
|
+
action.classList.remove('hidden')
|
|
3093
|
+
} else {
|
|
3094
|
+
action.removeAttribute('href')
|
|
3095
|
+
action.textContent = ''
|
|
3096
|
+
action.classList.add('hidden')
|
|
3097
|
+
}
|
|
3098
|
+
$('announcement-later').classList.toggle('hidden', item.force)
|
|
3099
|
+
$('modal-announcement').classList.remove('hidden')
|
|
3100
|
+
}
|
|
3101
|
+
function closeAnnouncement(markSeen) {
|
|
3102
|
+
if (markSeen && state.announcement) markAnnouncementSeen(state.announcement.id)
|
|
3103
|
+
state.announcement = null
|
|
3104
|
+
$('modal-announcement').classList.add('hidden')
|
|
3105
|
+
}
|
|
3106
|
+
async function checkAnnouncements() {
|
|
3107
|
+
const base = updateBase()
|
|
3108
|
+
if (!base || !state.localVersion) return false
|
|
3109
|
+
try {
|
|
3110
|
+
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
|
|
3111
|
+
const url = base + '/announcements.json?t=' + Date.now()
|
|
3112
|
+
const res = signal ? await fetch(url, { cache: 'no-store', signal }) : await fetch(url, { cache: 'no-store' })
|
|
3113
|
+
if (!res.ok) return false
|
|
3114
|
+
const raw = await res.text()
|
|
3115
|
+
if (raw.length > 512 * 1024) return false
|
|
3116
|
+
const data = JSON.parse(raw)
|
|
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)
|
|
3120
|
+
const seen = readSeenAnnouncements()
|
|
3121
|
+
const items = normalized.filter(item => !seen[item.id])
|
|
3122
|
+
.sort((a, b) => b.publishedAt - a.publishedAt)
|
|
3123
|
+
if (!items.length) return false
|
|
3124
|
+
openAnnouncementModal(items[0])
|
|
3125
|
+
return true
|
|
3126
|
+
} catch { return false }
|
|
3127
|
+
}
|
|
3128
|
+
|
|
2540
3129
|
/* ---------------- 更新内容弹窗 ---------------- */
|
|
2541
3130
|
const NOTES_KEY = 'seenNotesVersion'
|
|
2542
3131
|
let notesVersion = ''
|
|
@@ -2993,8 +3582,84 @@ function updateConn() {
|
|
|
2993
3582
|
}
|
|
2994
3583
|
|
|
2995
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
|
+
}
|
|
2996
3592
|
el.style.height = 'auto'
|
|
2997
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 })
|
|
2998
3663
|
}
|
|
2999
3664
|
|
|
3000
3665
|
/* ---------------- 初始化 ---------------- */
|
|
@@ -3220,6 +3885,7 @@ function bindUi() {
|
|
|
3220
3885
|
renderUpdateExpandBtn()
|
|
3221
3886
|
renderServers()
|
|
3222
3887
|
renderSessions()
|
|
3888
|
+
renderWorkbench()
|
|
3223
3889
|
renderPending(); renderQueue(); renderJobs()
|
|
3224
3890
|
updateConn()
|
|
3225
3891
|
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
@@ -3245,11 +3911,74 @@ function bindUi() {
|
|
|
3245
3911
|
// 底部导航
|
|
3246
3912
|
document.querySelectorAll('.nav-btn').forEach(b =>
|
|
3247
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
|
+
})
|
|
3248
3927
|
// 会话列表点击
|
|
3249
3928
|
$('session-list').addEventListener('click', (e) => {
|
|
3929
|
+
if (swipeSuppressClickUntil > Date.now()) { swipeSuppressClickUntil = 0; return }
|
|
3930
|
+
if (e.target.closest('[data-archived-toggle]')) {
|
|
3931
|
+
LS.set('showArchivedV1', LS.get('showArchivedV1', '0') === '1' ? '0' : '1')
|
|
3932
|
+
renderSessions()
|
|
3933
|
+
return
|
|
3934
|
+
}
|
|
3935
|
+
const archive = e.target.closest('[data-archive-session]')
|
|
3936
|
+
if (archive) {
|
|
3937
|
+
e.stopPropagation()
|
|
3938
|
+
archiveSession(archive.dataset.archiveSession)
|
|
3939
|
+
return
|
|
3940
|
+
}
|
|
3941
|
+
const swipeRow = e.target.closest('[data-session-swipe]')
|
|
3942
|
+
if (swipeRow?.classList.contains('revealed')) {
|
|
3943
|
+
swipeRow.classList.remove('revealed')
|
|
3944
|
+
return
|
|
3945
|
+
}
|
|
3250
3946
|
const card = e.target.closest('[data-id]')
|
|
3251
3947
|
if (card) openSession(card.dataset.id)
|
|
3252
3948
|
})
|
|
3949
|
+
bindSessionSwipe()
|
|
3950
|
+
$('wb-toggle').addEventListener('click', () => {
|
|
3951
|
+
if (!state.wb?.bound) return
|
|
3952
|
+
state.wbOpen = !state.wbOpen
|
|
3953
|
+
renderWorkbench()
|
|
3954
|
+
})
|
|
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
|
+
}
|
|
3962
|
+
const newButton = e.target.closest('[data-wb-new]')
|
|
3963
|
+
if (newButton) {
|
|
3964
|
+
safeRpc('session.create', { workspaceId: newButton.dataset.wbNew }, t('home.createFailed')).then(async v => {
|
|
3965
|
+
if (!v?.sessionId) return
|
|
3966
|
+
toast(t('home.created'), 'ok')
|
|
3967
|
+
await refreshSessions()
|
|
3968
|
+
openSession(v.sessionId)
|
|
3969
|
+
})
|
|
3970
|
+
return
|
|
3971
|
+
}
|
|
3972
|
+
const head = e.target.closest('.wb-project-head')
|
|
3973
|
+
if (head) {
|
|
3974
|
+
const project = head.closest('[data-wb-project]')
|
|
3975
|
+
const id = project?.dataset.wbProject
|
|
3976
|
+
if (id) { state.wbOpenProjects[id] = !state.wbOpenProjects[id]; renderWorkbench() }
|
|
3977
|
+
return
|
|
3978
|
+
}
|
|
3979
|
+
const session = e.target.closest('[data-wb-session]')
|
|
3980
|
+
if (session) openSession(session.dataset.wbSession)
|
|
3981
|
+
})
|
|
3253
3982
|
$('btn-back').addEventListener('click', closeSession)
|
|
3254
3983
|
$('btn-stats').addEventListener('click', () => { renderSessionCards(); $('modal-stats').classList.remove('hidden') })
|
|
3255
3984
|
$('stats-close').addEventListener('click', () => $('modal-stats').classList.add('hidden'))
|
|
@@ -3288,10 +4017,37 @@ function bindUi() {
|
|
|
3288
4017
|
})
|
|
3289
4018
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
3290
4019
|
$('btn-send').addEventListener('click', sendMessage)
|
|
4020
|
+
$('btn-fs-send').addEventListener('click', sendMessage)
|
|
3291
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
|
+
})
|
|
3292
4042
|
$('composer-menu').addEventListener('click', async (e) => {
|
|
3293
4043
|
const chip = e.target.closest('[data-cmd]')
|
|
3294
4044
|
if (chip) {
|
|
4045
|
+
if (chip.dataset.cmd === '/permission') {
|
|
4046
|
+
const submenu = $('permission-submenu')
|
|
4047
|
+
submenu?.classList.toggle('hidden')
|
|
4048
|
+
return
|
|
4049
|
+
}
|
|
4050
|
+
$('permission-submenu')?.classList.add('hidden')
|
|
3295
4051
|
const input = $('composer-input')
|
|
3296
4052
|
input.value = chip.dataset.cmd + ' '
|
|
3297
4053
|
input.focus()
|
|
@@ -3299,6 +4055,15 @@ function bindUi() {
|
|
|
3299
4055
|
hideComposerMenu()
|
|
3300
4056
|
return
|
|
3301
4057
|
}
|
|
4058
|
+
const perm = e.target.closest('[data-perm]')
|
|
4059
|
+
if (perm) {
|
|
4060
|
+
const input = $('composer-input')
|
|
4061
|
+
input.value = '/permission ' + perm.dataset.perm + ' '
|
|
4062
|
+
input.focus()
|
|
4063
|
+
autosize(input)
|
|
4064
|
+
hideComposerMenu()
|
|
4065
|
+
return
|
|
4066
|
+
}
|
|
3302
4067
|
const preset = e.target.closest('[data-preset]')
|
|
3303
4068
|
if (preset) {
|
|
3304
4069
|
const found = readPresets().find(x => x.id === preset.dataset.preset)
|
|
@@ -3314,8 +4079,13 @@ function bindUi() {
|
|
|
3314
4079
|
$('btn-model-refresh').addEventListener('click', loadSessionModels)
|
|
3315
4080
|
const input = $('composer-input')
|
|
3316
4081
|
input.addEventListener('input', () => autosize(input))
|
|
4082
|
+
$('btn-fs-toggle').addEventListener('click', () => setComposerFullscreen(!$('composer-wrap').classList.contains('fs')))
|
|
4083
|
+
bindComposerFullscreenGesture()
|
|
4084
|
+
updateComposerFullscreenButton()
|
|
3317
4085
|
input.addEventListener('keydown', (e) => {
|
|
3318
|
-
if (e.key
|
|
4086
|
+
if (e.key !== 'Enter' || e.isComposing) return
|
|
4087
|
+
if (isMobileDevice() && mobileEnterAction() !== 'send') return
|
|
4088
|
+
if (!e.shiftKey) { e.preventDefault(); sendMessage() }
|
|
3319
4089
|
})
|
|
3320
4090
|
|
|
3321
4091
|
// 审批
|
|
@@ -3337,6 +4107,18 @@ function bindUi() {
|
|
|
3337
4107
|
$('workspace-create').addEventListener('click', createWorkspace)
|
|
3338
4108
|
$('workspace-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createWorkspace() })
|
|
3339
4109
|
$('modal-workspace').addEventListener('click', (e) => { if (e.target === $('modal-workspace')) closeWorkspaceModal() })
|
|
4110
|
+
$('archive-cancel').addEventListener('click', closeArchiveConfirm)
|
|
4111
|
+
$('archive-confirm').addEventListener('click', confirmArchiveSession)
|
|
4112
|
+
$('modal-archive').addEventListener('click', (e) => { if (e.target === $('modal-archive')) closeArchiveConfirm() })
|
|
4113
|
+
$('announcement-later').addEventListener('click', () => closeAnnouncement(false))
|
|
4114
|
+
$('announcement-confirm').addEventListener('click', () => closeAnnouncement(true))
|
|
4115
|
+
$('modal-announcement').addEventListener('click', (e) => {
|
|
4116
|
+
if (e.target === $('modal-announcement') && !state.announcement?.force) closeAnnouncement(false)
|
|
4117
|
+
})
|
|
4118
|
+
$('announcement-history-close').addEventListener('click', closeAnnouncementHistory)
|
|
4119
|
+
$('modal-announcement-history').addEventListener('click', (e) => {
|
|
4120
|
+
if (e.target === $('modal-announcement-history')) closeAnnouncementHistory()
|
|
4121
|
+
})
|
|
3340
4122
|
// 设置
|
|
3341
4123
|
$('view-settings').addEventListener('click', (e) => {
|
|
3342
4124
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -3373,10 +4155,16 @@ function bindUi() {
|
|
|
3373
4155
|
$('btn-update-expand').addEventListener('click', toggleUpdateExpand)
|
|
3374
4156
|
$('btn-reset').addEventListener('click', () => {
|
|
3375
4157
|
if (!confirm(t('settings.confirmReset'))) return
|
|
3376
|
-
LS.del('token'); LS.del('notify'); LS.del('server')
|
|
4158
|
+
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY)
|
|
3377
4159
|
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
3378
4160
|
location.reload()
|
|
3379
4161
|
})
|
|
4162
|
+
$('mobile-enter-action').value = mobileEnterAction()
|
|
4163
|
+
$('mobile-enter-action').addEventListener('change', (e) => {
|
|
4164
|
+
const action = e.target.value === 'send' ? 'send' : 'newline'
|
|
4165
|
+
LS.set('mobileEnterAction', action)
|
|
4166
|
+
toast(t(action === 'send' ? 'settings.mobileEnterSend' : 'settings.mobileEnterNewline'), 'ok')
|
|
4167
|
+
})
|
|
3380
4168
|
$('opt-notify').checked = LS.get('notify', '0') === '1'
|
|
3381
4169
|
$('opt-notify').addEventListener('change', async (e) => {
|
|
3382
4170
|
if (e.target.checked) {
|
|
@@ -3411,6 +4199,7 @@ function bindUi() {
|
|
|
3411
4199
|
LS.set('peakRemind', e.target.checked ? '1' : '0')
|
|
3412
4200
|
})
|
|
3413
4201
|
$('btn-test-notify').addEventListener('click', sendTestNotification)
|
|
4202
|
+
$('btn-announcement-history').addEventListener('click', openAnnouncementHistory)
|
|
3414
4203
|
// 已开启则启动时重新调度, 防止系统清理后丢失
|
|
3415
4204
|
if (peakRemindOn() && CAP?.isNativePlatform?.()) schedulePeakReminders()
|
|
3416
4205
|
applyBgConfigFromNative()
|
|
@@ -3506,10 +4295,11 @@ async function boot() {
|
|
|
3506
4295
|
bindNativeBack()
|
|
3507
4296
|
bindNativeLinks()
|
|
3508
4297
|
applyNativeInsets()
|
|
4298
|
+
showView('view-activity')
|
|
3509
4299
|
updateConn()
|
|
3510
|
-
loadLocalVersion()
|
|
4300
|
+
await loadLocalVersion()
|
|
3511
4301
|
if (!state.token) {
|
|
3512
|
-
showView('view-
|
|
4302
|
+
showView('view-activity')
|
|
3513
4303
|
$('token-desc').textContent = t('token.notSetHint')
|
|
3514
4304
|
} else {
|
|
3515
4305
|
// 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
|
|
@@ -3519,9 +4309,12 @@ async function boot() {
|
|
|
3519
4309
|
const host = await safeRpc('host.describe', {}, '')
|
|
3520
4310
|
if (host) { state.hostInfo = host; $('host-desc').textContent = t('settings.hostDesc', { version: host.version, cwd: host.cwd, n: host.attachedSessions }) }
|
|
3521
4311
|
loadDshControl()
|
|
3522
|
-
// 启动后自动检查一次更新(静默)
|
|
3523
|
-
setTimeout(() => checkUpdate(true), 4000)
|
|
3524
4312
|
}
|
|
4313
|
+
// 公告与更新共用当前服务器;公告优先,避免启动时两个弹窗重叠。
|
|
4314
|
+
setTimeout(async () => {
|
|
4315
|
+
const shown = await checkAnnouncements()
|
|
4316
|
+
if (!shown && state.token) checkUpdate(true)
|
|
4317
|
+
}, 4000)
|
|
3525
4318
|
renderPending()
|
|
3526
4319
|
}
|
|
3527
4320
|
|