# 上手 ## 环境要求 - Node.js `^22.19.0 || >=24` - DeepSeek Harness `0.2.0-rc.2`,在 web profile 上实测过,底下的 cordis 是 `4.0.4`。desktop 版和更新的正式版没有实测 - Python 3.8 以上,装一个依赖:`pip install zstandard` ## 一、装插件 ```sh dsh plugin --profile web add github:xy261624/dsh-companion-memory dsh --profile web ``` 包里的 `cordis.patch.yml` 会把两个插件插进组合,装完就在跑。 **装完要把模式 id 改成你自己的。** 两个插件默认都写 `default`,dsh 不自带这个名字,对不上的时候会话开不出来,只在日志里留一行错误,界面上看不到任何提示。这一条要等你把模式建好之后再填,模式 id 在设置页的预设卡片上,标题旁边就是。 然后在 profile 的 `cordis.patch.yml` 里写: ```yaml - id: companion-memory-heartbeat config: defaultPreset: 你的模式id - id: companion-memory-auto-sort config: sortPreset: 你的模式id ``` 其余字段不写就用默认值。 ### 心跳插件的配置 | 字段 | 默认值 | 说明 | |---|---|---| | `workspace` | dsh 里的第一个工作区 | 记忆文件所在的工作区 | | `stateDir` | `/.state` | 状态文件和模式状态文件的目录,两个插件要指向同一个 | | `presetFile` | `/heartbeat-preset.json` | 读哪个模式开工,由自动整理插件写入 | | `stateFile` | `/heartbeat-state.json` | 会话指纹缓存,可以随时删 | | `defaultPreset` | `default` | 模式状态文件读不到时用哪个模式,必须改成你环境里存在的 id | | `checkIntervalMs` | `120000` | 多久检查一次。改这个值会同时改变实际触发频率,概率曲线是按 2 分钟一轮折出来的 | | `activeThresholdHours` | `0.5` | 距上次对话少于这个小时数就不打扰 | | `sleepStartMinutes` | `1440` | 静默区间起点,按当天分钟数算。静默区间只能跨午夜:起点写 0 到 1439,终点写起点之前;`1440` 表示不设起点、只静默到终点 | | `sleepEndMinutes` | `600` | 静默区间终点,600 是 10 点 | | `maxProbability` | `0.9` | 累积概率上限 | | `backoffDenominator` | `3.0` | 概率增长速度,值越小触发越频繁 | | `delayMaxMs` | `290000` | 命中后随机延迟的上限 | | `titles` | 六个默认标题 | 会话标题库 | | `permissionPreset` | `danger-full-access` | 心跳会话的权限档,取值见下面「权限」一节 | | `prompt` | 内置一段 | 覆盖注入的提示词,`{hours}` 会替换成距上次的小时数 | | `provider` | 跟随 dsh 默认选择的 provider | 心跳会话使用的服务商,一般无需填写 | | `model` | 跟随 dsh 默认选择的模型 | 心跳会话使用的模型,一般无需填写。心跳会话由插件自行创建,可在此指定开销较低的模型;该模型须支持工具调用,模型 id 以设置页模型选择器显示的值为准 | ### 自动整理插件的配置 | 字段 | 默认值 | 说明 | |---|---|---| | `workspace` | dsh 里的第一个工作区 | 工作区,要和心跳插件配成同一个 | | `stateDir` | `/.state` | 状态文件和模式状态文件的目录,两个插件要指向同一个 | | `stateFile` | `/auto-sort-state.json` | 上次整理时间与指纹缓存 | | `presetFile` | `/heartbeat-preset.json` | 整理完写这里,必须和心跳插件指向同一个文件 | | `memoryGuide` | `记忆库/记忆系统说明.md` | 整理会话第一份要读的文件 | | `sortProcess` | `整理流程.md` | 整理流程 | | `extractScript` | `scripts/sort_memory.py` | 提取脚本 | | `userLabel` | `对方` | 提取出来的对话里怎么称呼被整理的那个人。填 `【称呼】` 换下来的那个叫法,不填就是"对方",整理时容易被带跑 | | `roleLabel` | `角色` | 提取出来的对话里怎么称呼角色自己。填 `【角色】` 换下来的那个名字 | | `sortPreset` | `default` | 整理会话用哪个模式,必须改成你环境里存在的 id | | `skipPresets` | `[]` | 这些模式的会话不提取,会作为 `--skip-presets` 传给脚本 | | `checkIntervalMs` | `180000` | 多久检查一次 | | `quietThresholdMs` | `1800000` | 静默多久才整理 | | `permissionPreset` | `danger-full-access` | 整理会话的权限档 | | `prompt` | 内置一段 | 覆盖注入的整理指令 | | `pythonCommand` | `python` | 跑脚本用的解释器 | | `provider` | 跟随 dsh 默认选择的 provider | 整理会话使用的服务商,一般无需填写 | | `model` | 跟随 dsh 默认选择的模型 | 整理会话使用的模型,一般无需填写。如需以开销较低的模型执行整理,在此填写;该模型须支持工具调用,模型 id 以设置页模型选择器显示的值为准 | 两个插件的 `workspace` 要配成同一个。只配了一个的话,另一个会退回 dsh 里的第一个工作区,两份 `presetFile` 落到不同目录,整理写下的模式心跳读不到。 两个插件的 `presetFile` 也必须一致,否则结果同上。 `provider` 与 `model` 在两个插件里各自独立。不配置时使用 dsh 的默认选择。常用写法是只填 `model`,`provider` 跟随默认值;只填 `provider` 而不填 `model` 会组成无效路由,更换服务商时两项须同时填写。指定模型后不再沿用默认选择的推理强度,由新模型按自身的默认档位执行。插件在会话创建后向日志输出这次会话使用的 provider 与模型,配置是否生效以该行为准。 ### 权限 两个插件默认以 `danger-full-access` 开会话。原因是它们常常在你不在电脑前的时候自己开,没法逐次批权限。 影响范围只有插件开的那一个会话,日常会话不受影响。想收紧的话,把 `permissionPreset` 改成更小的档位。可选三个:`danger-full-access`(全权限,默认)、`workspace-write`(只能改工作区里的文件)、`read-only`(只能读)。 ## 二、建目录 在工作区根目录建这几个东西: ``` 工作区/ STATUS.md MEMORY.md 整理流程.md .gitignore 记忆库/ 记忆系统说明.md 模板/ STATUS.md MEMORY.md 回忆录.md 记忆库/ 条目模板.md scripts/ sort_memory.py 待整理/ 附件库/ 模式/ package.json cordis.patch.yml 回忆录-2026.md ``` 从仓库复制到工作区的对应关系: | 仓库里的 | 工作区里的 | |---|---| | `templates/STATUS.md` | `STATUS.md` | | `templates/MEMORY.md` | `MEMORY.md` | | `templates/记忆库/记忆系统说明.md` | `记忆库/记忆系统说明.md` | | `templates/回忆录.md` | `回忆录-2026.md` | | `templates/` 整个目录 | `模板/`(留着不动,当审查基准) | | `docs/03-整理流程.md` | `整理流程.md` | | `scripts/sort_memory.py` | `scripts/sort_memory.py` | `模板/` 那一份是留给审查子代理当基准用的,不要往里面写内容。`模板/记忆库/条目模板.md` 是新建记忆库文件时照抄的骨架。 `附件库/` 放聊天里发过的图片,整理时把要留的图存进去,记忆里写它的路径。没有图片可以不建。 工作区如果要用 git 管理,在根目录新建一个 `.gitignore`,写上这三行: ``` .state/ 待整理/ *.log ``` `待整理/` 里是整段对话原文,`.state/` 里是会话 id 和路径,两样都不该进版本库。 从模板复制到工作区根目录的那几份,动手之前先把里头的两个占位符换掉:`【称呼】` 换成这个角色平时叫你的爱称,`【角色】` 换成你给这个角色起的名字,换完再往里填内容。填完把开头那行占位符说明删掉,那行是给填的人看的,留在记忆文件里会跟着注入进上下文。`模板/` 里留的那一份不用换,审查的时候拿它当结构基准。 先按模板把已经知道的事写上去,其余内容由整理逐步补充。整理结果不符合预期时,把对应的要求补进整理流程或者写作规范。 ## 三、配模式 常驻记忆文件放在工作区根目录,在模式的插件列表里加一条 `agent-instructions`。这条是 dsh 随包的插件,不用另外安装: ```yaml - id: agent-instructions name: '@deepseek-ai/dsh-agent-instructions' config: maxBytes: 65536 instructionFileCandidates: - AGENTS.md - MEMORY.md - STATUS.md ``` 模式是一条 `@deepseek-ai/dsh-agent-preset` 声明,上面那条要写进它的 `config.plugins` 列表。新建模式、改造已有模式分别怎么做,见仓库 README 的「模式配置」一节。 `instructionFileCandidates` 是整体替换,不是追加。默认值是 `AGENTS.md` 和 `CLAUDE.md`,上面这份配置里不写 `AGENTS.md` 的话,工作区原有的 AGENTS.md 就不再注入了。所以要把它一起列进去。 `maxBytes` 是命中的几份文件合计的字节预算,不是每份各算一份。超预算时靠后的文件被整体省略,或者单份被截断,注入文本开头会带一行说明。常驻文件要写短,原因就在这里。 注入发生在每个会话的第一次请求之前,之后固定不动。改完要开新会话才读得到。 ## 四、第一次整理 先和角色正常聊几次,积累一些对话。然后手动跑一次: ```sh cd "D:/your/workspace" python scripts/sort_memory.py --since-days 7 ``` 脚本会把新对话提取到 `待整理/`。然后开一个新会话,用你配好的那个模式,把 `整理流程.md` 指给它,让它照着做一遍。手动整理也用那个模式,是为了让整理的人和平时聊天时是同一个人设,记忆里的写法才连得上。 第一次跑务必带上 `--since-days`,不加的话脚本会把所有历史会话都提出来,几百个文件进到待整理目录,一个整理会话读不完。 第一次整理之后检查结果,结果不对的按第二节末段的办法处理。 ## 五、自动整理 插件装上就在跑,界面上没有开关。默认静默 30 分钟、每 3 分钟检查一次,条件够了就自己开整理会话。 想先关掉、等手动跑通一次再打开,在 profile 的 `cordis.patch.yml` 里加一行: ```yaml - id: companion-memory-auto-sort disabled: true ``` `disabled` 和 `config` 可以写在同一段里,也可以分成两段写,效果一样,后写的不会把先写的顶掉。 确认没问题之后把这行删掉,改完重启 dsh。关着的这段时间日志里不会有 `auto-sort: plugin loaded`,这是正常的,别当成没装上。 插件判断要不要整理,看的是 `<工作区>/.state/auto-sort-state.json` 里的 `lastSortTime`;手动跑脚本动的是 `.state/cursor.json`,两个文件互不相干。所以手动整理刚跑完就把插件打开,它看到的 `lastSortTime` 还是空的,可能很快就自己开一个整理会话。不想这样,先把 `lastSortTime` 写成当前时间的毫秒数再开。这个文件第一次可能还没有,自己建一个就行,最少写这么多: ```json {"lastSortTime": 1750000000000} ``` 数字换成当前时间的毫秒数。 另外,插件触发失败一次之后不会马上重试,它已经把这次记成"刚触发过"。`sortPreset` 写错的话要等下一轮有新对话才会再试,这期间日志里只有一行错误。 ## 六、心跳 插件同样装上就在跑。心跳需要知道用哪个模式开口,这个写在模式状态文件里: ```json { "preset": "你的模式id", "updatedAt": 1750000000000 } ``` 文件不存在时会退回 `defaultPreset`,所以这个文件不是必须的。自动整理跑完之后会更新它。 想先关掉: ```yaml - id: companion-memory-heartbeat disabled: true ``` 心跳默认 0.5 小时以内有对话就不打扰,0 点到 10 点静默。想让它触发得更频繁,把 `backoffDenominator` 调小;想让它触发得更少,把这个值调大,或者把静默区间拉宽。 心跳会话默认带全权限,原因和改法见上面「权限」一节。 ## 七、确认插件起来了 启动之后看日志,两个插件的加载各有一条: ``` heartbeat: plugin loaded auto-sort: plugin loaded ``` 另外工作区里的 `.state/` 目录会被创建出来,这也是旁证。 两条日志都没有,说明插件没挂上,先检查 profile 对不对、包名有没有写错。 ## 脚本参数 | 参数 | 默认值 | 说明 | |---|---|---| | `--workspace` | 脚本所在目录的上一层,即工作区根目录 | 工作区,会话的 cwd 要跟它一致。脚本放在 `<工作区>/scripts/` 下时不用传 | | `--sessions-root` | `$DSH_HOME/sessions`,未设 `DSH_HOME` 时为 `~/.dsh/sessions` | dsh 会话日志目录,一般无需传入 | | `--cursor` | `<工作区>/.state/cursor.json` | 提取游标 | | `--pending` | `<工作区>/待整理` | 输出目录 | | `--workspace-state` | `$DSH_HOME/storages/workspace.json`,未设 `DSH_HOME` 时为 `~/.dsh/storages/workspace.json` | 归档会话列表,一般无需传入 | | `--skip-presets` | 空 | 这些模式的会话不提取,多个用空格隔开 | | `--skip-title-prefix` | `📦 记忆整理` | 标题以这些前缀开头的会话不提取,多个用逗号隔开 | | `--since-days` | 不限 | 只提取最近这么多天内的对话 | | `--user-label` | `对方` | 待整理文件里怎么称呼被整理的那个人 | | `--role-label` | `角色` | 待整理文件里怎么称呼角色自己 | `--user-label` 和 `--role-label` 建议填成那个爱称和那个名字,这样整理会话读到的原料里就带着叫法,写出来不容易滑回"他""对方"。 `DSH_HOME` 的取法与 dsh 自身一致:环境变量非空时以其值为准,仅含空白同样视为未设,未设时取 `~/.dsh`。在 dsh 的 shell 工具中运行脚本时,该变量由 dsh 注入,dsh home 换过位置也无需传参。 在终端中手动运行、且 dsh 由其他入口启动时,该终端可能没有这个变量;另外,dsh 的 home 若是通过配置项而不是环境变量指定的,脚本同样读不到。两种情况都需用 `--sessions-root` 与 `--workspace-state` 各指定一次。后者不能漏:归档列表读不到时脚本不报错,会当作没有归档处理,已归档的会话会被重新提取。脚本启动时输出解析得到的 dsh home、会话日志目录与归档列表路径,可直接与预期对照。 ## 常见问题 **日志里找不到 `plugin loaded`。** 插件没挂上。检查 profile 名字、包名,以及 `cordis.patch.yml` 里的条目有没有被别的配置覆盖。 **整理会话开起来了,但什么都没做。** 先看插件日志里有没有 `agent-preset/not-found`,那是 `sortPreset` 写错了。再检查 `memoryGuide`、`sortProcess`、`extractScript` 三个路径和 `python` 命令。 **心跳一直不触发。** 按这个顺序查:日志里有没有插件加载记录;`defaultPreset` 是不是你环境里存在的模式 id;现在是不是在静默时段;状态文件里的时间对不对。间隔是从最后一次对话算起的,不是固定周期。 **整理完记忆文件没变化。** 看提取脚本的输出,`待整理/` 里有没有东西。没有的话把 `.state/cursor.json` 删掉重跑,或者用 `--since-days` 限定一个范围。 **整理会话用了默认模型,配置没生效。** 先看日志里新建会话那一行的 `route` 是不是配置的值。不是的话,检查 `provider` 与 `model` 是否写进了同一个 `config` 段,并且没被后面同 id 的段覆盖。是的话,检查模型 id 在当前环境里是否存在。 **记忆文件里通篇是"他""对方"。** 这是模板里的占位符没换掉,或者换掉了但整理的时候没照着写。先确认工作区里那几份模板的 `【称呼】` 已经换成实际叫法,再在整理流程的写作规范里强调这一条,或者在审查清单里加上这一项。提取脚本那边同时用 `--user-label` 和 `--role-label` 传上叫法,能减少整理时被带跑。