记忆是文件,不是向量库
记忆落在 $DSH_HOME$DSH_HOME/bot/memory/ 下的 .md 文件树,设置页里能看、能改、能从磁盘重新导入。根文件(MEMORY.md / SOUL.md / USER.md / MEMORY-CORE.md)常驻上下文,子目录只在提示里贡献名字与一句话描述 —— 分层本身就是策略,移动一个文件就是改一层。
|
每条记忆带来源
条目记录它来自 user(对方亲口说的)、agent(它自己的结论)还是 tool(从文件或网页读到的)。一条保持自己标签的说法有用;同一条话穿上事实的衣服就是隐患。 旧数据默认按 agent 读 —— 那是更保守的读法。
|
敏感信息分两道门
硬门:身份证号、社保号、银行卡号、密码密钥、会话令牌 —— 无论设置怎么调都拒绝,并告诉你该怎么绕(用 bot_secret,而不是写下来)。软门:健康、财务、法律、家庭私事 —— 默认排除,改一个设置就能记,记了会一直带着敏感标签。
|
记忆有时效
每条记忆算得出「多少天前」。超过设定天数(默认 90)的条目被标成陈旧并在提示里注明。算不出时间时用 -1 表示未知,而不假装它很新 —— 未知不参与比较,这是刻意的。
|
审批是一等的任务状态
它想动工作区外面的东西时会停下来,任务进入 awaiting —— 那是合法的中间态,不是错误分支。停下时把工具名和完整参数摆给你看:你批准的是一个具体动作,不是「它想干点什么」。批准之后重跑不会再拦你(那是「这一次可以」,不是「我问过一次了」)。
|
一键豁免,但带过期
同一个动作反复问会把人磨钝,于是可以授一张通行证。通行证只匹配完全相同的工具名,而且一定带过期时间 —— 没有「永久允许」这个选项,因为没人能替未来的自己签字。改任务标题或备注会撤销批准:你当初批准的是当时读到的那个任务。
|
自主时间靠理由驱动
默认关闭,打开后空闲只是前置条件,不是理由本身。必须有一个可检查的条件成立:队列里有自己的活(默认)、有没回应过的外部消息、有到点的日程、工作区里有比自己上次看过更新的文件。找不到理由就明确报告「空闲够了,但没有任何符合条件的理由」,而不是被悄悄填满活动。
|
它自己的权限分三档
只读 / 完全权限 / 仅对话,逐个实例设置。自主时间的权限默认是只读 —— 主动行为拿不到写的能力,除非你明确给。工具集跟着权限走:只读的实例连写文件那一半工具都看不到。
|
MCP 是客户端,不是硬编码集成
插件不带任何 MCP 服务、不带地址、不带凭据 —— 它带一个客户端和一个输入框。你填一个 streamable HTTP 地址,那边的工具就出现在它能用的工具里。工具名沿用 mcp__服务器__工具 的通行约定,从别的客户端复制过来即可用。测试是个按钮,结果记在旁边的状态灯上。
|
状态灯三态,不撒谎
每个服务旁边的圆点:绿=真的测过一次并且通过,红=测过一次并且失败,灰=还没测过。「没人看过」和「它没问题」不是同一个说法,所以灯不合并这两种。绿点下还留着上次成功和上次失败两个时刻 —— 「14:02 还能用、14:31 停了」和「它从来没好过」是两种处境。
|
周期工作两种语义
循环任务可选续跑同一上下文(带上上次结论,例行公事要的就是这个)或另起独立任务(从零开始,审计要的是这个)。理由是:先读自己上次的结论,一个检查就悄悄变成了橡皮图章。
|
入口不止一个
入站连接把外面的消息带进来:Telegram 长轮询、或一个带令牌的 HTTP 端点。外部消息会被标明来源并限长(默认 4000 字,可调),因为「谁说的」和「说了多久」都是判断的一部分。
|
限额全部由用户定
没有任何写死的配额。每次召回几条、提示里回放多少轮、记忆总量、单次任务分钟数、每天多少分钟、入站消息上限、并发数与轮询间隔 —— 全在设置页里,默认值集中在一处可查。0 在这些项里一律表示不限。
|
头像与类型都能自己来
内置四种类型(伴侣 / 助手 / 研究员 / 记录员),名字、说明、人设都能改,也可以自己建。头像可以上传自己的图片(PNG / JPEG / WebP / GIF,4 MB 以内),按内容类型校验而不是看文件名。
|
## 为什么选它
- **它不是一个更长的会话**:常见的做法是把上下文做长,而这里是把状态搬到会话外面。会话是暂时的,实体是持续的 —— 两者不是同一件事,混起来会同时得到两者的缺点。
- **可治理优先于聪明**:记忆是文件、来源是标签、限额是设置、审批是状态。每一条都让「它为什么这样」有一个能查的答案,而不是一个需要相信的结论。
- **危险的事做成可见的**:界面上每一步都能看到它在请求什么、上次是什么时候好的、这条记忆是谁说的。**最危险的不是它会做错事,而是它悄悄做对了错事。**
## 工具参考
| 工具 | 用途 | 碰外部 |
| --- | --- | --- |
| `bot_status` | 实例清单、心跳、队列、当前生效的模型与来源 | – |
| `bot_remember` | 记一条持久条目(`text` + 可选 `kind` / `source`) | – |
| `bot_recall` | 按词检索记忆(中文按双字切分;可按类型筛选) | – |
| `bot_task` | 任务队列:排队 / 指定时刻 / 循环 / 续跑或另起 / 取消 | – |
| `bot_connector` | 让实例经用户的连接对外说话 | ✅ |
| `bot_secret` | 把凭据送进虚拟桌面(值不进上下文) | ✅ |
| `bot_agenda` | 记下一个时刻该做什么,到点经连接器提醒 | ✅ |
> 「碰外部」指该工具会让数据离开本机 —— 前四个只读写 `$DSH_HOME$DSH_HOME/bot/`。
### 记忆写在哪
`bot_remember` 按类型落到不同文件:`focus` 进 `USER.md`(用户在意什么)、`decision` / `fact` 进 `MEMORY-CORE.md`、其余进 `notes/YYYY-MM.md`。**这不是实现细节,是策略**:常驻上下文的那几份只放最能改变行为的,其余按需检索。
### 它会怎么读自己的记忆
根文件全文进系统提示;子目录里的每个文件只贡献 `name` 和 `description` 两行 frontmatter。所以**给一份笔记写一句好的 description,比给它写长正文更有用** —— 那决定它会不会想起来去读。
## 配置
插件通过 `cordis.patch.yml` 挂载**一行**(以裸包名注册)。用户可改的东西几乎都在设置页,不在这个文件里:
| 位置 | 内容 |
| --- | --- |
| `$DSH_HOME$DSH_HOME/bot/bot.json` | 实例、类型、记忆索引、任务、连接、MCP 服务、限额、规则 |
| `$DSH_HOME$DSH_HOME/bot/memory/` | 记忆本体(`.md` 文件树,可直接编辑) |
| `$DSH_HOME$DSH_HOME/bot/avatars/` | 上传的头像文件 |
设置页分这些栏目:实例与类型 · 记忆 · 任务与队列 · 自主时间 · 限额 · 规则 · 连接 · MCP 服务 · 头像 · 日程。
**限额项**(全部有默认值,全部可改,`0` 一律表示不限):
| 设置 | 默认 | 说明 |
| --- | --- | --- |
| 单次任务上限 | 30 分钟 | 一次后台任务最多跑多久 |
| 每天合计 | 不限 | 所有后台任务当天总时长 |
| 每任务工具轮数 | 8 | 一次任务里模型能连续调用几次工具 |
| 任务总数 | 300 | 队列保留多少条,超出丢最早的 |
| 提示里回放的消息数 | 12 | 系统提示带多少条历史消息 |
| 面板一次加载 | 60 | 界面上一次拉多少条消息 |
| 可检索记忆条数 | 200 | 索引上限;**记忆文件本身永不被裁掉** |
| 入站消息上限 | 4000 字 | 超出截断并注明来源 |
| 记忆陈旧阈值 | 90 天 | 超过标记为陈旧 |
| 敏感信息模式 | 排除 | `排除` / `记`(硬门不受影响) |
**唯一两个有上下界的**是自主时间的并发数与轮询间隔 —— 它们是机制参数而不是配额,所以给了范围而不是「不限」。
## 工作原理
```
会话侧(模型) 宿主侧(插件) 磁盘
bot_* 工具 ──→ ctx.tools 注册的处理器 ──→ $DSH_HOME$DSH_HOME/bot/bot.json
│ └─ memory/*.md
├─→ 执行器(可选,默认关) └─ avatars/
│ └─ 任务队列 → 一次模型会话 → 工具调用
├─→ 审批闸门(工作区边界判定)
└─→ 入站连接(Telegram 长轮询 / HTTP 端点)
Web 侧(浏览器) 同一条 HTTP 路由
侧边栏条目 ──→ /api/bot.state(只读轮询,3 秒)
bot 页面 ──→ /api/bot.manage(写操作)
设置页 ──→ 同一组路由
```
- **两半都在一个包里**:`impl.js` 是宿主(store、执行器、工具、路由),`client.js` 是 Web 半(侧边栏、页面、设置)。客户端那半通过 `window.__ModuleLoader__.load()` 注册,不 import 任何 `@deepseek-ai/dsh-client-*` 包;
- **一次轮询喂所有界面**:侧边栏的每个条目和页面订阅同一次 `/api/bot.state`,所以它们不会各说各话;
- **工作区边界是可判定的**:`workspaceVerdict(toolName, args, workspace)` 是纯函数,返回 `inside` / `guarded` / `refused` / `unchecked`。**`unchecked` 是其中最重要的一个** —— 「查过了,没问题」和「这里没东西可查」必须在代码里分得开,否则前者会冒充后者。
## 环境要求
- DeepSeek Harness(dsh),已安装 `web` profile
- Node.js ≥ 22.19
- 一个可用的模型(插件默认跟随 DSH 的默认模型,无需单独配置)
### 验证过的版本
| 组件 | 版本 |
| --- | --- |
| DeepSeek Harness(dsh) | `0.2.0-rc.2`(peer 声明 `>=0.2.0-rc.1 <0.3.0`) |
| Node.js | `22.20.0` |
| 操作系统 | Windows(10.0.26200) |
| dsh-bot | `1.0.0` |
> **当前仅在 Windows 环境实测**(macOS / Linux 未验证,暂不承诺)。宿主侧代码没有平台相关的分支,但没跑过就是没跑过。
## 已知限制
- **没有远程执行**:执行器跑在宿主进程里,机器关了就停。这是本机插件的结构性限制,不是待办 —— 「端休眠也照跑」需要云端,而这一版没有。
- **没有向量检索**:记忆全部按文件分层进上下文,没有「容量超阈值自动切检索」。在当前规模下,引入 RAG 只会多一个更不可解释的层;代价是它有规模上限。
- **没有浏览器集成**:DSH 自带的浏览器插件是会话级的,不是这个常驻实体的一部分。
- **没有语音、没有跨设备**:本机单点。
- **没有专属邮箱入口**:有 Telegram 与 HTTP 入站,但没有「转发邮件即派活」这种零成本入口。
- **自主时间的「逐条提示」只做了一半**:敏感信息默认是被**拒绝**而不是被**询问**;「不回溯」在实现层面成立(改设置不会改动已有条目),但界面上没有把这句话说出来。
- **MCP 只实现了客户端,而且是最小面**:`initialize` / `tools/list` / `tools/call` 三件事。没有 `resources`、没有 `prompts`、没有订阅、没有 OAuth 流程(需要认证的服务请把 token 填进请求头字段)。长耗时任务没有走官方的 `tasks` 扩展。
- **只支持 streamable HTTP 的 MCP 传输**:stdio 的服务需要用户自己套一层 HTTP。
- **入站连接的注入防御是限长与标注,不是内容过滤**:公开记录对检测式防御很不客气(多数在攻击者适应后被绕过 90% 以上)。所以这里选了持续有效的两条:**限制大小、标明来源**。
- **审批的豁免只匹配完全相同的工具名**:改一个参数就不算命中。这是保守的一侧,代价是同一类操作换个参数会再问一次。
- **`bot_secret` 依赖虚拟桌面插件**(`dsh-vdesktop`):没装时该工具会明确报错,而不是静默失败。
- **没有现成的「会话内联操作」**:所有管理都在设置页,聊天里只能通过 `bot_*` 工具。
## 开发
这个仓库是纯 JavaScript,没有构建步骤 —— `impl.js` / `client.js` / `entry.js` 就是运行的东西。
```sh
# 离线验证套件(不需要 dsh 在跑)
node verify-bot-plugin.mjs
# 客户端接线检查(改了界面之后跑)
node scripts/check-client-wiring.mjs
# 对照调研报告,回归审计「已实现 / 未实现」
node scripts/audit-against-research.mjs
```
代码结构:
| 文件 | 职责 |
| --- | --- |
| `entry.js` | 薄壳。`stat()` 实现文件的 mtime 并把它编进 import URL,于是改 `impl.js` 不必重启宿主。**它导出的 `inject` 才是宿主读的那一份**(见下) |
| `impl.js` | 宿主全部逻辑:store、记忆文件层、工作区判定、执行器、审批、连接、MCP 客户端、路由、7 个工具 |
| `client.js` | Web 全部:侧边栏条目、bot 页面、设置页。自带样式,不依赖任何客户端包的内部类名 |
| `scripts/` | 两个检查脚本 + 一个可跑的 MCP 示范服务 + 虚拟桌面探针 |
| `tests/` | 端到端验证(HTTP 路由 + store 语义 + 工具行为) |
> **一个踩过的坑**:Cordis 读的是 **entry 模块**导出的 `inject`,不是实现文件里的那一份。两者不一致时,插件会在缺服务的情况下启动,然后在第一次用到时报「cannot get property … without inject」。`entry.js` 与 `impl.js` 的 `inject` 必须逐字一致,验证套件里有一条断言守着这件事。
## 更新记录
> 完整明细见 [CHANGELOG.md](CHANGELOG.md)。本插件还没有版本化发布,下面是开发轮次。
| 轮次 | 内容 |
| --- | --- |
| 第一轮 | **落地第一版**:store、实例与类型、任务队列、执行器、侧边栏条目、bot 页面、设置页 |
| 第二轮 | **记忆改成文件**:`.md` 文件树 + frontmatter、根文件常驻、子目录只贡献描述、「从文件重新导入」 |
| 第三轮 | **可治理性**:来源标签(user / agent / tool)、时效与陈旧标记、敏感信息两道门、硬门永不放开 |
| 第四轮 | **审批链条**:`awaiting` 一等状态、停下时展示工具与完整参数、waiver 带过期、**修掉「批准后重跑再次拦下」的死循环** |
| 第五轮 | **限额全部可调**:去掉所有写死的配额,默认值收敛到一处;UI 去掉一切非交互元素的边框 |
| 第六轮 | **自主时间改为理由驱动**:空闲降为前置条件,4 个可检查条件,找不到理由就明说 |
| 第七轮 | **MCP 客户端**:streamable HTTP + 惰性工具名、用户自填地址、测试按钮与三态状态灯;用独立进程的真 server 端到端验证 |
| 第八轮 | **周期工作两种语义**:`continue` 带上次结论 / `fresh` 从零开始 |
| 第九轮 | **界面收口**:侧边栏条目宽度与右对齐修正(`:has()` 撑开两层)、输入框改成与 DSH 同形的卡片(共用同一根宽度轴)、头像可由用户上传、类型可编辑 |
## 特别感谢
特别感谢 [DeepSeek Harness 原始仓库](https://github.com/deepseek-ai/deepseek-harness) 与 DeepSeek AI 团队:本插件的插件体系、工具运行时与模型接口都构建在这个项目之上。
同时感谢 [Cordis](https://github.com/cordiverse/cordis) 提供的插件化基础。
本插件的**调研工作**参考了 2026 年主流个人助理与 Agent 产品的公开文档,逐条对照结果见[功能对照](docs/feature-matrix.md)。
## License
本项目遵循 [MIT License](LICENSE)。
> 本项目是 DeepSeek Harness 的社区插件,并非 DeepSeek 官方产品。