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