System Prompt Editor for Qwen Chat

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

  1. // ==UserScript==
  2. // @name System Prompt Editor for Qwen Chat
  3. // @name:ru Редактор системного промпта для Qwen Chat
  4. // @namespace https://chat.qwen.ai/
  5. // @version 2025-06-19
  6. // @description Adds the ability to modify system prompts in the Qwen Chat interface to customize AI behavior.
  7. // @description:ru Добавляет возможность изменения системных промптов в интерфейсе Qwen Chat для настройки поведения ИИ.
  8. // @author Mikhail Zuenko
  9. // @match https://chat.qwen.ai/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=qwen.ai
  11. // @grant unsafeWindow
  12. // ==/UserScript==
  13.  
  14. function generateUUID() {
  15. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
  16. const r = Math.random() * 16 | 0;
  17. const v = c === 'x' ? r : (r & 0x3) | 0x8;
  18. return v.toString(16);
  19. });
  20. }
  21.  
  22. function getSystemPromptMessage(data) {
  23. const rootMessages = Object.values(data.chat.history.messages).filter(msg => msg.parentId === null);
  24. const promptMessage = rootMessages.find(msg => msg.role === 'system');
  25. return promptMessage || null;
  26. }
  27.  
  28. function setSystemPrompt(data, systemPrompt) {
  29. const promptMessage = getSystemPromptMessage(data);
  30. if (promptMessage) {
  31. promptMessage.content = systemPrompt;
  32. data.chat.messages.find(msg => msg.id === promptMessage.id).content = systemPrompt;
  33. }
  34. else {
  35. const rootMessages = Object.values(data.chat.history.messages).filter(msg => msg.parentId === null);
  36.  
  37. const promptMessage = {
  38. id: generateUUID(),
  39. parentId: null,
  40. childrenIds: rootMessages.map(msg => msg.id),
  41. role: 'system',
  42. content: systemPrompt
  43. };
  44. for (const message of rootMessages) {
  45. message.parentId = promptMessage.id;
  46. }
  47. data.chat.history.messages[promptMessage.id] = promptMessage;
  48.  
  49. let firstIndex = null;
  50. for (let msg = 0; msg < data.chat.messages.length; ++msg) {
  51. if (data.chat.messages[msg].parentId === null) {
  52. data.chat.messages[msg].parentId = promptMessage.id;
  53. if (firstIndex === null) firstIndex = msg;
  54. }
  55. }
  56. data.chat.messages.splice(firstIndex, 0, promptMessage);
  57. }
  58. }
  59.  
  60. function deleteSystemPrompt(data) {
  61. const promptMessage = getSystemPromptMessage(data);
  62. if (!promptMessage) return;
  63.  
  64. const children = promptMessage.childrenIds;
  65. for (const childId of children) {
  66. data.chat.history.messages[childId].parentId = null;
  67. }
  68. for (const message of data.chat.messages) {
  69. if (children.includes(message.id)) message.parentId = null;
  70. }
  71. data.chat.messages.splice(data.chat.messages.findIndex(msg => msg.id === promptMessage.id), 1);
  72. delete data.chat.history.messages[promptMessage.id];
  73. }
  74.  
  75. let origFetch = unsafeWindow.fetch;
  76. unsafeWindow.fetch = async (input, init) => {
  77. if (init && init._noChange) return origFetch(input, init);
  78. if (init && input === '/api/chat/completions') {
  79. const body = JSON.parse(init.body);
  80. const promptMessage = getSystemPromptMessage(await request('/api/v1/chats/' + body.chat_id));
  81. if (promptMessage) {
  82. body.messages.unshift({ role: 'system', content: promptMessage.content });
  83. init.body = JSON.stringify(body);
  84. }
  85. }
  86. else if (typeof input === 'string') {
  87. 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)) {
  88. if (init && init.method === 'POST') {
  89. const promptMessage = getSystemPromptMessage(await request(input));
  90. if (promptMessage) {
  91. const body = JSON.parse(init.body);
  92. setSystemPrompt(body, promptMessage.content);
  93. init.body = JSON.stringify(body);
  94. }
  95. }
  96. const res = await origFetch(input, init);
  97. const data = await res.json();
  98. deleteSystemPrompt(data);
  99. return new Response(JSON.stringify(data), {
  100. status: res.status,
  101. statusText: res.statusText,
  102. headers: res.headers
  103. });
  104. }
  105. }
  106. return origFetch(input, init);
  107. };
  108.  
  109. function getIdFromUrl() {
  110. 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})\/?$/);
  111. return path ? path[1] : null;
  112. }
  113. function request(input, init) {
  114. return unsafeWindow.fetch(input, { _noChange: true, ...init }).then(res => res.json());
  115. }
  116.  
  117. function $E(tag, props, children) {
  118. const elem = document.createElement(tag);
  119. for (const prop in props) {
  120. if (prop.startsWith('on')) elem.addEventListener(prop.slice(2).toLowerCase(), props[prop]);
  121. else if (prop === 'classes') elem.classList.add(props[prop]);
  122. else {
  123. const snakeProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
  124. if (props[prop] === true) elem.setAttribute(snakeProp, '');
  125. else elem.setAttribute(snakeProp, props[prop]);
  126. }
  127. }
  128. elem.append(...children);
  129. return elem;
  130. }
  131. function $T(text) {
  132. return document.createTextNode(text || '');
  133. }
  134.  
  135. const textarea = $E('textarea', {
  136. class: 'block w-full h-[200px] p-2 bg-white dark:bg-[#2A2A2A] text-[#2C2C36] dark:text-[#FAFAFC] rounded-lg resize-none',
  137. placeholder: 'How should I answer you?'
  138. }, [])
  139.  
  140. const button = $E(
  141. 'button',
  142. {
  143. class: 'ml-1',
  144. async onClick() {
  145. const id = getIdFromUrl();
  146. if (id) {
  147. const data = await request('/api/v1/chats/' + id);
  148. const promptMessage = getSystemPromptMessage(data);
  149. textarea.value = promptMessage ? promptMessage.content : '';
  150.  
  151. document.body.append(editor);
  152. document.addEventListener('keydown', escCloseEditor);
  153. }
  154. }
  155. },
  156. [$E('i', { class: 'iconfont leading-none icon-line-message-circle-02 w-[32px] h-[32px] chat-input-feature-btn !mr-[0]' }, [])]
  157. );
  158.  
  159. const editor = $E('div', {
  160. class: 'modal fixed inset-0 z-[9999] flex h-full w-full items-center justify-center overflow-hidden bg-black/60',
  161. onMousedown: closeEditor
  162. }, [
  163. $E('div', {
  164. 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',
  165. onMousedown: event => event.stopPropagation()
  166. }, [
  167. $E('div', { class: 'flex justify-between px-5 pb-1 pt-4 dark:text-gray-300' }, [
  168. $E('div', { class: 'self-center text-lg font-medium' }, [$T('System Prompt')]),
  169. $E('button', {
  170. class: 'self-center',
  171. onClick: closeEditor
  172. }, [
  173. $E('i', { class: 'iconfont leading-none icon-line-x-02 font-bold' }, [])
  174. ])
  175. ]),
  176. $E('div', { class: 'px-4 pt-1' }, [textarea]),
  177. $E('div', { class: 'flex justify-end p-4 pt-3 text-sm font-medium' }, [
  178. $E('button', {
  179. 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',
  180. async onClick() {
  181. closeEditor();
  182.  
  183. const id = getIdFromUrl();
  184. if (id) {
  185. const data = await request('/api/v1/chats/' + id);
  186.  
  187. if (textarea.value === '') deleteSystemPrompt(data);
  188. else setSystemPrompt(data, textarea.value);
  189.  
  190. request('/api/v1/chats/' + id, {
  191. method: 'POST',
  192. headers: { 'Content-Type': 'application/json' },
  193. body: JSON.stringify(data)
  194. });
  195. }
  196. }
  197. }, [$T('Save')])
  198. ])
  199. ])
  200. ]);
  201.  
  202. function closeEditor() {
  203. editor.remove();
  204. document.removeEventListener('keydown', escCloseEditor);
  205. }
  206. function escCloseEditor(event) {
  207. if (event.code === 'Escape') closeEditor();
  208. }
  209.  
  210. let lastId = getIdFromUrl();
  211. addEventListener('popstate', () => {
  212. const newId = getIdFromUrl();
  213. if (lastId === newId) return;
  214. closeEditor();
  215. lastId = newId;
  216. });
  217.  
  218. new MutationObserver(() => {
  219. const elem = document.querySelector('div:has(>.operationBtn)>:first-child');
  220. if (document.getElementById('messages-container') && elem && button.parentNode !== elem) {
  221. elem.append(button);
  222. }
  223. }).observe(document.body, { childList: true, subtree: true });