zfdx123/dsh-atelier--packages-dsh-hooks-ordering ↗★ 0

@zfdx123/dsh-hooks-ordering

钩子排序:为 Cordis 的 waterfall / serial 钩子提供确定性的 before/after 排序,让互相独立的插件能声明彼此的先后关系(拓扑排序、环检测、可配置控制集与设置页)。 适合需要解决多插件间钩子执行先后顺序冲突的DSH开发者。

パッケージ
@zfdx123/dsh-hooks-ordering
互換性
未検証
Harness ピア範囲
^0.1.6-alpha.1
Cordis ピア範囲
^4.0.2
バージョン
1.0.1
ライセンス
NOASSERTION
最終更新
2026/09/21

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:zfdx123/dsh-atelier#8a3566cc5762e3e6d26b23c75c6ee3c4a336304f&path:packages/dsh-hooks-ordering

ドキュメント

README 全文を読む ↗

配置

三个字段可以直接在 dsh 的设置面板里编辑,页面名为左侧导航中的钩子排序(命名空间 hooks-ordering),无需改 profile:

字段含义
hooks要控制的 waterfall 钩子,一行一个。[] 完全禁用 waterfall 服务。
serialHooks要控制的 serial 钩子,一行一个。[] 完全禁用 serial 服务。
log约束 DAG(JSON)日志文件;留空表示「不记录」。顺序一旦看着不对就能派上用场。

第四个组装键 syncReturnHooks 刻意不进设置页:它描述的是宿主的派发方式(哪些钩子的返回值被同步消费),而不是用户的偏好,所以它只属于 profile 那一行。表单里给 hooks 填上一个同步返回的钩子是安全的——它会被控制,而 register() 会在插件试图注册参与者时拒绝,并说明原因。profile 行长这样(本包自带的 cordis.patch.yml 就是这个内容,只有在该包没有作为 bundle 注册时才需要手写):

- insert:
    - id: hooks-ordering
      # 裸包名——见下面的说明。`/dsh` 也能作为插件入口,
      # 但那样 dsh 就找不到浏览器端那一半了。
      name: '@zfdx123/dsh-hooks-ordering'
      config:
        # hooks: ['agent/pre-step', 'tools/post-execute']   # 默认:所有返回契约兼容的 dsh waterfall 钩子
        # serialHooks: ['agent/turn-stopping']              # 默认:[agent/turn-stopping]
        # syncReturnHooks: ['llm/stream', 'session-telemetry/record', 'compaction/summary-error']
        # log: './hooks-ordering-dag.json'                  # 可选的 DAG 日志

这一行必须写包名,不能写子路径。 dsh 会把某一行的 name 映射回一个包,以便找到该包的浏览器端那一半(dsh.client → 设置页),而它的 locatePkgJson 只接受裸包标识符——exactPackageSpecifier('@scope/name/subpath') 返回 undefined,因为切分后得到三段。写成 @zfdx123/dsh-hooks-ordering/dsh 的那一行能完美加载宿主插件,然后悄悄地永远找不到客户端 bundle:没有设置页,而且任何地方都没有报错。这就是插件表面放在根入口的原因。

出于同一类原因,根入口不带 default 导出。 加载器会先用 exports.default ?? exports 规范化导入的模块,然后才应用它,所以一个并非该插件本身的 default 导出会劫持这一行:本包当时把 default(waterfall 服务)挂成了插件,apply 从未运行,结果是服务活着、却没有控制任何钩子,没有 serial 服务,没有设置命名空间,而且依然悄无声息。HookOrdering 从根入口按名字导出,同时仍然是 /waterfall 的默认导出。

关于接线方式,还有四点值得知道:

  • profile 那一行是基础层。 解析顺序是 schema 默认值 → base(这一行;当这一行为空时是内置钩子集合)→ 用户层。所以表单是在你的组装配置之上编辑,「恢复组合默认」会回到组装配置,而不是回到空表单。
  • 编辑在重启后生效(applies: 'restart'),这是有意为之:改 hooks/serialHooks 意味着在活动钩子上安装或移除包夹监听器,重启能干净地应用这些变更,而不是在派发链运行中途重新接线。设置 log 本身很廉价,但命名空间是作为一个整体生效的。
  • 这个页面是客户端那一半的贡献。 宿主侧的 settings.register 只创建命名空间、它的存储和它的描述符;dsh 从浏览器平面渲染设置,所以是 client.js 把页面注册进 settings.section 插槽(order: 27)。移除或加载失败客户端那一半,命名空间依然存在——只是没有表单。
  • settings 是硬依赖(export const inject = ['settings']),这是承重的,而非偶然:cordis 会在一个插件声明的服务存在时立刻激活它,而 ctx.get('settings') 只是读取服务存储,不会建立那个需求。只做探测而不声明,意味着插件会在第一波加载中、宿主提供 settings 之前就加载,命名空间于是从未被注册——悄无声息,没有表单也没有报错。声明它同时也让 prepend 的包夹在启动流程中落得更晚,而排序保证正希望它们在那里。注册本身是降级而不是让挂载失败:如果它抛错,插件会告警并回退到组装配置,因为一个可选的表单绝不能阻止 harness 启动。

本包面向 dsh。 钩子名是 dsh 的,settings 是 dsh 的服务,所以两者都不做成可选的;/waterfall 和 /serial 才是不带宿主服务要求的入口。