Better Tencent YuanBao

Enhanced UI for Tencent YuanBao chat

  1. // ==UserScript==
  2. // @name Better Tencent YuanBao
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-06-06
  5. // @description Enhanced UI for Tencent YuanBao chat
  6. // @author AAur
  7. // @match https://yuanbao.tencent.com/chat/**
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=yuanbao.tencent.com
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // ========== 通用工具函数 ==========
  17. const waitForElement = (selector) => {
  18. return new Promise(resolve => {
  19. const el = document.querySelector(selector);
  20. if (el) return resolve(el);
  21.  
  22. const container = document.querySelector('.agent-chat__container') || document.body;
  23. const observer = new MutationObserver((_, obs) => {
  24. const target = document.querySelector(selector);
  25. if (target) {
  26. obs.disconnect();
  27. resolve(target);
  28. }
  29. });
  30.  
  31. observer.observe(container, {
  32. childList: true,
  33. subtree: true
  34. });
  35. });
  36. };
  37.  
  38. const debounce = (fn, delay) => {
  39. let timer;
  40. return (...args) => {
  41. clearTimeout(timer);
  42. timer = setTimeout(() => fn.apply(this, args), delay);
  43. };
  44. };
  45.  
  46. const createStyle = (css) => {
  47. const style = document.createElement('style');
  48. style.textContent = css;
  49. document.head.appendChild(style);
  50. return style;
  51. };
  52.  
  53. // ========== 功能1: 底栏收起按钮 ==========
  54. const initToggleButton = async () => {
  55. const inputBox = await waitForElement('.agent-dialogue__content--common__input.agent-chat__input-box');
  56.  
  57. // 添加相关样式
  58. createStyle(`
  59. #inputToggleBtn {
  60. position: fixed;
  61. left: 50%;
  62. transform: translateX(-50%);
  63. z-index: 9999;
  64. padding: 2px 15px;
  65. background: #3db057;
  66. color: white;
  67. border: none;
  68. border-radius: 4px;
  69. cursor: pointer;
  70. font-size: 14px;
  71. opacity: 0.7;
  72. transition: opacity 0.2s, top 0.2s;
  73. }
  74. #inputToggleBtn:hover {
  75. opacity: 1;
  76. }
  77. .hidden-input {
  78. display: none !important;
  79. }
  80. `);
  81.  
  82. // 创建按钮
  83. const toggleBtn = document.createElement('button');
  84. toggleBtn.id = 'inputToggleBtn';
  85. toggleBtn.textContent = 'Hide';
  86.  
  87. inputBox.parentNode.insertBefore(toggleBtn, inputBox);
  88.  
  89. // 更新按钮位置函数
  90. const updateButtonPosition = () => {
  91. const rect = inputBox.getBoundingClientRect();
  92. const btnWidth = toggleBtn.offsetWidth;
  93. const leftPosition = rect.left + (rect.width - btnWidth) / 2;
  94.  
  95. toggleBtn.style.left = `${leftPosition}px`;
  96. toggleBtn.style.top = `${rect.top + window.scrollY - 25}px`;
  97. };
  98.  
  99. // 监听输入框大小变化
  100. const observeInputBoxChanges = () => {
  101. const resizeObserver = new ResizeObserver(debounce(() => {
  102. if (!inputBox.classList.contains('hidden-input')) {
  103. updateButtonPosition();
  104. }
  105. }, 100));
  106. resizeObserver.observe(inputBox);
  107. return resizeObserver;
  108. };
  109.  
  110. let boxObserver = observeInputBoxChanges();
  111. updateButtonPosition();
  112.  
  113. // 按钮点击事件
  114. toggleBtn.addEventListener('click', () => {
  115. if (inputBox.classList.contains('hidden-input')) {
  116. inputBox.classList.remove('hidden-input');
  117. toggleBtn.textContent = 'Hide';
  118. boxObserver = observeInputBoxChanges();
  119. updateButtonPosition();
  120. } else {
  121. inputBox.classList.add('hidden-input');
  122. toggleBtn.textContent = 'Show';
  123. boxObserver.disconnect();
  124. toggleBtn.style.top = `${document.documentElement.scrollHeight - 40}px`;
  125. }
  126. });
  127.  
  128. // 窗口大小调整时更新按钮位置
  129. window.addEventListener('resize', debounce(() => {
  130. if (inputBox.classList.contains('hidden-input')) {
  131. toggleBtn.style.top = `${document.documentElement.scrollHeight - 30}px`;
  132. } else {
  133. updateButtonPosition();
  134. }
  135. }, 100));
  136. };
  137.  
  138. // ========== 功能2: 整理有序列表 ==========
  139. const initOlProcessor = () => {
  140. // 创建按钮样式
  141. createStyle(`
  142. #processOlBtn {
  143. position: fixed;
  144. bottom: 5px;
  145. right: 20px;
  146. z-index: 9999;
  147. padding: 5px 10px;
  148. background: #3db057;
  149. color: white;
  150. border: none;
  151. border-radius: 4px;
  152. cursor: pointer;
  153. font-size: 12px;
  154. opacity: 0.8;
  155. transition: opacity 0.2s;
  156. }
  157. #processOlBtn:hover {
  158. opacity: 1;
  159. }
  160. .numbered-item {
  161. display: block;
  162. margin-bottom: 8px;
  163. line-height: 1.5;
  164. position: relative;
  165. padding-left: 1.5em;
  166. }
  167. `);
  168.  
  169. // 创建并添加整理按钮
  170. const processOlBtn = document.createElement('button');
  171. processOlBtn.id = 'processOlBtn';
  172. processOlBtn.textContent = '整理OL';
  173. document.body.appendChild(processOlBtn);
  174.  
  175. // 计算节点内字符数
  176. const countTextContent = (element) => {
  177. let count = 0;
  178. const walker = document.createTreeWalker(
  179. element,
  180. NodeFilter.SHOW_TEXT,
  181. null,
  182. false
  183. );
  184.  
  185. while (walker.nextNode()) {
  186. count += walker.currentNode.textContent.trim().length;
  187. }
  188. return count;
  189. };
  190.  
  191. // 转换单个OL元素
  192. const processOlElement = (ol) => {
  193. // Mark as processed to avoid duplicate processing
  194. ol.classList.add('processed-ol');
  195.  
  196. // Get direct child LI elements only
  197. const lis = Array.from(ol.querySelectorAll(':scope > li'));
  198. const fragment = document.createDocumentFragment();
  199.  
  200. lis.forEach((li, index) => {
  201. // Create a new div to replace the li
  202. const numberedDiv = document.createElement('div');
  203. numberedDiv.className = 'numbered-item';
  204.  
  205. // Find the first text node inside the li (ignoring whitespace)
  206. function findFirstTextNode(element) {
  207. // 遍历所有子节点
  208. for (const child of element.childNodes) {
  209. if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== '') {
  210. return child; // 找到目标文本节点
  211. } else if (child.nodeType === Node.ELEMENT_NODE) {
  212. const found = findFirstTextNode(child); // 递归搜索子元素
  213. if (found) return found;
  214. }
  215. }
  216. return null; // 未找到
  217. }
  218.  
  219. const firstTextNode = findFirstTextNode(li);
  220.  
  221. if (firstTextNode) {
  222. // 在文本节点前插入序号(保留原文本)
  223. firstTextNode.textContent = `${index + 1}. ${firstTextNode.textContent.trim()}`;
  224. } else {
  225. // 如果没有文本节点,则在 <li> 开头插入序号
  226. const textNode = document.createTextNode(`${index + 1}. `);
  227. li.prepend(textNode);
  228. }
  229.  
  230. // Move all of li's children to the new div
  231. while (li.firstChild) {
  232. numberedDiv.appendChild(li.firstChild);
  233. }
  234.  
  235. fragment.appendChild(numberedDiv);
  236. });
  237.  
  238. // Replace the OL with our new structure
  239. ol.replaceWith(fragment);
  240. };
  241.  
  242. // 处理所有OL元素
  243. const processAllOls = () => {
  244. const ols = document.querySelectorAll('ol:not(.processed-ol)');
  245. let processedCount = 0;
  246.  
  247. ols.forEach(ol => {
  248. if (countTextContent(ol) > 200) {
  249. processOlElement(ol);
  250. processedCount++;
  251. }
  252. });
  253.  
  254. // 显示处理结果
  255. if (processedCount > 0) {
  256. processOlBtn.textContent = `已整理${processedCount}个OL`;
  257. setTimeout(() => {
  258. processOlBtn.textContent = '整理OL';
  259. }, 2000);
  260. } else {
  261. processOlBtn.textContent = '未发现需整理的OL';
  262. setTimeout(() => {
  263. processOlBtn.textContent = '整理OL';
  264. }, 2000);
  265. }
  266. };
  267.  
  268. // 观察内容变化并延迟处理
  269. const observeContentChanges = () => {
  270. const contentElement = document.querySelector('.agent-dialogue__content--common__content');
  271. if (!contentElement) return;
  272.  
  273. let changeTimer;
  274. const observer = new MutationObserver((mutations) => {
  275. // 检查是否有实际内容变化
  276. const hasRelevantChange = mutations.some(mutation =>
  277. mutation.type === 'childList' ||
  278. (mutation.type === 'characterData' && mutation.target.textContent.trim())
  279. );
  280.  
  281. if (hasRelevantChange) {
  282. clearTimeout(changeTimer);
  283. changeTimer = setTimeout(() => {
  284. processAllOls();
  285. }, 500);
  286. }
  287. });
  288.  
  289. observer.observe(contentElement, {
  290. childList: true,
  291. subtree: true,
  292. characterData: true
  293. });
  294.  
  295. return () => observer.disconnect();
  296. };
  297.  
  298. // 初始化内容观察
  299. let cleanupObserver;
  300. const initObserver = () => {
  301. if (cleanupObserver) cleanupObserver();
  302. cleanupObserver = observeContentChanges();
  303. };
  304.  
  305. // 延迟初始化观察器,确保元素已加载
  306. setTimeout(initObserver, 1000);
  307.  
  308. // 按钮点击事件
  309. processOlBtn.addEventListener('click', processAllOls);
  310.  
  311. // 返回initObserver以便外部调用
  312. return initObserver;
  313. };
  314.  
  315. // ========== 主初始化函数 ==========
  316. const main = () => {
  317. const initOlObserver = initOlProcessor(); // 获取返回的initObserver函数
  318. Promise.all([
  319. initToggleButton(),
  320. initOlObserver(),
  321. initOlObserver && initOlObserver() // 如果initOlProcessor返回了initObserver就调用
  322. ]).catch(error => {
  323. console.error('Better Tencent YuanBao initialization error:', error);
  324. });
  325. };
  326.  
  327. // ========== 启动脚本 ==========
  328. if ('requestIdleCallback' in window) {
  329. window.requestIdleCallback(main);
  330. } else {
  331. setTimeout(main, 500);
  332. }
  333. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址