Skip to content
dsh.fish
Bundle

@hpyperry/dsh-cajita

DSH 小工具箱插件:编辑消息 / web_fetch 内网放行 / 提醒通知(设置卡片统一开关)

Source
hpyperry
License
MIT
Updated
Updated 3 days ago

Readme

# dsh-cajita · DSH 小工具箱插件

> DeepSeek Harness(DSH)插件:**一个插件包、多个小工具**。当前包含:
>
> - **任务 1 · 编辑消息**——在用户消息的复制按钮旁新增「编辑」按钮,把误发送的未完成消息
>   在原消息位置覆盖一条与官方输入框同款的内联编辑卡继续编辑,编辑后作为一条新消息正常发送;
> - **任务 2 · web_fetch 内网放行**(web-fetch-policy)——给官方 `web_fetch` 换一个带
>   「内网放行开关」的 fetch provider:默认与官方完全一致(只允许公网目标),在
>   设置 → 插件配置 打开开关后放行解析到私网/回环等内网地址的目标(**TUN / 代理用户
>   的 DNS 常回 fake-ip/内网地址,不开会全部被拒**);
> - **任务 3 · 提醒通知**(turn-notify)——agent 回合完成、弹出提问或需要审批时,
>   页面不在前台则弹浏览器系统通知(标题带场景前缀,需先授权一次);开关在
>   cajita 设置卡里。
>
> npm:[@hpyperry/dsh-cajita](https://www.npmjs.com/package/@hpyperry/dsh-cajita) ·
> 仓库:<https://github.com/hpyperry/dsh-cajita>
>
> 安装(推荐锁版本):`dsh plugin --profile web add @hpyperry/dsh-cajita@0.1.5`

## ✨ 工具清单

| 工具 | 能力 | 说明 |
| --- | --- | --- |
| 📝 | **编辑消息** | 用户消息复制按钮旁新增「编辑」:内联覆盖编辑卡带出原消息文本,继续编写后发送新消息(与正常发送完全同管线) |
| 🌐 | **web_fetch 内网放行** | 为官方 `web_fetch` 换上带开关的 provider:默认与官方一致(仅公网);设置里打开开关后放行私网/回环/内网目标(TUN / 代理用户必开) |
| 🔔 | **提醒通知** | agent 回合完成 / 提问等待回答 / 需要审批时(页面不在前台)弹浏览器系统通知,标题带场景前缀;可开关 + 授权按钮,默认开 |

## 📦 安装

插件**已发布到 npm**(`@hpyperry/dsh-cajita`),直接按包名安装即可:

> 包名 `@hpyperry/dsh-cajita` 是 npm 安装/卸载身份;插件注册进 dsh 的
> **layer id 为 `cajita`**(不带 `dsh-` 前缀,与 ref-lib 同模式:
> `@hpyperry/dsh-ref-lib` ↔ layer id `ref-lib`)。

```bash
# 从 npm 安装(推荐锁版本)
dsh plugin --profile web add @hpyperry/dsh-cajita@0.1.5
# 或装最新标签
dsh plugin --profile web add @hpyperry/dsh-cajita@latest
```

> npm 包自带构建产物(lib/),无需源码构建/授权;想要最新开发版或未发布改动时,
> 可用本地 link 形态安装源码目录(见下)。

**升级插件的小坑**:pnpm 在“已装版本仍满足当前声明范围”(如声明 `^0.1.4`、新出
0.1.5)时,`add <包>@latest` 会被判定为无需改动(no-op)。要真正升级请**显式指定
新版本**(`…add @hpyperry/dsh-cajita@0.1.5`),或在 profile 里跑
`pnpm update @hpyperry/dsh-cajita --latest`。

安装后**重启生效**:`dsh --profile web`。

- **本地开发期**(改动即时生效,link 形态):`dsh plugin --profile web add /path/to/dsh-cajita`
- **卸载**:`dsh plugin --profile web remove @hpyperry/dsh-cajita`

### 发布(维护者)

```bash
cd /path/to/dsh-cajita
# 升版本(npm version patch 等)→ 构建并发布
npm publish        # prepare 自动跑 tsc + tsdown 产出 lib/
```

scoped 公开包所需配置(`publishConfig.access: public`)已在 package.json 内;
发布前请确认 `npm whoami` 已登录(账号需与 `@hpyperry` scope 一致)。

## 📝 编辑消息(任务 1)

### 行为

1. 每条用户消息(含运行中插入的 steering 消息)的气泡下方,复制按钮旁出现「编辑」按钮;
2. 点击后原消息位置**覆盖**一条与官方输入框同宽、同款视觉的内联编辑卡(ChatGPT 式),
   文本域带出原消息文本(已发送的序列化文本),可直接继续编写;
3. 点「发送」或按 Enter(Shift+Enter 换行、Esc 取消)即发送一条新消息——内容为
   编辑后的文本,与输入框正常发送**无任何差别**(同一 input machine → 同一
   adjudication → 同一 sink)。

> 编辑框为**简单文本编辑**(与 ChatGPT 一致,不含 `/` 命令与 `@` 引用补全——官方
> 未提供第三方输入框的可插拔补全 API;需要完整 `/` `@` 能力时,可把编辑文本
> `setDraft` 进主输入框继续编辑,见下「边界」)。

### 边界与异常处理

| 边界 | 处理 |
| --- | --- |
| 长文本(几万字) | 内联编辑框文本域是**纯本地 state**,不进输入机器、不跑装饰扫描,一次性回填/持续输入无 hang 风险;仅发送那一刻一次性 `setDraft`(O(n) 对账,毫秒级) |
| 原消息含 `/` | 已发送文本是序列化后的文本;重新发送走同一判定:`/未知命令` 仍按普通消息发送(行为一致);若编辑后以**已知命令**开头,发送会执行命令——与输入框手动输入完全一致(官方行为),编辑卡内以提示明示 |
| 原消息含 `@` | `@引用` 发送时已序列化为模型形态,回填后按**字面文本**发送(内容与气泡显示一致);编辑框为简单文本,无补全菜单(与 ChatGPT 一致) |
| 输入框已有未发送草稿 | 发送将替换输入框内容(发送成功后被清空),编辑卡内以提示明示 |
| 忙碌/会话移除 | adjudicating/submitting 或会话已移除时禁用发送 |
| UI 规范 | 全部使用官方 primitives 与 `--dsw-*` 设计令牌,编辑卡视觉与官方输入框一致(同款背景/圆角/阴影/宽度上限) |

## ⚙️ 插件设置(设置 → 插件配置 → cajita 工具箱)

插件级设置收成一张与官方插件配置**同风格的可展开卡片**,三个即写开关
(写 `$DSH_HOME/settings.yaml` 的 `cajita:` 节,**改完即时生效,无需重启**):

1. **编辑消息**(默认开):开启 = 用户消息气泡提供「编辑」按钮;关闭 = 立即恢复官方
   气泡显示,随时可再开;
2. **web_fetch 内网放行**(默认关):关闭 = 与官方完全一致(只允许公网目标);开启 =
   放行解析到私网/回环/内网(如 TUN fake-ip)的目标——**TUN/代理用户需要开启**。
   web_fetch 不设单独的"子功能启停":这个放行开关本身就是该功能;
3. **提醒通知**(默认开):agent 回合完成、弹出提问或需要审批时,页面不在前台则弹
   浏览器系统通知,标题带场景前缀(完成 / 提问 / 待审批 / 出错);首次使用先点
   「授权系统通知」。自己正盯着页时不打扰。

## 🌐 web_fetch 内网放行(任务 2 / web-fetch-policy)

### 为什么需要它

官方 `web_fetch` 只允许访问**公网**目标:解析结果里出现私网/回环/fake-ip(如 TUN
代理常见的 198.18.0.0/15 或内网 DNS 回包)会整组拒绝。TUN / 代理模式下 DNS 常被
劫持成非公网地址,导致 web_fetch 全部失败。官方没有关闭这项检查的开关。

### 行为

- **默认关 = 与官方行为一致**(只允许公网目标,安全不缩水);
- **打开开关 = 放行内网**:可抓取解析到私网/回环/内网地址的目标;
- 开关**即点即生效,无需重启**;模型侧 `web_fetch` 用法完全不变。

### 边界与异常处理

| 边界 | 处理 |
| --- | --- |
| 开关默认值 | **默认关**(与官方一致,仅公网);TUN/内网用户到设置里打开一次即持久化 |
| 关闭语义 | 关闭 = 官方等价行为;没有"真卸载式"的运行时开关——彻底回滚官方行为需卸载插件并重启 |
| 装卸 | 卸载插件即恢复官方行为(回滚干净);装卸后需重启 `dsh web` |
| 设置迁移 | v0.1.2 的 `cajita-web-fetch:` 节已并入 `cajita:` 节(v0.1.3);旧节残留无害,可手工删除 |
| 其它 profile | 本插件面向 `--profile web`;装进没有相关组件的 profile 时开关卡片不展示、无副作用 |
| 设置不可写 | 命名空间未提供 / 宿主只读时卡片整体隐藏或禁用(与官方卡片一致) |

## 🧑‍💻 开发

```bash
pnpm typecheck   # tsc --noEmit
pnpm lint        # eslint(typescript-eslint recommended)+ prettier
pnpm test        # vitest(L0 纯函数 / L1 装载)
pnpm build       # tsc(node half)+ tsdown(client bundle)→ lib/
```

- **结构**:`src/`(node half:插件身份 + web-fetch-policy 行)+ `src/client/`
  (web half:`index.ts` 编排「绑定设置 → TOOLS 动态注册 → 注册设置卡片」+
  `locales.ts` 汇总;每个功能独立目录 `src/client/<tool>/`,自带 register/组件/
  纯逻辑/字典片段/样式标签;插件级设置表面在 `src/client/settings/`)+ `tests/`
  (按功能子目录);
- **单插件多工具**:所有小工具共用一个插件包与 `cajita` 文案/设置命名空间;新功能 =
  新目录 + 在 `src/client/index.ts` 的 `TOOLS` 注册表登记(见 `AGENTS.md` §6.2);
- **热更新**:`src/client/*` 改动 `pnpm build:client` 后浏览器 ≤0.5s 自动热更新;
  node half 改动需重启 `dsh web`。

### 隔离开发环境(scripts/dev-isolate.sh)

所有开发/联调一律在独立 `DSH_HOME`(默认 `~/.dsh-dev`)进行,真实 `~/.dsh`
**零接触**(2026-08-17 事故教训:直接在真实 profile 联调可能污染真实会话日志);
`rm -rf ~/.dsh-dev` 即完全重置。

| 命令 | 说明 |
| --- | --- |
| `./scripts/dev-isolate.sh` | 首次运行自动把本插件装入隔离 profile(默认 `~/.dsh-dev`)并启动 web |
| `DEV_HOME=/tmp/dsh-dev ./scripts/dev-isolate.sh` | 自定义隔离 home |
| `PLUGIN=/path/to/other-plugin ./scripts/dev-isolate.sh` | 隔离其他插件 |
| `DEV_HOME=/tmp/dsh-dev ./scripts/dev-isolate.sh --port 3090` | 透传任意 dsh 参数(如 `--port`) |
| `DSH_BIN=<绝对路径> ./scripts/dev-isolate.sh` | 用**指定版本**的 dsh 可执行文件启动(多版本矩阵,见下) |
| `DSH_HOME=$HOME/.dsh-dev dsh plugin --profile web add <插件路径>` | 手动安装/重装插件(本地路径 = link 开发形态) |
| `DSH_HOME=$HOME/.dsh-dev dsh --profile web --port 3090 --no-open` | 手动启动隔离 web(`--port` 指定端口,`--no-open` 不自动开浏览器) |
| `DSH_HOME=$HOME/.dsh-dev dsh plugin --profile web remove @hpyperry/dsh-cajita` | 卸载插件 |
| `rm -rf $HOME/.dsh-dev` | 完全重置开发环境 |

#### 用指定版本的 dsh 测试(多版本兼容矩阵)

插件的依赖基线需要与宿主 dsh 版本族配对。默认 `dev-isolate.sh` 用 PATH 上的全局
`dsh`;要验证「本插件实现在 dsh 0.1.2-rc.1(当前 `latest`/`next`)上是否兼容」,
用 `scripts/dsh-local.sh` 取一个**版本化本地安装**(装进 `~/.dsh-tools/<版本>/`,
与 npm 全局安装完全无关),再经 `DSH_BIN` 指给隔离环境:

```bash
# 例:在隔离环境用 dsh 0.1.2-rc.1 验证本插件(首次自动安装该版本 CLI)
DSH_BIN="$(DSH_VERSION=0.1.2-rc.1 ./scripts/dsh-local.sh)" \
  DEV_HOME="$HOME/.dsh-dev-rc1" \
  ./scripts/dev-isolate.sh --port 3091
```

- 宿主 `@deepseek-ai/dsh` 的 npm dist-tag `latest`/`next` 均已是 `0.1.2-rc.1`:
  任何无 pin 的全局安装都会升级生产宿主,安装脚本/验证一律显式 pin 版本;
- 不同版本用独立 `DEV_HOME`(如 `~/.dsh-dev-rc1`)与 profile,互不干扰;
  依赖基线升级的破坏面/迁移结论记录在 `docs/upgrade-dsh-*.md`。
- 提示:若全局 npm 缓存目录(`~/.npm`)存在 root 属主文件导致 `npm install` 报
  `EPERM`,可先 `npm cache verify` 修复,或为 dsh-local.sh 指定可写的
  `npm_config_cache` 环境变量。

- **常用端口**:`--port 3090`,浏览器访问 `http://127.0.0.1:3090`;
- **验证加载**:启动日志(stdout)会列出装载的插件与 client bundle;也可
  `curl -s http://127.0.0.1:3090/ | head` 确认 web 在服务;client 注册是否生效
  以浏览器 UI 为准(编辑按钮出现在用户消息复制按钮旁);

## ⚠️ 已知限制

- **接管式渲染**:为在用户消息复制按钮旁加按钮,本插件 shadow 官方 `user`/`steering`
  气泡渲染器(priority -1,数值最低者渲染)。官方气泡视觉升级时需手动同步(快照结构变化由 typecheck
  兜底);同 key 的第三方接管者按 shadow 链数值低者渲染;**接管条目随「编辑消息」
  开关运行时增删**——关闭即 dispose、官方气泡即时恢复;
- **运行时"关闭 ≠ 卸载"(web_fetch)**:关闭放行 = provider 锁定限制模式(行为与
  官方逐字节等价,官方行仍被组合禁用);真回滚官方行需 `dsh plugin remove` 并重启;
- **设置节迁移**:v0.1.2 的 `cajita-web-fetch:` 设置节已并入 `cajita:` 节;
  旧节残留无害,可手工从 `settings.yaml` 删除;
- **系统通知受浏览器限制**:需用户授权一次(设置卡内按钮);标签页需保持打开
  (可后台/最小化);`http://127.0.0.1` 这类本地地址是安全上下文可用,局域网
  IP / `0.0.0.0` / 非 https 部署不可用;同 origin 多标签页会各自弹通知(官方无跨
  标签协调)。
- **简单文本编辑**:内联编辑框不含 `/` 命令与 `@` 引用补全(官方未提供第三方输入框
  的可插拔补全 API;官方输入框的补全落地硬接线到 composer 机器)。需要完整 `/` `@`
  能力时,可把编辑文本 `setDraft` 进主输入框继续编辑后发送;
- **不编辑图片消息的图片**:编辑框只编辑文本部分(图片随原消息保留在气泡中,编辑后
  的新消息仅含文本);
- **不重写已发送消息**:发送的是**一条新消息**(与需求一致),历史消息不变。

## 📄 License

[MIT](LICENSE),与 DeepSeek Harness 一致。

## 📚 更多

- 项目开发约定(开发规范 / 测试标准 / 开发环境):见仓库 `AGENTS.md`
- 上游仓库:<https://github.com/hpyperry/dsh-cajita>

Install

dsh plugin --profile web add github:hpyperry/dsh-cajita#aefdb662aedd32e2f7d87c6e6002517c4f8c97a3

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