JackZo400/dsh-group-feed ↗★ 0

dsh-group-feed

给 dsh 的群聊上下文投喂:非 @ 的消息零成本攒批挂进会话上下文,每天定时把每个会话日结成 markdown 再开新会话(摘要失败的不清)。 适合群聊场景,能大幅降低非@消息的模型调用成本。

패키지
dsh-group-feed
호환성
미검증
버전
0.1.0
라이선스
MIT
최근 업데이트
2026. 9. 27.

설치

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:JackZo400/dsh-group-feed

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, 虽然有自愈兜着,但"今天已做"这个信息会一起丢——于是重启就再摘一遍、再花一遍钱。

三条纪律(改代码前请先读这三条)

  1. 摘要失败不清会话。 summarize 抛错、返回空、读不到原文、没有 API key —— 一律 kept,那个会话一个字都不动。代价是"某天没整理",收益是"话不会丢"。
  2. 先落盘,后清会话。 摘要在内存里,文件才是唯一持久的副本。先清后写, 正好在磁盘满的时候把唯一一份记录丢在内存里。
  3. 同一天只做一次日结。 日期在动手之前就落盘。跑挂了明天再说——同一批会话 一分钟重试一次,只会反复烧摘要钱(内容没丢,明天照样摘得到)。

测试

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