// ==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 // @downloadURL https://update.greasyfork.icu/scripts/532623/System%20Prompt%20Editor%20for%20Qwen%20Chat.user.js // @updateURL https://update.greasyfork.icu/scripts/532623/System%20Prompt%20Editor%20for%20Qwen%20Chat.meta.js // ==/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 });