dsh-skill-invocation-monitor
English | 简体中文
> 一个只读的 DeepSeek Harness(DSH)插件,把技能调用摊在一屏里:
> 当前有哪些 Agent 在跑、每个正在调用哪个 Skill、是运行中 / 已完成 / 失败、已经跑了多久——在 Web GUI 侧边栏实时可见。
取自运行中的插件:点侧边栏入口打开面板,每个 Agent 一组,每一行是一次技能调用(状态、时长、起始时刻)。
装完之后你会得到什么
DSH 里的技能调用是隐形的。模型读了某个技能,你只看到结果——看不到那次调用、看不到耗时、更看不到那个
一直没回来的。装上这个插件之后:
- 侧边栏多一个 技能监控 入口,图标上带一个运行中调用数的实时徽标;
- 面板列出每个活动 Agent,以及它下面每一次技能调用的状态与时长;
- 技能的两条到达路径都被覆盖且可区分:模型调用
skill工具,以及你自己触发的技能(那一行带用户触发标记); - 全程不写任何东西:不注册工具、不改会话状态、不拦截也不延缓任何调用。
安装
# 从 npm 安装(推荐:装到的是预构建产物,不需要任何构建脚本授权)
dsh plugin --profile web add @yyc-jnu/dsh-skill-invocation-monitor
# 或直接从 GitHub 安装(纯 ESM JavaScript,没有构建步骤,pnpm 不会要求 allowBuilds)
dsh plugin --profile web add github:YYC-JNU/dsh-skill-invocation-monitor
然后重启 DSH 并刷新页面。两个半(宿主的只读观察器与路由、浏览器 bundle)都在宿主启动时扫描,
所以重启之前侧边栏不会出现入口。
启动前可先验证层是否生效:
dsh --profile web --dump-config # 应看到: # == @yyc-jnu/dsh-skill-invocation-monitor
如果这一层不存在,说明包只作为普通依赖装上了,插件并未激活。
卸载:
dsh plugin --profile web remove @yyc-jnu/dsh-skill-invocation-monitor
功能
| 实时 Agent 列表 | 每个活动 Agent、它的会话标题,以及它是运行中还是空闲 |
| 每次调用一行 | Skill 名、状态(运行中 / 已完成 / 失败 / 未观测到结果)、时长、起始时刻 |
| 两条调用路径 | 模型的 skill 工具调用,以及人手动触发的技能——后者标 用户触发 |
| 时长实时跳动 | 运行中的调用每秒刷新一次时长 |
| 侧边栏徽标 | 运行中计数直接挂在侧边栏入口上,不打开面板也能看出有没有活动 |
| 如实呈现失败 | 失败的调用附带截断后的原因,例如 skill "…" is unknown or no longer available |
| 自检诊断 | 每次响应都带一组计数,描述插件实际看到了什么,让错误假设可见而不是静默 |
| 零依赖 | 两个半都不 import 任何第三方包(浏览器半只用宿主模块表里的 react) |
权限与数据
这个插件存在的意义就是观察,它的全部设计都围绕"不参与"。
| 观察(只读) | 五个 Cordis 事件:tools/pre-execute、tools/result、session/event、agent/status、agent/disposed |
| 读取 | 活动 Agent 列表、会话标题、以及边落盘边推送的会话事件流 |
| 不写入 | 不写会话状态、不写 session log、不落盘、不写配置、不注册工具、不注册命令 |
| 不干预 | tools/pre-execute 监听器原样返回 next(),从不拒绝/取消/改写/延缓任何调用;每个观察器都整体包裹,插件自身的 bug 无法破坏工具派发 |
| HTTP 面 | 只有一条路由 GET /api/plugins/skill-monitor/state——无请求体、无写操作、cache-control: no-store |
| 网络暴露 | 仅回环:非回环来源、跨站标记或外部 Origin 一律 403;非 GET 方法一律 405 |
| 数据在哪 | 宿主内存里的一个 Map。记录有上限(默认 200)并按时间淘汰(结算后 10 分钟);重启 DSH 即清空 |
| 不收集 | 提示词正文、模型输出、文件内容、凭据,以及任何插件根本没有读的东西 |
兼容性
| DSH | 在 0.2.0-rc.2 上验证通过(本插件的开发与测试环境)。所用接缝均为官方核心面,预期其他 0.2.x 构建同样可用 |
| Node.js | 声明 >=20。插件运行在 DSH 宿主内,实际以宿主自身的 Node 要求为准 |
| 界面 | Web GUI(dsh.client.platform: "web")。只用官方 Slot——入口用 sidebar.panellist,面板用 main——不做任何 DOM 注入 |
| 技能路径 | 两条路径都走持久化契约:工具调用走 tools/pre-execute / tools/result;人的调用走技能接缝声明的 skill-invocation 消息来源 |
工作原理
两条调用路径——以及为什么人这条是读消息来源
技能在 DSH 里有两种结构性完全不同的到达方式,插件两条都观察。
| 模型路径 | 用户路径 | |
|---|---|---|
| 触发 | 模型调用 skill 工具 |
你在会话里调用一个技能(例如 /grill-me) |
| DSH 的行为 | 走工具流水线并产生工具结果 | 把技能正文作为一条 user/message 追加进对话——既不产生工具调用,也不产生命令事件 |
| 配对键 | callId |
该消息的 id |
| 时长口径 | tools/result − pre-execute |
turn/end − 注入时刻(见下) |
| 行内标记 | 无 | 用户触发 |
人这条路径读的是技能接缝自己附上的结构化来源(持久化目录里的 SkillInvocationSource):
{ "type": "user/message", "data": {
"source": { "kind": "skill-invocation", "name": "grill-me", "form": "instructions" },
"role": "user", "id": "7c2e0f5a-3b9d-4e18-9a6c-1d2f3a4b5c6d" } }
名字直接来自 source.name,既不需要查技能目录,也不需要解析正文。这是量出来的而不是猜的:早先的
实现假设人这条路径走 command/run / command/done,一份真实会话日志否掉了它——人手动调用的技能
完全不产生命令事件,于是那套目录缓存与它的 60 秒 TTL 一并删除。
一个刻意的口径选择:人类技能没有自己的结果事件,所以它的时长是该轮次的窗口(注入 → turn/end),
而不是技能自身的执行耗时。这是声明出来的约定,不是测量值——若你更想看到"瞬时事件、时长为 0",
要改的只是结算点。
状态语义
| 状态 | 含义 |
|---|---|
运行中 |
已观察到开始(pre-execute 或技能消息),结束事件还没到;时长实时增长 |
已完成 |
工具报告 isError === false,或承载该技能的那一轮正常结束 |
失败 |
工具报告 isError === true;该行附带截断后的原因 |
未观测到结果 |
15 分钟内没有任何结束事件(调用在派发前被拒、父调用被取消、宿主崩溃)。这是推导状态,从不写回记录 |
只读契约
这是本插件的核心约束,每一行都有测试断言。
| 约束 | 实现方式 |
|---|---|
| 不注册可写工具 | 从不触碰 ctx.tools——无 register / restrict / guard;apply() 不向 ctx 添加任何成员(有测试断言) |
| 不修改会话状态 | 从不 append 会话事件、从不写 session log、从不调用任何服务方法;只读 agents.list() / sessions.get() / sessionTitle.get() |
| 不拦截任何调用 | tools/pre-execute 监听器原样返回 next(),从不拒绝/取消/改写/计时;用恶意 Proxy 实参也无法让它阻断调用(有测试断言) |
| 不落盘 | 全部状态是内存里一个 Map,有上限、会淘汰;无历史、无持久化、无告警 |
| 单一只读路由 | 只有 GET /api/plugins/skill-monitor/state——无 POST、无请求体、无写路径 |
| 零运行时依赖 | 两个半都不 import 任何第三方包 |
自检诊断——让错误假设可见而不是静默
每次状态响应都带一个 diagnostics 对象,描述插件实际看到了什么:
| 字段 | 它告诉你什么 |
|---|---|
sessionEvents |
落盘事件流确实到达了插件 |
skillInjections / trackedInjections |
技能消息被看见并被记录 |
unmatchedInjections |
收到了一条 skill-invocation 消息,但名字读不出来 |
turnEnds / settledByTurnEnd |
轮次结束事件到达了,以及它结算掉了多少条人类记录 |
这不是装饰:最早那条被证伪的"命令路径"假设,正是被它抓出来的。
配置(可选)
- id: skill-invocation-monitor
name: '@yyc-jnu/dsh-skill-invocation-monitor'
config:
maxRecords: 200 # 内存中保留的调用记录上限
retentionMs: 600000 # 已结算记录保留 10 分钟
staleMs: 900000 # 15 分钟没有结果 → 未观测到结果
maxCallsPerAgent: 20 # 单个 Agent 在面板里展示的调用条数
开发
测试、脚本与仓库结构(面向贡献者)
cd dsh-skill-invocation-monitor
npm run verify # 对三个入口文件做 node --check
npm test # 72 个离线测试:node --test
72 个离线测试覆盖三层:纯聚合模型(test/tracker.test.mjs,36——两条路径的时长结算、配对键、陈旧超时、
淘汰与上限、Agent 合并、以及那个持久化载荷的按字段名解析)、只读契约本身(test/host-plugin.test.mjs,25
——恰好五个事件、恰好一条 GET 路由、pre-execute 是纯透传、回环围栏、释放时全部卸载)、以及浏览器 bundle
(test/client-bundle.test.mjs,11——按 shell 真实加载它的方式加载,断言 Slot 注册与渲染输出)。
本插件是手写 ESM 的普通 JavaScript:lib/ 就是源码,没有构建步骤,也没有 src/。
dsh-skill-invocation-monitor/
├── package.json # 名称 / exports / dsh.bundle.patch / dsh.client
├── cordis.patch.yml # 两个半都由这一行 profile 行挂载
├── lib/
│ ├── host/
│ │ ├── index.js # 宿主半:5 个只读观察器 + 1 条 GET 路由
│ │ └── tracker.js # 纯聚合模型(无 Cordis / 无 I/O / 显式时间戳)
│ └── client.js # 浏览器半:ModuleLoader bundle,2 个 Slot 注册
├── docs/
│ ├── screenshot.png # 上面那张截图
│ └── skill-monitor-flow.json # archify 时序图源规格
└── test/ # 72 个离线测试
许可证
MIT
致谢
本插件的代码在 AI 辅助下完成:作者提出需求、评估并验收结果,AI 负责实现与文档。所有行为都在真实
DSH 运行环境中逐项验证过;测试与文档中出现的会话标题、消息 id、时间戳等全部为合成值,不含任何
真实用户数据。