dsh-remote-plugin 0.6.24 → 0.6.25
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-stats.cjs +106 -84
- package/gateway.cjs +78 -7
- package/package.json +1 -1
- package/public/app.js +153 -244
- package/public/desktop/desktop.html +2 -0
- package/public/desktop/desktop.js +50 -2
- package/public/genui.css +2 -0
- package/public/genui.js +144 -0
- package/public/index.html +11 -26
- package/public/md.js +16 -3
- package/public/styles.css +4 -11
- package/public/update.json +8 -8
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/gateway-stats.cjs
CHANGED
|
@@ -22,6 +22,17 @@ const path = require('node:path')
|
|
|
22
22
|
const os = require('node:os')
|
|
23
23
|
const { spawn } = require('node:child_process')
|
|
24
24
|
const readline = require('node:readline')
|
|
25
|
+
const zlib = require('node:zlib')
|
|
26
|
+
|
|
27
|
+
function logGeneration(file) {
|
|
28
|
+
const match = /^session(?:\.v([1-9]\d*))?\.jsonl(?:\.zstd)?$/.exec(path.basename(file))
|
|
29
|
+
return match && Number.isSafeInteger(Number(match[1] || 0)) ? Number(match[1] || 0) : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// V3 preserves message IDs, times and usage while renumbering durable seqs.
|
|
33
|
+
function usageIdentity(event) {
|
|
34
|
+
return JSON.stringify([event.data?.message?.id || '', event.time, normalizeUsage(event)])
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
// ---------- 固定价格表(v1 硬编码; v2 将改为配置文件/环境变量) ----------
|
|
27
38
|
// 时段判定: 工作日北京时间 9:00-12:00 与 14:00-18:00 为高峰(含起点不含终点),周末全天谷时
|
|
@@ -238,9 +249,9 @@ class StatsStore {
|
|
|
238
249
|
return null
|
|
239
250
|
}
|
|
240
251
|
|
|
241
|
-
_setCursor(sessionId, lastSeq) {
|
|
252
|
+
_setCursor(sessionId, lastSeq, generation) {
|
|
242
253
|
const c = this._loadCursors()
|
|
243
|
-
c[sessionId] = { lastSeq, updatedAt: Date.now() }
|
|
254
|
+
c[sessionId] = { ...c[sessionId], lastSeq, updatedAt: Date.now(), ...(generation == null ? {} : { generation }) }
|
|
244
255
|
this._saveCursors()
|
|
245
256
|
}
|
|
246
257
|
|
|
@@ -296,102 +307,111 @@ class StatsStore {
|
|
|
296
307
|
return this._enqueue(() => this._scanFile(file, onProgress))
|
|
297
308
|
}
|
|
298
309
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
dirtyDays.clear()
|
|
314
|
-
if (lastSeq >= 0) this._setCursor(sessionId, lastSeq)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const zstd = this.spawn('zstd', ['-dc', file], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
318
|
-
const rl = readline.createInterface({ input: zstd.stdout })
|
|
319
|
-
|
|
320
|
-
zstd.on('error', (err) => {
|
|
321
|
-
if (err.code === 'ENOENT') {
|
|
322
|
-
console.warn(`[stats] 未找到 zstd 命令, 跳过历史回填: ${file}`)
|
|
323
|
-
} else {
|
|
324
|
-
console.warn(`[stats] zstd 解压失败 ${file}: ${err.message}`)
|
|
325
|
-
}
|
|
326
|
-
scanError = err.code || err.message
|
|
327
|
-
rl.close()
|
|
310
|
+
async _readLog(file, visit) {
|
|
311
|
+
let input, source, child, childResult
|
|
312
|
+
if (!file.endsWith('.zstd')) input = fs.createReadStream(file)
|
|
313
|
+
else if (this.spawn === spawn && typeof zlib.createZstdDecompress === 'function') {
|
|
314
|
+
source = fs.createReadStream(file)
|
|
315
|
+
input = zlib.createZstdDecompress()
|
|
316
|
+
source.on('error', error => input.destroy(error))
|
|
317
|
+
source.pipe(input)
|
|
318
|
+
} else {
|
|
319
|
+
child = this.spawn('zstd', ['-dc', file], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
320
|
+
input = child.stdout
|
|
321
|
+
childResult = new Promise(resolve => {
|
|
322
|
+
child.once('close', code => resolve({ code }))
|
|
323
|
+
child.once('error', error => resolve({ error }))
|
|
328
324
|
})
|
|
325
|
+
child.on('error', error => input.destroy(error))
|
|
326
|
+
}
|
|
327
|
+
const rl = readline.createInterface({ input })
|
|
328
|
+
let lines = 0
|
|
329
|
+
try {
|
|
330
|
+
for await (const line of rl) {
|
|
331
|
+
if (!line.trim()) continue
|
|
332
|
+
const event = JSON.parse(line)
|
|
333
|
+
if (Number.isSafeInteger(event.seq)) visit(event)
|
|
334
|
+
if (++lines % 500 === 0) await new Promise(resolve => setImmediate(resolve))
|
|
335
|
+
}
|
|
336
|
+
if (childResult) {
|
|
337
|
+
const result = await childResult
|
|
338
|
+
if (result.error) throw result.error
|
|
339
|
+
if (result.code !== 0) throw new Error('zstd-decompression-failed')
|
|
340
|
+
}
|
|
341
|
+
} finally {
|
|
342
|
+
rl.close()
|
|
343
|
+
input.destroy()
|
|
344
|
+
source?.destroy()
|
|
345
|
+
child?.kill?.()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
329
348
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (event.type === 'request/context' && event.data?.model) currentModel = event.data.model
|
|
352
|
-
} catch {}
|
|
353
|
-
if (event.type === 'assistant/message') {
|
|
354
|
-
const usage = normalizeUsage(event)
|
|
355
|
-
if (usage) {
|
|
356
|
-
const model = eventModel(event) || currentModel || 'unknown'
|
|
357
|
-
const { date, hour, period } = eventKey(event.time)
|
|
358
|
-
if (date >= PRICING_START_DATE) {
|
|
359
|
-
const day = dirtyDays.get(date) || this._loadDay(date)
|
|
360
|
-
dirtyDays.set(date, day)
|
|
361
|
-
const hourBucket = day.hours[hour] || (day.hours[hour] = {})
|
|
362
|
-
const modelBucket = hourBucket[model] || (hourBucket[model] = emptyBucket())
|
|
363
|
-
addUsage(modelBucket, model, period, usage)
|
|
364
|
-
processed++
|
|
365
|
-
}
|
|
349
|
+
async _scanFile(file, onProgress) {
|
|
350
|
+
const sessionId = path.basename(path.dirname(file))
|
|
351
|
+
const cur = this._cursor(sessionId)
|
|
352
|
+
const generation = logGeneration(file) ?? 0
|
|
353
|
+
const migrated = cur && generation !== (cur.generation ?? 0)
|
|
354
|
+
let lastSeq = migrated ? -1 : (cur?.lastSeq ?? -1)
|
|
355
|
+
let processed = 0
|
|
356
|
+
let currentModel = ''
|
|
357
|
+
const dirtyDays = new Map()
|
|
358
|
+
const counted = new Map()
|
|
359
|
+
try {
|
|
360
|
+
if (migrated) {
|
|
361
|
+
// Read the preserved predecessor before advancing to a renumbered log.
|
|
362
|
+
// Missing predecessor is an explicit error, never a blind double count.
|
|
363
|
+
const names = await fs.promises.readdir(path.dirname(file))
|
|
364
|
+
const previous = names.find(name => logGeneration(name) === (cur.generation ?? 0))
|
|
365
|
+
if (!previous) throw new Error('stats-migration-source-missing')
|
|
366
|
+
await this._readLog(path.join(path.dirname(file), previous), event => {
|
|
367
|
+
if (event.seq <= cur.lastSeq && event.type === 'assistant/message' && normalizeUsage(event)) {
|
|
368
|
+
const key = usageIdentity(event)
|
|
369
|
+
counted.set(key, (counted.get(key) || 0) + 1)
|
|
366
370
|
}
|
|
367
|
-
}
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
await this._readLog(file, event => {
|
|
374
|
+
if (event.type === 'request/header' && event.data?.config?.model) currentModel = event.data.config.model
|
|
375
|
+
if (event.type === 'request/context' && event.data?.model) currentModel = event.data.model
|
|
376
|
+
if (event.seq <= lastSeq) return
|
|
368
377
|
lastSeq = event.seq
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
378
|
+
if (event.type !== 'assistant/message') return
|
|
379
|
+
const usage = normalizeUsage(event)
|
|
380
|
+
if (!usage) return
|
|
381
|
+
const key = usageIdentity(event)
|
|
382
|
+
if (counted.get(key)) { counted.set(key, counted.get(key) - 1); return }
|
|
383
|
+
const model = eventModel(event) || currentModel || 'unknown'
|
|
384
|
+
const { date, hour, period } = eventKey(event.time)
|
|
385
|
+
if (date < PRICING_START_DATE) return
|
|
386
|
+
// Stage detached buckets: a failed read must not mutate cached totals.
|
|
387
|
+
const day = dirtyDays.get(date) || JSON.parse(JSON.stringify(this._loadDay(date)))
|
|
388
|
+
dirtyDays.set(date, day)
|
|
389
|
+
const hourBucket = day.hours[hour] || (day.hours[hour] = {})
|
|
390
|
+
addUsage(hourBucket[model] || (hourBucket[model] = emptyBucket()), model, period, usage)
|
|
391
|
+
processed++
|
|
375
392
|
})
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
393
|
+
if ([...counted.values()].some(count => count > 0)) throw new Error('stats-migration-usage-mismatch')
|
|
394
|
+
for (const day of dirtyDays.values()) this._saveDay(day)
|
|
395
|
+
if (lastSeq >= 0) this._setCursor(sessionId, lastSeq, generation)
|
|
396
|
+
if (onProgress) onProgress({ sessionId, processed })
|
|
397
|
+
return { sessionId, processed }
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return { sessionId, processed: 0, error: error.code || error.message }
|
|
400
|
+
}
|
|
383
401
|
}
|
|
384
402
|
|
|
385
403
|
/** 扫描 ~/.dsh/sessions 下全部 session.jsonl.zstd。 */
|
|
386
404
|
async scanAll(sessionsRoot, onProgress) {
|
|
387
|
-
const root = sessionsRoot || path.join(os.homedir(), '.dsh', 'sessions')
|
|
405
|
+
const root = sessionsRoot || path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'sessions')
|
|
388
406
|
let files = []
|
|
389
407
|
try {
|
|
390
408
|
const walk = async (dir) => {
|
|
391
409
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
410
|
+
const logs = entries.filter(ent => ent.isFile() && logGeneration(ent.name) !== null)
|
|
411
|
+
.sort((a, b) => logGeneration(b.name) - logGeneration(a.name) || a.name.localeCompare(b.name))
|
|
412
|
+
if (logs.length) files.push(path.join(dir, logs[0].name))
|
|
392
413
|
for (const ent of entries) {
|
|
393
414
|
if (ent.isDirectory()) await walk(path.join(dir, ent.name))
|
|
394
|
-
else if (ent.name === 'session.jsonl.zstd') files.push(path.join(dir, ent.name))
|
|
395
415
|
}
|
|
396
416
|
}
|
|
397
417
|
await walk(root)
|
|
@@ -400,12 +420,14 @@ class StatsStore {
|
|
|
400
420
|
return { files: 0, processed: 0 }
|
|
401
421
|
}
|
|
402
422
|
let processed = 0
|
|
423
|
+
const errors = []
|
|
403
424
|
// 串行扫描, 避免大量并发 zstd 子进程
|
|
404
425
|
for (const file of files) {
|
|
405
426
|
const out = await this.scanFile(file, onProgress)
|
|
406
427
|
processed += out.processed || 0
|
|
428
|
+
if (out.error) errors.push({ sessionId: out.sessionId, error: out.error })
|
|
407
429
|
}
|
|
408
|
-
return { files: files.length, processed }
|
|
430
|
+
return { files: files.length, processed, ...(errors.length ? { errors } : {}) }
|
|
409
431
|
}
|
|
410
432
|
|
|
411
433
|
summary(days) {
|
package/gateway.cjs
CHANGED
|
@@ -208,7 +208,7 @@ const MIME = {
|
|
|
208
208
|
|
|
209
209
|
// ---------- /fs 文件传输 ----------
|
|
210
210
|
// 允许访问的根目录: DSH_REMOTE_FS_ROOT 使用系统路径分隔符分隔多个根,
|
|
211
|
-
// POSIX 为 ':'、Windows 为 ';'
|
|
211
|
+
// POSIX 为 ':'、Windows 为 ';';Windows 默认用户目录及 C 盘以外的可用盘符。
|
|
212
212
|
// 所有 /fs/* 路径 resolve 后都必须位于某个根内, 已存在的路径还会用 realpath
|
|
213
213
|
// 复核一次, 防止 ../ 穿越与符号链接逃逸。
|
|
214
214
|
const FS_DEFAULT_ROOT = path.resolve(os.homedir())
|
|
@@ -218,10 +218,20 @@ function fsConfiguredRoot(value) {
|
|
|
218
218
|
if (/^~[\\/]/.test(raw)) return path.resolve(FS_DEFAULT_ROOT, raw.slice(2))
|
|
219
219
|
return path.resolve(raw)
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
function fsDefaultRoots(platform, home, isDirectory) {
|
|
222
|
+
const roots = [home]
|
|
223
|
+
if (platform === 'win32') {
|
|
224
|
+
for (const letter of 'ABDEFGHIJKLMNOPQRSTUVWXYZ') {
|
|
225
|
+
const root = `${letter}:\\`
|
|
226
|
+
try { if (isDirectory(root)) roots.push(root) } catch {}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return roots
|
|
230
|
+
}
|
|
231
|
+
const FS_ROOTS = process.env.DSH_REMOTE_FS_ROOT
|
|
232
|
+
? process.env.DSH_REMOTE_FS_ROOT.split(path.delimiter).map(fsConfiguredRoot).filter(Boolean)
|
|
233
|
+
: fsDefaultRoots(process.platform, FS_DEFAULT_ROOT, root => fs.statSync(root).isDirectory())
|
|
234
|
+
const FS_WINDOWS_DEFAULT = process.platform === 'win32' && !process.env.DSH_REMOTE_FS_ROOT
|
|
225
235
|
const FS_WORKSPACE_CACHE_MS = durationEnv('DSH_REMOTE_FS_WORKSPACE_CACHE_MS', 15_000, 1000, 10 * 60_000)
|
|
226
236
|
const FS_MAX_UPLOAD = Number(process.env.DSH_REMOTE_FS_MAX_UPLOAD) || 2 * 1024 * 1024 * 1024
|
|
227
237
|
const FS_UPLOAD_TTL_MS = durationEnv('DSH_REMOTE_FS_UPLOAD_TTL_MS', 24 * 60 * 60 * 1000, 60_000, 7 * 24 * 60 * 60 * 1000)
|
|
@@ -235,6 +245,8 @@ function fsRootReals() {
|
|
|
235
245
|
return FS_ROOT_REALS
|
|
236
246
|
}
|
|
237
247
|
function fsInsideReal(real) {
|
|
248
|
+
// 默认 Windows 授权不能被已登记工作区或盘符的 junction / SUBST 目标扩大。
|
|
249
|
+
if (FS_WINDOWS_DEFAULT) return FS_ROOTS.some(root => fsInsideRoot(real, root))
|
|
238
250
|
for (const root of [...fsRootReals(), ...fsWorkspaceRootsCache.reals]) {
|
|
239
251
|
if (fsInsideRoot(real, root)) return true
|
|
240
252
|
}
|
|
@@ -1401,6 +1413,7 @@ const modernState = {
|
|
|
1401
1413
|
sessionCursors: new Map(),
|
|
1402
1414
|
workspaces: { items: [], archivedSessionIds: [] },
|
|
1403
1415
|
pendingEvents: new Map(),
|
|
1416
|
+
assistantStreams: new Map(),
|
|
1404
1417
|
}
|
|
1405
1418
|
|
|
1406
1419
|
async function callUpstreamRemote(endpoint, args, rpcId = crypto.randomUUID()) {
|
|
@@ -1536,7 +1549,7 @@ async function translateModernRpc(method, payload, rpcId) {
|
|
|
1536
1549
|
...(payload.beforeSeq === undefined ? {} : { beforeSeq: payload.beforeSeq }),
|
|
1537
1550
|
...(payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
|
|
1538
1551
|
} }
|
|
1539
|
-
transform = value => legacyHistoryValue(value, summary)
|
|
1552
|
+
transform = value => ({ ...legacyHistoryValue(value, summary), ...modernReasoningValue(payload.sessionId) })
|
|
1540
1553
|
} else if (method === 'session.models') {
|
|
1541
1554
|
endpoint = 'session/modelCatalog'
|
|
1542
1555
|
args = {}
|
|
@@ -1670,6 +1683,7 @@ function rememberCollectorReplay(kind, full, raw) {
|
|
|
1670
1683
|
const replay = collectorReplay[kind]
|
|
1671
1684
|
let key = ''
|
|
1672
1685
|
if (payload.type === 'session/subscribed' && payload.sessionId) key = `session:${payload.sessionId}`
|
|
1686
|
+
else if (payload.type === 'session/reasoning' && payload.sessionId) key = `reasoning:${payload.sessionId}`
|
|
1673
1687
|
else if (payload.type === 'approval/requested' && payload.approvalId) key = `approval:${payload.approvalId}`
|
|
1674
1688
|
else if (payload.type === 'question/requested' && full.rpcId) key = `question:${full.rpcId}`
|
|
1675
1689
|
else if (payload.type === 'approval/resolved' && payload.approvalId) replay.delete(`approval:${payload.approvalId}`)
|
|
@@ -1940,7 +1954,7 @@ function openModernSessionStream(ws, sessionId) {
|
|
|
1940
1954
|
const streamId = 'session:' + sessionId
|
|
1941
1955
|
ws.send(JSON.stringify({
|
|
1942
1956
|
type: 'open', streamId, endpoint: 'session/follow',
|
|
1943
|
-
payload: { args: { request: { address: { kind: 'session', sessionId } } } },
|
|
1957
|
+
payload: { args: { request: { address: { kind: 'session', sessionId }, assistantStream: true } } },
|
|
1944
1958
|
}))
|
|
1945
1959
|
}
|
|
1946
1960
|
|
|
@@ -1997,8 +2011,59 @@ function applyModernWorkspaceFrame(value) {
|
|
|
1997
2011
|
}
|
|
1998
2012
|
}
|
|
1999
2013
|
|
|
2014
|
+
// 0.1.5 streams are process-local and have no durable seq. Keep a bounded,
|
|
2015
|
+
// replaceable reasoning baseline for HTTP history, polling and late WS clients.
|
|
2016
|
+
function modernReasoningValue(sessionId) {
|
|
2017
|
+
const stream = modernState.assistantStreams.get(sessionId)
|
|
2018
|
+
return stream ? { partialReasoning: [...stream.blocks.values()] } : {}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function applyModernAssistantStream(sessionId, value, baseline = false) {
|
|
2022
|
+
let stream = modernState.assistantStreams.get(sessionId)
|
|
2023
|
+
const publish = () => legacyPush('mux', { type: 'session/reasoning', sessionId, ...modernReasoningValue(sessionId) })
|
|
2024
|
+
const fold = chunk => {
|
|
2025
|
+
if (!chunk || !Number.isSafeInteger(chunk.index) || chunk.index < 0 || chunk.index >= 64) return false
|
|
2026
|
+
const item = stream.blocks.get(chunk.index) || { turn: stream.turn, step: stream.step, index: chunk.index, text: '' }
|
|
2027
|
+
if (chunk.type === 'reasoning-delta') item.text = (item.text + String(chunk.text || '')).slice(0, 12000)
|
|
2028
|
+
else if (chunk.type === 'block-start' && chunk.blockType === 'reasoning') item.text = ''
|
|
2029
|
+
else if (chunk.type === 'block-end' && chunk.block?.type === 'reasoning') item.text = String(chunk.block.text || '').slice(0, 12000)
|
|
2030
|
+
else return false
|
|
2031
|
+
stream.blocks.set(chunk.index, item)
|
|
2032
|
+
return true
|
|
2033
|
+
}
|
|
2034
|
+
if (baseline || value?.type === 'start') {
|
|
2035
|
+
const attempt = baseline ? value?.activeAttempt : value
|
|
2036
|
+
stream = { revision: value?.revision || 0, attemptId: attempt?.attemptId, turn: attempt?.turn, step: attempt?.step, nextIndex: attempt?.nextIndex || 0, blocks: new Map() }
|
|
2037
|
+
modernState.assistantStreams.set(sessionId, stream)
|
|
2038
|
+
for (const record of attempt?.stream || []) {
|
|
2039
|
+
if (record.type === 'chunk') fold(record.chunk)
|
|
2040
|
+
else if (record.type === 'reasoning-chunks') fold({ type: 'reasoning-delta', index: record.index, text: (record.texts || []).join('') })
|
|
2041
|
+
}
|
|
2042
|
+
publish()
|
|
2043
|
+
return
|
|
2044
|
+
}
|
|
2045
|
+
if (!stream || value?.revision <= stream.revision) return
|
|
2046
|
+
if (value?.revision !== stream.revision + 1 || value.attemptId !== stream.attemptId || value.index !== stream.nextIndex) {
|
|
2047
|
+
stream.blocks.clear()
|
|
2048
|
+
stream.attemptId = undefined
|
|
2049
|
+
stream.revision = value?.revision || stream.revision
|
|
2050
|
+
publish()
|
|
2051
|
+
return
|
|
2052
|
+
}
|
|
2053
|
+
stream.revision = value.revision
|
|
2054
|
+
if (value.type === 'end') {
|
|
2055
|
+
stream.blocks.clear()
|
|
2056
|
+
stream.attemptId = undefined
|
|
2057
|
+
publish()
|
|
2058
|
+
} else if (value.type === 'chunk') {
|
|
2059
|
+
stream.nextIndex++
|
|
2060
|
+
if (fold(value.chunk)) publish()
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2000
2064
|
function applyModernSessionFrame(sessionId, value) {
|
|
2001
2065
|
if (value?.type === 'snapshot') {
|
|
2066
|
+
if (value.assistantStream) applyModernAssistantStream(sessionId, value.assistantStream, true)
|
|
2002
2067
|
modernState.sessionCursors.set(sessionId, value.cursor)
|
|
2003
2068
|
legacyPush('mux', { type: 'session/subscribed', sessionId, lastSeq: value.cursor })
|
|
2004
2069
|
for (const record of value.records || []) {
|
|
@@ -2012,6 +2077,8 @@ function applyModernSessionFrame(sessionId, value) {
|
|
|
2012
2077
|
if (value?.type === 'event' && value.event) {
|
|
2013
2078
|
modernState.sessionCursors.set(sessionId, value.event.seq)
|
|
2014
2079
|
legacyPush('mux', { type: 'session/event', sessionId, event: value.event })
|
|
2080
|
+
} else if (value?.type === 'assistant-stream') {
|
|
2081
|
+
applyModernAssistantStream(sessionId, value.frame)
|
|
2015
2082
|
}
|
|
2016
2083
|
}
|
|
2017
2084
|
|
|
@@ -2074,6 +2141,8 @@ function applyModernRemoteEvent(ws, value) {
|
|
|
2074
2141
|
} else if (value.event === 'api-session/removed') {
|
|
2075
2142
|
modernState.sessions.delete(args[0])
|
|
2076
2143
|
modernState.sessionCursors.delete(args[0])
|
|
2144
|
+
modernState.assistantStreams.delete(args[0])
|
|
2145
|
+
collectorReplay.mux.delete(`reasoning:${args[0]}`)
|
|
2077
2146
|
try { ws.send(JSON.stringify({ type: 'cancel', streamId: 'session:' + args[0] })) } catch {}
|
|
2078
2147
|
legacyPush('host', { type: 'host/session-removed', sessionId: args[0] })
|
|
2079
2148
|
} else if (value.event === 'api-session/status') {
|
|
@@ -2229,6 +2298,7 @@ async function scanStatsOnce(delay) {
|
|
|
2229
2298
|
try {
|
|
2230
2299
|
const out = await statsStore.scanAll()
|
|
2231
2300
|
if (out.files) console.log(`[stats] 历史回填扫描完成: ${out.processed} 个新事件 (${out.files} 个会话文件)`)
|
|
2301
|
+
if (out.errors?.length) console.warn(`[stats] ${out.errors.length} 个会话回填失败,保留原统计游标: ${out.errors.map(item => item.error).join(', ')}`)
|
|
2232
2302
|
} catch (err) {
|
|
2233
2303
|
console.warn('[stats] 历史回填扫描失败: ' + (err?.message || err))
|
|
2234
2304
|
} finally {
|
|
@@ -3035,6 +3105,7 @@ async function fsResolve(input) {
|
|
|
3035
3105
|
else if (path.isAbsolute(raw)) abs = path.resolve(raw)
|
|
3036
3106
|
else abs = path.resolve(FS_ROOTS[0], raw) // 相对路径按默认根解析
|
|
3037
3107
|
if (FS_ROOTS.some(root => fsInsideRoot(abs, root))) return { abs }
|
|
3108
|
+
if (FS_WINDOWS_DEFAULT) return { error: 'forbidden' }
|
|
3038
3109
|
let workspaces = await loadFsWorkspaceRoots(false)
|
|
3039
3110
|
if (workspaces.roots.some(root => fsInsideRoot(abs, root))) return { abs }
|
|
3040
3111
|
// 新建/刚加入的工作区可能还没进入 15s 缓存,未命中时强制刷新一次。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.25",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|