@gehennawu/dsh-service 1.6.0 → 1.6.2
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 +9 -7
- package/README.md +9 -7
- package/client.js +1 -1
- package/index.js +96 -9
- package/package.json +3 -2
- package/plugin-compat.js +15 -0
package/index.js
CHANGED
|
@@ -156,6 +156,12 @@ const SESSIONS_TITLE_VERSION = 1
|
|
|
156
156
|
const SESSIONS_TITLE_MAX_ENTRIES = 2000
|
|
157
157
|
const SESSIONS_TITLE_LIVE_TTL_MS = 30 * 1000
|
|
158
158
|
const SESSIONS_TITLE_COLD_TTL_MS = 5 * 60 * 1000
|
|
159
|
+
// 详情里视为「工具消息」的事件类型前缀(官方 0.1.5 装机包 SessionEventMap 核实:tool/call、
|
|
160
|
+
// tool/result、tool/ptc-dispatch、tool/ptc-dispatch-start 全落在 tool/ 命名空间)——按前缀归类,
|
|
161
|
+
// 官方日后新增工具事件类型无需改表。另有「通篇只有工具调用的 assistant/message」同归此类,
|
|
162
|
+
// 判定见 assistantMessageCarriesOnlyToolCalls。
|
|
163
|
+
const SESSION_TOOL_TYPE_PREFIX = 'tool/'
|
|
164
|
+
|
|
159
165
|
// 详情/检索里视为「机制性噪声」的事件类型:折叠展示计数,用户可展开。
|
|
160
166
|
const SESSION_NOISE_TYPES = new Set([
|
|
161
167
|
'turn/start', 'step/start', 'step/end', 'assistant/chunk', 'request/header',
|
|
@@ -3621,7 +3627,41 @@ async function saveSessionTitles(dshHome, cache) {
|
|
|
3621
3627
|
}
|
|
3622
3628
|
}
|
|
3623
3629
|
|
|
3624
|
-
/**
|
|
3630
|
+
/**
|
|
3631
|
+
* assistant/message 是否「通篇只有工具调用」——含 tool-call 块、且没有任何非空文本块。
|
|
3632
|
+
* 官方 agent-loop 把工具调用写进 assistant/message 的 content(append 后才发 tool/call + tool/result),
|
|
3633
|
+
* 真实长会话里这类消息占绝大多数(52k 事件样本:873 条 assistant/message,其中 526 条通篇工具调用),
|
|
3634
|
+
* 详情页逐条铺开就是「工具相关消息」的主噪音源,故与 tool/* 事件同归折叠块。
|
|
3635
|
+
*/
|
|
3636
|
+
function assistantMessageCarriesOnlyToolCalls(event) {
|
|
3637
|
+
const content = event?.data?.message?.content
|
|
3638
|
+
if (!Array.isArray(content)) return false
|
|
3639
|
+
let hasToolCall = false
|
|
3640
|
+
for (const block of content) {
|
|
3641
|
+
if (typeof block !== 'object' || block === null) continue
|
|
3642
|
+
if (block.type === 'tool-call') { hasToolCall = true; continue }
|
|
3643
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim() !== '') return false
|
|
3644
|
+
}
|
|
3645
|
+
return hasToolCall
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
/** 详情列表的事件归类:'noise'(机制性系统事件)/ 'tool'(工具消息)/ undefined(原样渲染)。 */
|
|
3649
|
+
function sessionEventCollapseKind(event) {
|
|
3650
|
+
const type = event?.type
|
|
3651
|
+
if (typeof type !== 'string') return undefined
|
|
3652
|
+
if (SESSION_NOISE_TYPES.has(type)) return 'noise'
|
|
3653
|
+
if (type.startsWith(SESSION_TOOL_TYPE_PREFIX)) return 'tool'
|
|
3654
|
+
if (type === 'assistant/message' && assistantMessageCarriesOnlyToolCalls(event)) return 'tool'
|
|
3655
|
+
return undefined
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
/**
|
|
3659
|
+
* 单个会话的事件文本(官方 extractSessionEventText 语义,不依赖官方内部包)。
|
|
3660
|
+
* **一处有意偏离官方**(v1.6.x 用户点名「tool 相关的消息默认折叠」):既有文本又有工具调用的
|
|
3661
|
+
* assistant/message,正文只留文本——工具调用的 name/arguments 由紧随其后的 tool/call 事件
|
|
3662
|
+
* (同一份内容)承载,展开工具消息折叠块即可看到,界面整体不丢信息;通篇只有工具调用的
|
|
3663
|
+
* assistant/message 本身就在折叠块里,保留原文,展开后仍见当时的参数。
|
|
3664
|
+
*/
|
|
3625
3665
|
function sessionEventText(event) {
|
|
3626
3666
|
if (typeof event !== 'object' || event === null) return ''
|
|
3627
3667
|
const data = event.data
|
|
@@ -3642,7 +3682,9 @@ function sessionEventText(event) {
|
|
|
3642
3682
|
default: return []
|
|
3643
3683
|
}
|
|
3644
3684
|
}
|
|
3645
|
-
const contentText = (content) => joinText(Array.isArray(content)
|
|
3685
|
+
const contentText = (content, options) => joinText(Array.isArray(content)
|
|
3686
|
+
? content.flatMap((block) => (options?.skipToolCalls === true && block?.type === 'tool-call' ? [] : blockText(block)))
|
|
3687
|
+
: [])
|
|
3646
3688
|
const turnEndText = (reason) => {
|
|
3647
3689
|
switch (reason?.kind) {
|
|
3648
3690
|
case 'error': return joinText(['error', reason.error?.message])
|
|
@@ -3655,7 +3697,7 @@ function sessionEventText(event) {
|
|
|
3655
3697
|
}
|
|
3656
3698
|
switch (event.type) {
|
|
3657
3699
|
case 'user/message': return contentText(data.content)
|
|
3658
|
-
case 'assistant/message': return contentText(data.message?.content)
|
|
3700
|
+
case 'assistant/message': return contentText(data.message?.content, { skipToolCalls: !assistantMessageCarriesOnlyToolCalls(event) })
|
|
3659
3701
|
case 'tool/call': return joinText([data.name, data.arguments])
|
|
3660
3702
|
case 'tool/result': return joinText([contentText(data.message?.content), data.error?.name, data.error?.code])
|
|
3661
3703
|
case 'todo/write': return joinText(Array.isArray(data.todos) ? data.todos.flatMap((todo) => [todo.status, todo.content]) : [])
|
|
@@ -3707,12 +3749,13 @@ function sessionTitleFresh(entry, record, revisions, now) {
|
|
|
3707
3749
|
async function listSessionsForManage(ctx, dshHome, scope = 'all', titleCache = null) {
|
|
3708
3750
|
const sessionQuery = ctx.get('sessionQuery')
|
|
3709
3751
|
const workspaceRegistry = ctx.get('workspaceRegistry')
|
|
3752
|
+
const canUnarchive = workspaceRegistry !== undefined && typeof workspaceRegistry.unarchiveSession === 'function'
|
|
3710
3753
|
const available = sessionQuery !== undefined && typeof sessionQuery.listSessions === 'function'
|
|
3711
3754
|
if (!available) {
|
|
3712
|
-
return { available: false, items: [], archivedIds: [], deleted: [] }
|
|
3755
|
+
return { available: false, items: [], archivedIds: [], deleted: [], canUnarchive: false }
|
|
3713
3756
|
}
|
|
3714
3757
|
if (scope === 'deleted') {
|
|
3715
|
-
return { available: true, items: [], archivedIds: [], deleted: (await loadDeletedSessions(dshHome)).items }
|
|
3758
|
+
return { available: true, items: [], archivedIds: [], deleted: (await loadDeletedSessions(dshHome)).items, canUnarchive }
|
|
3716
3759
|
}
|
|
3717
3760
|
const now = Date.now()
|
|
3718
3761
|
// revision 源:缓存启用时尽早并行拉取(一次 header-only 目录遍历 + 每会话一次 stat,
|
|
@@ -3796,7 +3839,7 @@ async function listSessionsForManage(ctx, dshHome, scope = 'all', titleCache = n
|
|
|
3796
3839
|
})
|
|
3797
3840
|
}
|
|
3798
3841
|
items.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))
|
|
3799
|
-
return { available: true, items, archivedIds, deleted }
|
|
3842
|
+
return { available: true, items, archivedIds, deleted, canUnarchive }
|
|
3800
3843
|
}
|
|
3801
3844
|
|
|
3802
3845
|
// 批量取会话体积(sessions-bytes 用):命中缓存秒回、不碰磁盘;未命中再做一次
|
|
@@ -3895,13 +3938,16 @@ async function viewSessionPage(ctx, id, cursor, cacheRef, limit = SESSIONS_VIEW_
|
|
|
3895
3938
|
const slice = events.slice(start, start + limit)
|
|
3896
3939
|
const items = slice.map((event) => {
|
|
3897
3940
|
const seq = Number(event.seq)
|
|
3898
|
-
|
|
3941
|
+
// v1.6.x(用户点名):工具消息与系统事件同样默认折叠——客户端按这两个标志各自合并连续
|
|
3942
|
+
// 同类事件成块(noise=机制性系统事件、tool=工具消息),未标记的事件原样渲染。
|
|
3943
|
+
const kind = sessionEventCollapseKind(event)
|
|
3899
3944
|
return {
|
|
3900
3945
|
seq,
|
|
3901
3946
|
type: event.type,
|
|
3902
3947
|
time: typeof event.time === 'number' ? event.time : undefined,
|
|
3903
3948
|
text: sessionEventText(event),
|
|
3904
|
-
noise:
|
|
3949
|
+
noise: kind === 'noise',
|
|
3950
|
+
tool: kind === 'tool',
|
|
3905
3951
|
}
|
|
3906
3952
|
})
|
|
3907
3953
|
const lastSeq = items.length > 0 ? items[items.length - 1].seq : (cursor ?? -1)
|
|
@@ -4549,7 +4595,9 @@ function apply(ctx) {
|
|
|
4549
4595
|
const { request: decorated, dispatch } = applyInjection(spec.request)
|
|
4550
4596
|
return runWithDispatch(dispatch, () => originalStartContinuable.call(subagents, { ...spec, request: decorated }))
|
|
4551
4597
|
}
|
|
4552
|
-
// agent/created
|
|
4598
|
+
// agent/created(0.1.6 起由 announce await ctx.serial 异步串行派发;0.1.5 及之前同步派发在创建栈内):
|
|
4599
|
+
// 记录派发路由 + 绑定等级(仅当确有非空等级)。监听器内部全 try/catch 严格保证绝不抛错,
|
|
4600
|
+
// 避免 0.1.6 serial 派发失败时 reject 整个 Agent 创建。
|
|
4553
4601
|
// agent/request:最终 proposal 阶段补入 reasoningEffort;已建立的值(提案自带/其他插件)永不覆盖。
|
|
4554
4602
|
// 补标前按 proposal 实际 provider/model 复审等级仍受支持(适配器元数据漂移即丢弃并告警),
|
|
4555
4603
|
// 与 isRoutable 的「实时判定、不让派生失败」契约同 philosophy。绑定无论结果如何都消费一次。
|
|
@@ -4573,6 +4621,8 @@ function apply(ctx) {
|
|
|
4573
4621
|
} catch (error) {
|
|
4574
4622
|
ctx.logger?.warn?.(`dsh-service: subagent dispatch record failed: ${error?.message ?? String(error)}`)
|
|
4575
4623
|
}
|
|
4624
|
+
} else if (agent?.session?.header?.parentSession && subagentRouteConfig.mode !== 'inherit') {
|
|
4625
|
+
ctx.logger?.warn?.(`dsh-service: subagent "${agent?.id}" created without ALS dispatch context, subagent route skipped`)
|
|
4576
4626
|
}
|
|
4577
4627
|
const effort = dispatch?.reasoningEffort
|
|
4578
4628
|
if (typeof effort === 'string' && effort !== '' && agent !== null && typeof agent === 'object') managedEfforts.set(agent, effort)
|
|
@@ -5708,6 +5758,26 @@ function apply(ctx) {
|
|
|
5708
5758
|
return rpcTechnicalFailure(error)
|
|
5709
5759
|
}
|
|
5710
5760
|
|
|
5761
|
+
} },
|
|
5762
|
+
'sessions-unarchive': { feature: 'sessionManager', audit: true, handle: async (payload, rpcEndpoint) => {
|
|
5763
|
+
const id = typeof payload?.id === 'string' ? payload.id : ''
|
|
5764
|
+
if (id === '') return { ok: false, error: 'invalid-session-id' }
|
|
5765
|
+
const workspaceRegistry = ctx.get('workspaceRegistry')
|
|
5766
|
+
if (workspaceRegistry === undefined) return { ok: false, error: 'workspace-unavailable' }
|
|
5767
|
+
if (typeof workspaceRegistry.unarchiveSession !== 'function') return { ok: false, error: 'unarchive-unsupported' }
|
|
5768
|
+
try {
|
|
5769
|
+
await workspaceRegistry.unarchiveSession(id)
|
|
5770
|
+
return {
|
|
5771
|
+
ok: true,
|
|
5772
|
+
value: {
|
|
5773
|
+
archived: false,
|
|
5774
|
+
archivedSessionIds: Array.isArray(workspaceRegistry.archivedSessionIds) ? [...workspaceRegistry.archivedSessionIds] : [],
|
|
5775
|
+
},
|
|
5776
|
+
}
|
|
5777
|
+
} catch (error) {
|
|
5778
|
+
return rpcTechnicalFailure(error)
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5711
5781
|
} },
|
|
5712
5782
|
'sessions-delete-plan': { feature: 'sessionManager', audit: true, handle: async (payload, rpcEndpoint) => {
|
|
5713
5783
|
const id = typeof payload?.id === 'string' ? payload.id : ''
|
|
@@ -5771,6 +5841,19 @@ function apply(ctx) {
|
|
|
5771
5841
|
}
|
|
5772
5842
|
sessionBytesCache.delete(plan.id)
|
|
5773
5843
|
if (sessionViewCache.id === plan.id) sessionViewCache.id = null
|
|
5844
|
+
// 同步官方侧(不然要刷新浏览器才正确):官方客户端会话列表只吃 session/disposed
|
|
5845
|
+
// 派生的 api-session/removed(该事件在官方远程事件 allowlist 内),而插件是在官方
|
|
5846
|
+
// API 之外 rm 日志目录——不补发这条事件,官方侧栏与「已归档会话」页会一直留着幽灵行。
|
|
5847
|
+
// 旧版宿主没有对应监听器时纯等于空操作;监听器抛错也不能反过来把已落盘的删除报成失败
|
|
5848
|
+
// (与 file-write 的 fs/observed 广播同口径)。
|
|
5849
|
+
try { ctx.emit('api-session/removed', plan.id) } catch (_) {}
|
|
5850
|
+
// 归档集合里的死 id 一并清掉:官方归档页把「集合里有、会话已不在」的条目渲染成
|
|
5851
|
+
// 「这里没有可恢复的已归档会话」,死 id 留着会让该页永远停在不可恢复态。官方自己的
|
|
5852
|
+
// 「取消归档」对死 id 也是移除(幂等、无存在性校验),语义一致;删不掉不影响删除结果。
|
|
5853
|
+
const registry = ctx.get('workspaceRegistry')
|
|
5854
|
+
if (registry !== undefined && typeof registry.unarchiveSession === 'function') {
|
|
5855
|
+
try { await registry.unarchiveSession(plan.id) } catch (_) {}
|
|
5856
|
+
}
|
|
5774
5857
|
return { ok: true, value: { deleted: true, id: plan.id } }
|
|
5775
5858
|
} catch (error) {
|
|
5776
5859
|
return rpcTechnicalFailure(error)
|
|
@@ -5882,6 +5965,7 @@ export {
|
|
|
5882
5965
|
SKILL_SOURCE_RANK,
|
|
5883
5966
|
appendVaryToken,
|
|
5884
5967
|
apply,
|
|
5968
|
+
assistantMessageCarriesOnlyToolCalls,
|
|
5885
5969
|
buildCliproxyAccountPlan,
|
|
5886
5970
|
buildSubagentDispatchRecord,
|
|
5887
5971
|
cliproxyFetchGuard,
|
|
@@ -5943,6 +6027,7 @@ export {
|
|
|
5943
6027
|
sanitizeSkillDraftText,
|
|
5944
6028
|
searchSessionsContent,
|
|
5945
6029
|
selectSkillBatchCandidates,
|
|
6030
|
+
sessionEventCollapseKind,
|
|
5946
6031
|
sessionEventText,
|
|
5947
6032
|
setSkillInvocationKey,
|
|
5948
6033
|
skillIdFor,
|
|
@@ -5955,6 +6040,7 @@ export default {
|
|
|
5955
6040
|
SKILL_SOURCE_RANK,
|
|
5956
6041
|
appendVaryToken,
|
|
5957
6042
|
apply,
|
|
6043
|
+
assistantMessageCarriesOnlyToolCalls,
|
|
5958
6044
|
buildCliproxyAccountPlan,
|
|
5959
6045
|
cliproxyFetchGuard,
|
|
5960
6046
|
cliproxyPinHostFromBaseURL,
|
|
@@ -6015,6 +6101,7 @@ export default {
|
|
|
6015
6101
|
sanitizeSkillDraftText,
|
|
6016
6102
|
searchSessionsContent,
|
|
6017
6103
|
selectSkillBatchCandidates,
|
|
6104
|
+
sessionEventCollapseKind,
|
|
6018
6105
|
sessionEventText,
|
|
6019
6106
|
setSkillInvocationKey,
|
|
6020
6107
|
skillIdFor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gehennawu/dsh-service",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/gehennawu/dsh-service#readme",
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=22"
|
|
49
|
+
"node": ">=22",
|
|
50
|
+
"dsh": ">=0.1.1-rc.2 <0.1.6-alpha.2"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@deepseek-ai/schemastery": "^3.18.1"
|
package/plugin-compat.js
CHANGED
|
@@ -66,6 +66,21 @@ export const COMPAT_BREAKS = Object.freeze([
|
|
|
66
66
|
layer: 'manifest',
|
|
67
67
|
match: '@deepseek-ai/dsh-session-persistence-sqlite',
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
id: 'code-runtime',
|
|
71
|
+
layer: 'manifest',
|
|
72
|
+
match: '@deepseek-ai/dsh-code-runtime',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'e2b-runtime',
|
|
76
|
+
layer: 'manifest',
|
|
77
|
+
match: '@deepseek-ai/dsh-e2b',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'session-start-event',
|
|
81
|
+
layer: 'code',
|
|
82
|
+
match: 'agent/session-start',
|
|
83
|
+
},
|
|
69
84
|
{
|
|
70
85
|
id: 'chat-hash',
|
|
71
86
|
layer: 'code',
|