DeepSeek 防撤回脚本

防止DeepSeek撤回消息,被撤回的消息将保存在本地

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         DeepSeek Anti-recall
// @name:zh-CN   DeepSeek 防撤回脚本
// @namespace    http://tampermonkey.net/
// @version      2025-10-31
// @description  Prevent deepseek from recalling response and cache the recalled message locally
// @description:zh-CN 防止DeepSeek撤回消息,被撤回的消息将保存在本地
// @author       Franky T
// @match        https://chat.deepseek.com/*
// @icon         https://www.deepseek.com/favicon.ico
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const TEMPLATE_RESPONSE = "TEMPLATE_RESPONSE";
    const CONTENT_FILTER = "CONTENT_FILTER";
    const RECALL_TIP_EN = "⚠️ This response has been is RECALLED and archived only on this browser";
    const RECALL_TIP_CH = "⚠️ 此回复已被撤回,仅在本浏览器存档";
    const RECALL_NOT_FOUND_EN = "⚠️ This response has been RECALLED and cannot be found in local cache.";
    const RECALL_NOT_FOUND_CH = "⚠️ 此回复已被撤回,且无法在本地缓存中找到";

    function getRecalledTipMessage(locale) {
        return locale == "zh_CN" ? RECALL_TIP_CH : RECALL_TIP_EN;
    }

    function getRecallNotFoundMessage(locale) {
        return locale == "zh_CN" ? RECALL_NOT_FOUND_CH : RECALL_NOT_FOUND_EN;
    }

    /**
     * Generate a key for local storage of deleted message
     *  @param {string} sessId - The session Id
     *  @param {number} msgId - The message Id
     *  @returns {string} The local storage key
     */
    function _getKey(sessId, msgId) {
        return "deleted-chat-sess-" + sessId + "-msg-" + msgId;
    }

    function _parseKey(key) {
        if (key.match(/^\d+$/)) {
            return parseInt(key);
        }
        return key;
    }

    /**
     * Util function for setting a object's field with a string path
     */
    function _setValueByPath(obj, path, value, isAppend) {
        const keys = path.split("/");
        let current = obj;

        for (let i = 0; i < keys.length - 1; i++) {
            let key = _parseKey(keys[i]);

            if (!(key in current)) {
                const nextKey = _parseKey(keys[i + 1]);
                current[key] = typeof nextKey === 'number' ? [] : {};
            }

            current = current[key];
        }

        const lastKey = _parseKey(keys[keys.length - 1]);

        let lastVal = current[lastKey];
        if (isAppend) {
            if (Array.isArray(current[lastKey])) {
                for (let k = 0; k < value.length; k++) {
                    current[lastKey].push(value[k]);
                }
            } else {
                current[lastKey] = lastVal + value;
            }
        } else {
            current[lastKey] = value;
        }
        return obj;
    }

    /**
     * DSState is a class holding DeepSeek response state
     */
    function DSState() {
        this.fields = {};
        this.sessId = "";
        this.locale = "en_US";
        this.recalled = false;

        this._updatePath = "";
        this._updateMode = "SET"; // Default update mode is set
    }

    /**
     * Perform a single update with SSE on the state object. Return modified SSE if recall action detected
     * @param {object} data - A single SSE item from completion response
     * @returns {string} if not empty, the current SSE item should be modified (in case of recall action detected)
     */
    DSState.prototype.update = function(data) {
        let precheck = this.preCheck(data); // Pre-check SSE first
        if (data.p) {
            this._updatePath = data.p;
        }

        if (data.o) {
            this._updateMode = data.o;
        }

        let value = data.v;

        // If the value is object (typically "response" field), and no path defined
        // This could be the first SSE event we need, here we should initialize the fields
        if (typeof value == 'object' && this._updatePath == "") {
            for (var key in value) {
                this.fields[key] = value[key];
            }

            return precheck;
        }

        this.setField(this._updatePath, value, this._updateMode);

        return precheck;
    }

    /**
     * Precheck the SSE before applying update. Return modified SSE if recall action detected
     * @param {object} data - A single SSE item from completion response
     * @returns {string} if not empty, the current SSE item should be modified (in case of recall action detected)
     */
    DSState.prototype.preCheck = function(data) {
        let path = data.p ? data.p : this._updatePath;
        let mode = data.o ? data.o : this._updateMode;
        let modified = false;

        // Here we only consider a BATCH operation at the end of conversation.
        if (mode == "BATCH" && path == "response") {
            for (let i = 0; i < data.v.length; i++) {
                let v = data.v[i];
                // If TEMPLATE_RESPONSE detected in update of fragments, this must be a recall action!
                if (v.p == "fragments" && v.v[0].type == TEMPLATE_RESPONSE) {
                    this.recalled = true;
                    modified = true;

                    // Save the recalled message fragments
                    saveRecalledMessage(this.sessId, this.fields.response.message_id, this.fields.response.fragments);

                    // Append a tip for recalled message
                    data.v[i] = {"v": [{"id": this.fields.response.fragments.length + 1, "type": "TIP", style: "WARNING", "content": getRecalledTipMessage(this.locale)}], "p": "fragments", "o": "APPEND"};
                }
            }
        }

        if (modified) {
            return JSON.stringify(data);
        }

        return "";
    }

    /**
     * Set fields on the state object based on SSE event
     * @param {string} path - The field path
     * @param {any} value - The new value of the field. If mode is BATCH this should be an array of operations on sub-fields
     * @param {string} mode - SET: Apply value to the field; APPEND: Append the value at the end of the field; BATCH: Perform operations in value array to the sub-fields of the field
     */
    DSState.prototype.setField = function(path, value, mode) {
        if (mode == "BATCH") {
            let subMode = "SET";
            for (let i = 0; i < value.length; i++) {
                let v = value[i];
                if (v.o) {
                    subMode = v.o;
                }

                // Set sub-fields recursively
                this.setField(path + "/" + v.p, v.v, subMode);
            }
        } else if (mode == "SET") {
            _setValueByPath(this.fields, path, value, false);
        } else if (mode == "APPEND") {
            _setValueByPath(this.fields, path, value, true);
        }
    }


    /**
     *  Save a recalled message to the local storage
     *  @param {string} sessId - The session Id
     *  @param {number} msgId - The message Id
     *  @param {Array} fragments - Framgments of the message
     */
    function saveRecalledMessage(sessId, msgId, fragments) {
        localStorage.setItem(_getKey(sessId, msgId), JSON.stringify(fragments));
    }

    /**
     *  Get a recalled message from the local storage
     *  @param {string} sessId - The session Id
     *  @param {number} msgId - The message Id
     *  @returns {Array} Framgments of the message, if not found, would be a error message
     */
    function getRecalledMessage(req, sessId, msgId) {
        let frags = JSON.parse(localStorage.getItem(_getKey(sessId, msgId)));
        if (!frags) {
            // The message not exist in the local storage, show a error message in original template format
            return [{content: getRecallNotFoundMessage(req.__locale), id: 2, type: TEMPLATE_RESPONSE}];
        }

        // Append a tip, indicates the response have been recalled
        frags.push({"id": frags.length + 1, "type": "TIP", style: "WARNING", "content": getRecalledTipMessage(req.__locale)});
        return frags;
    }

    /**
     *  Handler of single line of completion message
     *  @param {XMLHttpRequest} req - The XHR object
     *  @param {string} msg - The message line
     *  @returns {string} empty string or replaced text if recall detected
     */
    function handleEventItem(req, msg) {
        if (!msg.v) {
            return "";
        }

        // console.log(msg);

        return req.__dsState.update(msg);
    }

    /**
     *  Handler for Completion APIs, including completion, edit, continue and regenerate.
     *  @param {XMLHttpRequest} req - The XHR object
     *  @param {string} res - The response text
     *  @returns {string} The original or modified response
     */
    function onEventStreamResp(req, res) {
        // Extra fields of XHR object
        if (req.__messagesCount === undefined) {
            req.__messagesCount = 0; // Processed message count
            req.__dsState = new DSState();
        }

        let lastMessageCount = req.__messagesCount;

        // Extract session Id
        if (req._data) {
            let json = JSON.parse(req._data);
            req.__dsState.sessId = json.chat_session_id;
        }

        if (req._reqHeaders && req._reqHeaders["x-client-locale"]) {
            req.__dsState.locale = req._reqHeaders["x-client-locale"];
        }

        let messages = res.split("\n");
        // Process the new messages in the response
        for (let i = lastMessageCount; i < messages.length - 1; i++) {
            let msg = messages[i];
            let data = {};
            req.__messagesCount++;
            if (!msg.startsWith("data: ")) {
                // Here, only lines with "data: " will be considered.
                continue;
            }

            // Extract the data event item, and process
            data = JSON.parse(msg.replace("data:", ""));
            let handleRes = handleEventItem(req, data);
            if (handleRes != "") {
                // This could be a recall message, now replace it
                messages[i] = "data: " + handleRes;
            }
        }

        // If this message get recalled, reconstruct it with lines
        if (req.__dsState.recalled) {
            let res2 = "";
            for (let l = 0; l < messages.length; l++) {
                res2 += messages[l] + "\n";
            }

            return res2;
        }

        return res;
    }

    /**
     *  History message response handler, if recalled message exists, replace them with cached message.
     *  @param {XMLHttpRequest} req - The XHR object
     *  @param {string} res - The response text
     *  @returns {string} The original or modified response
     */
    function onHistoryMessageResp(req, res) {
        let json = JSON.parse(res);
        if (!json.data || !json.data.biz_data) {
            return res;
        }

        if (req._reqHeaders && req._reqHeaders["x-client-locale"]) {
            req.__locale = req._reqHeaders["x-client-locale"];
        }

        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++) {
            // If a message get recalled, its status will be CONTENT_FILTER
            if (data.chat_messages[i].status == CONTENT_FILTER) {
                // Replace the message
                data.chat_messages[i].fragments = getRecalledMessage(req, sessId, data.chat_messages[i].message_id);

                // Replace the message status to finished, otherwise the think progress would not be shown
                data.chat_messages[i].status = "FINISHED";
                modified = true;
            }
        }

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

        // console.log(json);
        return res;
    }

    /**
     * XHR Response handler, return the original or modified (if needed) response
     *  @param {XMLHttpRequest} req - The XHR object
     *  @param {string} res - The response text
     *  @returns {string} The original or modified response
     */
    function onResponse(req, res) {
        if (!req._url) {
            return res;
        }

        const [url] = req._url.split("?");

        // Response handlers
        const routeHandlers = {
            // History message, will replace recalled message with cached ones
            '/api/v0/chat/history_messages': onHistoryMessageResp,

            // Completion APIs, will remove TEMPLATE_RESPONSE if found
            '/api/v0/chat/completion': onEventStreamResp,
            '/api/v0/chat/edit_message': onEventStreamResp,
            '/api/v0/chat/regenerate': onEventStreamResp,
            '/api/v0/chat/continue': onEventStreamResp,
            '/api/v0/chat/resume_stream': onEventStreamResp
        };

        // Find handler
        const handler = routeHandlers[url];

        // If handler exist then call, otherwise return original response
        return handler ? handler(req, res) : res;
    }

    /**
     *  Monkey-patch the XMLHttpResponse object to intercept response
     */
    function installXhrHook() {
        let originXhrResponse = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "response");
        let originXhrResponseText = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "responseText");

        Object.defineProperty(XMLHttpRequest.prototype, "response", {
            get: function() {
                let resp = originXhrResponse.get.call(this);
                resp = onResponse(this, resp);
                return resp;
            },
            set: function(body) {
                return originXhrResponse.set.call(this, body);
            }
        });

        Object.defineProperty(XMLHttpRequest.prototype, "responseText", {
            get: function() {
                let resp = originXhrResponseText.get.call(this);
                resp = onResponse(this, resp);
                return resp;
            },
            set: function(body) {
                return originXhrResponseText.set.call(this, body);
            }
        });
    }

    // Install hook to intercept response
    installXhrHook();
})();