Greasy Fork

Greasy Fork is available in English.

DeepSeek Toolkit Core

Core framework for DeepSeek chat enhancements. Provides plugin API, wide chat view, and anti-recall protection.

当前为 2025-11-09 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/555353/1692445/DeepSeek%20Toolkit%20Core.js

// ==UserScript==
// @name         DeepSeek Toolkit Core
// @namespace    http://tampermonkey.net/
// @version      6.0.0
// @description  Core framework for DeepSeek chat enhancements. Provides plugin API, wide chat view, and anti-recall protection.
// @author       okagame
// @match        https://chat.deepseek.com/*
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deepseek.com
// @license      MIT
// @run-at       document-start
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    console.log('[DeepSeek Toolkit Core] v6.0.0 initializing...');

    // ==================== UTILITY FUNCTIONS ====================

    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve) => {
            const existing = document.querySelector(selector);
            if (existing) {
                resolve(existing);
                return;
            }

            const observer = new MutationObserver(() => {
                const element = document.querySelector(selector);
                if (element) {
                    observer.disconnect();
                    resolve(element);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            setTimeout(() => {
                observer.disconnect();
                resolve(null);
            }, timeout);
        });
    }

    // ==================== WIDE CHAT MODULE ====================

    const WideChat = {
        init() {
            // Inject CSS for wide chat view
            const style = document.createElement('style');
            style.textContent = `
                /* Wide chat container - target by textarea placeholder */
                div:has(> div > textarea[placeholder*="DeepSeek"]),
                div:has(> textarea[placeholder*="DeepSeek"]),
                div:has(> div > textarea[placeholder*="Message"]),
                div:has(> textarea[placeholder*="Message"]),
                div:has(> div > textarea[placeholder*="消息"]),
                div:has(> textarea[placeholder*="消息"]) {
                    width: 95% !important;
                    max-width: 95vw !important;
                }

                :root {
                    --message-list-max-width: calc(100% - 20px) !important;
                    --chat-max-width: 95vw !important;
                }

                [data-message-id] {
                    max-width: 95% !important;
                }

                main [class*="mx-auto"],
                main [class*="max-w"] {
                    max-width: 95% !important;
                }

                pre > div[class*="overflow"] {
                    max-height: 50vh;
                    overflow-y: auto;
                }

                textarea[placeholder*="DeepSeek"],
                textarea[placeholder*="Message"],
                textarea[placeholder*="消息"] {
                    max-width: 100% !important;
                }
            `;
            document.head.appendChild(style);

            // Apply enforcement
            this.enforce();

            // Setup observer
            const observer = new MutationObserver(() => this.enforce());
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            console.log('[Wide Chat] Initialized');
        },

        enforce() {
            const textarea = document.querySelector('textarea[placeholder*="DeepSeek"]') ||
                           document.querySelector('textarea[placeholder*="Message"]') ||
                           document.querySelector('textarea[placeholder*="消息"]');

            if (textarea) {
                let element = textarea;
                for (let i = 0; i < 3 && element; i++) {
                    element.style.width = '95%';
                    element.style.maxWidth = '95vw';
                    element = element.parentElement;
                }
            }

            document.querySelectorAll('[data-message-id]').forEach(msgEl => {
                msgEl.style.maxWidth = '95%';
                const parent = msgEl.parentElement;
                if (parent) parent.style.maxWidth = '95vw';
            });
        }
    };

    // ==================== ANTI-RECALL MODULE ====================

    const AntiRecall = {
        init() {
            // SSE parser
            class SSE {
                static parse(text) {
                    return text.trimEnd()
                        .split('\n\n')
                        .map(event => event.split('\n'))
                        .map(fields => fields.map(field => [...this.split(field, ': ', 2)]))
                        .map(fields => Object.fromEntries(fields));
                }

                static stringify(events) {
                    return events.map(event => Object.entries(event))
                        .map(fields => fields.map(field => field.join(': ')))
                        .map(fields => fields.join('\n'))
                        .join('\n\n') + '\n\n';
                }

                static *split(text, separator, limit) {
                    let lastI = 0;
                    for (let separatorI = text.indexOf(separator), n = 1;
                         separatorI !== -1 && n < limit;
                         separatorI = text.indexOf(separator, separatorI + separator.length), n++) {
                        yield text.slice(lastI, separatorI);
                        lastI = separatorI + separator.length;
                    }
                    yield text.slice(lastI);
                }
            }

            // State management for recalled messages
            class DSState {
                constructor() {
                    this.fields = {};
                    this.sessId = "";
                    this.locale = "en_US";
                    this.recalled = false;
                    this._updatePath = "";
                    this._updateMode = "SET";
                }

                update(data) {
                    let precheck = this.preCheck(data);
                    if (data.p) this._updatePath = data.p;
                    if (data.o) this._updateMode = data.o;
                    let value = data.v;

                    if (typeof value === 'object' && this._updatePath === "") {
                        for (let key in value) this.fields[key] = value[key];
                        return precheck;
                    }

                    this.setField(this._updatePath, value, this._updateMode);
                    return precheck;
                }

                preCheck(data) {
                    let path = data.p || this._updatePath;
                    let mode = data.o || this._updateMode;
                    let modified = false;

                    if (mode === "BATCH" && path === "response") {
                        for (let i = 0; i < data.v.length; i++) {
                            let v = data.v[i];
                            if (v.p === "fragments" && v.v[0].type === "TEMPLATE_RESPONSE") {
                                this.recalled = true;
                                modified = true;

                                const key = `deleted-chat-sess-${this.sessId}-msg-${this.fields.response.message_id}`;
                                localStorage.setItem(key, JSON.stringify(this.fields.response.fragments));

                                const recallTip = this.locale === "zh_CN"
                                    ? "⚠️ 此回复已被撤回,仅在本浏览器存档"
                                    : "⚠️ This response has been recalled and archived only on this browser";

                                data.v[i] = {
                                    "v": [{
                                        "id": this.fields.response.fragments.length + 1,
                                        "type": "TIP",
                                        "style": "WARNING",
                                        "content": recallTip
                                    }],
                                    "p": "fragments",
                                    "o": "APPEND"
                                };
                            }
                        }
                    }

                    return modified ? JSON.stringify(data) : "";
                }

                setField(path, value, mode) {
                    if (mode === "BATCH") {
                        let subMode = "SET";
                        for (let v of value) {
                            if (v.o) subMode = v.o;
                            this.setField(path + "/" + v.p, v.v, subMode);
                        }
                    } else if (mode === "SET") {
                        this._setValueByPath(this.fields, path, value, false);
                    } else if (mode === "APPEND") {
                        this._setValueByPath(this.fields, path, value, true);
                    }
                }

                _setValueByPath(obj, path, value, isAppend) {
                    const keys = path.split("/");
                    let current = obj;

                    for (let i = 0; i < keys.length - 1; i++) {
                        let key = keys[i].match(/^\d+$/) ? parseInt(keys[i]) : keys[i];
                        if (!(key in current)) {
                            const nextKey = keys[i + 1].match(/^\d+$/) ? parseInt(keys[i + 1]) : keys[i + 1];
                            current[key] = typeof nextKey === 'number' ? [] : {};
                        }
                        current = current[key];
                    }

                    const lastKey = keys[keys.length - 1].match(/^\d+$/)
                        ? parseInt(keys[keys.length - 1])
                        : keys[keys.length - 1];

                    if (isAppend) {
                        if (Array.isArray(current[lastKey])) {
                            current[lastKey].push(...value);
                        } else {
                            current[lastKey] = (current[lastKey] || "") + value;
                        }
                    } else {
                        current[lastKey] = value;
                    }
                }
            }

            // Install XHR hook
            const originalResponse = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "response");
            const originalResponseText = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "responseText");

            function handleResponse(req, res) {
                if (!req._url) return res;
                const [url] = req._url.split("?");

                // Handle history messages
                if (url === '/api/v0/chat/history_messages') {
                    try {
                        let json = JSON.parse(res);
                        if (!json.data?.biz_data) return res;

                        const data = json.data.biz_data;
                        const sessId = data.chat_session.id;
                        const locale = req._reqHeaders?.["x-client-locale"] || "en_US";
                        let modified = false;

                        for (let msg of data.chat_messages) {
                            if (msg.status === "CONTENT_FILTER") {
                                const key = `deleted-chat-sess-${sessId}-msg-${msg.message_id}`;
                                const cached = localStorage.getItem(key);

                                if (cached) {
                                    msg.fragments = JSON.parse(cached);
                                    const recallTip = locale === "zh_CN"
                                        ? "⚠️ 此回复已被撤回,仅在本浏览器存档"
                                        : "⚠️ This response has been recalled and archived only on this browser";
                                    msg.fragments.push({
                                        "id": msg.fragments.length + 1,
                                        "type": "TIP",
                                        "style": "WARNING",
                                        "content": recallTip
                                    });
                                } else {
                                    const notFoundMsg = locale === "zh_CN"
                                        ? "⚠️ 此回复已被撤回,且无法在本地缓存中找到"
                                        : "⚠️ This response has been recalled and cannot be found in local cache";
                                    msg.fragments = [{
                                        "content": notFoundMsg,
                                        "id": 2,
                                        "type": "TEMPLATE_RESPONSE"
                                    }];
                                }

                                msg.status = "FINISHED";
                                modified = true;
                            }
                        }

                        if (modified) return JSON.stringify(json);
                    } catch (e) {
                        console.error('[Anti-Recall] Error:', e);
                    }
                    return res;
                }

                // Handle completion streams
                const streamEndpoints = [
                    '/api/v0/chat/completion',
                    '/api/v0/chat/edit_message',
                    '/api/v0/chat/regenerate',
                    '/api/v0/chat/continue',
                    '/api/v0/chat/resume_stream'
                ];

                if (!streamEndpoints.includes(url)) return res;

                if (!req.__dsState) {
                    req.__dsState = new DSState();
                    req.__messagesCount = 0;

                    if (req._data) {
                        const json = JSON.parse(req._data);
                        req.__dsState.sessId = json.chat_session_id;
                    }

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

                const messages = res.split("\n");
                const lastCount = req.__messagesCount;

                for (let i = lastCount; i < messages.length - 1; i++) {
                    let msg = messages[i];
                    req.__messagesCount++;

                    if (!msg.startsWith("data: ")) continue;

                    try {
                        const data = JSON.parse(msg.replace("data:", ""));
                        const modified = req.__dsState.update(data);
                        if (modified) messages[i] = "data: " + modified;
                    } catch (e) {
                        continue;
                    }
                }

                if (req.__dsState.recalled) {
                    return messages.join("\n");
                }

                return res;
            }

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

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

            console.log('[Anti-Recall] XHR hook installed');
        }
    };

    // ==================== PLUGIN FRAMEWORK ====================

    window.DeepSeekToolkit = {
        version: '6.0.0',
        tools: [],
        actionBar: null,
        iconButtonArea: null,
        initialized: false,

        registerTool(config) {
            // Validate required fields
            if (!config || !config.id || !config.type || !config.style) {
                console.error('[Toolkit] Registration failed: missing required fields', config);
                return false;
            }

            // Validate type/style combination
            if (config.type === 'toggle' && config.style !== 'toggle-button') {
                console.error('[Toolkit] Toggle tools must use style: "toggle-button"', config);
                return false;
            }
            if (config.type === 'action' && config.style !== 'icon-button') {
                console.error('[Toolkit] Action tools must use style: "icon-button"', config);
                return false;
            }

            // Check for duplicates
            if (this.tools.find(t => t.id === config.id)) {
                console.warn('[Toolkit] Tool already registered:', config.id);
                return false;
            }

            // Add to registry
            this.tools.push(config);
            console.log('[Toolkit] Tool registered:', config.id);

            // Inject if already initialized
            if (this.initialized) {
                this._injectTool(config);
            }

            return true;
        },

        async init() {
            if (this.initialized) return;

            console.log('[Toolkit] Initializing plugin framework...');

            // Wait for action bar
            this.actionBar = await waitForElement('.ec4f5d61');
            if (!this.actionBar) {
                console.error('[Toolkit] Action bar not found, retrying...');
                setTimeout(() => this.init(), 2000);
                return;
            }

            // Find icon button area
            this.iconButtonArea = this.actionBar.querySelector('.bf38813a');

            this.initialized = true;

            // Inject all registered tools
            this.tools.forEach(tool => this._injectTool(tool));

            // Setup observer for re-injection
            this._setupObserver();

            console.log('[Toolkit] Framework initialized');
        },

        _injectTool(config) {
            if (!this.actionBar) {
                console.warn('[Toolkit] Cannot inject: action bar not ready');
                return;
            }

            // Check if already injected
            if (document.getElementById(`toolkit-${config.id}`)) {
                return;
            }

            if (config.style === 'toggle-button') {
                this._injectToggleButton(config);
            } else if (config.style === 'icon-button') {
                this._injectIconButton(config);
            }
        },

        _injectToggleButton(config) {
            const button = document.createElement('button');
            button.role = 'button';
            button.setAttribute('aria-disabled', 'false');
            button.className = 'ds-atom-button f79352dc ds-toggle-button ds-toggle-button--md';
            button.style.transform = 'translateZ(0px)';
            button.tabIndex = 0;
            button.id = `toolkit-${config.id}`;
            button.title = config.name || config.id;

            const iconSvg = config.icon || '<svg width="14" height="14"><circle cx="7" cy="7" r="5" fill="currentColor"/></svg>';
            const nameText = config.name || config.id;

            button.innerHTML = `
                <div class="ds-icon ds-atom-button__icon" style="font-size: 14px; width: 14px; height: 14px; margin-right: 0px;">
                    ${iconSvg}
                </div>
                <span class=""><span class="_6dbc175">${escapeHtml(nameText)}</span></span>
            `;

            // Set initial state
            if (config.initialState && config.initialState.enabled) {
                button.classList.add('ds-toggle-button--selected');
            }

            // Add click handler
            button.addEventListener('click', () => {
                const isEnabled = button.classList.toggle('ds-toggle-button--selected');
                if (config.onToggle) {
                    try {
                        config.onToggle(isEnabled);
                    } catch (e) {
                        console.error(`[Toolkit] Tool ${config.id} error:`, e);
                    }
                }
            });

            // Insert after Search button
            const searchButton = Array.from(this.actionBar.querySelectorAll('.ds-toggle-button'))
                .find(btn => btn.textContent.includes('Search') || btn.textContent.includes('搜索'));

            if (searchButton) {
                searchButton.parentNode.insertBefore(button, searchButton.nextSibling);
            } else {
                this.actionBar.insertBefore(button, this.actionBar.firstChild);
            }
        },

        _injectIconButton(config) {
            if (!this.iconButtonArea) {
                console.warn('[Toolkit] Icon button area not found');
                return;
            }

            const wrapper = document.createElement('div');
            wrapper.className = 'bf38813a';
            wrapper.dataset.toolkitId = config.id;

            const button = document.createElement('div');
            button.className = 'ds-icon-button f02f0e25';
            button.style.cssText = '--hover-size: 34px; width: 34px; height: 34px;';
            button.tabIndex = 0;
            button.role = 'button';
            button.setAttribute('aria-disabled', 'false');
            button.id = `toolkit-${config.id}`;
            button.title = config.name || config.id;

            const iconSvg = config.icon || '<svg width="16" height="16"><circle cx="8" cy="8" r="6" fill="currentColor"/></svg>';

            button.innerHTML = `
                <div class="ds-icon-button__hover-bg"></div>
                <div class="ds-icon" style="font-size: 16px; width: 16px; height: 16px;">
                    ${iconSvg}
                </div>
            `;

            button.addEventListener('click', () => {
                if (config.onClick) {
                    try {
                        config.onClick();
                    } catch (e) {
                        console.error(`[Toolkit] Tool ${config.id} error:`, e);
                    }
                }
            });

            wrapper.appendChild(button);
            this.iconButtonArea.parentNode.insertBefore(wrapper, this.iconButtonArea);
        },

        _setupObserver() {
            const observer = new MutationObserver(() => {
                const missingTools = this.tools.filter(tool =>
                    !document.getElementById(`toolkit-${tool.id}`)
                );

                if (missingTools.length > 0) {
                    missingTools.forEach(tool => this._injectTool(tool));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    };

    // ==================== INITIALIZATION ====================

    async function initialize() {
        // Wait for DOM
        if (document.readyState === 'loading') {
            await new Promise(resolve => {
                document.addEventListener('DOMContentLoaded', resolve, { once: true });
            });
        }

        // Initialize core modules
        WideChat.init();
        AntiRecall.init();

        // Initialize plugin framework
        await window.DeepSeekToolkit.init();

        console.log('[DeepSeek Toolkit Core] Fully initialized');
    }

    // Start
    initialize().catch(error => {
        console.error('[Toolkit] Initialization failed:', error);
        setTimeout(initialize, 3000);
    });

})();