DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-plugin-life

d

dsh-plugin-life

Manifest valid★ 1

DeepSeek Harness plugin for agent to simulate a real life

hasBundlePatch

dsh-plugin-life

给 DeepSeek Harness 里的角色装一套模拟的生活,以及一套跨会话记忆。

设计目标来自一句话:「这种虚拟感来源于静止」。所以这个插件的全部工作, 就是让 agent 不再停在原地 —— 有作息、有状态、有自己去做的事、有记忆、 会因为被打断而生气,而且记得住你。

  • 她自己会去搜索、抓网页、把读到的内容存成笔记。不是模拟,是真发生。
  • 她有自己的作息:睡、吃、醒。醒着的时候在做事,做完会写感受。
  • 她和你说话时,上下文里会多出两段背景:她自己的生活,和关于你的事。

一、它到底做什么

1. 生理约束的有限状态机

睡 / 吃 / 清醒。睡眠 6–10 小时、两餐间隔 4–11 小时、连续清醒不超过 16 小时 —— 这些是边界,不是日程表。边界内的取值来自历史统计与随机采样。

第 2 天起由前 n 天决定:睡眠时长、醒来时刻、餐点都是前几天的条件采样结果。 跑久了会形成稳定的昼夜节律。

硬编码的只有生理学边界(在 lib/physiology.js 里,一眼能看全)。 其余全部随机 —— 这是刻意的:写死的东西不会让她像活的。

2. 真实活动

清醒时由插件自己真的搜索、真的抓取页面、真的把读到的内容存成笔记文件。

  • 搜索走 ctx.web.search
  • 抓取用 Node 自带的 fetch。不走 ctx.web.fetch —— 某些构建里注册了多个 fetch provider,web.fetch 不知道该用哪个会直接抛 WEB_PROVIDER_AMBIGUOUS, 连会话里的 web_fetch 工具都会跟着不可用。插件不该依赖一个需要全局配置才能工作的通道。
  • 为什么不是"派个后台 agent 去做":实测 ctx.agents.create() 不指定 agent preset 时 只继承根作用域插件,而网络工具挂在 preset 作用域里 —— 活动 agent 只能看到日程、 记忆和文件读写,它自己报告「没有浏览器,没有网络请求」。
  • 抓不到就试下一个:候选比目标多(最多多 2 个),凑够 1–3 个成功就停。

抓取链按"能拿到最多内容"分级,任何网址都能抓,不是白名单:

级别什么时候用得到什么
1. PDF链接是 .pdf 或 arxiv.org/pdf/…论文原文(PyMuPDF 抽文本)
2. 站点接口arXiv / B站 / GitHub / Wikipedia摘要、简介+字幕、README
3. 普通 HTTP其余所有网址HTML 剥标签
4. 无头浏览器前一级内容太薄或返回 403Playwright 渲染后取正文

第 4 级只在前面失败时触发,因为启动浏览器要几秒。

3. 事件库与压缩

每件真发生过的事写进 SQLite,带摘要、经过、感受、两个分数(interesting / important)。

只记录真发生的事:一次活动要入库,必须至少抓到一个 2xx 且正文非空的页面。 搜不到、抓不到、正文为空,一律丢弃重排 —— 写手给出的检索目标不会被当成事件写进去。 目标单独存 intent 列,与 content 分开。

压缩是分级的,不是一刀切:

综合分 = importance·0.6 + 新鲜度·0.4     新鲜度按 7 天半衰期衰减
分数前 10%   完整保留,不计入压缩次数
中间 70%     剥成"标题 + 感受"
最老 20%     只留标题
第 3 次被压缩 → 整行删除

keep = 1(importance ≥ 0.75 的重要事件)不参与压缩,永久保留。

4. 记忆挂载

每轮对话前把记忆挂进上下文,但永远不压过人格,也永远压不过你当前说的话。

挂载是有优先级的,而且是三块不重叠的:

永远记得的:   keep=1 的事件,全部挂载,不设条数上限
记得这些事:   importance ≥ 0.5 的,取 2 条
最近:         最近的,取 2 条

同一件事不会在两块里出现两次。

5. 跨会话记忆:关于你的事

这是和"她自己的生活"分开的一个库(captain.db)。 分开的理由很实际:任务和代码写进生活记忆只会污染它。

每轮对话结束后:

1. 规则闸门判「这一轮有没有关于你的持久信息」      —— 不花模型钱
2. 过了闸门的,把「你说的话 + 她回的话」一起喂给模型,
   压成一句第三人称、≤60 字的概括              —— 压整轮,不是只压你的原句
3. 用这句话的句向量去判重:像就去重,不像就入库

然后按热度决定谁能被挂载:

挂载 = 近 7 天写的  ∪  热度 ≥ 0.5
热度 = 每次"被你再次提到"贡献 exp(-Δt/τ),半衰期 14 天

没有任何事件能一直挂载。 7 天后只有半衰期内被提过的才继续浮着; 超过 14 天没提且热度低于 0.25,正文剥掉沉底 —— 但行和向量留着, 你下次再提,热度回来就又挂上了,只是细节想不起来了。

6. 打断与追赶

DSH 关掉期间的时间会被快进。重启后她说得出「你关掉的三个小时里有一顿饭被错过了」—— 不是空泛的抱怨,是按延迟算出来的严重程度。


二、安装

需要 DeepSeek Harness 已经跑起来,并且装了 dsh-plugin-masterprompt (人格来源;没有它插件也能跑,只是没有后台人格可用)。

# 1. 放到插件的搜索路径下
git clone <this-repo> "%DSH_HOME%/plugins/dsh-plugin-life"

# 2. 装依赖(只有一个 peer)
cd "%DSH_HOME%/plugins/dsh-plugin-life"
npm install

# 3. 重启 DeepSeek Harness

%DSH_HOME% 默认是 C:\Users\<你>\.dsh。 插件的加载靠 cordis.patch.yml,它在仓库根目录,不要改名字。

跨会话记忆使用了embedding模型完成相似任务(可选,不装也能用,只是没有向量):

同一个模型也用来做活动话题查重:这次想做的事和最近 7 天做过的事比相似度, 太像就换方向。所以装了它有两个好处,不装就两个都没有。

# 模型:约 91 MB,从 modelscope 下(huggingface 在国内常常连不上)
python -c "from modelscope.hub.snapshot_download import snapshot_download; \
snapshot_download('AI-ModelScope/bge-small-zh-v1.5', cache_dir=r'C:\Users\<你>\.cache\modelscope')"

默认会到 ~/.cache/modelscope/AI-ModelScope/bge-small-zh-v1___5 找模型, 也可以放在 %DSH_HOME%/dsh-plugin-life/models/bge-small-zh-v1.5。 找不到模型时插件照常工作,只是判重和"提及检测"退化成纯关键词。

python 解释器默认找 E:\anaconda\envs\tts0\python.exe,可以用环境变量 CAPTAIN_PYTHON 指定。


三、依赖

运行时(必需)

依赖用途说明
@deepseek-ai/cordis ^4.0.1插件宿主peer dependency
Node.js ≥ 22node:sqlite用了内置的 DatabaseSync,不装 better-sqlite3

跨会话记忆(可选,但要先装好)

依赖用途
Python 3.12 + PyTorch跑句向量模型
transformers加载 bge-small-zh-v1.5
modelscope下载模型(也可以用 hf-mirror)
bge-small-zh-v1.5512 维中文句向量,24M 参数,CPU 单句约 13 ms

抓取增强(可选,缺了就降级)

依赖用途
Python requests / beautifulsoup4 / lxml普通抓取
Python PyMuPDFPDF 抽文本
Python playwright第 4 级渲染(反爬站点)
Python yt-dlpB 站视频信息

插件启动时会自检这些,结果写进 status.json。探不到就整块降级,不会崩。


四、目录结构

dsh-plugin-life/
├── cordis.patch.yml          插件声明(DSH 靠它加载)
├── package.json
├── README.md                 本文件
├── prompts/                  ★ 所有影响"她怎么说话"的文字
│   ├── README.md             怎么改,以及"后台人格必须和前台一致"
│   ├── system-wrapper.md     人格包装层(system prompt 的前两行)
│   ├── writer-task.md        写手:决定此刻去做什么
│   ├── writer-feeling.md     写手:做完之后写记录
│   └── captain-summary.md    跨会话记忆:把一轮对话压成一句
├── lib/
│   ├── index.js              入口:心跳、状态机、工具注册、记忆挂载、话题查重
│   ├── clock.js              作息排程(边界内采样)
│   ├── physiology.js         ★ 唯一的硬编码生理边界
│   ├── schedule.js           活动排期与错过判定
│   ├── catchup.js            下线追赶
│   ├── activities.js         活动类别池
│   ├── writer.js             ★ 两个写手调用(带人格)
│   ├── persona.js            ★ 人格来源 + system prompt 组装
│   ├── prompts.js            ★ prompt 加载器(md 优先,代码兜底)
│   ├── model.js              模型路由、额度熔断、流式收集
│   ├── db.js                 life.db 的表与查询
│   ├── compress.js           事件压缩(三级 + 第三次删除)
│   ├── memory.js             生活记忆挂载
│   ├── reading.js            笔记文件(reading/)
│   ├── webactivity.js        真实抓取
│   ├── pysupport.js          Python 桥自检
│   ├── netcheck.js           网络探活
│   ├── executor.js           活动执行与产出观察
│   ├── notify.js             主动通知
│   ├── icr.js                人格一致性回归(ICR)
│   ├── prng.js               确定性随机
│   ├── embed.js              ★ 句向量服务(活动去重 + 跨会话记忆共用)
│   ├── embed_server.py       ★ 常驻 python 嵌入进程
│   ├── topic.js              ★ 话题查重:这次想做的事最近做过吗
│   └── captain/              ★ 跨会话记忆
│       ├── index.js          串流程:捕获 / 判定 / 挂载
│       ├── engine.js         热度、判重、挂载选择、沉底
│       ├── rules.js          规则闸门(词表可被 json 覆盖)
│       ├── db.js             captain.db 的表与查询
│       └── summarize.js      LLM 压缩(带人格)
├── test/                     自包含测试
│   ├── prompts.test.mjs
│   ├── topic-dedupe.test.mjs
│   └── captain-memory.test.mjs
├── eval/probes.json          ICR 探针集
└── tools/                    开发用脚本

运行时数据全部在 %DSH_HOME%/dsh-plugin-life/,和代码分开:

life.db          她自己的生活事件
captain.db       关于你的事
status.json      加载状态、模型、熔断、上次错误
user_profile.md  你的偏好(影响活动选择,也会挂载进上下文)
persona.txt      可选:指定用哪个人格模板
reading/         她存的笔记
keepers.md       影响足够大的事(只写给看,不参与挂载)

五、配置与使用

配置文件

文件作用
user_profile.md你的偏好。影响活动选择、事件打分,并且每轮全量挂载
persona.txt可选。写一个 masterprompt 模板 id 指定用哪个人格
captain_rules.json可选。覆盖跨会话记忆的规则词表,{"plan": [...], "task": [...]}

对话里的两个工具

工具用途
life_status查心跳、库行数、上次错误。tick=true 强制一次心跳;probeWeb=true 真发一次搜索验证网络
life_open_detail开一个新会话,详细讲某件事

诊断端点

DSH 的 GUI 里看不到插件日志,所以状态落盘 + 本机回环端点:

位置用途
status.json加载状态、人格、模型、熔断状态、上次错误
POST /dsh-life/icr/start跑人格一致性回归(后台执行)
GET /dsh-life/icr/status轮询进度
GET /dsh-life/icr/result完整结果

六、设计逻辑

为什么不硬编码

写死的东西不会让她像活的。所以:

  • 硬编码的只有生理学边界(睡多久、隔多久吃饭、最多清醒多久)
  • 边界内的每一个数来自前几天的条件采样 + 确定性随机(按日期做种子,同一天重跑结果一致)
  • 活动内容由 LLM 生成,不是剧本

为什么两个 LLM 调用分成两步

环节时机人格的作用
composeTask执行前决定她挑什么、怎么框定。产出必须自包含(执行者是全新会话,没有记忆)
composeFeeling执行后决定语气和留下的痕迹。吃的是真实执行结果,不是预写

为什么分开:执行前写的感受是编的;而且两条指令要求相反 —— 任务要"冷",感受要"热"。 混在一起,人格腔调会渗进任务指令,产出"你最近不是想看 X 吗"这种依赖执行者记忆的说法。

人格怎么进后台

后台用的 system prompt 和前台会话里那份是同一份:包装层 + 人格全文。 不是摘要,不是"你是一个 XX 性格的助手"。

理由见 prompts/README.md:一旦两边不是同一份,读数据库里的记录就像在看 别人照着她的语气写的日记。

为什么记忆是"背景"不是"指令"

挂载的记忆作为 user 角色消息插在你当前这句话之前,而且明确写着 「是背景,不是对舰长的指令」和「舰长当前说的话优先于这里的一切;人格优先于记忆」。

不放进 system prompt 的原因:那会让记忆获得比人格更高的稳定性 —— 记忆是会陈旧的,人格不该被一段三周前的经历改写。

跨会话记忆为什么先过规则再过向量

  • 规则回答"值不值得当候选":词表加加减减,零成本,可解释,出错能定位到具体哪个词
  • 向量回答"是不是同一件事":这是相似度任务的看家本领,实测同一件事的不同说法 0.700.80,不同的事 0.290.55,中间空得很宽
  • 热度回答"以后还会不会被提到":这是时间的活,不是分类的活

三者分工明确。用 LLM 做第一件事会烧钱且不可解释;用规则做第二件事会误判。

为什么记忆要压缩成一句话而不是存原话

原话直存等于把"啰嗦"当成"重要"。一段五百字的吐槽和一句"我下周去杭州"占的预算差十倍, 但后者才是要记住的那件。

所以用同一个人格把整轮对话(你说 + 她回)压成一句 60 字的第三人称概括。 prompt 里钉了两条:不许照抄原句、超过字数硬砍(不信任模型自己数)。

为什么"同一件事"不能用标题判断

早期用的判据是标题的 token 重叠(Jaccard ≥ 0.85)。跑了一周之后实测:

33 条事件里,标题最高相似度只有 0.56 —— 阈值 0.85 一次都没命中过

因为同一件事的两次记录,标题长得完全不一样:

布洛妮娅检索生成式艺术资料并阅读两份创意编程资源页面
布洛妮娅打开 OpenProcessing 的流场作品列表页,抓取作品清单并存成笔记

结果 5 天里记了 10 次"生成艺术",每次换个说法。

现在比的是意图的向量。实测 17 对真实意图:

同一话题   0.616 ~ 0.856
不同话题   0.322 ~ 0.461

阈值取 0.62(一开始定的 0.70 漏了一条 0.669 的,而两种错的代价不对等: 误拦只是这一轮不做、45 分钟后再来;漏拦是多一条重复记忆、占几天挂载预算)。

被拦下来之后不是硬跳过,而是换一个类别重挑一次,并把这几个相近方向明说给写手 ({{AVOID}})。两次都撞上同一片地才放弃这一轮 —— 不做,比写一条重复的记忆好。

为什么压缩看"可压缩内容"而不是文件大小

life.db 文件永远比 256 KB 大,这是设计上必然的:

  • 永久记忆(keep=1)不参与删减,也就不该占一般事件的预算(这是刻意的决定)
  • 文件里还有 SQLite 页开销、索引、空闲页

所以触发口径是"可压缩内容的字节数",而 status.json 的 dbBytes 把四个数都写出来:

fileBytes          磁盘上的文件多大
contentBytes       真正的内容字节(全量,含永久记忆)
compressibleBytes  可压缩的内容字节  ← 压缩看的是这个
limitBytes         上限(默认 256 KB)
usedPercent        到哪了

看到 fileBytes 涨而 compressibleBytes 没到线,那不是 bug —— 是永久记忆在长。


七、技术实现

关键机制

机制实现
心跳ctx.interval,默认 4 分钟一次 tick
记忆挂载agent/pre-step 钩子,把 payload 插到最后一条 user 消息之前。不写入会话历史,所以不会累积
回合判定session/event 的 turn/end
状态机推进每个 tick 读 status.json 的待办活动,判"该不该、来不来得及、错过了多重"
话题查重意图 → 512 维句向量 → 和最近 7 天的意图比余弦,≥ 0.62 就换方向重挑
额度熔断识别 429/402/quota/余额错误后关掉所有 LLM 入口,只保留不需要模型的部分
模型跟随每次调用前重新读主会话的选择,主会话换模型后台最多 2 秒跟上

node:sqlite 的两个坑

  1. LENGTH() 数的是字符不是字节。中文一个字 3 字节,直接少算三分之二。 要用 length(CAST(x AS BLOB))。
  2. 统计库体积时漏掉最大的字段(detail,每个活动 10–11 KB),阈值会形同虚设。 两个错叠起来,2 MB 的阈值实际对应 11 MB 磁盘、1700 个事件,压缩几乎永远不触发。

句向量服务

常驻 python 子进程,模型只加载一次(冷启动约 4 秒),单句约 13 ms。 两个地方共用同一个进程:活动话题查重、跨会话记忆的判重与提及检测 —— 起两个进程等于白吃两倍内存。

  • 不在插件加载时启动,推迟到第一次心跳 —— DSH 刚开那几秒不该跟它抢资源, 但也不能等到第一次活动才起(那样第一次活动正好查不了重)
  • 子进程 unref():宿主退出不等它;宿主一退 stdin 断开,python 自己就退了
  • 失败、超时、模型缺失 → 返回 null,查重和判重都放行,不影响任何事

话题查重怎么和活动流程配合

1. 抽类别 → 写手给出 intent
2. 把 intent 转成向量,和最近 7 天的 intent 比
3. ≥ 0.62 就拦下来:
     - 换一个类别,把相近方向作为 {{AVOID}} 明说给写手,重挑一次
     - 第二次还撞 → 这一轮不做,45 分钟后再来
4. 没撞 → 照常执行

被拦下来时不写库,只在 status.json 的 lastActivity.skipped 留一条记录: 和哪一条撞了、相似度多少、两次尝试的 intent 分别是什么。


八、不做什么

  • 不改人格。dsh-plugin-masterprompt 的 state.json 只读,一个字节都不动。
  • 不硬编码日程。所有可变的量来自历史 + 随机。
  • 不在用户会话里产生回复。活动不占用任何会话,结果只进数据库。 只有"值得说的事"才会通过通知进来。
  • 不跨库写入。生活事件进 life.db,关于你的事进 captain.db,互不读写。
  • 不为了"有事做"而做重复的事。同一片地 7 天内已经走过,就换方向或者这一轮不做。

九、已知限制

  • DSH 没有浏览器自动化,只有 HTTP。所以「看视频」是抓取视频页的标题/简介/评论, 「看电影」是抓取影评/剧情 —— 都是真实网络活动,但不是播放。
  • 活动 agent 会在 GUI 会话列表里短暂出现再消失(创建后立刻销毁)。DSH 的会话列表没有隐藏开关。
  • 跨会话记忆的规则闸门是词表:没见过的表达会漏判。要补就改 captain_rules.json。
  • 同一件事后来补充的细节不会写进已有那条记忆(那需要再压一次,现在没做)。
  • 类别池会被写手不断提出新类别撑大,目前只靠话题查重兜住重复, 没有做"同义类别合并"(生成式艺术 / 生成艺术 / 生成艺术史 现在并存)。
  • 挂载记忆有轻微的身份漂移风险。回归测试见 ICR 端点。

十、开发者

cd dsh-plugin-life
node test/prompts.test.mjs          # prompt 加载器(不依赖模型,秒过)
node test/topic-dedupe.test.mjs     # 话题查重(需要 python 环境 + 模型)
node test/captain-memory.test.mjs   # 跨会话记忆端到端(同上)

三个测试在缺 python / 缺模型时会跳过相关断言并明确说出来,不会假装通过。

许可

MIT

Comments

Loading…