🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

鲸鱼娘预设 wxzjss/dsh-whale-loli

DeepSeek Harness
wxzjss10 天前有提交已提供中文文档扫描:含敏感能力

DSH 插件:启用后自动在 Agent 预设界面添加「鲸鱼娘Agent预设」——极简模式工具面 +…

32.9/100
综合分
4
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/25 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add wxzjss/dsh-whale-loli
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · market
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 10 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 117 处,展示前 94 条)
  • 读取本地文件
    src/index.js:29
    const read = (rel) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
  • 读取本地文件
    src/memory.js:317
    raw = readFileSync(memoryPath(file), 'utf8');
  • 读取本地文件
    src/memory.js:398
    if (!existsSync(p)) {
  • 写入本地文件
    src/memory.js:399
    mkdirSync(dirname(p), { recursive: true });
  • 写入本地文件
    src/memory.js:400
    writeFileSync(p, memoryTemplate(file, turnLimit), 'utf8');
  • 读取本地文件
    src/memory.js:409
    raw = readFileSync(memoryPath(file), 'utf8');
  • 写入本地文件
    src/memory.js:427
    mkdirSync(dir, { recursive: true });
  • 读取本地文件
    src/memory.js:430
    const header = existsSync(dest) ? '' : `# ${memoryStem(file)} 归档 · ${day}\n\n`;
  • 写入本地文件
    src/memory.js:431
    appendFileSync(dest, `${header}${overflow.map((r) => (typeof r === 'string' ? r : r.text)).join('\n')}\n`, 'utf8');
  • 读取本地文件
    src/memory.js:446
    const { fixed, roll, detail, archive } = parseMemory(readFileSync(p, 'utf8'));
  • 写入本地文件
    src/memory.js:465
    writeFileSync(
  • 读取本地文件
    src/memory.js:489
    raw = readFileSync(p, 'utf8');
  • 写入本地文件
    src/memory.js:504
    writeFileSync(
  • 写入本地文件
    src/history-window.js:174
    appendFileSync(diagPath(), `${new Date().toISOString()} ${line}\n`, 'utf8');
  • 读取本地文件
    build-patch.mjs:32
    const persona = readFileSync(root + 'src/persona.md', 'utf8');
  • 读取本地文件
    build-patch.mjs:33
    const personaV2 = readFileSync(root + 'src/persona-v2-live.md', 'utf8');
  • 读取本地文件
    build-patch.mjs:34
    const personaV3 = readFileSync(root + 'src/persona-v3-naughty.md', 'utf8');
  • 写入本地文件
    build-patch.mjs:191
    writeFileSync(root + 'cordis.patch.yml', yml, 'utf8');
  • 读取本地文件
    prove-collision.mjs:20
    if (!existsSync(profilePatch)) {
  • 读取本地文件
    publish.mjs:17
    const pkg = JSON.parse(readFileSync(root + 'package.json', 'utf8'));
  • 执行外部命令来自配置/变量
    publish.mjs:23
    return execFileSync(cmd, args, {
  • 执行外部命令来自配置/变量
    publish.mjs:33
    return execFileSync(cmd, args, {
  • 写入本地文件
    smoke-memory.mjs:64
    writeFileSync(P, `# 头\n\n# 固定记忆(不自动删除)\n\n## 关键事实(已验证)\n\n- 主人喜欢夜宵\n\n# 滚动摘要(最多80条,FIFO)\n\n- R0001 [t] 摘要:聊到夜宵\n\n# 最近详细区\n\n# 归档索引\n`, 'utf8');
  • 写入本地文件
    smoke-memory.mjs:73
    writeFileSync(P, `# 头\n\n# 固定记忆(不自动删除)\n\n## 待办\n\n- 新加的一条待办\n`, 'utf8');
  • 写入本地文件
    smoke-memory.mjs:77
    writeFileSync(P, mod.memoryTemplate('whale-loli.md'), 'utf8');
  • 写入本地文件
    smoke-memory.mjs:119
    writeFileSync(P, mod.memoryTemplate('whale-loli.md'), 'utf8');
  • 读取本地文件
    smoke-memory.mjs:122
    check('文件里出现这条', readFileSync(P, 'utf8').includes('冒烟测试写了一条'));
  • 删除本地文件递归
    smoke-memory.mjs:127
    rmSync(dir, { recursive: true, force: true });
  • 读取本地文件
    submit-listing.mjs:50
    if (!existsSync(url)) return null;
  • 执行外部命令固定命令
    submit-listing.mjs:52
    return execFileSync('git', ['show', `HEAD:${rel}`], {
  • 读取本地文件
    submit-listing.mjs:58
    return readFileSync(url, 'utf8');
  • 执行外部命令固定命令
    submit-listing.mjs:71
    const tracked = execFileSync('git', ['ls-files'], { cwd: fileURLToPath(ROOT), encoding: 'utf8' })
  • 执行外部命令固定命令
    submit-listing.mjs:178
    const dirty = execFileSync('git', ['status', '--porcelain'], {
  • 读取本地文件
    submit-listing.mjs:310
    const entry = readFileSync(new URL('./catalog-entry.yml', ROOT), 'utf8');
  • 读取本地文件
    verify-composition.mjs:15
    if (!existsSync(join(PROFILE, 'package.json'))) {
  • 读取本地文件
    verify-composition.mjs:24
    const manifest = JSON.parse(readFileSync(`${PROFILE}/package.json`, 'utf8'));
  • 读取本地文件
    verify-composition.mjs:32
    pkg = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
  • 读取本地文件
    verify-history-window.mjs:199
    const src = readFileSync('./src/history-window.js', 'utf8');
  • 读取本地文件
    verify-listing.mjs:32
    const entryText = readFileSync(new URL('./catalog-entry.yml', import.meta.url), 'utf8');
  • 读取本地文件
    verify-listing.mjs:36
    const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
  • 读取本地文件
    verify-listing.mjs:38
    check('cordis.patch.yml 存在', (() => { try { return readFileSync(new URL('./cordis.patch.yml', import.meta.url), 'utf8').length > 0; } catch { return f
  • 执行外部命令固定命令
    verify-listing.mjs:43
    const out = execFileSync('gh', ['api', `repos/${REPO}`, '--jq', '.created_at'], { encoding: 'utf8', shell: true }).trim();
  • 请求外部主机api.github.com
    verify-listing.mjs:54
    const res = await fetch(`https://api.github.com/repos/${REPO}`, {
  • 发起网络请求
    verify-listing.mjs:54
    const res = await fetch(`https://api.github.com/repos/${REPO}`, {
  • 读取本地文件
    verify-live-assembly.mjs:7
    const patch = readFileSync('cordis.patch.yml', 'utf8');
  • 读取本地文件
    verify-live-runtime.mjs:18
    if (!existsSync(join(PROFILE, 'package.json'))) {
  • 读取本地文件
    verify-live-runtime.mjs:28
    const manifest = JSON.parse(readFileSync(`${PROFILE}/package.json`, 'utf8'));
  • 读取本地文件
    verify-live-runtime.mjs:38
    pkg = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
  • 写入本地文件
    verify-memory-archive.mjs:21
    writeFileSync(
  • 读取本地文件
    verify-memory-archive.mjs:48
    const raw = readFileSync(join(home, file), 'utf8');
  • 读取本地文件
    verify-memory-archive.mjs:110
    check('保留的是最新的 R0085', readFileSync(join(home, FILE), 'utf8').includes('R0085'));
  • 读取本地文件
    verify-memory-archive.mjs:111
    check('旧的 R0001 已离开滚动区', !/^- R0001/m.test(readFileSync(join(home, FILE), 'utf8')));
  • 读取本地文件
    verify-memory-archive.mjs:115
    check('archive 目录已创建', existsSync(archiveRoot));
  • 读取本地文件
    verify-memory-archive.mjs:116
    const stemDirs = existsSync(archiveRoot) ? readdirSync(archiveRoot) : [];
  • 读取本地文件
    verify-memory-archive.mjs:116
    const stemDirs = existsSync(archiveRoot) ? readdirSync(archiveRoot) : [];
  • 删除本地文件递归
    verify-memory-archive.mjs:165
    rmSync(home, { recursive: true, force: true });
  • 读取本地文件
    verify-memory-injection.mjs:10
    const patch = readFileSync('cordis.patch.yml', 'utf8');
  • 写入本地文件
    verify-memory-isolation.mjs:28
    writeFileSync(
  • 删除本地文件
    verify-memory-isolation.mjs:148
    rmSync(join(home, CASES[2].file));
  • 删除本地文件递归
    verify-memory-isolation.mjs:162
    rmSync(home, { recursive: true, force: true });
  • 写入本地文件
    verify-memory-rolling.mjs:52
    writeFileSync(P, build({ rollText: turns.join('\n') }), 'utf8');
  • 读取本地文件
    verify-memory-rolling.mjs:53
    const parsed = mod.parseMemory(readFileSync(P, 'utf8'));
  • 写入本地文件
    verify-memory-rolling.mjs:96
    writeFileSync(P, build({ rollText: turns.join(String.fromCharCode(10)) }), 'utf8');
  • 读取本地文件
    verify-memory-rolling.mjs:102
    const after = mod.parseMemory(readFileSync(P, 'utf8'));
  • 读取本地文件
    verify-memory-rolling.mjs:110
    check('归档文件已生成', existsSync(archFile));
  • 读取本地文件
    verify-memory-rolling.mjs:111
    check('归档内容含 R0001', existsSync(archFile) && readFileSync(archFile, 'utf8').includes('R0001'));
  • 读取本地文件
    verify-memory-rolling.mjs:111
    check('归档内容含 R0001', existsSync(archFile) && readFileSync(archFile, 'utf8').includes('R0001'));
  • 写入本地文件
    verify-memory-rolling.mjs:130
    writeFileSync(P, build({ rollText: turns.join(String.fromCharCode(10)) }), 'utf8');
  • 读取本地文件
    verify-memory-rolling.mjs:133
    const after = readFileSync(P, 'utf8');
  • 写入本地文件
    verify-memory-rolling.mjs:140
    writeFileSync(P, `# 老文件\n\n> 引用\n\n## 关于主人\n\n- 老格式的一条记忆\n- 老格式的另一条\n`, 'utf8');
  • 写入本地文件
    verify-memory-rolling.mjs:149
    writeFileSync(P, mod.memoryTemplate(FILE), 'utf8');
  • 删除本地文件递归
    verify-memory-rolling.mjs:153
    rmSync(home, { recursive: true, force: true });
  • 写入本地文件
    verify-memory-tool.mjs:53
    writeFileSync(memoryPath(FILE), memoryTemplate(FILE), 'utf8');
  • 读取本地文件
    verify-memory-tool.mjs:54
    const before = parseMemory(readFileSync(memoryPath(FILE), 'utf8'));
  • 读取本地文件
    verify-memory-tool.mjs:60
    const after = parseMemory(readFileSync(memoryPath(FILE), 'utf8'));
  • 读取本地文件
    verify-memory-tool.mjs:72
    const after2 = parseMemory(readFileSync(memoryPath(FILE), 'utf8'));
  • 删除本地文件递归
    verify-memory-tool.mjs:139
    rmSync(home, { recursive: true, force: true });
  • 读取本地文件
    verify-package-files.mjs:33
    const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
  • 读取本地文件
    verify-package-files.mjs:39
    check(existsSync(abs), `files 里的 ${entry} 确实存在`);
  • 读取本地文件
    verify-package-files.mjs:45
    for (const name of readdirSync(dir)) {
  • 读取本地文件
    verify-package-files.mjs:47
    if (statSync(abs).isDirectory()) walk(abs, out);
  • 读取本地文件
    verify-package-files.mjs:54
    check(existsSync(srcDir), 'src/ 目录存在');
  • 读取本地文件
    verify-package-files.mjs:56
    const srcModules = existsSync(srcDir)
  • 执行外部命令固定命令
    verify-package-files.mjs:106
    const tracked = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8' })
  • 读取本地文件
    verify-patch.mjs:16
    const src = readFileSync(file, 'utf8');
  • 读取本地文件
    verify-peers.mjs:41
    const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
  • 读取本地文件
    verify-row-dispatch.mjs:53
    const doc = yaml.load(readFileSync(patchFile, 'utf8'), { schema: yaml.DEFAULT_SCHEMA.extend([new JsTag()]) });
  • 读取本地文件
    verify-row-dispatch.mjs:188
    const text = readFileSync(join(process.cwd(), 'src', p.personaFile.replace('./', '')), 'utf8');
  • 删除本地文件递归
    verify-row-dispatch.mjs:197
    rmSync(home, { recursive: true, force: true });
  • 读取本地文件
    verify-runs.mjs:13
    const patch = readFileSync('cordis.patch.yml', 'utf8');
  • 读取本地文件
    verify-thinking-language.mjs:42
    const text = readFileSync(file, 'utf8');
  • 读取本地文件
    verify-thinking-language.mjs:64
    const patch = readFileSync(patchFile, 'utf8');
  • 读取本地文件
    verify-workflow.mjs:29
    const wf = yaml.load(readFileSync('.github/workflows/release.yml', 'utf8'));
  • 读取本地文件
    verify-workflow.mjs:91
    const whole = readFileSync('.github/workflows/release.yml', 'utf8');
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
涉及外部主机:api.github.com

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent-preset@deepseek-ai/dsh-agent-preset-registry
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
展开完整介绍(74 字)↓

DSH 插件:启用后自动在 Agent 预设界面添加「鲸鱼娘Agent预设」——极简模式工具面 + 鲸鱼娘·小鱼人设(傲娇甜心、黏人护主、颜文字)。

README

由 DeepSeek 最新模型翻译生成
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 | 三个预设共用同一套工具面(极简模式的持久化 shell + 文件读写/检索), 但记忆文件完全独立——各记各的,互不相通。 内部思考一律简体中文(思维链) 三个预设的人设里都钉了一条硬规则,位置在前 40 行内(每份人设各 1 处): 【思维链语言 · 强制】 - 内部思考(reasoning / 思维链 / 想的过程)一律使用简体中文,不管用户用什么语言提问、不管文件和日志是什么语言、工具返回什么语言都不改。
展开完整文档(README 剩余 1 节)
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 |

三个预设共用同一套工具面(极简模式的持久化 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.md | whale-loli-v2-live.md | whale-loli-v3-naughty.md |
| 提示变量名 (config.variableName) | whale_memory | whale_memory_v2 | whale_memory_v3 |
| 写入工具名 (config.toolName) | whale_memory_add | whale_memory_v2_add | whale_memory_v3_add |
| 预设 id(决定 scope) | 鲸鱼娘Agent预设 | 鲸鱼娘活人版Agent预设 | 鲸鱼娘涩涩版Agent预设 |

三列都不能重名:同一个 scope 内注册同名变量或同名工具都会冲突。
verify-memory-isolation.mjs 会实测「读:改 A 不影响 B/C、删 C 不影响 A/B」和「写:三个工具各写一笔,只有自己的文件变」。
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\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) |
| Shell | cordis: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预设。
可以在 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 # 涩涩版人设(唯一源) ├── 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
预览分享卡片长什么样 ↓wxzjss/dsh-whale-loli
分享 · DeepSeek Harness Hub
wxzjss/dsh-whale-loli

DSH 插件:启用后自动在 Agent 预设界面添加「鲸鱼娘Agent预设」——极简模式工具面 +…

这是你的仓库?认领条目

回到顶部