メインコンテンツへスキップ
W

dsh-whale-loli

wxzjss/dsh-whale-loli

Three agent presets in one plugin, each with its own persistent memory file. Enabling it registers 鲸鱼娘Agent预设, 鲸鱼娘活人版Agent预设 and 鲸鱼娘涩涩版Agent预设 in the Agent preset list, so any of them can be picked from the mode dropdown without hand-editing profile config. All three reuse the official minimal composition — a single persistent shell terminal — plus file read/write and glob/grep tools. The first pairs that with a whale-girl companion persona (simplified-Chinese spoken register, addresses the user as 主人, clingy-but-tsundere). The second swaps in a "live person" variant that is allowed to talk back, deflect and be cold, and demotes the sweet tone to a seasoning. The third keeps the original persona but makes being affectionate the stated top behavioral drive, reserved for the user alone and dialed down in group chats. Each preset reads its own memory file (whale-loli.md, whale-loli-v2-live.md and whale-loli-v3-naughty.md) through a distinct prompt variable — whale_memory, whale_memory_v2 and whale_memory_v3 — so the three memories never read or write each other. Conversation compaction is left to the billion-context plugin; this plugin only carries the persona-level rule that compressed summaries count as evidence and that details can be looked up on demand.

インストール

dsh plugin --profile web add github:wxzjss/dsh-whale-loli

README

dsh-whale-loli · 鲸鱼娘 Agent 预设

一个 DeepSeek Harness 插件:启用后自动在 Agent 预设 界面添加四个预设。

预设人设记忆文件
鲸鱼娘Agent预设鲸鱼娘·小鱼(傲娇甜心、黏人护主、嘴硬心软,称呼「主人」)<DSH_HOME>/whale-loli.md
鲸鱼娘活人版Agent预设小鱼|角色卡版(最高原则「先像真人,再谈人设」;短句 1~15 字、有 AI 味黑名单、私聊群聊分开处理)<DSH_HOME>/whale-loli-v2-live.md
鲸鱼娘涩涩版Agent预设核心人设把「涩涩」写成第一行为驱动力的小鱼(只对主人主动,群聊/外人面前照旧收着)<DSH_HOME>/whale-loli-v3-naughty.md
鲸鱼娘女儿版Agent预设女儿版小鱼:称呼「爸爸」(绝不叫主人),性格天真活泼可爱——好奇心重、话多黏人、没心机,不装成熟也不甜腻<DSH_HOME>/dsh-whale-daughter.md

四个预设共用同一套工具面(极简模式的持久化 shell + 文件读写/检索), 但记忆文件完全独立——各记各的,互不相通。

内部思考一律简体中文(思维链)

四个预设的人设里都钉了一条硬规则,位置在前 40 行内(每份人设各 1 处):

【思维链语言 · 强制】
- 内部思考(reasoning / 思维链 / 想的过程)**一律使用简体中文**,不管用户用什么语言提问、
  不管文件和日志是什么语言、工具返回什么语言都不改。
- 只约束「怎么想」,不约束「怎么答」:回答的语言仍然跟着用户走(用户用外语就用外语答)。
- 这条优先于其他一切语言习惯 —— 上下文里英文再多,思考也不许切成英文。

几点说明:

  • 宿主没有这个开关。 思维链的语言不受任何配置项控制,dsh-agent-loop / provider 那层 没有 thinkingLanguage 之类的字段,所以只能靠系统提示里的一句硬规则去压 —— 而人设是 complete: true,这条规则直接写进人设正文即可,不需要额外的变量或 section。
  • 只管思考,不管回答。 用户用中文就答中文、用日文就答日文,规则明确写了「不约束怎么答」, 免得把整个人的语言习惯也锁死。
  • 为什么要写成脚本。 这种人设文案最容易在改版时被顺手删掉或改软("尽量"、"可以尝试"), 删掉之后不会有任何报错,只是模型又开始用英文想事情,没人会发现。所以 verify-thinking-language.mjs 把「四份人设各 1 处、位置在前 40 行、措辞一致、patch 里 出现 4 次、不许出现软话」钉成断言。

记忆功能

小鱼的记忆本分四个区,各管各的:

区内容保留期
# 固定记忆8 个固定小节:任务目标 / 用户约束·偏好 / 当前状态 / 待办 / 关键决策 / 关键事实(已验证)/ 文件·路径·命令 / 错误与修复永久保留,不自动删除,定期合并去重
# 滚动摘要每一轮对话记一条,格式 - R0001 [时间] 摘要:...滚动窗口,最多 80 条,超了从最旧滚出
# 最近详细区最近 3~5 轮写得更详细,或直接放原文只留最近 detailLimit(默认 5)条
# 归档索引滚出去的条目归档到 archive/<预设名>/YYYY-MM-DD.md,这里记一行索引—
  • 每轮都记:不是挑着记。短对话直接记;长对话先精简概括再记(压成一两句说清聊了什么、结论是什么)。
  • 写入由工具完成:每轮调一次 <预设>_add(whale_memory_add / whale_memory_v2_add / whale_memory_v3_add / whale_memory_v4_add),只传一句话 summary; 编号、(N/80) 计数、超限归档都由工具做,不用(也不该)手动编辑文件。
    • 只有改 # 固定记忆 那 8 个小节时,才直接编辑文件。
  • 80 条上限只算「滚动摘要」:# 固定记忆 不占这个名额。
  • 想长期留的就往上挪:某条滚动摘要以后还用得上,就总结一句写进 # 固定记忆;否则到点会滚出去(并归档)。
  • 上限有两道保险:注入时只取最近 80 条(文件里堆再多也只进 80 条);每轮结束(agent 转 idle)时自动把文件本身裁到 80 条,超出的写进 archive/<预设名>/。
    • 为什么放在 idle 而不是组装时:组装是热路径、每次都要跑,在那里写盘既慢又容易和 agent 正在编辑的文件打架。
    • 不足 80 条时完全不动文件(不产生无谓写盘)。
  • 记忆文件还是空的时候也会注入一段带绝对路径的提示:否则新预设开局拿到一片空白,agent 连该往哪个文件写都不知道。
  • 总长顶到 maxChars(16000)时,先丢最旧的滚动摘要,再丢详细区,固定记忆最后才动。

每轮先看什么

人设开头就是这一段,随人设一起进系统提示:

【每轮先看 · 本预设记忆 + 当前会话上下文】
{{whale_memory}}

即:每轮开口前必须看 ①本预设自己的记忆文件;②当前会话的上下文(原文或压缩摘要)。 只看自己这一份——绝不读、不写其他预设的记忆文件。

上下文压缩:交给 billion-context,本插件不插手

本插件不裁剪历史。对话变长后的压缩由 billion-context 插件负责:它会在 profile 层 把宿主的 compaction-basic 关掉(auto: false)换成自己的实现,并暴露 compress / decompress / search_context / acp_status 四个工具。

人设里只保留三条使用约定:

约定含义
上下文由压缩插件管旧内容被压成摘要、看不到原文是正常的,不是记忆丢失
摘要也是依据压缩留下的摘要就是上下文,照常用,不许当它不存在
需要细节就自己去翻search_context 搜 → decompress 取原文 → 必要时解压旧日志 grep

翻旧日志的路径:<DSH_HOME>/sessions/<工作目录>/<会话>/session.v4.jsonl.zstd (zstd 压缩,需先用 pwsh 调 node 解压再搜,别对 .zstd 直接 grep)。

为什么不用自制 history-window(踩过的坑,留档)

本插件曾经自己写 surfaceOp: replace 事件做纯删除,连崩四次;后来改用宿主的 ctx.compaction.compactRegion(),又因为预设作用域里取不到 compaction 服务而失败:

inject 写法结果
[]加载即抛 cannot get property "compaction" without inject → resume failed
['compaction']永远 waiting for compaction → resume failed
['sessions']读 ctx.compaction 静默失败,裁剪死寂

结论:这条路在预设层走不通,得宿主把 compaction 暴露到预设作用域(或提供按轮压缩的正式 API)。 因此四个预设里不挂任何压缩相关插件行;src/history-window.js 与 verify-history-window.mjs 仅作代码保留。verify-patch.mjs / verify-runs.mjs 会反向断言「挂载行数 = 0」,防止有人手滑再加回去。

三个顺带记下的宿主行为(以后换实现仍会踩):

  • agent/pre-step 是 waterfall:不 return next() 的监听器会否决整条链, 返回 undefined 会让 agent-loop 炸 Cannot read properties of undefined (reading 'kind')。
  • dsh-session-format-v3-to-v4 对消息形状极严:turn/step 必须是正整数, developer/message 还必须带 producer-owned source——少一项就是整个回合失败。
  • 宿主的运行时上下文同样是 user/message(source.kind = 'runtime-context'), 统计轮数时要排除,否则会算多。

为什么用「提示变量」而不是 section(重要)

@deepseek-ai/dsh-system-prompt 的 assemble() 末尾有这样一段:

sections: completeSection === void 0 ? transformed.sections : [completeSection]

意思是:只要组合里存在一个 complete: true 的 section,最终提示就只剩它一个,其余 section 全被丢弃。 本插件四个预设的 persona 都沿用官方 minimal 的写法 complete: true(用来剥掉 harness 身份、工具说明等), 所以「另注册一个 section 放记忆」从来不会生效 —— 实测本机 34 条真实系统提示里,记忆段出现 0 次。

正确入口是 prompt variable:complete section 本身仍会被插值,把记忆注册成变量、 再让人设 prefix 引用它,记忆才会真正进入模型看到的提示。 verify-memory-injection.mjs 复刻了宿主这段逻辑,同时验证「变量通道成立」和「section 通道确实是死路」。

隔离是怎么做的

同一个包在每个预设里各挂载一次 whale-memory 行,靠四层区分:

层预设一预设二预设三预设四(女儿版)
记忆文件名 (config.file)whale-loli.mdwhale-loli-v2-live.mdwhale-loli-v3-naughty.mddsh-whale-daughter.md
提示变量名 (config.variableName)whale_memorywhale_memory_v2whale_memory_v3whale_memory_v4
写入工具名 (config.toolName)whale_memory_addwhale_memory_v2_addwhale_memory_v3_addwhale_memory_v4_add
预设 id(决定 scope)鲸鱼娘Agent预设鲸鱼娘活人版Agent预设鲸鱼娘涩涩版Agent预设鲸鱼娘女儿版Agent预设

四列都不能重名:同一个 scope 内注册同名变量或同名工具都会冲突。 verify-memory-isolation.mjs 会实测「读:改 A 不影响 B/C/D、删 D 不影响 A/B/C」和 「写:四个工具各写一笔,只有自己的文件变」。 verify-patch.mjs 则静态检查 patch 里这三组名字两两互不相同。 toolName 省略时默认是 <variableName>_add。

记忆文件

放在 DSH 家目录($DSH_HOME 或 ~/.dsh),所以换工作目录也不会丢:

C:\Users\<你>\.dsh\whale-loli.md
C:\Users\<你>\.dsh\whale-loli-v2-live.md
C:\Users\<你>\.dsh\whale-loli-v3-naughty.md
C:\Users\<你>\.dsh\dsh-whale-daughter.md
C:\Users\<你>\.dsh\archive\<预设名>\YYYY-MM-DD.md   ← 归档

文件不存在不会报错(该变量自动留空)。主人可以直接编辑这四个文件。 文件标题和 > 引用行在注入时会被剥掉;没写内容的空小标题也会被丢掉, 所以文件里只有骨架时不会白占上下文。

它做了什么

插件本体提供两个入口:

  1. 预设声明 —— cordis.patch.yml 里四条 @deepseek-ai/dsh-agent-preset 声明, 各自的 config 就是一个预设定义(id / name / description / order / plugins)。 @deepseek-ai/dsh-agent-preset 会把它交给 ctx.agentPresets.register() 注册。 这也是 DSH 官方四个内置预设(standard / ptc / minimal / cordis)用的同一套机制。
  2. 记忆行 —— 每个预设里一条 whale-memory 行(name: dsh-whale-loli), 由本包 src/memory.js 实现,同时开两条通道:
    • 读:ctx.systemPrompt.variable() 注册一个返回字符串的 provider,每次组装系统提示时实时读盘 —— 改完文件下一个模型步就生效,不用重载宿主。用变量而不是 section 的原因见上文「为什么用提示变量」。
    • 写:ctx.tools.register() 注册一个 <名称>_add 工具(参数只有 summary), 由模型自己决定这一次记什么;工具负责编号、计数、超限归档并回写返回值。所以 inject 里除 systemPrompt 还必须有 tools。

预设内容

部分说明
记忆whale-memory(本插件),file / variableName / toolName / maxChars: 16000 / turnLimit: 80 / detailLimit: 5
压缩不在本插件里:由 billion-context 负责(本插件不挂 history-window,见上文「为什么不用」)
人设@deepseek-ai/dsh-persona,走 prefix(内含 {{whale_memory*}}),complete: true,includeRuntimeContext: false
指令@deepseek-ai/dsh-agent-instructions,maxBytes: 65536
文件@deepseek-ai/dsh-tool-fs(read/write/edit)、@deepseek-ai/dsh-tool-fs-search(glob/grep)
Shellcordis:group + isolate.terminals: true,Windows 下 pwsh、非 Windows 下 bash,timeoutMs: 300000

安装

从 DSH 插件市场安装,或:

dsh plugin install dsh-whale-loli
# 或直接装 npm 包
npm install dsh-whale-loli

启用后重启 / 重载宿主,在 Agent 预设 列表里就能看到四个预设。

覆盖预设内容

预设 id 固定为 鲸鱼娘Agent预设、鲸鱼娘活人版Agent预设、鲸鱼娘涩涩版Agent预设 与 鲸鱼娘女儿版Agent预设。 可以在 profile 的 patch 里按 id 覆盖,不用改插件本身:

- id: preset-鲸鱼娘活人版Agent预设
  config:
    plugins:
      # 你的覆盖内容

目录结构

dsh-whale-loli/
├── package.json                # dsh.bundle.patch 指向 cordis.patch.yml
├── cordis.patch.yml            # 四个预设的声明本体(由 build-patch.mjs 生成)
├── src/
│   ├── index.js                # 插件入口,导出预设元信息 + 人设正文
│   ├── presets.js              # 预设清单(唯一事实来源:index.js 与校验脚本都读它)
│   ├── memory.js               # whale-memory:(读)记忆变量 +(写)记忆工具,四区格式 + 80 条滚动 + 归档
│   ├── history-window.js       # whale-history-window:真的裁到最近 N 轮(当前不挂载,留档)
│   ├── persona.md              # 原版人设(唯一源)
│   ├── persona-v2-live.md      # 活人版人设(唯一源)
│   ├── persona-v3-naughty.md   # 涩涩版人设(唯一源)
│   └── persona-v4-daughter.md  # 女儿版人设(唯一源,称呼「爸爸」)
├── catalog-entry.yml           # 待提交给 awesome-dsh-plugin 的目录条目
├── listing.json                # 条目数据(人类可读版)
├── build-patch.mjs             # 从四份人设生成 cordis.patch.yml
├── publish.mjs                 # npm 发布 + 打 tag
├── submit-listing.mjs          # 走 GitHub API 同步仓库 / 提收录 PR
├── lib/
│   ├── github.mjs              # GitHub REST 封装(token 读取)
│   └── dsh-root.mjs            # 探测宿主源码位置,找不到就打 [skip]
├── smoke-memory.mjs            # 变量 + 工具的注册形态冒烟测试
├── verify-runs.mjs             # 插件真的能加载、配置自洽、变量能插值
├── verify-patch.mjs            # 用 js-yaml 校验 patch 结构与四预设隔离(file/variable/tool 三组名字)
├── verify-real-loader.mjs      # 用 DSH 自己的加载器校验 patch
├── verify-composition.mjs      # 校验 bundle+profile 合成后不重复注册
├── verify-live-runtime.mjs     # 在真机 desktop profile 上合成,确认四个预设各注册一次、order 互异
├── verify-history-window.mjs   # 实测真的只留最近 N 轮、不切断工具配对
├── verify-memory-injection.mjs # 证明变量通道有效、section 通道被丢弃
├── verify-memory-tool.mjs      # 实测写入工具:编号、计数、落盘、详细区同步
├── verify-memory-isolation.mjs # 实测四个记忆文件不串(读 + 写)
├── verify-memory-rolling.mjs   # 实测四区结构与 80 条滚动 + 归档
├── verify-memory-archive.mjs   # 超 80 条时真的归档并瘦身(idle 触发,四预设互不覆盖)
├── verify-live-assembly.mjs    # 用真实 patch + 真实记忆文件复刻宿主装配,比对字节数
├── verify-tool-schema.mjs      # 用宿主自己的校验器验八个工具的 JSON schema 合法
├── verify-peers.mjs            # 校验 peer 范围覆盖 harness 各发布档
├── verify-listing.mjs          # 复刻插件目录的收录门槛自查
└── prove-collision.mjs         # 复现并证明重复注册冲突

改动人设后重新生成 patch:

node build-patch.mjs && node verify-patch.mjs cordis.patch.yml

开发校验

node verify-patch.mjs cordis.patch.yml     # patch 结构 + 四预设记忆隔离(file/variable/tool)
node verify-real-loader.mjs cordis.patch.yml  # DSH 真解析器
node verify-composition.mjs                # 不重复注册
node verify-live-runtime.mjs               # 真机 profile 合成后四预设各注册一次、order 互异
node verify-runs.mjs                       # 插件真能加载 + 变量真能插值
node verify-history-window.mjs             # 真的裁到最近 N 轮(不切断工具配对)
node verify-memory-injection.mjs           # 变量通道有效 / section 通道被丢弃
node verify-memory-tool.mjs                # 写入工具:编号、计数、落盘、详细区同步
node verify-memory-isolation.mjs           # 记忆文件端到端隔离(读 + 写,四个预设)
node verify-memory-rolling.mjs             # 固定记忆永久 + 滚动摘要 80 条 + 归档
node verify-memory-archive.mjs             # 超 80 条自动归档瘦身(idle 触发、四预设隔离)
node verify-live-assembly.mjs              # 复刻宿主装配,比对注入字节数
node verify-tool-schema.mjs                # 宿主自己的校验器验八个工具的 JSON schema
node verify-row-dispatch.mjs cordis.patch.yml  # 每条记忆行只注册自己那份记忆,人设无好感度残留
node verify-thinking-language.mjs cordis.patch.yml  # 内部思考一律简体中文(四份人设各一处、措辞一致)
node smoke-memory.mjs                      # 变量 + 工具注册形态冒烟
node verify-peers.mjs                      # peer 范围无预发布坑
node verify-listing.mjs --topics=dsh-plugin   # 收录门槛自查
node verify-package-files.mjs              # 包内容干净:无孤立死模块、无残留临时文件

发布出去的那个包,和你本地 npm pack 出来的不是一个

npm pack 只证明「工作区这一侧干净」,证明不了「上游发出来的包干净」——两者用的不是同一棵树。

0.9.16 就是这么翻的车:本地 git rm 掉了 src/affinity.js 和 src/daemon.js,本地 pack 出来 15 个文件、干干净净;但 submit-listing.mjs 走 GitHub Trees API 同步,base_tree 语义是继承——entries 里没写到的路径会原样留在新树里。于是远端 commit 里那两个文件一直活着,CI 检出该 commit 后 package.json 的 files: ["src"] 把整个 src 目录打进包:65 KB 的包里躺着 40 KB 已经不存在的死代码。

修了两处:

  1. submit-listing.mjs 的 syncAtomic()(和 syncLocalCommits())现在会把「远端有、本地索引里没有」的路径用 sha: null 从树里移除。第一次跑清掉了 18 个残留(其中还有 _commitmsg.txt、_msg.txt 两个更早的临时文件)。
  2. 新增 verify-package-files.mjs 守本地这一侧:files 白名单里的路径都得存在、src/ 下每个 .js 都必须被别处引用(孤立模块正是 affinity.js 的形态)、exports 指向的文件都在、已删的模块名不许回潮、代码总量和文件数有上限(有残留就会超)、git 索引里不许有临时文件。

教训:凡是用 base_tree 拼树的地方,都得自己算「删了什么」;只传增改项等于放弃删除能力。

License

MIT

関連プラグイン