# 谷时补答:要不要做,怎么做 > **最终状态(2026-10-10 更新):这套东西已经整体砍掉了。** > V1(队列 + 一键补发)与 V2(谷时自动补发)都实现过、也在真机上跑过,最后按判断全部移除: > 投递链路脆弱(跨会话 resume + 串行等待 + 跳过运行中会话 + 失败重试,复杂度远超收益), > 而且和插件本身的目的自相矛盾 —— **「高峰期别烧钱」与「谷时替你自动发一遍」是两件相反的事**。 > 现在只保留**只读的被拦截列表**:拦下、记账、让你看见,不动手。 > > 删掉的东西:`POST /peak-guard/release` 路由、`/peak replay`、`/peak auto`、 > 界面上的「一键补发」按钮与「自动补发」开关及其警告弹窗、对 `sessionController` 的依赖, > 以及 `autoReplay` / `replayPrompt` / `queueLimit` / `replayMaxAgeMinutes` / > `replayMaxPerFlush` / `replayDelayMs` 六个配置键。 > 旧的 `state.json` 里若还有 `queue` 键,读的时候会自动搬进 `list`(不丢记录)。 > > 下面保留最初的设计推演,作为「当时为什么会想做、以及为什么最后不做」的记录。 > 你的原话:*「要不要加入在进入谷时发送所有会话里被拦截的消息,包括新会话的?这个功能可能较难实现, > 因为要对每个会话进行管理,万一用户新建了好几个会话还要保证不乱套、不吞消息。」* ## 结论先说 **技术上完全可行,但我建议默认不做自动补发 —— 改成「谷时提醒 + 你点一下补答」。** 理由不是难,是**产品上自相矛盾**:这个插件存在的意义是「高峰期别烧钱」, 而「谷时自动替你发一遍」会在你不在场的时候替你花钱、替你答过期的问题。 ## 技术可行性(已经查实,不是估的) 宿主服务 `sessionController` 有: ``` @Remote('prompt') prompt(request: SessionPromptRequest, signal): Promise<{accepted: true}> ``` ```ts interface SessionPromptRequest { requestId: SessionRequestId sessionId: SessionId // 任意会话,冷的会先被 resume mode: 'queue' | 'steer' content: readonly PromptContentPart[] clientTimeZone?: string } ``` 也就是说:宿主插件可以往**任何一个会话**(包括用户没打开的、刚新建的)注入一条提示, 这与客户端点发送走的是同一条路。所以「补答」在机制上没有障碍。 另外拦截点上拿得到 `options.sessionId` 和 `options.messages`(原文), 队列的四要素(哪个会话、问了什么、什么时候、第几轮)都能记下来。 ## 为什么我不建议「自动发」 | 风险 | 具体会发生什么 | |---|---| | **替你花钱** | 10 个会话排队 = 谷时那一刻并发 10 次模型调用。这个插件的卖点是省钱,结果它自己制造账单 | | **答过期的问题** | 早上 10 点问「现在这个报错怎么回事」,晚上 6 点自动答 —— 你可能早手改完了。产出是噪音,钱照花 | | **上下文漂移** | 会话里停在「用户问 → 助手回了那句话」,隔 8 小时再续,模型看到的世界状态已经变了 | | **会话生命周期** | 会话可能被归档、删除、正在被别的窗口跑;自动发要处理「目标已不存在」和「用户正在打字」 | | **惊扰** | 用户没开的会话突然自己动起来、冒出输出,还改工作区文件 —— 这是最坏的一种「惊喜」 | | **重复/丢失** | 发出去之后进程崩了,没有持久化的「已放行」标记就会重复发;标记写早了又会吞掉没发成的 | ## 我建议的替代方案(安全、仍然省事) **V1:谷时提醒 + 一键补答**(默认) 1. 拦截时照旧回那句话,同时在宿主侧记一条队列项:`{sessionId, 首条用户消息摘要, atMs, turn}`, 落到 `plugin-data/dsh-peak-guard/queue.json`(和开关同一个目录,原子替换)。 2. 进入谷时(`nextTransition()` 已经能算出准确时刻)**不调用模型**,只做两件事: - 输入框那个开关上挂一个角标:`3 条待补答`; - 在对应会话里落一条**可见的说明**(不是模型调用):「谷时到了,你在高峰被拦下的 3 条请求还在这儿, 点开关上的角标放行」。 3. 用户点角标(或 `/peak replay`)才真的补答 —— 一次只发一个会话,串行,发完即标记。 **V2:真自动**(如果确实想要,得加满护栏) 要做得不「乱套」、不「吞消息」,至少需要: | 护栏 | 值(建议) | |---|---| | 过期丢弃 | 队列项超过 6 小时自动丢,并在 UI 上说明丢了几条 | | 串行放行 | 全局并发 1,两次之间留 ≥10 秒,避免瞬间并发 | | 总量上限 | 单次放行最多 N 条(默认 20),超出留在队列里等下一轮 | | 活会话校验 | 放行前用 `sessionController.inspect(sessionId)` 确认会话还在;不在就丢并记数 | | 忙碌跳过 | 该会话 `running === true` 就跳过(不打断用户正在进行的对话) | | 先标记后放行 | 先写「已放行 attempt」再 `prompt()`;失败则回滚标记(宁可重试也不吞) | | 幂等 | 队列项带唯一 id + `turn`,`prompt()` 的 `requestId` 用它派生,重复投递可被识别 | | 顺序 | 同一会话内 FIFO;跨会话之间不保证顺序(也不该保证) | | 可见性 | 每条补答在会话里留一行「谷时补答(高峰期间被拦)」来源说明,用户永远知道这话从哪来 | | 会话数上限 | 一次最多补 5 个会话,剩下的下一轮 | **我的建议:先做 V1。** 它的「省事」程度已经解决你 90% 的痛点(不用重新打字), 而把「花不花这笔钱」的决定权始终留在你手上。等 V1 用一段时间、你确认自己真的想要无人值守, 再把 V2 作为**显式开关**加上(默认关)。 ## 无论做哪个,都得先解决的三个小事 1. **翻译用户原文**:`options.messages` 里最后一条 user 消息是原文,但补答时**不该重发原文** (会重复出现在会话里),应该发一句短的触发语(如「继续」),让模型接着历史里那条请求回答。 2. **提示语要说人话**:补答触发语会作为用户消息落进会话,得让人一眼看出是插件发的。 3. **UI 位置**:角标挂在输入框那个开关上(同一个控件第二态),不要再新占一个插槽。 ## 现在的状态 **没做**。当前行为:高峰拦截只回那一句话,不排队、不补答。 `/peak status` 会照实说本次运行拦了几次。