Skip to content
dsh.fish
Bundle

dsh-agent-manager

多 Agent 管理与配置:新建 Agent 预设、编辑人格 / 知识库注入 / 知识库工具开关 / MCP 按 Agent 收窄,并在 Agent 预设卡片上提供编辑入口

Source
let-mi-think
License
MIT
Updated
Updated 2 hours ago

Readme

# dsh-agent-manager

多 Agent 管理与配置插件(DeepSeek Harness)。与 `dsh-eli-mode` **并存**,不改动它的任何文件。

## 安装

```sh
dsh plugin --profile web add dsh-agent-manager
```

装完重启 `dsh web`,设置里会出现 **Agent 管理** 分区。

- **宿主要求**:`dsh` `0.1.0-rc.7`(见 `package.json` 的 `dsh.engines.dsh`);Node `^22.19.0 || ^24.0.0`。
- **知识库是可选依赖**:知识库清单 / 导入 / 重建索引这组功能复用 `dsh-eli-mode` 的 `eliKb` 服务。
  没装 eli-mode 时它们显示为禁用态,其余配置照常可用。
- **客户端产物随包分发**(`lib/client.js` 已提交),安装时不需要构建。

## 它做什么

- 在 DSH 设置区新增一个「Agent 管理」分区:**先选 Agent,再配置它**(比 Eli Mode 的单体配置多出「多 Agent 选择」这一步)。
- 在官方「Agent 预设」卡片上叠加一个「编辑」按钮,点开直接进入该 Agent 的配置。
- 每个 Agent 可配置:人格 prompt、知识库注入 prompt 模板、**可勾选的知识库条目清单**(勾选后把条目清单注入提示词,正文由 `kb_read` 按需取)、四个知识库工具的独立开关、**可用哪些 MCP 连接**。
- 注入清单的每行是 `- <条目路径> — <标题>`(与 eli-mode 生成的目录页同格式):标题让 Agent 光看清单就知道该读哪一篇,中文文件名下标题自然就是中文。
- 新建 Agent:选一个现成预设作蓝本,插件复制它的目录并改写组合文件,新 Agent 立刻出现在预设选择器里(新会话生效)。
- **导入知识库**:填一个目录路径 → 扫描预览 → 整批导入(用于初始化 / 迁移一批文档),顺带补上知识库本身没有的「重建索引」入口。

## 三个平面

| 平面 | 文件 | 挂载方式 |
| --- | --- | --- |
| host | `lib/index.js` | 本插件 `cordis.patch.yml` 的 `insert` |
| preset | `lib/preset.js` | **每个生成的 Agent 预设**在自己的 `agent.cordis.yml` 里按 id 挂载 |
| 浏览器 | `lib/client.js` | `package.json` 的 `dsh.client` |

preset 平面由预设自己挂载,是为了让它天然绑定到「自己服务哪个 Agent」——挂载行里带 `config.presetId`。

## 配置存哪

- 配置数据:`~/.dsh/settings.yaml` 的 `agent-manager` 段,形状 `agents: { <presetId>: { ... } }`。由插件注册的设置命名空间读写,**不写进预设的组合文本**。
- 归属标记:新建的预设目录里写一个侧车文件 `.agent-manager.json`,记录 `managedBy` / `blueprintId` / `createdAt`。删除只允许删带这个标记、且位于 `~/.dsh/.agent-presets/` 之下的目录,避免误删 `eli-mode`、`wechat` 等既有预设。

## 知识库

复用 `dsh-eli-mode` 的 `eliKb` 服务读取 `~/.dsh/eli-knowledge`(同一份 wiki),**不重复实现一套**。因此:`dsh-eli-mode` 被停用或升级导致 `eliKb` 不可用时,知识库清单会显示禁用态,其余配置不受影响。

### 导入(初始化一批文档)

「Agent 管理」头部的「导入知识库」:填源目录(绝对路径)→ `扫描` 出预览(`+ 新增 / = 已存在`、并列出被跳过的文件及原因)→ `导入 N 项`。

- 目录结构原样变成条目路径,可再套一个「落点前缀」,例如把 `C:\...\docs` 导成 `tripartite/...`。
- 只认 `.md` / `.markdown`;跳过 `index.md`(各级目录页)、隐藏目录、`node_modules` 等;默认跳过已存在的同名条目(可勾选覆盖)。
- **默认补标题行**:源文件首行不是 `# 标题` 时,导入时会在最前面补一行 `# <文件名>`(原内容一行不删)。原因见下。
- 只**复制**(或补标题行后写入)文件,源目录不动;导入后插件会自动调一次 `eliKb.regenerateAllIndexes()`。

### 为什么必须补标题行(eli-kb 会丢首行)

`kb.js` 的 `_readRaw` 是这么取正文的:

```js
let title = norm.split('/').pop()
if (lines[0].startsWith('# ')) title = lines[0].slice(2).trim()   // 标题
const body = lines.slice(1).join('\n')                            // ← 首行被丢掉
return { id, title, content: '# ' + title + '\n\n' + body }
```

**首行永远被丢弃**:是 `# 标题` 时"丢得正好"(标题另外存了),不是时那一行正文就**永久读不到** —— Agents 读不到、知识库网页看不到、`search` 也搜不到(search 基于同一份裁过的 content)。上游自己因此约定「首行即标题」(`write()` 强制写 `# title`,自带种子 wiki 的文件首行全是 `# 标题`)。

已有条目如果踩了这个坑(例如手拷进 `wiki/` 的文件),用脚本就地补,不必删了重导(在**包目录**里执行):

```powershell
node scripts/fix-title-lines.mjs           # 预览(不写文件)
node scripts/fix-title-lines.mjs --apply   # 执行:先备份,再补行,最后重建索引
# 个别条目的标题取自自身小标题(如 `# 1. 概述`)时,可用文件名重写:
node scripts/fix-title-lines.mjs --apply --force=6
```

备份落在 `~/.dsh/eli-knowledge/.titleline-backup-<时间戳>/`(在 `wiki/` 之外,不会被当成条目)。

### 为什么必须由插件来重建索引

会话清单读的是知识库**根索引** `~/.dsh/eli-knowledge/index.md`,而它只在 eli-kb 自己 `write()` / `remove()` / `init()`(且根索引不存在)时重建,知识库**没有任何路由或按钮**能手动触发。所以直接往 `wiki/` 拷文件会出现「文件在、`list()` 看得见、Agent 却完全不知道」的哑谜。本插件为此提供两处入口:

```powershell
# 面板内:导入知识库 → 仅重建索引
# 命令行(DSH 没跑起来时,在包目录里执行):
node scripts/reindex-kb.mjs
# 调 prompt 模板/清单格式时,先看看 Agent 到底会收到什么:
node scripts/preview-injection.mjs [presetId]
```

三个脚本都用 `scripts/eli-kb.mjs` 定位 `dsh-eli-mode` 的 `lib/kb.js`:先看环境变量 `ELI_KB_MODULE`,再按 Node 解析规则从当前目录找 `dsh-eli-mode`,最后试仓库内 vendored 快照。找不到时会直接告诉你该装什么或该设哪个环境变量。

### 中文条目名需要 dsh-eli-mode 的一个补丁

中文 / 下划线 / 空格命名的条目能否被**读取**,取决于 `dsh-eli-mode` 的 `lib/kb.js` 里 `normalizeId` 的字符集限制:

- **上游原版**只接受 `[a-z0-9-]` 段名 → 这类条目「能列出但读不到」(`list()`/`tree()`/`search()` 不走校验,`read()`/`write()`/`remove()`/双链解析才走),表现为知识库网页点不开、`kb_read` 返回空、`kb_write` 还会把中文标题 slugify 成 `entry-xxxx`。
- **补丁后**只校验「段非空且不是 `.` / `..`」并拒绝 NUL:字符集任意,**仍拒绝路径穿越**,id 永远落在 `wiki/` 之内。

补丁的判定只影响**读取**。要放中文条目,请让 eli-mode 使用打了补丁的版本(差异就是 `kb.js` 里 `normalizeId` 那一个函数)。

## MCP 连接(按 Agent 收窄)

- **数据来源是工具注册表,不是连接器的存储或它的 status 接口**:连接器把每个 server 的工具注册成全局工具 `mcp__<serverName>__<toolName>`,所以「装了哪些、有哪些工具」以 `ctx.tools.schemas()` 为准。实测连接器自己的 `status` 接口报 0 个连接时,工具表里仍有 1 个真实连接(5 个工具)——按 status 做会误判成「没有 MCP」。
- **怎么收窄**:预设平面的 ctx 就是「该 Agent 的 scope」(DSH 为每个预设 `createScope(selfCtx, { agentPreset: <id> })`),因此在 preset 平面调用官方 `ctx.tools.restrict({ deny })`,未勾选连接的工具会同时从 schema、lookup 与 dispatch 中消失。这与 MCP 连接器收窄工作区用的是同一套机制。
- **默认不限制**:`mcpRestrict` 默认 false = 沿用「全部已连接的 MCP 都能用」,避免升级后静默砍功能。开启后才按 `mcpServers` 精确放行(勾选为空 = 全禁)。
- **会跟随工具表变化重算**:订阅 `tools/registered`,连接增删后按新工具表重新计算(否则重连之后仍被旧限制挡住)。重算前会先撤销自己的限制再读工具表,避免「读到被自己遮蔽后的子集、越算越少」。
- **失败即降级**:`restrict()` 若抛错(例如名字刚好失效、或该 scope 不允许限制),只记一条 warn 并保持不限制——不会让整个预设挂载失败。
- **一个命名边界**:连接器对超长/含特殊字符的工具名会截断并加 12 位哈希后缀,那种名字解析不出 serverName,本插件会把整条名字当成一个独立分组列出来(可选可放行),不会静默丢弃或静默禁用。

## 已知限制

- **卡片上的「编辑」按钮是 DOM 注入**:DSH 官方没有预设卡片级插槽(只有 `settings.section` 这类通用插槽)。选择器只用本地名后缀匹配(`[class*="_card"]` 等),官方卡片结构变化时按钮会自动不注入并记一条 warn——独立分区仍然可用,功能不丢。
- 工具开关是**执行期拦截**:预设组合按 preset 只挂载一次,注册期裁剪要重启才生效,所以四个工具一次性全部注册,在 `execute` 里按开关拒绝并返回说明。代价是禁用中的工具仍会出现在工具清单里。
- 新建 Agent 需要直接写 `~/.dsh/.agent-presets/<id>/`,绕过了官方 `AgentPresets` 服务「只允许整目录复制」的授权限制;因此改写只做**按行文本修改 + YAML 校验 + 失败回滚**,尽量保留蓝本里的注释。
- 导入是「复制 + 重建索引」,不做去重合并:同一条目改了别处不会自动同步;想更新就开「覆盖」重新导一次。
- 用 `eli-kb` 自己的 `remove()` 删条目时,若该目录已生成了 `index.md`(导入必然生成),空目录不会被自底向上清掉,会留一个只有 `index.md` 的空壳目录;条目列表不受影响(`index.md` 不算条目),需要时手动删目录。

## 开发

```powershell
npm install          # 需要 devDependencies(tsdown / typescript / @types/*)
npm run build        # 产出 lib/client.js(已随仓库提交,运行时无需构建)
npm run typecheck
npm test             # node --test tests/*.test.mjs(不依赖 dsh-eli-mode)
```

host 半与 preset 半是免构建的 `lib/*.js`;只有浏览器半需要 tsdown 打包,再由 `scripts/wrap-client.ts`
包成 DSH 的 `window.__ModuleLoader__.load` 形状。`npm publish` 前 `prepack` 会自动跑一次构建。

需要真实 `eliKb` 实例的那部分测试(含中文 id 补丁与索引重建的验证)在开发用的单体仓库里维护,不随包发布。

## 上架(awesome-dsh-plugin / dsh-market)

插件市场里的列表来自精选列表仓库 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin),
收录方式是**往那边提 PR 新增一个条目文件**(不是往 dsh-market 提):

1. fork 该仓库,把本仓库的 `contrib/let-mi-think__agent-manager.yml` 复制成 `data/plugins/let-mi-think__agent-manager.yml`(文件名必须与仓库一致);
2. PR 前确认本仓库已声明 `dsh.bundle` 且根目录有 `cordis.patch.yml`(两个都已具备);
3. 给 GitHub 仓库加 `dsh-plugin` topic,并确保仓库创建已满 1 天(CI 会检查);
4. 提 PR(一个 PR 最多 3 条,且只改自己那一条)。合并后站点与市场的 README 会自动重建,**不要手工编辑那边的 README**。

npm 包不是收录的必要条件,但发了才能显示下载量:`npm publish` 后映射会被自动采集
(条目里**不要**写 `npm:` 键,写了会被校验拒绝)。本仓库的 `repository` 已指向自己,
`prepack` 会在发布前自动跑一次构建,`lib/` 也已随仓库提交,因此从 npm 安装无需构建。

可选增强:在仓库根放 `screenshots.json`(1–8 张图,路径相对该文件、不能跳出仓库)
可以让市场显示应用商店式截图;不放也行,市场会从 README 自动抽取。

## 许可

MIT

Install

dsh plugin --profile web add github:let-mi-think/agent-manager#4a692cbd1f97c5885b989967497f13842b294e60

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source