@gehennawu/dsh-service 1.1.2 → 1.3.0
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 +4 -2
- package/README.md +4 -2
- package/client.js +1 -1
- package/index.js +200 -26
- package/package.json +3 -1
- package/plugin-compat.js +374 -0
- package/plugin-health.js +138 -0
package/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import { promisify } from 'node:util'
|
|
|
15
15
|
import { ServerResponse as NodeServerResponse } from 'node:http'
|
|
16
16
|
import z from '@deepseek-ai/schemastery'
|
|
17
17
|
import { createBackupIntegrity } from './backup-integrity.js'
|
|
18
|
+
import { collectPluginCompat, pluginCompatCheckItem } from './plugin-compat.js'
|
|
19
|
+
import { collectPluginHealth, pluginCheckItem, restartPluginEntry } from './plugin-health.js'
|
|
18
20
|
import {
|
|
19
21
|
buildCliproxyAccountPlan as buildCliproxyAccountPlanAdapter,
|
|
20
22
|
cliproxyFetchGuard as cliproxyFetchGuardAdapter,
|
|
@@ -131,6 +133,16 @@ const SESSIONS_BYTES_MAX_IDS = 200
|
|
|
131
133
|
const SESSIONS_BYTES_MAX_ENTRIES = 1000
|
|
132
134
|
// 详情快照缓存(v0.36 查看渲染优化):live 会话 30s 后重读保新鲜,冷会话长期复用至槽位失效/删除。
|
|
133
135
|
const SESSIONS_VIEW_LIVE_TTL_MS = 30 * 1000
|
|
136
|
+
// 会话标题缓存(v1.1.3 列表加载优化):0.1.2-alpha.3 的 readTitleSnapshots 经 SessionCorpus.
|
|
137
|
+
// projectMany → persistence.inspect 对每个冷会话全量解析整份 JSONL 日志且零官方缓存——会话一多,
|
|
138
|
+
// 每次 sessions-list 都是整库日志重读,远贵于体积 stat。标题按 revision(stat 指纹,跨重启稳定)
|
|
139
|
+
// 键控缓存:冷会话文件不变标题不变,只对新增/revision 变更/live 超时条目重读;缓存持久化到
|
|
140
|
+
// DSH_HOME(仅标题+revision,不含内容),宿主重启后首个列表加载同样命中。
|
|
141
|
+
const SESSIONS_TITLE_FILE = 'dsh-service-session-titles.json'
|
|
142
|
+
const SESSIONS_TITLE_VERSION = 1
|
|
143
|
+
const SESSIONS_TITLE_MAX_ENTRIES = 2000
|
|
144
|
+
const SESSIONS_TITLE_LIVE_TTL_MS = 30 * 1000
|
|
145
|
+
const SESSIONS_TITLE_COLD_TTL_MS = 5 * 60 * 1000
|
|
134
146
|
// 详情/检索里视为「机制性噪声」的事件类型:折叠展示计数,用户可展开。
|
|
135
147
|
const SESSION_NOISE_TYPES = new Set([
|
|
136
148
|
'turn/start', 'step/start', 'step/end', 'assistant/chunk', 'request/header',
|
|
@@ -2253,11 +2265,42 @@ async function collectDiagnostics(ctx, dshHome, runtimeEnv) {
|
|
|
2253
2265
|
const nodeMajor = parseInt(process.versions.node, 10)
|
|
2254
2266
|
add('node-version', Number.isFinite(nodeMajor) && nodeMajor >= requiredNodeMajor ? 'ok' : 'warning', `${process.version}:${requiredNodeMajor}`)
|
|
2255
2267
|
|
|
2268
|
+
// v1.3 插件健康检查:只检查异常——loader 缺席降级为 info 检查项、不带异常行数组;
|
|
2269
|
+
// 停用条目(无论内置/自定义)一律不算异常,正常插件不下发(官方设置页已有完整清单与开关)。
|
|
2270
|
+
let pluginReport
|
|
2271
|
+
try {
|
|
2272
|
+
pluginReport = await collectPluginHealth(ctx)
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
pluginReport = { available: false, total: 0, issues: [] }
|
|
2275
|
+
}
|
|
2276
|
+
if (!pluginReport.available) add('plugins', 'info', 'unavailable')
|
|
2277
|
+
else checks.push(pluginCheckItem(pluginReport))
|
|
2278
|
+
|
|
2279
|
+
// v1.3 插件兼容性:对照已核实的 alpha 破坏面清单(client-runtime 供应商移除、SQLite
|
|
2280
|
+
// persistence 移除、聊天/统计条 CSS 哈希漂移、data-time-hover-root 删除)扫描启用插件的
|
|
2281
|
+
// 清单与入口代码;loader 缺席降级为 info 检查项、不带扫描结果。
|
|
2282
|
+
let compatReport
|
|
2283
|
+
try {
|
|
2284
|
+
compatReport = await collectPluginCompat(ctx)
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
compatReport = { available: false, scanned: 0, issues: [], declaredOnly: [], unknown: [] }
|
|
2287
|
+
}
|
|
2288
|
+
if (!compatReport.available) add('plugin-compat', 'info', 'unavailable')
|
|
2289
|
+
else checks.push(pluginCompatCheckItem(compatReport))
|
|
2290
|
+
|
|
2256
2291
|
let status = 'ok'
|
|
2257
2292
|
// advisory 警告(手动启动环境的黄色提示)只做行内呈现:不把 overall 拉成 warning。
|
|
2258
2293
|
if (checks.some((check) => check.status === 'error')) status = 'error'
|
|
2259
2294
|
else if (checks.some((check) => check.status === 'warning' && check.advisory !== true)) status = 'warning'
|
|
2260
|
-
return {
|
|
2295
|
+
return {
|
|
2296
|
+
status,
|
|
2297
|
+
checkedAt: Date.now(),
|
|
2298
|
+
checks,
|
|
2299
|
+
...(pluginReport.available ? { pluginIssues: pluginReport.issues } : {}),
|
|
2300
|
+
...(compatReport.available
|
|
2301
|
+
? { pluginCompat: { scanned: compatReport.scanned, issues: compatReport.issues, declaredOnly: compatReport.declaredOnly, unknown: compatReport.unknown } }
|
|
2302
|
+
: {}),
|
|
2303
|
+
}
|
|
2261
2304
|
}
|
|
2262
2305
|
|
|
2263
2306
|
async function collectHealth(ctx) {
|
|
@@ -3256,6 +3299,46 @@ async function saveDeletedSessions(dshHome, data) {
|
|
|
3256
3299
|
}
|
|
3257
3300
|
}
|
|
3258
3301
|
|
|
3302
|
+
// 标题缓存磁盘持久化:{version, items: {id: {title, revision?, live}}}——只存标题与指纹,
|
|
3303
|
+
// 不含任何会话内容;损坏/版本不符整体丢弃(回退全量拉取,宁慢勿错)。at 不落盘:
|
|
3304
|
+
// 加载后按 0 处理,revision 对不上或缺失时自然判定过期重读。
|
|
3305
|
+
function createEmptySessionTitles() {
|
|
3306
|
+
return { version: SESSIONS_TITLE_VERSION, items: {} }
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
async function loadSessionTitles(dshHome) {
|
|
3310
|
+
try {
|
|
3311
|
+
const parsed = JSON.parse(await readFile(join(dshHome, SESSIONS_TITLE_FILE), 'utf8'))
|
|
3312
|
+
if (parsed?.version !== SESSIONS_TITLE_VERSION || typeof parsed.items !== 'object' || parsed.items === null) return new Map()
|
|
3313
|
+
const entries = new Map()
|
|
3314
|
+
for (const [id, entry] of Object.entries(parsed.items)) {
|
|
3315
|
+
if (typeof id !== 'string' || id === '') continue
|
|
3316
|
+
if (typeof entry?.title !== 'string' || typeof entry.revision !== 'string') continue
|
|
3317
|
+
entries.set(id, { title: entry.title, revision: entry.revision, live: entry.live === true, at: 0 })
|
|
3318
|
+
}
|
|
3319
|
+
return entries
|
|
3320
|
+
} catch (_) {
|
|
3321
|
+
return new Map()
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
async function saveSessionTitles(dshHome, cache) {
|
|
3326
|
+
const items = {}
|
|
3327
|
+
for (const [id, entry] of cache) {
|
|
3328
|
+
if (typeof entry?.title !== 'string') continue
|
|
3329
|
+
items[id] = { title: entry.title, ...(typeof entry.revision === 'string' ? { revision: entry.revision } : {}), live: entry.live === true }
|
|
3330
|
+
}
|
|
3331
|
+
await mkdir(dshHome, { recursive: true })
|
|
3332
|
+
const target = join(dshHome, SESSIONS_TITLE_FILE)
|
|
3333
|
+
const temporary = `${target}.${randomUUID()}.tmp`
|
|
3334
|
+
try {
|
|
3335
|
+
await writeFile(temporary, JSON.stringify({ version: SESSIONS_TITLE_VERSION, items }), { mode: 0o600 })
|
|
3336
|
+
await rename(temporary, target)
|
|
3337
|
+
} finally {
|
|
3338
|
+
await rm(temporary, { force: true })
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3259
3342
|
/** 单个会话的事件文本(与官方 extractSessionEventText 语义一致,不依赖官方内部包)。 */
|
|
3260
3343
|
function sessionEventText(event) {
|
|
3261
3344
|
if (typeof event !== 'object' || event === null) return ''
|
|
@@ -3320,9 +3403,26 @@ async function sessionDirectoryBytes(ctx, header) {
|
|
|
3320
3403
|
}
|
|
3321
3404
|
}
|
|
3322
3405
|
|
|
3406
|
+
// 标题缓存条目是否仍新鲜:有 revision 源(sessionPersistence.listSnapshots 可用)时按指纹
|
|
3407
|
+
// 精确判定——文件没变标题就不变,冷会话永久命中;快照清单里缺席的 id(纯内存 live 会话)
|
|
3408
|
+
// 及 revision 源不可用时按 TTL 兜底。宁重读不展示旧标题。
|
|
3409
|
+
function sessionTitleFresh(entry, record, revisions, now) {
|
|
3410
|
+
if (entry === undefined) return false
|
|
3411
|
+
if (revisions !== null) {
|
|
3412
|
+
const revision = revisions.get(record.header.id)
|
|
3413
|
+
if (revision !== undefined) return entry.revision === revision
|
|
3414
|
+
return now - entry.at < SESSIONS_TITLE_LIVE_TTL_MS
|
|
3415
|
+
}
|
|
3416
|
+
const ttl = record.live === true ? SESSIONS_TITLE_LIVE_TTL_MS : SESSIONS_TITLE_COLD_TTL_MS
|
|
3417
|
+
return now - entry.at < ttl
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3323
3420
|
// 会话管理列表:live + 冷会话合并,标注归档/已删除,补标题。列表不携带体积——
|
|
3324
3421
|
// 体积走 sessions-bytes 懒加载(v0.36:全量下发意味着打开/切换就要逐个 readdir+stat)。
|
|
3325
|
-
|
|
3422
|
+
// v1.1.3:readTitleSnapshots 对每个冷会话全量解析整份日志(官方零缓存),改为 revision
|
|
3423
|
+
// 键控缓存——列表加载只对新增/变更条目重读,重复加载/面板重开零整库扫描;listing 与
|
|
3424
|
+
// 已删除清单并行获取。
|
|
3425
|
+
async function listSessionsForManage(ctx, dshHome, scope = 'all', titleCache = null) {
|
|
3326
3426
|
const sessionQuery = ctx.get('sessionQuery')
|
|
3327
3427
|
const workspaceRegistry = ctx.get('workspaceRegistry')
|
|
3328
3428
|
const available = sessionQuery !== undefined && typeof sessionQuery.listSessions === 'function'
|
|
@@ -3332,27 +3432,72 @@ async function listSessionsForManage(ctx, dshHome, scope = 'all') {
|
|
|
3332
3432
|
if (scope === 'deleted') {
|
|
3333
3433
|
return { available: true, items: [], archivedIds: [], deleted: (await loadDeletedSessions(dshHome)).items }
|
|
3334
3434
|
}
|
|
3335
|
-
const
|
|
3435
|
+
const now = Date.now()
|
|
3436
|
+
// revision 源:缓存启用时尽早并行拉取(一次 header-only 目录遍历 + 每会话一次 stat,
|
|
3437
|
+
// 与 listSessions 内部同量级);不可用/失败回落 null → TTL 兜底。
|
|
3438
|
+
const sessionPersistence = titleCache !== null ? ctx.get('sessionPersistence') : undefined
|
|
3439
|
+
const revisionsPromise = sessionPersistence !== undefined && typeof sessionPersistence.listSnapshots === 'function'
|
|
3440
|
+
? sessionPersistence.listSnapshots().then((snapshots) => {
|
|
3441
|
+
const revisions = new Map()
|
|
3442
|
+
for (const snapshot of Array.isArray(snapshots) ? snapshots : []) {
|
|
3443
|
+
if (typeof snapshot?.header?.id === 'string' && typeof snapshot.revision === 'string') revisions.set(snapshot.header.id, snapshot.revision)
|
|
3444
|
+
}
|
|
3445
|
+
return revisions
|
|
3446
|
+
}).catch(() => null)
|
|
3447
|
+
: Promise.resolve(null)
|
|
3448
|
+
const [records, deletedSnapshot] = await Promise.all([
|
|
3449
|
+
sessionQuery.listSessions(),
|
|
3450
|
+
loadDeletedSessions(dshHome),
|
|
3451
|
+
])
|
|
3452
|
+
const deleted = deletedSnapshot.items
|
|
3453
|
+
const revisions = await revisionsPromise
|
|
3336
3454
|
const archivedIds = workspaceRegistry !== undefined && Array.isArray(workspaceRegistry.archivedSessionIds)
|
|
3337
3455
|
? [...workspaceRegistry.archivedSessionIds]
|
|
3338
3456
|
: []
|
|
3339
3457
|
const archivedSet = new Set(archivedIds)
|
|
3340
|
-
const deleted = (await loadDeletedSessions(dshHome)).items
|
|
3341
3458
|
const deletedSet = new Set(deleted.map((item) => item.id))
|
|
3342
3459
|
// scope=archived 只保留归档条目(含 live 会话被归档的情况);此时仍需标题。
|
|
3343
3460
|
const recordsInScope = scope === 'archived'
|
|
3344
3461
|
? records.filter((record) => archivedSet.has(record.header.id))
|
|
3345
3462
|
: records
|
|
3346
|
-
//
|
|
3347
|
-
|
|
3463
|
+
// 标题折叠(缓存优先):只重读新增/revision 变更/live 超时的条目;fulfilled 才入缓存
|
|
3464
|
+
//(rejected 留空且不缓存,下次自动重试)。缓存条目按插入序 LRU 式淘汰封顶。
|
|
3465
|
+
const staleRecords = []
|
|
3466
|
+
if (titleCache !== null) {
|
|
3467
|
+
for (const record of recordsInScope) {
|
|
3468
|
+
if (deletedSet.has(record.header.id)) continue
|
|
3469
|
+
if (!sessionTitleFresh(titleCache.get(record.header.id), record, revisions, now)) staleRecords.push(record)
|
|
3470
|
+
}
|
|
3471
|
+
} else {
|
|
3472
|
+
for (const record of recordsInScope) {
|
|
3473
|
+
if (!deletedSet.has(record.header.id)) staleRecords.push(record)
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3348
3476
|
const titles = new Map()
|
|
3349
|
-
if (typeof sessionQuery.readTitleSnapshots === 'function' &&
|
|
3350
|
-
const observed = await sessionQuery.readTitleSnapshots(
|
|
3477
|
+
if (typeof sessionQuery.readTitleSnapshots === 'function' && staleRecords.length > 0) {
|
|
3478
|
+
const observed = await sessionQuery.readTitleSnapshots(staleRecords.map((record) => record.header.id))
|
|
3351
3479
|
for (const entry of observed) {
|
|
3352
3480
|
if (entry?.sessionId !== undefined && entry?.status === 'fulfilled' && entry.value?.title?.title !== undefined) {
|
|
3353
3481
|
titles.set(entry.sessionId, entry.value.title.title)
|
|
3354
3482
|
}
|
|
3355
3483
|
}
|
|
3484
|
+
if (titleCache !== null) {
|
|
3485
|
+
const recordById = new Map(staleRecords.map((record) => [record.header.id, record]))
|
|
3486
|
+
for (const [id, title] of titles) {
|
|
3487
|
+
const record = recordById.get(id)
|
|
3488
|
+
if (record === undefined) continue
|
|
3489
|
+
titleCache.set(id, { title, revision: revisions !== null ? revisions.get(id) : undefined, at: now, live: record.live === true })
|
|
3490
|
+
}
|
|
3491
|
+
while (titleCache.size > SESSIONS_TITLE_MAX_ENTRIES) {
|
|
3492
|
+
const oldest = titleCache.keys().next().value
|
|
3493
|
+
if (oldest === undefined) break
|
|
3494
|
+
titleCache.delete(oldest)
|
|
3495
|
+
}
|
|
3496
|
+
// 落盘随 RPC 收敛(不留在 Fiber 外):仅本次确实重读过标题才写,原子替换 0600。
|
|
3497
|
+
if (titles.size > 0) {
|
|
3498
|
+
try { await saveSessionTitles(dshHome, titleCache) } catch (_) {}
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3356
3501
|
}
|
|
3357
3502
|
const items = []
|
|
3358
3503
|
for (const record of recordsInScope) {
|
|
@@ -3360,7 +3505,7 @@ async function listSessionsForManage(ctx, dshHome, scope = 'all') {
|
|
|
3360
3505
|
if (deletedSet.has(id)) continue
|
|
3361
3506
|
items.push({
|
|
3362
3507
|
id,
|
|
3363
|
-
title: titles.get(id) ?? '',
|
|
3508
|
+
title: titles.get(id) ?? (titleCache !== null ? titleCache.get(id)?.title : undefined) ?? '',
|
|
3364
3509
|
cwd: record.header.cwd ?? null,
|
|
3365
3510
|
createdAt: record.header.createdAt ?? 0,
|
|
3366
3511
|
live: record.live === true,
|
|
@@ -3397,20 +3542,27 @@ async function resolveSessionBytesForIds(ctx, ids, cache) {
|
|
|
3397
3542
|
headerById = null
|
|
3398
3543
|
}
|
|
3399
3544
|
}
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3545
|
+
// 未命中目录 stat 并发执行(8 路封顶):首个列表加载的 stat 风暴从串行排队压成小批并行。
|
|
3546
|
+
const SESSIONS_BYTES_STAT_CONCURRENCY = 8
|
|
3547
|
+
let statCursor = 0
|
|
3548
|
+
const statWorkers = Array.from({ length: Math.min(SESSIONS_BYTES_STAT_CONCURRENCY, misses.length) }, async () => {
|
|
3549
|
+
while (statCursor < misses.length) {
|
|
3550
|
+
const id = misses[statCursor++]
|
|
3551
|
+
const header = headerById === null ? undefined : headerById.get(id)
|
|
3552
|
+
if (header === undefined) {
|
|
3553
|
+
bytes[id] = null
|
|
3554
|
+
continue
|
|
3555
|
+
}
|
|
3556
|
+
const value = await sessionDirectoryBytes(ctx, header)
|
|
3557
|
+
cache.set(id, { bytes: value, at: Date.now() })
|
|
3558
|
+
if (cache.size > SESSIONS_BYTES_MAX_ENTRIES) {
|
|
3559
|
+
const oldest = cache.keys().next().value
|
|
3560
|
+
if (oldest !== undefined) cache.delete(oldest)
|
|
3561
|
+
}
|
|
3562
|
+
bytes[id] = value
|
|
3411
3563
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3564
|
+
})
|
|
3565
|
+
await Promise.all(statWorkers)
|
|
3414
3566
|
return bytes
|
|
3415
3567
|
}
|
|
3416
3568
|
|
|
@@ -3489,13 +3641,13 @@ async function viewSessionPage(ctx, id, cursor, cacheRef, limit = SESSIONS_VIEW_
|
|
|
3489
3641
|
}
|
|
3490
3642
|
|
|
3491
3643
|
// 内容搜索:逐会话 filterEvents 文本谓词(语义、大小写不敏感、空白灵活),带预算约束。
|
|
3492
|
-
async function searchSessionsContent(ctx, dshHome, query, scope = 'all') {
|
|
3644
|
+
async function searchSessionsContent(ctx, dshHome, query, scope = 'all', titleCache = null) {
|
|
3493
3645
|
const sessionQuery = ctx.get('sessionQuery')
|
|
3494
3646
|
const result = { available: sessionQuery !== undefined && typeof sessionQuery.filterEvents === 'function', query, scope, hits: [] }
|
|
3495
3647
|
if (!result.available) return result
|
|
3496
3648
|
const q = typeof query === 'string' ? query.trim() : ''
|
|
3497
3649
|
if (q === '') return result
|
|
3498
|
-
const listed = await listSessionsForManage(ctx, dshHome)
|
|
3650
|
+
const listed = await listSessionsForManage(ctx, dshHome, 'all', titleCache)
|
|
3499
3651
|
if (!listed.available) return result
|
|
3500
3652
|
// scope=all 搜全部会话(含 live,readSession/filterEvents 均支持 live 快照);archived 只搜归档冷会话。
|
|
3501
3653
|
const targets = scope === 'archived'
|
|
@@ -3858,6 +4010,12 @@ function apply(ctx) {
|
|
|
3858
4010
|
// 会话体积懒加载缓存(v0.36):宿主进程不重启就一直在——浏览器刷新/面板重开直接命中,
|
|
3859
4011
|
// 不用重新 readdir+stat(归档冷会话体积不变;live 会话靠 TTL 兜底、删除时主动失效)。
|
|
3860
4012
|
const sessionBytesCache = new Map()
|
|
4013
|
+
// 会话标题缓存(v1.1.3):revision 键控 + 磁盘持久化,宿主启动即异步预加载,
|
|
4014
|
+
// 首个 sessions-list 前等待填充完成(失败静默回落全量拉取)。
|
|
4015
|
+
const sessionTitleCache = new Map()
|
|
4016
|
+
const sessionTitlesReady = loadSessionTitles(dshHome).then((entries) => {
|
|
4017
|
+
for (const [id, entry] of entries) sessionTitleCache.set(id, entry)
|
|
4018
|
+
}).catch(() => {})
|
|
3861
4019
|
// 会话详情快照缓存(v0.36):单槽位只留最近打开的会话,翻页零重复 readSession。
|
|
3862
4020
|
const sessionViewCache = { id: null, snapshot: null, at: 0, live: false }
|
|
3863
4021
|
let usageIndexPromise = loadUsageIndex(dshHome)
|
|
@@ -4214,6 +4372,7 @@ function apply(ctx) {
|
|
|
4214
4372
|
ctx.effect(() => () => { backupIntegrity.dispose().catch(() => {}) }, 'dsh-service backup restore plans')
|
|
4215
4373
|
ctx.effect(() => () => sessionDeletePlans.clear(), 'dsh-service session delete plans')
|
|
4216
4374
|
ctx.effect(() => () => sessionBytesCache.clear(), 'dsh-service session bytes cache')
|
|
4375
|
+
ctx.effect(() => () => sessionTitleCache.clear(), 'dsh-service session title cache')
|
|
4217
4376
|
ctx.effect(() => { sessionViewCache.id = null; sessionViewCache.snapshot = null }, 'dsh-service session view cache')
|
|
4218
4377
|
ctx.effect(() => () => {
|
|
4219
4378
|
quotaDisposed = true
|
|
@@ -4367,6 +4526,19 @@ function apply(ctx) {
|
|
|
4367
4526
|
'diagnostics': { feature: 'healthDiagnostics', handle: async (payload, rpcEndpoint) => {
|
|
4368
4527
|
return { ok: true, value: await collectDiagnostics(ctx, dshHome, runtimeEnv) }
|
|
4369
4528
|
|
|
4529
|
+
} },
|
|
4530
|
+
'plugin-restart': { feature: 'healthDiagnostics', audit: true, handle: async (payload, rpcEndpoint) => {
|
|
4531
|
+
try {
|
|
4532
|
+
const result = await restartPluginEntry(ctx, payload?.entryId)
|
|
4533
|
+
if (result.ok) return { ok: true, value: {} }
|
|
4534
|
+
// 稳定业务码(unknown-plugin / loader-unavailable / plugin-disabled / not-failed /
|
|
4535
|
+
// restart-failed)走严格 RPC 归一(strictRpcResult),客户端词典映射;restart-failed
|
|
4536
|
+
// 的底层错误原文并入 message(码: 详情 后缀语义)。
|
|
4537
|
+
return rpcFailure(new Error(result.code === 'restart-failed' ? `restart-failed: ${result.error}` : result.code))
|
|
4538
|
+
} catch (error) {
|
|
4539
|
+
return rpcTechnicalFailure(error)
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4370
4542
|
} },
|
|
4371
4543
|
'usage': { feature: 'modelUsage', handle: async (payload, rpcEndpoint) => {
|
|
4372
4544
|
return { ok: true, value: publicUsage(await usageIndexPromise, payload?.timezoneOffsetMinutes) }
|
|
@@ -5071,7 +5243,8 @@ function apply(ctx) {
|
|
|
5071
5243
|
'sessions-list': { feature: 'sessionManager', handle: async (payload, rpcEndpoint) => {
|
|
5072
5244
|
const scope = payload?.scope === 'archived' || payload?.scope === 'deleted' ? payload.scope : 'all'
|
|
5073
5245
|
try {
|
|
5074
|
-
|
|
5246
|
+
await sessionTitlesReady
|
|
5247
|
+
const value = await listSessionsForManage(ctx, dshHome, scope, sessionTitleCache)
|
|
5075
5248
|
// 已删除记录独立下发(供「已删除」筛选):字段只为展示,绝不包含内容。
|
|
5076
5249
|
return { ok: true, value }
|
|
5077
5250
|
} catch (error) {
|
|
@@ -5115,7 +5288,8 @@ function apply(ctx) {
|
|
|
5115
5288
|
const query = typeof payload?.query === 'string' ? payload.query : ''
|
|
5116
5289
|
const scope = payload?.scope === 'archived' ? 'archived' : 'all'
|
|
5117
5290
|
try {
|
|
5118
|
-
|
|
5291
|
+
await sessionTitlesReady
|
|
5292
|
+
return { ok: true, value: await searchSessionsContent(ctx, dshHome, query, scope, sessionTitleCache) }
|
|
5119
5293
|
} catch (error) {
|
|
5120
5294
|
return rpcTechnicalFailure(error)
|
|
5121
5295
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gehennawu/dsh-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"index.js",
|
|
20
20
|
"quota-adapters.js",
|
|
21
21
|
"backup-integrity.js",
|
|
22
|
+
"plugin-health.js",
|
|
23
|
+
"plugin-compat.js",
|
|
22
24
|
"client.js",
|
|
23
25
|
"cordis.patch.yml",
|
|
24
26
|
"README.md",
|