# dsh-suite

> create-dsh-plugin 脚手架内的事件/生命周期模板示例：零运行时依赖，演示监听 session/event、tools/change、tools/pre-execute 与 ctx.effect 资源清理。

## Metadata

- Author: [@whyihaveyou](https://github.com/whyihaveyou)
- Repo: <https://github.com/whyihaveyou/dsh-suite.git>
- GitHub: [whyihaveyou/dsh-suite](https://github.com/whyihaveyou/dsh-suite)
- Stars: 43
- Language: HTML
- License: [MIT](https://spdx.org/licenses/MIT.html)
- Homepage: <https://whyihaveyou.github.io/dsh-suite/>
- Topics: `agent-framework`, `awesome-list`, `cordis`, `deepseek`, `deepseek-harness`, `developer-tools`, `dsh`, `dsh-plugin`, `plugins`, `scaffold`
- Forks: 8
- Open Issues: 27
- Last push: 2026-08-21T01:59:18.000Z
- Added: 2026-08-20T00:00:00.000Z

## Install

```bash
dsh plugin --profile web add github:whyihaveyou/dsh-suite/packages/create-dsh-plugin/templates/events
```

## Wiki

## 一句话定位
create-dsh-plugin 脚手架内嵌的事件/生命周期示例模板：零运行时依赖、纯类型导入，演示如何订阅 `session/event`、`tools/change`、`tools/pre-execute` 三个事件，并用 `ctx.effect()` 包装定时器证明资源清理可逆。可以作为最小可运行的"事件插件"参考，也可以直接 `dsh plugin add` 装进 profile 跑起来观察日志。

## 核心能力
- 订阅会话事件总线：当 session 日志新增（turn/step 边界、用户/助手消息、工具结果等）时按 type + 计数打印前 5 条和每 25 条一次的节流日志。
- 订阅工具注册表变更：当任何工具被注册或注销时即时打印计数（含同组合内其它兄弟插件的注册）。
- 接入工具执行水线：在每次工具调用前打印工具名，并通过 `next()` 显式放行；不调 `next()` 会短路阻断工具调用。
- 用 `ctx.effect()` 包裹一个 30 秒心跳定时器，返回的 disposer 在插件卸载时打印 `DISPOSED` 并清掉定时器，演示宿主外资源如何正确回收。
- 通过 `cordis.patch.yml` + `dsh.bundle.patch` 声明把自己注入宿主 loader，模板目录本身就是合法 bundle，可以直接 `dsh plugin add` 装入 profile。

## 技术实现
- **语言**: TypeScript（ESM，`"type": "module"` + `tsc` 输出纯 ESM 的 `dist/index.js`，`verbatimModuleSyntax: true` 保证 `import type` 在编译期被擦除）
- **关键依赖**: `@deepseek-ai/cordis`（peerDependency，运行时由宿主注入 ctx）、`@deepseek-ai/dsh-tools`（devDependency，仅取类型 `ToolExecution` / `PreToolDecision`）、`@deepseek-ai/dsh-session`（devDependency，仅取类型 `Session` / `SessionEvent`）；模板自身 `dependencies` 为空
- **架构模式**: cordis bundle 事件/生命周期插件 —— host 暴露 `ctx`，插件 `apply(ctx)` 用 `ctx.on()` 订阅三类事件 + `ctx.effect()` 包定时器；包级别声明 `dsh.bundle.patch: ./cordis.patch.yml` 让宿主 loader 在加载时插入 `id: {{PLUGIN_ID}}, name: {{PKG_NAME}}`
- **入口文件**: `templates/events/src/index.ts`（被 tsc 编译为 `templates/events/dist/index.js`，由 package.json 的 `main` / `exports` 指向）

## 适用场景
想搞清楚 DSH 插件如何"听"事件而不是"添"工具的人——比如做遥测埋点、审计日志、工具调用限流、或者把外部 webhook 桥接到会话流。模板代码本身已经是最小可跑的事件订阅样例，直接拷贝过来换监听器逻辑就能变成自己的事件插件；不想从零写时也可以用 create-dsh-plugin 脚手架 + `-t events` 把它当作脚手架起点。

## 前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| Node.js | `^22.19.0 \|\| >=24.0.0` | 来自 create-dsh-plugin 根 package.json 的 engines；老 Node 只会 EBADENGINE 警告但可能踩到运行时坑（脚手架 PITFALLS 第 1 条） |
| DSH | `>=0.1.0-rc.6` | 由 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-session` 的 devDependencies next-tag 版本隐式锁定；离线生成时默认回退 0.1.0-rc.6 |
| `@deepseek-ai/cordis` | `^{{CORDIS_VERSION}}`（peerDependency，默认 4.0.1） | 运行时由宿主注入，模板代码只 `import type` 不在运行时 require |
| 平台 | 跨平台 | 无原生模块，纯 ESM |
| 原生模块 | 无 | 模板 `dependencies` 为空 |

## 安装方式
```bash
dsh plugin --profile web add github:whyihaveyou/dsh-suite/packages/create-dsh-plugin/templates/events
```

## 配置项
本插件无需额外配置。模板不读 `process.env` / `options` / `Schema`，所有可调点都在源码里（监听哪些事件、定时器间隔 30_000 ms、节流阈值 5/25）。要修改行为直接改 `src/index.ts` 后重跑 `pnpm run build`。

## 常见问题

**Q: 模板里 `import type` 写法和普通 `import` 有什么区别？**

A: `import type { Context } from '@deepseek-ai/cordis'` 告诉 tsc"只取类型，编译时擦除"，最终 `dist/index.js` 里不会出现 `require('@deepseek-ai/cordis')`。这样模板可以把 cordis / dh-tools / dsh-session 全留作 devDependency 而不用塞进 runtime dependencies，宿主加载时直接把 ctx 喂给 `apply(ctx)`。

**Q: 我在 `tools/pre-execute` 里忘了调 `next()`，会有什么后果？**

A: 工具调用会被短路、永远得不到执行。模板源码注释明确写了这是一个常见坑（templates/events/src/index.ts:38），`next()` 是水线下行的通行证。要"否决"调用应返回一个 `PreToolDecision`，而不是直接 return。

**Q: 模板里的 30 秒心跳是必须保留的吗？**

A: 不是。它存在的唯一目的是配合 `ctx.effect()` 演示"宿主不管理的资源如何在卸载时被清理"——卸载时日志会打印 `DISPOSED — listeners removed, timer cleared`。实际插件里如果不需要心跳，直接删掉那个 `ctx.effect()` 块即可。

**Q: 想给这个模板加新的环境变量配置项，流程是什么？**

A: 不推荐在模板里加。事件模板的设计意图是"零运行时依赖、最小可读"；真要做可配置，应改用 `tool` 模板（带 `defineTool` + Schema），或复制 events 模板后自己加 `ctx.effect(() => applyConfig(process.env.MY_FLAG))`。

**Q: `cordis.patch.yml` 里 `name: {{PKG_NAME}}` 没被替换怎么办？**

A: 说明你绕过了脚手架、直接复制了模板目录。`{{PKG_NAME}}` / `{{PLUGIN_ID}}` / `{{CORDIS_VERSION}}` 都是 token，必须经过 `create-dsh-plugin` 的 render 流程替换（packages/create-dsh-plugin/src/generate.js:64-65）；smoke 测试会断言"不得残留 `{{`"（packages/create-dsh-plugin/test/smoke.test.mjs:47-50）。

## 上手难度
入门 — 文件只有一个 `src/index.ts`（62 行）、三类订阅 + 一个 effect 示例，没有运行时依赖，复制粘贴再改名就是自己的事件插件。

## 已知问题与限制
- **必须先 `tsc` 才有 `dist/`**：模板根目录里只有 `src/`，package.json 的 `main` 指向 `dist/index.js`；直接 `dsh plugin add` 后宿主会找不到入口，需要先在模板目录跑 `pnpm install && pnpm run build`。
- **`{{token}}` 必须经脚手架替换**：裸模板里的 `{{PKG_NAME}}` / `{{PLUGIN_ID}}` / `{{CORDIS_VERSION}}` 是占位符，不会被宿主解析；不走脚手架等于装了个"洞"。
- **`@deepseek-ai/dsh-tools` 的 `latest` dist-tag 是过期的 `0.0.1-rc.1`**：脚手架强制把生成项目的 dsh-tools / dsh-session 锁到 next-tag（默认 0.1.0-rc.6），不要手动 `npm i @deepseek-ai/dsh-tools` 覆盖（脚手架 PITFALLS 第 2 条）。
- **多 dsh 包必须锁同一版本线**：所有 `@deepseek-ai/dsh-*` 包需统一 `0.1.0-rc.x`，否则 pnpm 会装两份模块导致类型不一致（脚手架 PITFALLS 第 3 条）。
- **`dsh plugin add <dir>` 的相对路径锚定调用目录**：模板被作为 bundle 装入时路径解析走包名而不是相对路径，但本地调试若用 `./<dir>`，必须从父目录而非模板内部运行（脚手架 PITFALLS 第 6 条）。
- **`cordis.patch.yml` 的 `name` 必须用包名**：用相对路径会让宿主 loader 找不到入口，模板顶部注释专门把这个坑写出来防呆（templates/events/cordis.patch.yml:4-7）。

---

This document is auto-generated by [deepseek-plugin.org](https://deepseek-plugin.org). HTML page: [dsh-suite](https://deepseek-plugin.org/plugins/whyihaveyou/dsh-suite/packages/create-dsh-plugin/templates/events)
Wiki generated by AI (model: `MiniMax-M3`)
