System Prompt Editor for Qwen Chat

Adds the ability to modify system prompts in the Qwen Chat interface to customize AI behavior.

当前为 2025-04-12 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           System Prompt Editor for Qwen Chat
// @name:ru        Редактор системного промпта для Qwen Chat
// @namespace      https://chat.qwen.ai/
// @version        2025-04-12
// @description    Adds the ability to modify system prompts in the Qwen Chat interface to customize AI behavior.
// @description:ru Добавляет возможность изменения системных промптов в интерфейсе Qwen Chat для настройки поведения ИИ.
// @author         Mikhail Zuenko
// @match          https://chat.qwen.ai/*
// @icon           https://www.google.com/s2/favicons?sz=64&domain=qwen.ai
// @grant          unsafeWindow
// ==/UserScript==

function generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3) | 0x8;
        return v.toString(16);
    });
}

function getSystemPromptMessage(data) {
    const rootMessages = Object.values(data.chat.history.messages).filter(msg => msg.parentId === null);
    const promptMessage = rootMessages.find(msg => msg.role === 'system');
    return promptMessage || null;
}

function setSystemPrompt(data, systemPrompt) {
    const promptMessage = getSystemPromptMessage(data);
    if (promptMessage) {
        promptMessage.content = systemPrompt;
        data.chat.messages.find(msg => msg.id === promptMessage.id).content = systemPrompt;
    }
    else {
        const rootMessages = Object.values(data.chat.history.messages).filter(msg => msg.parentId === null);

        const promptMessage = {
            id: generateUUID(),
            parentId: null,
            childrenIds: rootMessages.map(msg => msg.id),
            role: 'system',
            content: systemPrompt
        };
        for (const message of rootMessages) {
            message.parentId = promptMessage.id;
        }
        data.chat.history.messages[promptMessage.id] = promptMessage;

        let firstIndex = null;
        for (let msg = 0; msg < data.chat.messages.length; ++msg) {
            if (data.chat.messages[msg].parentId === null) {
                data.chat.messages[msg].parentId = promptMessage.id;
                if (firstIndex === null) firstIndex = msg;
            }
        }
        data.chat.messages.splice(firstIndex, 0, promptMessage);
    }
}

function deleteSystemPrompt(data) {
    const promptMessage = getSystemPromptMessage(data);
    if (!promptMessage) return;

    const children = promptMessage.childrenIds;
    for (const childId of children) {
        data.chat.history.messages[childId].parentId = null;
    }
    for (const message of data.chat.messages) {
        if (children.includes(message.id)) message.parentId = null;
    }
    data.chat.messages.splice(data.chat.messages.findIndex(msg => msg.id === promptMessage.id), 1);
    delete data.chat.history.messages[promptMessage.id];
}

let origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async (input, init) => {
    if (init && init._noChange) return origFetch(input, init);
    if (init && input === '/api/chat/completions') {
        const body = JSON.parse(init.body);
        const promptMessage = getSystemPromptMessage(await request('/api/v1/chats/' + body.chat_id));
        if (promptMessage) {
            body.messages.unshift({ role: 'system', content: promptMessage.content });
            init.body = JSON.stringify(body);
        }
    }
    else if (typeof input === 'string') {
        if (/^\/api\/v1\/chats\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/?$/.test(input)) {
            if (init && init.method === 'POST') {
                const promptMessage = getSystemPromptMessage(await request(input));
                if (promptMessage) {
                    const body = JSON.parse(init.body);
                    setSystemPrompt(body, promptMessage.content);
                    init.body = JSON.stringify(body);
                }
            }
            const res = await origFetch(input, init);
            const data = await res.json();
            deleteSystemPrompt(data);
            return new Response(JSON.stringify(data), {
                status: res.status,
                statusText: res.statusText,
                headers: res.headers
            });
        }
    }
    return origFetch(input, init);
};

function getIdFromUrl() {
    const path = location.pathname.match(/^\/c\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/?$/);
    return path ? path[1] : null;
}
function request(input, init) {
    return unsafeWindow.fetch(input, { _noChange: true, ...init }).then(res => res.json());
}

function $E(tag, props, children) {
    const elem = document.createElement(tag);
    for (const prop in props) {
        if (prop.startsWith('on')) elem.addEventListener(prop.slice(2).toLowerCase(), props[prop]);
        else if (prop === 'classes') elem.classList.add(props[prop]);
        else {
            const snakeProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
            if (props[prop] === true) elem.setAttribute(snakeProp, '');
            else elem.setAttribute(snakeProp, props[prop]);
        }
    }
    elem.append(...children);
    return elem;
}
function $T(text) {
    return document.createTextNode(text || '');
}

const textarea = $E('textarea', {
    class: 'block w-full h-[200px] p-2 bg-white dark:bg-[#2A2A2A] text-[#2C2C36] dark:text-[#FAFAFC] rounded-lg resize-none',
    placeholder: 'How should I answer you?'
}, [])

const button = $E(
    'button',
    {
        class: 'flex-none size-9 cursor-pointer rounded-xl transition hover:bg-gray-50 dark:hover:bg-gray-850',
        async onClick() {
            const id = getIdFromUrl();
            if (id) {
                const data = await request('/api/v1/chats/' + id);
                const promptMessage = getSystemPromptMessage(data);
                textarea.value = promptMessage ? promptMessage.content : '';

                document.body.append(editor);
                document.addEventListener('keydown', escCloseEditor);
            }
        }
    },
    [$E('i', { class: 'iconfont leading-none icon-line-message-circle-02' }, [])]
);

const editor = $E('div', {
    class: 'modal fixed inset-0 z-[9999] flex h-full w-full items-center justify-center overflow-hidden bg-black/60',
    onMousedown: closeEditor
}, [
    $E('div', {
        class: 'm-auto max-w-full w-[480px] mx-2 shadow-3xl scrollbar-hidden max-h-[90vh] overflow-y-auto bg-gray-50 dark:bg-gray-900 rounded-2xl',
        onMousedown: event => event.stopPropagation()
    }, [
        $E('div', { class: 'flex justify-between px-5 pb-1 pt-4 dark:text-gray-300' }, [
            $E('div', { class: 'self-center text-lg font-medium' }, [$T('System Prompt')]),
            $E('button', {
                class: 'self-center',
                onClick: closeEditor
            }, [
                $E('i', { class: 'iconfont leading-none icon-line-x-02 font-bold' }, [])
            ])
        ]),
        $E('div', { class: 'px-4 pt-1' }, [textarea]),
        $E('div', { class: 'flex justify-end p-4 pt-3 text-sm font-medium' }, [
            $E('button', {
                class: 'dark:purple-500 dark:hover:purple-400 rounded-full bg-purple-500 px-3.5 py-1.5 text-sm font-medium text-white transition hover:bg-purple-400',
                async onClick() {
                    closeEditor();

                    const id = getIdFromUrl();
                    if (id) {
                        const data = await request('/api/v1/chats/' + id);

                        if (textarea.value === '') deleteSystemPrompt(data);
                        else setSystemPrompt(data, textarea.value);

                        request('/api/v1/chats/' + id, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(data)
                        });
                    }
                }
            }, [$T('Save')])
        ])
    ])
]);

function closeEditor() {
    editor.remove();
    document.removeEventListener('keydown', escCloseEditor);
}
function escCloseEditor(event) {
    if (event.code === 'Escape') closeEditor();
}

let lastId = getIdFromUrl();
addEventListener('popstate', () => {
    const newId = getIdFromUrl();
    if (lastId === newId) return;
    closeEditor();
    lastId = newId;
});

new MutationObserver(() => {
    const elem = document.querySelector('#chat-container :has(>[aria-label])>div:not(:has(>button)):not(:empty)');
    if (elem && button.previousElementSibling !== elem) {
        elem.after(button);
    }
}).observe(document.body, { childList: true, subtree: true });