@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/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
- /** 单个会话的事件文本(与官方 extractSessionEventText 语义一致,不依赖官方内部包)。 */
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) ? content.flatMap(blockText) : [])
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
- const isNoise = SESSION_NOISE_TYPES.has(event.type)
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: isNoise,
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.0",
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',