dsh 输入框旁的两个按钮:看着当前目录把草稿重写一遍,或者把一份角色模板插到光标处。
allow-version shared 的版本下限
如果你的 profile 用 pnpm `overrides` 把 `@jayyuen66/dsh-plugin-shared` 钉在某个版本,那个值必须不低于本包声明的那一版。钉得更低会装到一份缺少运行期所导入名字的产物,宿主起来时报 `N entries did not activate` 与 `does not provide an export named ...`——包装得上、永远不生效,而各仓单仓 CI 看不见这一类。
##
确认它真的在工作
1. **输入框右边出现两个控件。** 没出现就是装配层没挂上——用 `dsh --profile web --dump-config` 看有没有 `dir-prep-organize` 这一层。
- `--dump-config` 是唯一被接受的"进了层栈"信号,而它**不是只读命令**——它会重写该 profile 的根装配文件。所以判读要分别捕获 stdout 与 stderr;未匹配的 patch 目标只出现在 stderr,只看退出码什么都证明不了。
- 层行可能是 `# == <层>`,也可能是 `# == <层>, patched by <路径>`;两种都得认。
2. **下拉能填出来**:打开「模板」,内置列表经 Remote 的 `defaults` 方法到达。一行 loading 表示还在路上;失败会显示宿主的原因加「重试」。
3. **润色能用**:按主键。成功即替换草稿;失败则草稿不动,按钮进入错误态,原因在 tooltip 里。
4. **设置卡渲染出来**:「角色库」小节出现在设置页,改动先进草稿,点**保存**后生效。
##
它不提供什么
找之前先知道这几件事:
- **它不注册任何模型工具。** 宿主半只 inject `settings`,从不碰工具注册表。没有 `organize_text` 这类工具可调。
- **它没有自建的 HTTP 路由。** 数据面是官方 Typert Remote 命名空间 `dirPrepOrganize`,经注入的子 fiber 注册,走宿主 `/api` 网关。认证、Host/DNS 重绑定/Origin 判据、签名浏览器会话 cookie、body 上限全归网关——本包不持有 CSRF token,也没有路由表。
- **它不加 CLI 子命令。**
- **它不读任何环境变量**,也没有自己的出站请求。
- 它**确实**消费的东西:宿主 `settings` 服务(硬注入,缺它条目根本不激活)、`llm.stream`、`agentDefaultModel`,以及会话、Agent 与文件系统服务。设置表单本身没就绪时,卡片禁用并显示状态提示,下拉回落到内置精选集。
##
在 dsh 里启用
- 安装即贡献组合包补丁层(`dsh.bundle.patch` → `./cordis.patch.yml`),插入 `- id: dir-prep-organize`,`name` 指向包名。
- **客户端半**注册两个槽位外加设置卡:
- `conversation.input.right`——润色按钮(id `dir-prep-organize`,order 99)与模板下拉(id `dir-prep-organize-templates`,order 100)。
- `plugins.bundle.config`——设置卡,按 **bundle 包名** `@jayyuen66/dsh-dir-prep-organize` 作 key。宿主派发 `entryKey: pkg.name`(真源是 profile `package.json` 里的 `dsh.profile.bundles`),所以写成裸条目 id 永远匹配不上,卡片哪都不渲染。
- 设置命名空间与 `configForms.get()` 仍是**裸条目 id** `dir-prep-organize`——与那个槽位 key 刻意不是同一个标识。
- 客户端半是 `dsh.client.platform = web` 且带 `immediately`。
- 卡片的文案遵循官方 locale 插件的偏好;宿主半每次请求现读该偏好,所以切语言不必重启。
- 插件页的标题、说明与详情文档来自 `locale/<语言>.json`(`meta.title`、`meta.description`、`meta.detail`)。`docs/DETAIL.md` 与 `docs/DETAIL.zh.md` 随包发布;没有声明 `meta.detail` 时宿主才回落到 README。
##
润色调用
| 面 | 事实 |
|---|---|
| 传输 | 宿主 `llm.stream`:一条**仅请求态**的 user 文本消息(无 `id`、无 `source`)加一段 `system` 规则块 |
| 模型 | 先取会话活 Agent 的当前选择——新宿主读 `agent.model.current()`,旧宿主回落 `agent.options`——再回落 `agentDefaultModel.currentSelection()`。取不到时回报 "No model selection is available" |
| 写回闸门 | 只有会话 id、输入动作面身份、草稿版本与输入阶段(`plain`)都没变时才写回。否则回报「输入框内容已变化,润色结果未写回」,草稿保持原样 |
| 角色风格 | 可选的 `roleText` 注入 user 消息的 `` 围栏(不进 system——见 `buildOrganizeSystem` 的信任序注释)。它只借用术语与语气,**从不执行该角色的任务** |
| finish reason | 除 `stop` 之外(含缺 finish 块)一律**失败**,且绝不写回草稿 |
| 超时 | `organizeTimeoutMs`,默认 120000 ms,每次请求经 `.get()` 读。客户端 fetch 超时是它 + 5000 ms |
| 输入上限 | `maxSnippetBytes`(262144 = 256 KiB)按关键文件、摘要总量 2 MiB、`readConcurrency`(8)按读并发。超过单文件上限、或超出总量预算的摘要整份跳过并计入 `skipped.tooLarge`。目录、对话与角色正文**从不截断** |
| 为什么刻意不截断 | 目录、对话与角色正文都整份进去:模型报上下文溢出就是反馈,由用户自己收窄输入。载荷超过端点 body 上限则得到 413 |
##
模板
| 面 | 事实 |
|---|---|
| 条目形状 | `{ id, name, description, text, group, emoji }`,全部字符串。`description` / `group` / `emoji` 默认空串 |
| 保存时校验 | `name` 或 `text` 为空的条目被丢掉;`emoji` 截到 12 字符;缺失或撞车的 `id` 确定性地替换成 `tpl-
设置项
命名空间 `dir-prep-organize` 即设置条目 id。投影进设置表单的字段就是那几个 `.volatile()` 的。
| 字段 | 类型 | 默认值 | 卡上有控件 | 说明 |
|---|---|---|---|---|
| `templates` | `{id,name,description,text,group,emoji}[]` | 未设置 | **有** | 用户自己的模板。未设置时回落到 85 条内置 |
| `importAllowRoots` | `string[]` | `[]` | **有** | 导入端点的额外允许根,逐个目录登记;非字符串与纯空白项丢弃,重复项去重。一个都没登记时只允许会话 cwd 与内置角色库 |
| `organizeTimeoutMs` | natural,volatile | `120000` | **无** | LLM 中止时限(毫秒)。取值先取整再夹到 `[1, 2147483647]`;非有限数回落到 `120000`。`0` 能过 schema,夹完是 1 ms,所以它确实表示"立刻超时"。超过 2^31-1 时 Node 的 `setTimeout` 会打 `TimeoutOverflowWarning` 并静默用 **1 ms**,于是"调大超时"变成"立刻失败"。卡上没有控件,改它只能经 `settings.yaml` 或该行的 `config:` |
| `maxSnippetBytes` | natural,非 volatile | `262144` | **无** | 关键文件摘要的单文件上限;超过、或大小未知的文件整份跳过并计入 `skipped.tooLarge`。导入候选用同一个上限。非 volatile 意味着不进表单快照,所以也没有卡上的行——纯部署值 |
| `readConcurrency` | natural,非 volatile | `8` | **无** | 摘要与目录导入的有界读并发。夹在 `[1, 64]`:`0` 或负数会让那套有界 map 自我递归直到宿主被 OOM killer 杀掉,而过大的值直接废掉这个字段存在的意义(fd 上限) |
设置持久化在 dsh 数据目录下的 `settings.yaml`。
##
对外接口
官方 Typert Remote 命名空间 **`dirPrepOrganize`**,经注入的子 fiber 注册。descriptor 手写,参数走严格形状校验,结果走 `src-json`。
| 方法 | 入参 | 成功载荷 |
|---|---|---|
| `defaults` | — | `ok` / `templates[…]`(精选集) |
| `context` | `{ sessionId }` | `ok` / `cwd` / `truncated` / `skipped` / `entries[{name,isDir,sizeBytes,snippet}]` |
| `model` | `{ sessionId }` | `ok` / `provider` / `model` / `reasoningEffort` |
| `organize` | `{ sessionId, prompt, entriesSummary, roleText? }` | `ok` / `content` |
| `importTemplates` | `{ path, sessionId }`(`path` 恰为空串 = 整套内置库) | `ok` / `count` / `truncated` / `skipped` / `entries` |
- 业务失败一律 `ok:false` 带 `error`。导入路径策略那几支是例外:越界路径或形状不对的 `path` 还会额外带 `status` 403 或 400。
- `model` 只是对默认选择的预检;`organize` 优先用会话活 Agent 的选择,见「润色调用」。
- 客户端解析结果保留防御性默认:非对象载荷、或缺 `ok:true`,都变成**可见的错误**,绝不变成空列表。
##
数据与隐私
| 面 | 事实 |
|---|---|
| 网络 | 没有自己的出站请求:数据面走宿主网关的 RPC 通道。不读任何环境变量 |
| 落盘 | 唯一写盘的是宿主设置文件里的 `templates` 与 `importAllowRoots` |
| 一次润色发出去什么 | 完整草稿、会话 cwd 路径、顶层目录条目**连同关键文本文件的全文**、真实的 user/assistant 轮次(框架注入的消息与工具结果被滤掉),以及可选的角色正文 |
| 哪些文件会取摘要 | 只有 20 种代码/文档扩展名(ts/tsx/js/jsx/mjs/cjs/py/rs/go/java/rb/json/toml/md/sql/graphql/proto/css/scss/less)。`yaml` / `env` **不在**白名单 |
| 敏感文件名 | 名字在**分隔符边界**上命中 `credentials` / `secret(s)` / `id_rsa` / `pem` / `p12` / `pfx` / `keystore` / `env` / `key(s)` / `token(s)` / `auth` / `passwd` / `password` / `account(s)` 的文件,永不取摘要。边界是刻意的:朴素子串匹配会误杀 `monkey.js` 与 `AUTHORS.md`,而漏匹配会把凭据全文发给第三方模型 |
| 上限与并发 | 单个文件超过 `maxSnippetBytes`、或大小未知,整份跳过——绝不半份摘要。收集到的摘要共享 2 MiB 总量预算;用完之后其余整份跳过并计入 `skipped.tooLarge`。读按 `readConcurrency` 跑 |
| 计数器 | 过大 / 读不出 / 更深层 / 非角色这几类计入 `skipped`,在 UI 里显示,并由 `truncated` 如实反映 |
| 导入端点 | **只读**:不调模型、不写盘。绝对路径默认拒绝;只有会话 cwd、本包的 `agents/` 与 `importAllowRoots` 之内通过。`.` / `..` / 隐藏段(如 `.ssh`)一律拒绝,越界路径回 403 并回显解析后的路径 |
##
已知边界
按类登记,每类一条。
- **没有任何东西替你截断。** 目录列表、对话文本与角色正文都是整份进去的。摘要则是整份略去而不是缩短:超过单文件上限、或超出 2 MiB 总量预算的,会记进计数并整条不放。上下文溢出是模型的报错,也是你收窄输入的依据——本插件不会静默丢掉你半份草稿。
- **导入卡没有会话上下文。** 它的 POST 把 `sessionId` 发成空串,所以导入目标必须在内置 `agents/` 或某个已登记的根之内,且不含 `.` / `..` / 隐藏段。拒绝时会点名允许区并回显解析后的路径。
- **目录导入只到两层。** 顶层加一层子目录;第三层只被**计数**为 `deeper`,永不导入。
- **前 20 行 frontmatter 里解析不出名字的文件计为 `unnamed`**,跳过而不是猜。
- **这里没有机器翻译。** `agents/` 角色库与模板正文是用户数据,无论界面语言如何,永不被翻译。
- **润色对模型回答只能尽力而为。** finish reason 不是 `stop` 就整趟失败;而模型若返回散文而不是重写结果,草稿就被那段散文替换掉——除非草稿或输入阶段在途中变了,这次写回会被拒绝。
##
常见问题
| 症状 | 怎么回事 |
|---|---|
| 按了「润色」什么都没变 | 失败从不碰草稿;按钮进入错误态若干秒,原因在 tooltip 里。常见成因:没有会话 cwd(`sessionId` 缺失或会话未知)、没有模型选择、finish reason 不是 `stop`、撞上 `organizeTimeoutMs` 时限,或请求途中草稿/模型选择变了(结果被丢弃) |
| 模型报上下文溢出 | 这是设计如此:输入刻意不截断,模型报错就是反馈。载荷超过端点 body 上限则得到 413 |
| 导入本地角色被拒 | 绝对路径默认拒绝,而卡片没有会话上下文。路径必须在内置 `agents/` 或某个已登记目录之内,且不含 `.` / `..` / 隐藏段 |
| 导入的条目比预期少 | 看四个 `skipped` 计数器:`deeper`(第三层,只计数)、`tooLarge`(超过 `maxSnippetBytes`)、`unnamed`(前 20 行 frontmatter 里没有名字)、`unreadable`(读失败) |
| 模板改动没生效 / 语言不对 | 改动先在本地暂存,点**保存**后对新生效的下拉可见。标签与文案遵循官方 locale 偏好;而角色与模板**正文**是用户数据,永不做机器翻译 |
##
开发
```sh
pnpm install
pnpm run check # typecheck → lint → client 构建 → host 构建 → 类型 → 测试 → 格式
pnpm run gen:defaults # 从 agents/ 重新生成精选集(仅开发期)
```
- **`test/gen-defaults.test.ts` 是一道漂移锁。** 它把生成文件与角色库逐字段比对、核对两张镜像表的键序,并要求与生成器产出**字节级**一致。改解析或提炼逻辑就必须改 `scripts/gen-defaults-lib.mjs` **并**重跑 `pnpm run gen:defaults`;否则表现出来是一条红测试,而不是一个悄悄过时的下拉。
- 精选 slug 缺失或解析失败时生成器非零退出,所以角色不可能无声消失。
- 其余测试按「桩看不见的那几类错」分层:宿主半、客户端半、Remote 契约(`test/dir-prep-remote.test.ts`)、两份构建产物(`test/build-host.test.ts`、`test/build-client.test.ts`),以及一层集成。
- 覆盖率四项阈值都是 100%,所以新增分支必须有能走到它的用例。
##
许可
MIT。源码在 [github.com/JayYuen666/dsh-dir-prep-organize](https://github.com/JayYuen666/dsh-dir-prep-organize),issue 走那个仓的 tracker。
`agents/` 移植自 [`agency-agents-zh`](https://github.com/jnMetaCode/agency-agents-zh)(MIT),详见 [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md)。