@gehennawu/dsh-service 1.3.0 → 1.4.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 -4
- package/README.md +5 -5
- package/client.js +1 -1
- package/index.js +45 -7
- package/package.json +1 -1
- package/plugin-compat.js +3 -0
- package/plugin-health.js +49 -12
package/index.js
CHANGED
|
@@ -1745,12 +1745,37 @@ function pruneSessionErrors(session, cutoff) {
|
|
|
1745
1745
|
}
|
|
1746
1746
|
}
|
|
1747
1747
|
|
|
1748
|
-
function
|
|
1748
|
+
function validSessionOffset(value) {
|
|
1749
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0) ? value : undefined
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// alpha.4 moves the fork cut out of SessionHeader and into body-bearing reads;
|
|
1753
|
+
// older DSH releases expose it as header/meta.seedLength. Keep this normalization
|
|
1754
|
+
// at the persistence seam so the usage fold never has to know which runtime spoke.
|
|
1755
|
+
function inheritedEventCountFor(record, read) {
|
|
1756
|
+
const fromRead = validSessionOffset(read?.inheritedEventCount)
|
|
1757
|
+
if (fromRead !== undefined) return fromRead
|
|
1758
|
+
const fromMeta = validSessionOffset(read?.meta?.seedLength)
|
|
1759
|
+
if (fromMeta !== undefined) return fromMeta
|
|
1760
|
+
const fromHeader = validSessionOffset(record?.header?.seedLength)
|
|
1761
|
+
return fromHeader ?? 0
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function usageReadStart(previous) {
|
|
1765
|
+
return previous === undefined ? 0 : Math.max(0, previous.lastSeq + 1)
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function usageReadEvents(read) {
|
|
1769
|
+
return Array.isArray(read?.events) ? read.events : []
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function foldUsageEvents(ctx, record, previous, events, inheritedEventCount = 0) {
|
|
1749
1773
|
const project = projectForCwd(ctx, record.header.cwd)
|
|
1750
|
-
const
|
|
1774
|
+
const cut = validSessionOffset(inheritedEventCount) ?? 0
|
|
1775
|
+
const session = previous || { revision: '', lastSeq: cut - 1, project, currentModel: null, hours: {} }
|
|
1751
1776
|
session.project = project
|
|
1752
1777
|
for (const event of events) {
|
|
1753
|
-
if (event.seq <
|
|
1778
|
+
if (event.seq < cut) continue
|
|
1754
1779
|
session.lastSeq = Math.max(session.lastSeq, event.seq)
|
|
1755
1780
|
if (event.type === 'request/header') {
|
|
1756
1781
|
const provider = event.data?.header?.config?.provider
|
|
@@ -1848,9 +1873,10 @@ async function refreshUsageIndex(ctx, dshHome, index) {
|
|
|
1848
1873
|
const revision = revisionKey(record.revision)
|
|
1849
1874
|
const previous = index.sessions[id]
|
|
1850
1875
|
if (previous?.revision === revision) continue
|
|
1851
|
-
const fromSeq = previous
|
|
1876
|
+
const fromSeq = usageReadStart(previous)
|
|
1852
1877
|
const read = await persistence.readFrom(record.header.id, fromSeq)
|
|
1853
|
-
const
|
|
1878
|
+
const inheritedEventCount = inheritedEventCountFor(record, read)
|
|
1879
|
+
const next = foldUsageEvents(ctx, record, previous, usageReadEvents(read), inheritedEventCount)
|
|
1854
1880
|
next.revision = revision
|
|
1855
1881
|
index.sessions[id] = next
|
|
1856
1882
|
}
|
|
@@ -2274,7 +2300,11 @@ async function collectDiagnostics(ctx, dshHome, runtimeEnv) {
|
|
|
2274
2300
|
pluginReport = { available: false, total: 0, issues: [] }
|
|
2275
2301
|
}
|
|
2276
2302
|
if (!pluginReport.available) add('plugins', 'info', 'unavailable')
|
|
2277
|
-
else
|
|
2303
|
+
else {
|
|
2304
|
+
const pluginCheck = pluginCheckItem(pluginReport)
|
|
2305
|
+
if (pluginCheck.status === 'info') pluginCheck.advisory = true
|
|
2306
|
+
checks.push(pluginCheck)
|
|
2307
|
+
}
|
|
2278
2308
|
|
|
2279
2309
|
// v1.3 插件兼容性:对照已核实的 alpha 破坏面清单(client-runtime 供应商移除、SQLite
|
|
2280
2310
|
// persistence 移除、聊天/统计条 CSS 哈希漂移、data-time-hover-root 删除)扫描启用插件的
|
|
@@ -3187,7 +3217,15 @@ function buildSubagentDispatchRecord(agent, parent, dispatch, options = {}) {
|
|
|
3187
3217
|
|
|
3188
3218
|
/** 父会话事件尾向上扫最近一条带数字 `data.turn` 的事件(≤scanLimit 条);无则 undefined。 */
|
|
3189
3219
|
function lastSubagentTurn(parent, scanLimit = 50) {
|
|
3190
|
-
|
|
3220
|
+
let events
|
|
3221
|
+
try {
|
|
3222
|
+
if (typeof parent?.session?.snapshotEvents === 'function') events = parent.session.snapshotEvents()
|
|
3223
|
+
} catch (_) {
|
|
3224
|
+
events = undefined
|
|
3225
|
+
}
|
|
3226
|
+
if (!Array.isArray(events)) {
|
|
3227
|
+
try { events = parent?.session?.events } catch (_) { events = undefined }
|
|
3228
|
+
}
|
|
3191
3229
|
if (!Array.isArray(events)) return undefined
|
|
3192
3230
|
const limit = Number.isFinite(scanLimit) && scanLimit > 0 ? scanLimit : 50
|
|
3193
3231
|
const from = Math.max(0, events.length - limit)
|
package/package.json
CHANGED
package/plugin-compat.js
CHANGED
|
@@ -338,6 +338,9 @@ export async function collectPluginCompat(ctx, options = {}) {
|
|
|
338
338
|
const moduleName = entry?.options?.name
|
|
339
339
|
if (typeof entry?.id !== 'string' || typeof moduleName !== 'string') continue
|
|
340
340
|
if (entry.disabled === true) continue
|
|
341
|
+
// 无法形成合法包名的条目跳过且不计 scanned:`cordis:include` 这类 Cordis 内建声明名、
|
|
342
|
+
// 相对路径/`file:` 形态不是可扫描的 npm 插件(没有清单可查),不该渲染「未能扫描」提示。
|
|
343
|
+
if (packageNameOf(moduleName) === null) continue
|
|
341
344
|
scanned += 1
|
|
342
345
|
let result
|
|
343
346
|
if (options.noCache === true) {
|
package/plugin-health.js
CHANGED
|
@@ -27,41 +27,76 @@ export const PLUGIN_FIBER_STATE = Object.freeze({
|
|
|
27
27
|
UNLOADING: 5,
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
/** 异常相位 →
|
|
30
|
+
/** 异常相位 → 检查项等级:失败是真故障;启动中按 warning;已释放/未知按 info。 */
|
|
31
31
|
export function issueLevelOf(phase) {
|
|
32
32
|
switch (phase) {
|
|
33
33
|
case 'failed': return 'error'
|
|
34
34
|
case 'pending':
|
|
35
35
|
case 'loading':
|
|
36
36
|
case 'unloading': return 'warning'
|
|
37
|
+
case 'disposed':
|
|
38
|
+
case 'unknown': return 'info'
|
|
37
39
|
default: return null
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
/** fiber.state 数值 →
|
|
43
|
+
/** fiber.state 数值 → 相位串;无 fiber 是启动瞬态,未知数值明确标为 unknown。 */
|
|
42
44
|
function phaseOfState(state) {
|
|
43
45
|
switch (state) {
|
|
44
46
|
case PLUGIN_FIBER_STATE.PENDING: return 'pending'
|
|
45
47
|
case PLUGIN_FIBER_STATE.LOADING: return 'loading'
|
|
46
48
|
case PLUGIN_FIBER_STATE.ACTIVE: return 'active'
|
|
47
49
|
case PLUGIN_FIBER_STATE.FAILED: return 'failed'
|
|
50
|
+
case PLUGIN_FIBER_STATE.DISPOSED: return 'disposed'
|
|
48
51
|
case PLUGIN_FIBER_STATE.UNLOADING: return 'unloading'
|
|
49
|
-
default: return '
|
|
52
|
+
default: return 'unknown'
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
const DEFAULT_STARTUP_GRACE_MS = 5000
|
|
57
|
+
const MAX_PLUGIN_ERROR_LENGTH = 4096
|
|
58
|
+
|
|
59
|
+
function clipPluginError(value) {
|
|
60
|
+
let message
|
|
61
|
+
try {
|
|
62
|
+
message = String(value?.message || value)
|
|
63
|
+
} catch (_) {
|
|
64
|
+
message = 'unknown plugin failure'
|
|
65
|
+
}
|
|
66
|
+
message = message
|
|
67
|
+
.replace(/(Bearer\s+)[^\s]+/gi, '$1<redacted>')
|
|
68
|
+
.replace(/([?&](?:api[_-]?key|token|access[_-]?token|secret|password)=)[^&\s]+/gi, '$1<redacted>')
|
|
69
|
+
.replace(/((?:api[_-]?key|access[_-]?token|secret|password)\s*[:=]\s*)[^,;\s]+/gi, '$1<redacted>')
|
|
70
|
+
.replace(/(?:[A-Za-z]:[\\/]|\/home\/|\/workspace\/)[^\s"'`]+/g, '<redacted-path>')
|
|
71
|
+
return message.length > MAX_PLUGIN_ERROR_LENGTH ? `${message.slice(0, MAX_PLUGIN_ERROR_LENGTH - 1)}…` : message
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function startupAgeMs(entry, now, options) {
|
|
75
|
+
if (typeof options.ageOf === 'function') {
|
|
76
|
+
const age = Number(options.ageOf(entry))
|
|
77
|
+
return Number.isFinite(age) && age >= 0 ? age : Infinity
|
|
78
|
+
}
|
|
79
|
+
const startedAt = Number(entry?.fiber?.startedAt ?? entry?.startedAt)
|
|
80
|
+
return Number.isFinite(startedAt) && startedAt >= 0 ? Math.max(0, now - startedAt) : 0
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
/**
|
|
54
84
|
* 收集插件健康体检结果。
|
|
55
85
|
*
|
|
56
86
|
* @param ctx 宿主插件上下文(供 `ctx.get('loader')`)
|
|
87
|
+
* @param options 可选测试/运行时参数:`now`、`startupGraceMs`、`ageOf(entry)`
|
|
57
88
|
* @returns {available: boolean, total: number, issues: PluginIssue[]}——loader 缺席时
|
|
58
|
-
* available=false(调用方降级为 info
|
|
89
|
+
* available=false(调用方降级为 info 检查项,不含任何插件数据)。启动中的 pending/loading 默认有 5 秒宽限。
|
|
59
90
|
*/
|
|
60
|
-
export async function collectPluginHealth(ctx) {
|
|
91
|
+
export async function collectPluginHealth(ctx, options = {}) {
|
|
61
92
|
const loader = ctx.get('loader')
|
|
62
93
|
if (loader === undefined) return { available: false, total: 0, issues: [] }
|
|
63
94
|
const issues = []
|
|
64
95
|
let total = 0
|
|
96
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now()
|
|
97
|
+
const startupGraceMs = Number.isFinite(options.startupGraceMs) && options.startupGraceMs >= 0
|
|
98
|
+
? options.startupGraceMs
|
|
99
|
+
: DEFAULT_STARTUP_GRACE_MS
|
|
65
100
|
for (const entry of loader.entries()) {
|
|
66
101
|
if (entry?.options?.group === true) continue
|
|
67
102
|
const moduleName = entry?.options?.name
|
|
@@ -72,8 +107,9 @@ export async function collectPluginHealth(ctx) {
|
|
|
72
107
|
const fiber = entry.fiber
|
|
73
108
|
const phase = fiber === undefined ? 'loading' : phaseOfState(fiber.state)
|
|
74
109
|
if (phase === 'active') continue
|
|
110
|
+
if ((phase === 'pending' || phase === 'loading') && startupGraceMs > 0 && startupAgeMs(entry, now, options) < startupGraceMs) continue
|
|
75
111
|
const error = phase === 'failed' && fiber?._error != null
|
|
76
|
-
?
|
|
112
|
+
? clipPluginError(fiber._error)
|
|
77
113
|
: null
|
|
78
114
|
const missingDeps = phase === 'pending'
|
|
79
115
|
? Object.keys(fiber.inject || {}).filter((key) => !(fiber.store !== undefined && fiber.store[key]))
|
|
@@ -90,17 +126,18 @@ export async function collectPluginHealth(ctx) {
|
|
|
90
126
|
}
|
|
91
127
|
|
|
92
128
|
/**
|
|
93
|
-
* 聚合为一个诊断检查项(追加在 checks 尾部)。detail
|
|
94
|
-
* 客户端用它渲染「共 N
|
|
95
|
-
*
|
|
129
|
+
* 聚合为一个诊断检查项(追加在 checks 尾部)。detail 四段 `total:failed:pending:informational`:
|
|
130
|
+
* 客户端用它渲染「共 N 个插件,全部正常」或异常摘要;停用插件完全不进统计。
|
|
131
|
+
* 旧客户端仍可读取前三段;failed 是 error,启动/卸载相位按 warning,disposed/unknown 单独作为 info。
|
|
96
132
|
*/
|
|
97
133
|
export function pluginCheckItem(report) {
|
|
98
134
|
const failed = report.issues.filter((issue) => issue.phase === 'failed').length
|
|
99
|
-
const pending = report.issues.filter((issue) => issue.phase
|
|
135
|
+
const pending = report.issues.filter((issue) => issue.phase === 'pending' || issue.phase === 'loading' || issue.phase === 'unloading').length
|
|
136
|
+
const informational = report.issues.filter((issue) => issue.phase === 'disposed' || issue.phase === 'unknown').length
|
|
100
137
|
return {
|
|
101
138
|
id: 'plugins',
|
|
102
|
-
status: failed > 0 ? 'error' : pending > 0 ? 'warning' : 'ok',
|
|
103
|
-
detail: `${report.total}:${failed}:${pending}`,
|
|
139
|
+
status: failed > 0 ? 'error' : pending > 0 ? 'warning' : informational > 0 ? 'info' : 'ok',
|
|
140
|
+
detail: `${report.total}:${failed}:${pending}:${informational}`,
|
|
104
141
|
}
|
|
105
142
|
}
|
|
106
143
|
|