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
A group-chat context feed plugin for DeepSeek Harness (dsh): it keeps the agent from being deaf in a group chat, without waking it for every message in the group.
It is not yet another "chatbot memory". It manages two accounts: money and context. The overwhelming majority of group messages are not addressed to the agent at all, and the way you handle them decides whether this agent "picks up the thread" or "answers something wildly off".
What happens if you don't.
There are only two endings, and both have been seen in production:
Ending one: the agent is deaf. In the group it is only woken when it is mentioned. When it wakes up it knows nothing about what was just being discussed — three people were saying "then eight o'clock, the usual place", and after being mentioned it replies "what eight o'clock?". The more often it is mentioned, the more of these off-target answers you get.
Ending two: it hears you, but you pay for every message. Making every group message trigger a model call looks like the easiest option. But only about 1 of every 30 group messages mentions it; the other 29 each wake the model and each carry a whole stretch of context — the bill is dozens of times that of the first option, and it will start talking to itself when nobody is talking to it.
This plugin takes a third path: non-mention messages are buffered, batched into one block, and attached to that session's context the zero-token way.
dsh plugin --profile web add github:JackZo400/dsh-group-feed
Just copy the configuration from cordis.patch.yml.
群里有人说话(没 @ 它)
→ note(key, who, text) 只是攒着,不唤醒模型、不花钱
→ 攒够 25 条,或者等了 40 分钟
→ 拼成一段(从最新往旧取,最多 40 条 / 1200 字)
→ sink.inject(key, 段) 挂进那个会话的上下文:零 token、不唤醒
→ 它下次真被 @ 的时候,这段背景和 @ 一起来到它面前
The two thresholds are an "or" relationship: a busy group is fed by count, a quiet group by time, so that nothing has to wait a whole day.
Per session at most 3 unread blocks are attached: feeding more only blows up the input of the next mention round, so the return is negative. The blocks that stay buffered are not lost; as soon as one of them is read, the next one is fed right away.
Every day at 04:10 (local time), once for every session that has content:
读当天原文 → 压成一段 300~500 字的中文摘要(一次便宜模型调用)
→ 所有会话的摘要先写进 <dir>/digest/<日期>.md
→ 写成功了,才回头把会话开新的(= 清掉,下次消息重建)
A session whose summary failed must not have a single byte cleared. Rather skip the tidying and keep it one more day than end up with "the session is gone and the summary is gone too". This discipline matters more than saving money.
Two small rules, both derived from the same discipline:
This plugin knows nothing about any chat protocol. It only knows a "session key" (a
string such as group:demo). Whether your side is QQ, Discord or Telegram makes no
difference to it.
// 收到一条群里**非 @** 的消息(@ 的那些照旧走你自己的流程,别喂进来)
const feed = ctx.get('groupFeed')
feed.note(`group:${chatId}`, senderName, text)
note() is a pure in-memory operation: no network, no money, no model wake-up, call it
as often as you like.
Register it as a cordis service (default name groupFeedSink), or attach it a bit later
with 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),
})
Load order does not matter: the sink is looked up fresh on every use. If your channel
plugin loads later than this one, it just attaches a bit later with
ctx.get('groupFeed').attach(sink).
groupFeed)| Method | What it does |
|---|---|
note(key, who, text, at?) | buffer one non-mention message. Returns how many are buffered in this session now |
flush(key?, at?) | feed immediately (skipping the count and wait checks, the "3 blocks attached" gate still applies). Call it when you know it has just finished reading |
digest(at?) | run the daily digest immediately (ignoring the clock). Once done, it will not run a second time that day |
tick(at?) | push one heartbeat by hand (for self-tests / when embedded in someone else's timer) |
status() | how much is buffered, how many blocks were fed, when the digest last ran, whether the sink is attached |
attach(sink) | attach the feed sink at runtime (pass null to detach it) |
<dir>/ 默认 <启动目录>/.group-feed
├── state.json 只记一件事:日结今天做过没有(重启不重复做)
└── digest/2026-09-28.md 当天每个会话一段摘要 + 状态(谁清了、谁没清、为什么)
state.json is written atomically (write .tmp first, then rename): a half-written file
leaves a broken JSON behind, and although self-healing covers that, the "already done
today" information is lost along with it — so a restart summarizes everything again and
spends the money again.
summarize throws, returns empty, the
transcript cannot be read, there is no API key — all of these mean kept, and not a
single character of that session is touched. The cost is "one day was not tidied up",
the gain is "words are not lost".node test/selftest.mjs # 纯逻辑:阈值、截断、日结编排、状态自愈(假时钟 + 假注入 + 假摘要)
node test/plugin-selftest.mjs # 接线:假 ctx 把 apply() 跑一遍,验服务与配置
Neither of them touches the network, calls a model, or really waits 40 minutes; they
finish in seconds, and a failure means exit 1. selftest.mjs covers the failure paths
of those three disciplines: the summary throws, returns an empty string, the transcript
cannot be read, the file write throws, reset returns false — each one asserts that "that
session was not cleared".
transcript it never clears a session (the safe side).digest() yourself.textMax: feeding is "background", not an "archive".MIT © 2026 JackZo400