dsh-group-feed
给 dsh 的群聊上下文投喂:非 @ 的消息零成本攒批挂进会话上下文,每天定时把每个会话日结成 markdown 再开新会话(摘要失败的不清)。 适合群聊场景,能大幅降低非@消息的模型调用成本。
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:JackZo400/dsh-group-feedREADME
Read the full README ↗dsh-group-feed
English | 简体中文
给 DeepSeek Harness(dsh)的群聊上下文投喂插件: 让 Agent 在群里不当聋子,又不用为群里每一句话醒一次。
它不是又一个"聊天机器人记忆",它管的是钱和上下文这两笔账——群里绝大多数消息 根本不是对 Agent 说的,处理它们的方式决定了这个 Agent 是"接得上话"还是"答得驴唇不对马嘴"。
为什么需要它
不这么做,会怎样
只有两种结局,都在线上见过:
结局一:Agent 是个聋子。 群里只在被 @ 的时候才唤醒它。它醒来时对刚才聊了什么一无所知——前面三个人在说"那就八点, 老地方",它被 @ 之后回了一句"什么八点?"。被 @ 的次数越多,这种答非所问越多。
结局二:它听得见,但你为每一句话付钱。 让每条群消息都触发一次模型调用,看起来最省事。但群里 30 条消息里大概只有 1 条是 @ 它的, 剩下 29 条每一句都唤醒一次模型、都带一整段上下文——账单是前者的几十倍, 而且它会在没人跟它说话的时候自说自话。
这个插件走第三条路:非 @ 的消息攒着,攒成一段,用零 token 的方式挂进那个会话的上下文。
安装
dsh plugin --profile web add github:JackZo400/dsh-group-feed
配置抄 cordis.patch.yml 里那份就行。
两个机制
1. 攒批投喂
群里有人说话(没 @ 它)
→ note(key, who, text) 只是攒着,不唤醒模型、不花钱
→ 攒够 25 条,或者等了 40 分钟
→ 拼成一段(从最新往旧取,最多 40 条 / 1200 字)
→ sink.inject(key, 段) 挂进那个会话的上下文:零 token、不唤醒
→ 它下次真被 @ 的时候,这段背景和 @ 一起来到它面前
两个阈值是"或者"的关系:热闹的群按条数投,冷清的群按时间投,免得等一整天。
同一会话最多挂 3 段没被读到的:再投只是把下次被 @ 那一轮的输入撑爆,收益是负的。 攒着不投的那几段不会丢,等它读掉一段立刻补上。
2. 每日日结
每天 04:10(本地时间),对每个有内容的会话做一次:
读当天原文 → 压成一段 300~500 字的中文摘要(一次便宜模型调用)
→ 所有会话的摘要先写进 /digest/.md
→ 写成功了,才回头把会话开新的(= 清掉,下次消息重建)
摘要失败的那个会话,一个字节都不许清。 宁可不整理、多留一天,也不能出现 "会话没了、摘要也没有"。这条纪律比省钱重要。
两个小细则,都是同一条纪律推出来的:
- 不到 40 字的会话(就一两句寒暄)不花摘要钱,直接开新会话——它本来也没什么可留下的。
- 出口还没挂上的时候(一个会话都看不见),这次日结直接跳过,连日期都不记: 记了就等于白白跳过一天,等你的通道插件起来时,今天这份已经"做过了"。
通道插件怎么接进来
这个插件不认识任何聊天协议。它只认一个"会话 key"(group:demo 这种字符串)。
你那边是 QQ、Discord 还是 Telegram,对它没区别。
进:把消息喂进来
// 收到一条群里**非 @** 的消息(@ 的那些照旧走你自己的流程,别喂进来)
const feed = ctx.get('groupFeed')
feed.note(`group:${chatId}`, senderName, text)
note() 是纯内存操作:不联网、不花钱、不唤醒模型,随便调。
出:实现一个「投喂出口」(sink)
注册成 cordis 服务(默认名 groupFeedSink),或者晚一点用 attach() 挂上去:
ctx.provide('groupFeedSink', {
// 把一段背景挂进这个会话的上下文。**不许唤醒模型**——这是整个插件的立身之本。
// 会话还没开(投不进去)就返回 false,插件会继续攒着,下一轮再试。
inject(key, text) {
const rec = sessions.get(key)
if (!rec?.agent) return false
rec.agent.inject({ id: randomUUID(), role: 'user', content: [{ type: 'text', text }], source: { kind: 'user' } })
return true
},
// 这个会话里还挂着几段没被读到的(答不上来就返回 0,别返回大数——那会把投喂永久堵死)
pending: (key) => sessions.get(key)?.agent?.inbox?.nextStep?.length ?? 0,
// 清会话 = 开新的。没清成请**返回 false**,插件会把它记进日结文件和日志
reset(key) {
sessions.get(key)?.agent?.dispose?.()
sessions.delete(key)
return true
},
// 现在有内容的会话有哪些(日结整理这些)
keys: () => [...sessions.keys()],
// 当天的原文,日结喂给摘要器用。
// 读不到就 **抛错**,或者干脆不提供这个函数 —— 插件会退成"读不到内容,不清会话"。
// 千万不要返回空字符串:那会被当成"今天没什么可整理的",会话就被清了。
transcript: async (key) => readSessionText(key),
// 显示名,日志和日结标题用
title: (key) => groupNameOf(key),
})
加载顺序无所谓:出口是每次用的时候现取的。你的通道插件比它晚加载,就晚一点
ctx.get('groupFeed').attach(sink) 挂上去。
服务接口(暴露出去的那个 groupFeed)
| 方法 | 干什么 |
|---|---|
note(key, who, text, at?) | 攒一条非 @ 的消息。返回这个会话现在攒了几条 |
flush(key?, at?) | 立刻投(跳过条数与等待判定,"挂满 3 段"那道闸还在)。你知道它刚读完时调 |
digest(at?) | 立刻做一次日结(不看时刻)。做完当天不会再做第二遍 |
tick(at?) | 手动推一次心跳(自检 / 嵌进别人的定时器时用) |
status() | 攒了多少、投了几段、日结上次什么时候跑的、出口挂上没有 |
attach(sink) | 运行时挂投喂出口(传 null 摘掉) |
数据落在哪
/ 默认 /.group-feed
├── state.json 只记一件事:日结今天做过没有(重启不重复做)
└── digest/2026-09-28.md 当天每个会话一段摘要 + 状态(谁清了、谁没清、为什么)
state.json 是原子写的(先写 .tmp 再 rename):半截写会留下一个坏 JSON,
虽然有自愈兜着,但"今天已做"这个信息会一起丢——于是重启就再摘一遍、再花一遍钱。
三条纪律(改代码前请先读这三条)
- 摘要失败不清会话。
summarize抛错、返回空、读不到原文、没有 API key —— 一律kept,那个会话一个字都不动。代价是"某天没整理",收益是"话不会丢"。 - 先落盘,后清会话。 摘要在内存里,文件才是唯一持久的副本。先清后写, 正好在磁盘满的时候把唯一一份记录丢在内存里。
- 同一天只做一次日结。 日期在动手之前就落盘。跑挂了明天再说——同一批会话 一分钟重试一次,只会反复烧摘要钱(内容没丢,明天照样摘得到)。
测试
node test/selftest.mjs # 纯逻辑:阈值、截断、日结编排、状态自愈(假时钟 + 假注入 + 假摘要)
node test/plugin-selftest.mjs # 接线:假 ctx 把 apply() 跑一遍,验服务与配置
两个都不联网、不调模型、不真等 40 分钟,秒级跑完,而且挂了就是 exit 1。
selftest.mjs 覆盖了那三条纪律的失败路径:摘要抛错、返回空串、读不到原文、
写文件抛错、清会话返回 false —— 每一个都断言"那个会话没有被清"。
已知局限
- 攒批只在内存里。 进程重启,还没投出去的那几段就没了(原始聊天记录还在, 丢的只是"背景"这一段)。要持久化就得让通道自己把没投的存下来,那是通道的事。
- 日结要通道给原文。 插件不读别人的会话轨迹格式——那是每个通道自己的事。
没给
transcript就永远不清会话(安全的那一边)。 - 04:10 到 08:00 之间进程没在跑,当天就不做了。 宁可少做一次,也不在白天
把人正在用的会话清掉。要补就自己调
digest()。 - 每段投喂本身还是要花钱的(下次被 @ 时多带那 1200 字)。所以
textMax别往大调, 投喂是"背景",不是"档案"。 - 没有工具(tool)出口:它的输入是别人说的话,不是模型提的问题;产出(日结) 本来就该出现在下一轮的上下文里。少一个工具就少一份常驻提示词开销。
License
MIT © 2026 JackZo400
English
→ Full English README: README.en.md