feat(plugin): inject 🔚 ending instruction for bypass/end-symbol discord turns

This commit is contained in:
2026-02-25 11:04:01 +00:00
parent 798c402ab0
commit 32405fa3e2
3 changed files with 36 additions and 2 deletions

View File

@@ -9,6 +9,10 @@ WhisperGate evaluates in strict order:
3. message ending symbol check
4. fallback to no-reply model override
Additional prompt behavior:
- when decision is `bypass_sender` or `end_symbol:*`, plugin prepends:
- `你的这次发言必须以🔚作为结尾。`
## Why before_model_resolve
- deterministic

View File

@@ -4,6 +4,9 @@
- `message:received` caches a per-session decision from deterministic rules.
- `before_model_resolve` applies `providerOverride + modelOverride` when decision says no-reply.
- `before_prompt_build` prepends instruction `你的这次发言必须以🔚作为结尾。` when decision is:
- `bypass_sender`
- `end_symbol:*`
## Rules (in order)

View File

@@ -9,6 +9,7 @@ type DecisionRecord = {
const sessionDecision = new Map<string, DecisionRecord>();
const MAX_SESSION_DECISIONS = 2000;
const DECISION_TTL_MS = 5 * 60 * 1000;
const END_MARKER_INSTRUCTION = "你的这次发言必须以🔚作为结尾。";
function normalizeChannel(ctx: Record<string, unknown>): string {
const candidates = [ctx.commandSource, ctx.messageProvider, ctx.channelId, ctx.channel];
@@ -48,6 +49,10 @@ function pruneDecisionMap(now = Date.now()) {
}
}
function shouldInjectEndMarker(reason: string): boolean {
return reason === "bypass_sender" || reason.startsWith("end_symbol:");
}
export default {
id: "whispergate",
name: "WhisperGate",
@@ -87,10 +92,10 @@ export default {
return;
}
// one-shot decision per inbound turn
sessionDecision.delete(key);
if (!rec.decision.shouldUseNoReply) return;
// no-reply path is consumed here
sessionDecision.delete(key);
api.logger.info(
`whispergate: override model for session=${key}, provider=${config.noReplyProvider}, model=${config.noReplyModel}, reason=${rec.decision.reason}`,
);
@@ -100,5 +105,27 @@ export default {
modelOverride: config.noReplyModel,
};
});
api.on("before_prompt_build", async (_event, ctx) => {
const key = ctx.sessionKey;
if (!key) return;
const rec = sessionDecision.get(key);
if (!rec) return;
if (Date.now() - rec.createdAt > DECISION_TTL_MS) {
sessionDecision.delete(key);
return;
}
// consume non-no-reply paths here to avoid stale carry-over
sessionDecision.delete(key);
if (!shouldInjectEndMarker(rec.decision.reason)) return;
api.logger.info(`whispergate: prepend end marker instruction for session=${key}, reason=${rec.decision.reason}`);
return {
prependContext: END_MARKER_INSTRUCTION,
};
});
},
};