Greasy Fork is available in English.

System Prompt Editor for Qwen Chat

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

נכון ליום 12-04-2025. ראה הגרסה האחרונה.

// ==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 });