Multi-site Chat Manager

Manage chats across different platforms (GitHub Copilot, Flomo, Doubao)

  1. // ==UserScript==
  2. // @name Multi-site Chat Manager
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Manage chats across different platforms (GitHub Copilot, Flomo, Doubao)
  6. // @author Your name
  7. // @match https://github.com/copilot*
  8. // @match https://v.flomoapp.com/mine
  9. // @match https://www.doubao.com/chat/thread/list*
  10. // @icon https://v.flomoapp.com/favicon.ico
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Common styles for buttons
  18. const buttonStyles = {
  19. base: {
  20. padding: '8px 16px',
  21. border: 'none',
  22. borderRadius: '6px',
  23. cursor: 'pointer',
  24. color: 'white',
  25. fontSize: '14px',
  26. fontWeight: '500',
  27. boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
  28. transition: 'all 0.3s ease',
  29. margin: '5px'
  30. },
  31. green: {
  32. backgroundColor: '#2ea44f',
  33. '&:hover': {
  34. backgroundColor: '#2c974b'
  35. }
  36. },
  37. red: {
  38. backgroundColor: '#d73a49',
  39. '&:hover': {
  40. backgroundColor: '#cb2431'
  41. }
  42. }
  43. };
  44.  
  45. // Apply styles to button
  46. function applyButtonStyles(button, type = 'base') {
  47. Object.assign(button.style, buttonStyles.base);
  48. if (type === 'green') {
  49. Object.assign(button.style, buttonStyles.green);
  50. } else if (type === 'red') {
  51. Object.assign(button.style, buttonStyles.red);
  52. }
  53.  
  54. // Add hover effect
  55. button.addEventListener('mouseenter', () => {
  56. button.style.transform = 'translateY(-1px)';
  57. button.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
  58. });
  59.  
  60. button.addEventListener('mouseleave', () => {
  61. button.style.transform = 'translateY(0)';
  62. button.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)';
  63. });
  64. }
  65.  
  66. // Site configurations
  67. const siteConfigs = {
  68. 'github.com': {
  69. init: function() {
  70. this.waitForChatList();
  71. },
  72.  
  73. waitForChatList: function() {
  74. const observer = new MutationObserver((mutations, obs) => {
  75. if (document.querySelector('.ConversationList-module__ConversationList__item--dD6z4')) {
  76. this.addButtons();
  77. obs.disconnect();
  78. }
  79. });
  80.  
  81. observer.observe(document.body, {
  82. childList: true,
  83. subtree: true
  84. });
  85. },
  86.  
  87. addButtons: function() {
  88. const buttonContainer = document.createElement('div');
  89. buttonContainer.style.position = 'fixed';
  90. buttonContainer.style.bottom = '20px';
  91. buttonContainer.style.left = '20px';
  92. buttonContainer.style.zIndex = '9999';
  93. buttonContainer.style.display = 'flex';
  94. buttonContainer.style.flexDirection = 'column';
  95. buttonContainer.style.gap = '10px';
  96.  
  97. const openChatsButton = document.createElement('button');
  98. openChatsButton.textContent = '打开chat';
  99. applyButtonStyles(openChatsButton, 'green');
  100.  
  101. const clearChatsButton = document.createElement('button');
  102. clearChatsButton.textContent = '清空chat';
  103. applyButtonStyles(clearChatsButton, 'red');
  104.  
  105. buttonContainer.appendChild(openChatsButton);
  106. buttonContainer.appendChild(clearChatsButton);
  107. document.body.appendChild(buttonContainer);
  108.  
  109. openChatsButton.addEventListener('click', this.openAllChats);
  110. clearChatsButton.addEventListener('click', this.clearAllChats);
  111. },
  112.  
  113. openAllChats: function() {
  114. const chatLinks = document.querySelectorAll('.ConversationList-module__ConversationList__link--Byc2c');
  115. chatLinks.forEach(link => {
  116. const newWindow = window.open(link.href, '_blank');
  117. if (newWindow) {
  118. newWindow.addEventListener('load', () => {
  119. newWindow.scrollTo(0, 0);
  120. });
  121. }
  122. });
  123. },
  124.  
  125. clearAllChats: async function() {
  126. const kebabButtons = document.querySelectorAll('button[data-component="IconButton"]');
  127.  
  128. for (const button of kebabButtons) {
  129. if (button.closest('.ConversationList-module__ConversationList__item--dD6z4')) {
  130. button.click();
  131. await new Promise(resolve => setTimeout(resolve, 1000));
  132.  
  133. const deleteButton = Array.from(document.querySelectorAll('li[role="menuitem"]'))
  134. .find(item => item.textContent.includes('Delete'));
  135.  
  136. if (deleteButton) {
  137. deleteButton.click();
  138. await new Promise(resolve => setTimeout(resolve, 1000));
  139. }
  140. }
  141. }
  142. }
  143. },
  144.  
  145. 'flomoapp.com': {
  146. init: function() {
  147. this.addClearButton();
  148. },
  149.  
  150. addClearButton: function() {
  151. const button = document.createElement('button');
  152. button.textContent = '清空笔记';
  153. button.style.position = 'fixed';
  154. button.style.bottom = '20px';
  155. button.style.left = '20px';
  156. button.style.zIndex = '9999';
  157.  
  158. applyButtonStyles(button, 'red');
  159.  
  160. button.onclick = () => {
  161. if (confirm('确定要清空笔记吗?')) {
  162. this.scrollAndCheck();
  163. }
  164. };
  165.  
  166. document.body.appendChild(button);
  167. },
  168.  
  169. scrollToBottom: function() {
  170. const element = document.querySelector('.memos');
  171. if (element) {
  172. element.scrollTop = element.scrollHeight;
  173. }
  174. },
  175.  
  176. isScrolledToBottom: function() {
  177. const element = document.querySelector('.end');
  178. return element ? element.getBoundingClientRect().bottom <= window.innerHeight : false;
  179. },
  180.  
  181. scrollAndCheck: function() {
  182. this.scrollToBottom();
  183.  
  184. if (!this.isScrolledToBottom()) {
  185. console.log('No element with class "end" was found, continue scrolling...');
  186. setTimeout(() => this.scrollAndCheck(), 1000);
  187. } else {
  188. console.log('页面已下滑到最底部!');
  189. const elements = document.querySelectorAll('.item.danger');
  190.  
  191. elements.forEach(element => {
  192. if (element.textContent.includes('删除')) {
  193. element.click();
  194. }
  195. });
  196. }
  197. }
  198. },
  199.  
  200. 'doubao.com': {
  201. init: function() {
  202. this.addButtons();
  203. },
  204.  
  205. addButtons: function() {
  206. const buttonContainer = document.createElement('div');
  207. buttonContainer.style.position = 'fixed';
  208. buttonContainer.style.bottom = '20px';
  209. buttonContainer.style.left = '20px';
  210. buttonContainer.style.zIndex = '9999';
  211. buttonContainer.style.display = 'flex';
  212. buttonContainer.style.flexDirection = 'column';
  213. buttonContainer.style.gap = '10px';
  214.  
  215. const openChatsButton = document.createElement('button');
  216. openChatsButton.textContent = '打开chat';
  217. applyButtonStyles(openChatsButton, 'green');
  218.  
  219. const clearChatsButton = document.createElement('button');
  220. clearChatsButton.textContent = '清空chat';
  221. applyButtonStyles(clearChatsButton, 'red');
  222.  
  223. buttonContainer.appendChild(openChatsButton);
  224. buttonContainer.appendChild(clearChatsButton);
  225. document.body.appendChild(buttonContainer);
  226.  
  227. openChatsButton.addEventListener('click', this.openAllChats);
  228. clearChatsButton.addEventListener('click', this.clearAllChats);
  229. },
  230.  
  231. openAllChats: async function() {
  232. const chatItems = document.querySelectorAll('[data-testid="thread_detail_item"]');
  233. console.log(`找到 ${chatItems.length} 个聊天项`);
  234.  
  235. if (!chatItems || chatItems.length === 0) {
  236. console.log('没有找到聊天项');
  237. return;
  238. }
  239.  
  240. // 存储原始页面的滚动位置
  241. const originalScroll = window.scrollY;
  242.  
  243. const maxItems = chatItems.length;
  244.  
  245. for (let i = 0; i < maxItems; i++) {
  246. try {
  247. console.log(`\n===== 处理第 ${i + 1}/${maxItems} 个聊天 =====`);
  248.  
  249. const currentChatItems = document.querySelectorAll('[data-testid="thread_detail_item"]');
  250. if (!currentChatItems[i]) {
  251. console.log('❌ 未找到目标聊天项,刷新页面');
  252. location.reload();
  253. await new Promise(resolve => setTimeout(resolve, 400));
  254. continue;
  255. }
  256.  
  257. const chatItem = currentChatItems[i];
  258.  
  259. chatItem.scrollIntoView({ behavior: "smooth", block: "center" });
  260. await new Promise(resolve => setTimeout(resolve, 200));
  261.  
  262. const beforeClickUrl = window.location.href;
  263. console.log('点击前URL:', beforeClickUrl);
  264.  
  265. try {
  266. chatItem.click();
  267. console.log('原生点击已执行');
  268. } catch (clickError) {
  269. console.log('原生点击失败,尝试模拟点击事件');
  270. ['mouseenter', 'mousedown', 'mouseup', 'click'].forEach(eventName => {
  271. chatItem.dispatchEvent(new MouseEvent(eventName, {
  272. view: window,
  273. bubbles: true,
  274. cancelable: true,
  275. buttons: eventName === 'mousedown' ? 1 : 0
  276. }));
  277. });
  278. }
  279.  
  280. let newUrl = '';
  281. let attempts = 0;
  282. const maxAttempts = 20;
  283.  
  284. while (attempts < maxAttempts) {
  285. await new Promise(resolve => setTimeout(resolve, 100));
  286. newUrl = window.location.href;
  287. if (newUrl !== beforeClickUrl && !newUrl.includes('/thread/list')) {
  288. console.log('✓ URL已变化到具体的chat页面');
  289. break;
  290. }
  291. attempts++;
  292. }
  293.  
  294. if (newUrl === beforeClickUrl || newUrl.includes('/thread/list')) {
  295. console.log('❌ URL未能成功变化到具体chat页面,尝试重新加载页面');
  296. location.reload();
  297. await new Promise(resolve => setTimeout(resolve, 400));
  298. continue;
  299. }
  300.  
  301. window.open(newUrl, '_blank');
  302.  
  303. console.log('返回列表页面...');
  304. history.back();
  305.  
  306. attempts = 0;
  307. while (attempts < maxAttempts) {
  308. await new Promise(resolve => setTimeout(resolve, 100));
  309. const currentUrl = window.location.href;
  310. if (currentUrl.includes('/thread/list')) {
  311. console.log('✓ 成功返回列表页面');
  312. await new Promise(resolve => setTimeout(resolve, 400));
  313. break;
  314. }
  315. attempts++;
  316. }
  317.  
  318. } catch (error) {
  319. console.error('处理聊天项时出错:', error);
  320. location.reload();
  321. await new Promise(resolve => setTimeout(resolve, 400));
  322. }
  323. }
  324.  
  325. window.scrollTo(0, originalScroll);
  326. console.log('全部处理完成');
  327. },
  328.  
  329. clearAllChats: async function() {
  330. const menuButtons = document.querySelectorAll('.chat-item-menu-button-outline-Ic2b7D');
  331. console.log(`找到 ${menuButtons.length} 个菜单按钮`);
  332.  
  333. for (const menuButton of menuButtons) {
  334. try {
  335. console.log('\n===== 处理新的聊天项 =====');
  336.  
  337. // 点击菜单按钮
  338. menuButton.querySelector('div').click();
  339. await new Promise(resolve => setTimeout(resolve, 200));
  340.  
  341. // 查找删除按钮
  342. const deleteButton = document.querySelector('li.remove-btn-TOaQi0.semi-dropdown-item');
  343. if (!deleteButton) {
  344. console.log('该菜单无删除按钮,点击空白处关闭菜单');
  345. document.body.click();
  346. await new Promise(resolve => setTimeout(resolve, 100));
  347. console.log('继续处理下一个');
  348. continue;
  349. }
  350.  
  351. // 尝试多种点击方法
  352. const clickMethods = [
  353. // 方法1:点击整个li元素
  354. () => deleteButton.click(),
  355. // 方法2:点击内部的div
  356. () => deleteButton.querySelector('.semi-dropdown-item-icon').click(),
  357. // 方法3:完整事件模拟
  358. () => {
  359. ['mousedown', 'mouseup', 'click'].forEach(eventName => {
  360. deleteButton.dispatchEvent(new MouseEvent(eventName, {
  361. view: window,
  362. bubbles: true,
  363. cancelable: true,
  364. buttons: eventName === 'mousedown' ? 1 : 0
  365. }));
  366. });
  367. }
  368. ];
  369.  
  370. // 尝试每种点击方法
  371. let clicked = false;
  372. for (const method of clickMethods) {
  373. try {
  374. method();
  375. // 等待确认对话框
  376. await new Promise(resolve => setTimeout(resolve, 100));
  377. const confirmButton = document.querySelector('button.semi-button-danger');
  378. if (confirmButton) {
  379. clicked = true;
  380. console.log('成功触发删除按钮');
  381. break;
  382. }
  383. } catch (e) {
  384. console.log('点击方法失败,尝试下一种');
  385. }
  386. }
  387.  
  388. if (!clicked) {
  389. console.log('自动点击失败,请手动点击删除按钮');
  390. document.body.click();
  391. await new Promise(resolve => setTimeout(resolve, 100));
  392. continue;
  393. }
  394.  
  395. // 点击确认删除按钮
  396. const confirmButton = document.querySelector('button.semi-button-danger');
  397. if (confirmButton) {
  398. confirmButton.click();
  399. console.log('点击确认删除');
  400. await new Promise(resolve => setTimeout(resolve, 400));
  401. }
  402.  
  403. } catch (error) {
  404. console.error('删除出错:', error);
  405. document.body.click();
  406. await new Promise(resolve => setTimeout(resolve, 100));
  407. }
  408. }
  409.  
  410. console.log('\n全部处理完成!');
  411. },
  412. }
  413. };
  414.  
  415. // Get current domain and execute corresponding code
  416. const domain = window.location.hostname.replace('www.', '').split('.').slice(-2).join('.');
  417. const config = siteConfigs[domain];
  418.  
  419. if (config) {
  420. config.init();
  421. }
  422. })();