dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 插件:角色设定与长期记忆(角色库 persona.yml/SOUL.md/USER.md + FACT.md/MEMORY.md + JOURNAL.jsonl),注册在宿主组合层,对所有 agent 预设生效,附右侧栏管理面板。MIT。
DSH 的角色设定 + 长期记忆管理插件。替换掉 0.1.5 之前那套「预设级记忆加载器」 (memory / memory-boot / ptc-memory 三个 agent 预设 + router-standard 内嵌的 memory-loader)——那些只在选中对应预设时才注入,新方案注册在宿主组合里, 对所有 agent 预设的每个会话生效。
· 许可证 · MIT,见 LICENSE 安装规格 · github:hu568/dsh-plugin-persona-memory 实测 DSH · 0.1.5-rc.1 ~ 0.2.0-rc.2 peer 要求 · cordis >=4.0.0-rc <5、@deepseek-ai/dsh-tools、@deepseek-ai/dsh-system-prompt(随 DSH 提供) 运行依赖 · schemastery(别名安装 npm:@deepseek-ai/schemastery@3.18.2,从 npm registry 拉取)
# DSH 内置插件管理器(创造模式):
# plugin_manager action=install_bundle target="github:hu568/dsh-plugin-persona-memory"
# 或命令行。桌面端(Electron)profile 由应用独占管理,请改用插件管理器或「设置 → 插件」:
dsh plugin --profile <profile> add github:hu568/dsh-plugin-persona-memory安装后本包被追加进 profile 的 dsh.profile.bundles,其 cordis.patch.yml 作为组合包层 把 persona-memory 这一行插进宿主组合。profile 在运行中通常即时生效,否则重启一次 DSH。
验证:
dsh --profile <profile> --dump-config # 应出现以本包名为标题的层与 persona-memory 行也可以在 agent 里直接 GET /persona-memory/api/state:返回 200 + ok: true 说明面板路由已注册。
卸载:
dsh plugin --profile <profile> remove @dsh-external/dsh-persona-memory卸载不会删除角色库与记忆文件($DSH_HOME/personas、$DSH_HOME/memory),需要时手动清理。
更新时注意:DSH 通过 profile 依赖差分识别「这次装的是哪个包」。若内容变了但安装规格字符串
没变,会报ambiguous-install;请先remove再add,或让版本号/文件名变化。
平面 · 位置 · 说明 角色设定 · $DSH_HOME/personas//{persona.yml,SOUL.md,USER.md} · 角色库,同一时刻一个「当前角色」 全局记忆 · $DSH_HOME/memory/FACT.md · 跨会话、跨项目的稳定事实;写入硬上限 100 行 / 8000 字节 长期日志 · $DSH_HOME/memory/JOURNAL.jsonl · 一次性事件/会话笔记,仅追加 + 可检索;不注入提示词 项目记忆 · /MEMORY.md · 随项目走;存在才注入,不自动创建;写入硬上限 8000 字符 会话快照 · $DSH_HOME/memory/snapshots/.json · 首条消息时固化的注入内容
注入的东西(提示词段落,顺序紧贴部署人格之后):
memory:soul → `` + 角色编辑契约memory:user → ``memory:facts → `` 全局 + 项目 + 记忆写入契约段落名为什么用 `memory:` 前缀:用户自建的 router-standard 预设会在精简模式里剥掉除 memory:* 外的全部段落。沿用这个前缀,角色与记忆在那种模式下也能存活,不需要改预设里的剥离逻辑。
为什么都设 `interpolate: false`:宿主 @deepseek-ai/dsh-system-prompt 的renderPrompt()会对未标记的段落做{{变量}}插值,而interpolate()对
未注册的名字是抛错(不是原样保留)——它认为「malformed prompt 比响亮地失败更糟」。
本插件的段落内容全是用户可写的自由文本(FACT.md、SOUL.md…),
只要哪篇记忆里写了一句 {{变量}},整个 turn 就会以malformed prompt variable reference "{{变量}}" in section "memory:facts" 失败。因此这些段落一律 interpolate: false:内容原样进提示词,`{{…}}` 不再有特殊含义。JOURNAL.jsonl 是无上限增长的流水账。0.1.x 曾把最近 6 条注入 `` 段落, 0.2.0 起整段移除,代价与收益:
· 0.1.x(注入最近 6 条) · 0.2.0(不注入) 每轮 token · 每轮都为最近 6 条付费,随文件增长 · 0(快照里也不存了) 内容相关性 · 最近几条常常与当前任务无关 · 由模型自己判断要不要查 快照体积 · 随日志增长 · 恒定 取旧笔记 · 自动到手 · 要模型主动 memory action=search
不至于「忘了自己有历史」:记忆契约里留了一行指针,写明日志有多少条、正文未注入, 并明确要求模型在觉得「以前可能聊过 / 踩过坑」时主动 action=search。子代理契约里也有一行。
日志的写与读能力完全保留:memory 工具的 append / search / read、 面板的「日志」标签页、/api/journal/{append,search} 路由都照旧可用。 变的只是「不再自动灌进上下文」。
旧 profile 里若写了journalEnabled/journalInject/journalLimit,删掉即可 ——
schemastery 对未知键是静默保留不抛错,留着也不会让插件起不来(实测)。三个平面各有闸口,都在写入侧卡住(超限抛错拒绝,不落盘,绝不静默截断):
文件 · 闸口 · 默认 · 依据 全局 FACT.md · 行数 AND 字节(UTF-8) · 100 行 / 8000 字节 · 注入每个主会话,最坏开销要可预测 项目 MEMORY.md · 字符 · 8000 字符 · 与注入侧 maxChars 同口径 长期日志 JOURNAL.jsonl · 无上限(有意) · — · 不注入提示词,是流水账的去处
FACT.md 是所有项目、所有主会话都要注入的提示词段落。项目专属结论一旦写进来, 就变成每个项目、每一轮都要付的固定开销,还会把真正「跨项目稳定」的事实挤出去。
只卡行数是不够的:行长完全自由 ⇒ 100 行可以写 7 KB,也可以写 20 KB。 0.4.0 起两个闸口同时生效,任一超限即拒写:
入口 · 行为 memory action=update, scope=global · 行数 > maxFactLines 或 字节 > maxFactBytes ⇒ 抛错拒绝 面板 POST /api/memory/save(scope=global) · 同一道闸口,避免从面板绕开 memory action=read · 报告「当前行数 / 上限,当前字节 / 上限」,已超限时附警告 记忆契约(进提示词) · 事先写明双上限,并要求「本工作区的内容一律写 scope=project」
为什么 8000 字节:本机实测真实文风约 75.9 B/行(4,100 B / 54 行)⇒ 80 B/行 × 100 行 = 8000 B 是贴合真实文风、略留余量的取值。满配约 ≈2,560 tokens (实测换算比 0.3205 tok/字节),把最坏情况钉住。
字节口径:UTF-8(中文 3 字节/字),且先做 \r\n → \n 归一化 (与 countLines() 同口径,不把 \r 算进去)。
0.4.0 之前这里是不对称的:
侧 · 0.3.0 行为 注入侧 · 有闸口 —— maxChars 默认 8000,超出被 clip() 静默截断 写入侧 · 完全没有 —— scope=project 的 update 不校验体量
⇒ 「写的时候不拦,读的时候悄悄截断」:文件里会有一大段内容模型永远看不到, 而写入时毫无警示。这正是本插件明确反对的模式(静默截断等于悄悄丢用户内容), 此前只堵了 global 一侧。0.4.0 补上:
入口 · 行为 memory action=update, scope=project · 字符 > maxProjectChars(默认 8000)⇒ 抛错拒绝,并指向 action=append 面板 POST /api/memory/save(scope=project) · 同一道闸口 memory action=read · 报告项目记忆的「当前字符 / 上限」,已超限时附警告 主会话契约 + 子代理契约 · 都事先写明项目记忆上限
单位用字符,与注入侧 maxChars 逐字同口径(都取 trim() 后的 JS 字符串长度, \r\n 按 2 个字符算、不做归一化)⇒ 「通过写入闸口」即「必然不会被注入侧截断」, 边界正好落在 8000 与 8001 之间。中文按字符算(3000 个汉字 = 3000 字符)。
JOURNAL.jsonl 故意不设上限:它是无上限增长的流水账、且不注入提示词, body 不进上下文 ⇒ 加限制只会逼用户丢历史。要写长内容就往这里写。
段落 · 主会话 · 子代理会话 memory:soul / memory:user(角色) · ✅ · ❌ 不注入 memory:facts → 全局 FACT.md · ✅ · ❌ 不注入 memory:facts → 项目 MEMORY.md · ✅ · ✅ 注入 长期日志 JOURNAL.jsonl · ❌ 一律不注入(0.2.0 起) · ❌
理由:子代理拿到的是一份具体委派任务。角色人设、跨项目全局事实、历史流水账 都与该任务无关,灌进去只会让子代理的上下文变宽、更容易跑偏;项目记忆才是它需要的 那部分(本仓库/本项目的约定与事实)。
判定「是不是子代理」读的是宿主真实写入的 session.header:origin === 'subagent' 优先,另外 parentSession 存在或 delegationDepth > 0 也认(兼容只写血缘的旧日志 与外部 provider)。宁可判多,也不要漏判后把主会话的角色灌进委派任务。
子代理只注入项目记忆时,写入契约也换成项目版:明确告诉它「全局记忆与角色未注入」, 并允许它只读地 action=read / action=search 去查全局记忆,而不是以为不存在。
想恢复旧行为(子代理与主会话注入完全相同)就设 subagentProjectOnly: false。
于是 KV 缓存前缀绝对稳定,会话中途改文件不影响它,新会话才看到更新。 快照里不含日志正文(0.2.0 起),只留一个 journalTotal 计数供契约措辞使用。
snapshots/ 随委派次数膨胀)。两个工具都是全局注册,没有路径参数,走进程内 node:fs(不受文件沙箱策略影响):
memoryaction · 作用 read · 读全局 FACT.md + 当前项目 MEMORY.md + 最近日志(两者都报告「当前体量 / 上限」) update · 整篇覆盖写入。scope=global → FACT.md(超 100 行或 8000 字节直接报错);scope=project → /MEMORY.md(不存在则创建,超 8000 字符直接报错) append · 向 JOURNAL.jsonl 追加一条 {ts,tags,text} search · 按子串(可选 tag)检索日志,新→旧
personaaction · 作用 list / current / show · 列角色 / 看当前角色 / 读某角色三件套 use · 切换当前角色(新会话生效) create / update / delete · 新建 / 改 / 删角色(当前角色不允许直接删)
persona-creator(按访谈流程把一个角色写成三件套 + 质量检查清单)不作为技能注册, 而是作为物料安装到角色库——角色库是模型和人都能看到的地方,指针写在 personas/README.md:
仓库 skills/persona-creator/SKILL.md # 正本,在 git 里维护
│ 安装 = 拷过去(一次性步骤,见下)
▼
角色库 $DSH_HOME/personas/skills/persona-creator/SKILL.md
$DSH_HOME/personas/README.md # 以相对路径 skills/persona-creator/SKILL.md 指向它Copy-Item skills\persona-creator\SKILL.md $env:USERPROFILE.dsh\personas\skills\persona-creator\SKILL.md
package.json 的 files 也不含 `skills`——正本只在仓库,不进 tarball,所以磁盘上只有「仓库源码」和「角色库」两份(lib/index.js 里那段 register 已删,勿再加回)。
persona 工具的描述里写着「角色管理请走 $DSH_HOME\personas」⇒ 模型顺指针进角色库 ⇒读 personas/README.md ⇒ 找到本技能文件。
「角色 / 记忆」标签页(会话标题右侧的「角色」按钮打开):