dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
dsh 插件:群聊上下文投喂——非 @ 的消息攒批挂进上下文(零 token、不唤醒模型),每天再压成日结 · Batched group-chat context feeding for dsh: zero-token injection plus daily digests.
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 里那份就行。
群里有人说话(没 @ 它)
→ note(key, who, text) 只是攒着,不唤醒模型、不花钱
→ 攒够 25 条,或者等了 40 分钟
→ 拼成一段(从最新往旧取,最多 40 条 / 1200 字)
→ sink.inject(key, 段) 挂进那个会话的上下文:零 token、不唤醒
→ 它下次真被 @ 的时候,这段背景和 @ 一起来到它面前
两个阈值是"或者"的关系:热闹的群按条数投,冷清的群按时间投,免得等一整天。
同一会话最多挂 3 段没被读到的:再投只是把下次被 @ 那一轮的输入撑爆,收益是负的。 攒着不投的那几段不会丢,等它读掉一段立刻补上。
每天 04:10(本地时间),对每个有内容的会话做一次:
读当天原文 → 压成一段 300~500 字的中文摘要(一次便宜模型调用)
→ 所有会话的摘要先写进 <dir>/digest/<日期>.md
→ 写成功了,才回头把会话开新的(= 清掉,下次消息重建)
摘要失败的那个会话,一个字节都不许清。 宁可不整理、多留一天,也不能出现 "会话没了、摘要也没有"。这条纪律比省钱重要。
两个小细则,都是同一条纪律推出来的:
这个插件不认识任何聊天协议。它只认一个"会话 key"(group:demo 这种字符串)。
你那边是 QQ、Discord 还是 Telegram,对它没区别。
// 收到一条群里**非 @** 的消息(@ 的那些照旧走你自己的流程,别喂进来)
const feed = ctx.get('groupFeed')
feed.note(`group:${chatId}`, senderName, text)
note() 是纯内存操作:不联网、不花钱、不唤醒模型,随便调。
注册成 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 摘掉) |
<dir>/ 默认 <启动目录>/.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 就永远不清会话(安全的那一边)。digest()。textMax 别往大调,
投喂是"背景",不是"档案"。MIT © 2026 JackZo400
→ Full English README: README.en.md