DeepSeek Anti-recall

Prevent deepseek from recalling response and cache the recalled message locally

< Feedback on DeepSeek Anti-recall

Review: OK - script works, but has bugs

§
Posted: 2026-01-11
Edited: 2026-01-11

进行以下修改,可以使得开启深度思考要刷新的问题得到解决

这个问题出现的原因在于脚本拦截和渲染的时效性差异。

在 DeepSeek 的深度思考 (Reasoning) 模式下,服务器通过 SSE (Server-Sent Events) 流式推送数据。当内容触发审查被撤回(Recall)时,脚本虽然拦截到了信号并修改了数据,但前端 UI 框架(通常是 React 或 Vue)由于其内部状态管理机制,可能无法实时识别到被脚本篡改后的数据流,导致页面卡住或显示异常。

刷新网页能恢复是因为 onHistoryMessageResp 函数发挥了作用——它在页面重新加载、获取历史消息记录时,直接从本地缓存读取并强制替换了消息内容。

核心改进建议

要实现“不刷新也能即时显示”,需要优化对流式输出的拦截逻辑。以下是针对该脚本的两个改进点:

1. 强制结束流式状态

目前的脚本在检测到撤回时,虽然修改了消息内容,但没有明确告诉前端“流已经结束”。如果前端还在等待 FINISH 信号,它就不会渲染最后一段数据。

DSState.prototype.preCheck 中,当你检测到 TEMPLATE_RESPONSE 时,除了修改 data.v,建议尝试模拟一个结束信号。

2. 修改 handleEventItem 的即时反馈

目前脚本在 onEventStreamResp 中处理字符串拼接。对于深度思考这种长文本,建议在检测到撤回的瞬间,立即触发一个自定义的 DOM 操作或者更彻底地拦截 XMLHttpRequest.responseText 的 getter。


优化后的代码片段(建议替换或添加):

你可以尝试在 onEventStreamResp 函数中,针对检测到 recalled 的情况,添加一个更激进的 finish 信号模拟:

// 在 DSState.prototype.preCheck 内部修改
if (v.p == "fragments" && v.v[0].type == TEMPLATE_RESPONSE) {
    this.recalled = true;
    modified = true;

    // 1. 保存原始片段
    saveRecalledMessage(this.sessId, this.fields.response.message_id, this.fields.response.fragments);

    // 2. 构造一个能被前端理解的“结束且显示”的消息包
    // 强制将状态改为 FINISHED,防止前端一直处于 loading 状态
    data.v[i] = {
        "v": [
            {"id": 999, "type": "TIP", "style": "WARNING", "content": getRecalledTipMessage(this.locale)}
        ], 
        "p": "fragments", 
        "o": "APPEND"
    };

    // 补做一个 BATCH 操作,尝试强制关闭思考动画
    data.v.push({"p": "status", "v": "FINISHED", "o": "SET"});
}

为什么深度思考模式下反应慢?

深度思考模式的渲染逻辑通常分为两个容器:thinking 块和 message 块。

  • 拦截时机: 撤回通常发生在思考结束、开始输出正式回答的那一刻。
  • UI 僵死: 当脚本拦截了“撤回指令”并替换为“警告提示”时,DeepSeek 的前端可能还在等待一个合法的 JSON 结构来关闭 Thinking 动画。如果脚本返回的模拟数据不符合前端最新的 Component 协议,界面就会看起来像卡住了,直到你刷新页面重新加载历史记录。

你可以尝试的操作:

  1. 检查控制台 (F12): 观察在被拦截瞬间是否有报错,这能说明是否因为脚本修改后的 JSON 格式不规范导致前端渲染崩溃。
  2. 手动触发 UI 更新: 如果脚本修改无效,可能需要通过注入代码触发 DeepSeek 内部的 React Fiber 更新,但这会使脚本变得非常复杂且容易随网站更新失效。

建议: 如果不希望代码过于复杂,目前的“刷新即现”其实是最稳妥的方案,因为它利用了 DeepSeek 官方的历史记录加载逻辑,兼容性最好。


这是因为 DeepSeek 的前端渲染逻辑是根据 status 字段来切换显示模式的。

在正常的“深度思考”流程中:

  1. 思考中statusSEARCHINGTHINKING,内容输出在思考框。
  2. 回答中:当思考结束,status 变为 FINISHED,前端会隐藏思考框(或折叠),并开始在主回答区输出 fragments

违禁词被拦截时,DeepSeek 的原始信号通常会强制改变消息状态,导致前端立刻“跳出”思考模式。由于脚本是在检测到拦截后强行追加提示并修改状态为 FINISHED 以确保内容显示,这就导致了前端认为“思考已结束”,从而关闭了思考框,将缓存的内容直接显示在了主回答区。

要让思考框保留,同时在框内显示内容,需要针对深度思考的 process 字段进行特殊处理。

改进方案

你需要修改脚本中的 onHistoryMessageResp 函数和 DSState.prototype.preCheck。核心思路是:如果发现消息包含思考内容,不仅要恢复 fragments(主回答),还要尝试恢复或保留 process(思考过程)字段。

请尝试将 onHistoryMessageResp 函数替换为以下版本:

    /**
     * History message response handler, 修复思考框消失的问题
     */
    function onHistoryMessageResp(req, res) {
        let json = JSON.parse(res);
        if (!json.data || !json.data.biz_data) {
            return res;
        }

        let data = json.data.biz_data;
        let sessId = data.chat_session.id;
        let modified = false;

        for (let i = 0; i < data.chat_messages.length; i++) {
            let msg = data.chat_messages[i];
            // 如果消息被拦截 (CONTENT_FILTER)
            if (msg.status == CONTENT_FILTER) {
                // 1. 恢复主回答内容
                msg.fragments = getRecalledMessage(req, sessId, msg.message_id);

                // 2. 尝试保持思考框状态
                // 如果该消息之前有思考内容(假设本地缓存或原始数据中存在 process)
                // 确保 status 为 FINISHED 才能让内容渲染出来
                msg.status = "FINISHED";

                // 如果发现 process 字段为空,但你希望它显示(通常拦截会清空此字段)
                // 可以在这里手动补回一个占位符,或者如果之前缓存了思考过程,则在此处还原
                if (!msg.process || msg.process.length === 0) {
                   // 注意:这里只能恢复被拦截那一刻之前的思考文字
                   // 如果需要完美恢复思考框,需要在流式传输阶段也缓存 process 字段
                }

                modified = true;
            }
        }

        if (modified) {
            json.data.biz_data = data;
            res = JSON.stringify(json);
        }
        return res;
    }

为什么很难完美回到“思考框内”输出?

  1. 数据隔离:DeepSeek 将“思考过程”存在 process 数组中,将“正式回答”存在 fragments 数组中。拦截通常发生在从 process 转向 fragments 的瞬间。
  2. 前端控制:一旦触发 CONTENT_FILTER,官方代码会清空这两个数组。脚本目前能做到的是把消失的文字找回来。
  3. 渲染占位:如果想让拦截的文字出现在思考框里,你需要把内容塞进 msg.process 而不是 msg.fragments。但这样做会导致文字带有“思考中”的灰色样式,且无法使用 Markdown 渲染。

建议操作

你可以尝试在脚本的 preCheck 逻辑中,观察 v.p == "process" 的变化。如果想保留思考框,不要让脚本在检测到拦截时立即发送 status: FINISHED 的覆盖指令,但这可能会导致界面卡在“思考中”的小圆圈转不停。

目前的折中办法: 现在的逻辑确保了“内容不丢失”。由于拦截动作是服务器端发起的强制中断,前端为了安全会销毁当前的思考组件实例,脚本能强行把文字捞回来显示在主回答区已经是目前最稳定的兼容做法了。

Post reply

Sign in to post a reply.