AI网页内容智能总结助手

网页内容智能总结,支持自定义API和提示词

  1. // ==UserScript==
  2. // @name AI Page Summarizer Pro
  3. // @name:zh-CN AI网页内容智能总结助手
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.1
  6. // @description 网页内容智能总结,支持自定义API和提示词
  7. // @description:zh-CN 网页内容智能总结,支持自定义API和提示词
  8. // @author Your Name
  9. // @match *://*/*
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_addStyle
  15. // @grant GM.xmlHttpRequest
  16. // @grant GM.setValue
  17. // @grant GM.getValue
  18. // @grant GM.registerMenuCommand
  19. // @grant GM.addStyle
  20. // @grant window.fetch
  21. // @grant window.localStorage
  22. // @connect api.openai.com
  23. // @connect *
  24. // @require https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js
  25. // @run-at document-start
  26. // @noframes
  27. // @license MIT
  28. // @compatible chrome
  29. // @compatible firefox
  30. // @compatible edge
  31. // @compatible opera
  32. // @compatible safari
  33. // @compatible android
  34. // ==/UserScript==
  35.  
  36. (function() {
  37. 'use strict';
  38.  
  39. // 添加全局错误处理
  40. window.addEventListener('error', function(event) {
  41. console.error('脚本错误:', event.error);
  42. if (event.error && event.error.stack) {
  43. console.error('错误堆栈:', event.error.stack);
  44. }
  45. });
  46.  
  47. window.addEventListener('unhandledrejection', function(event) {
  48. console.error('未处理的Promise错误:', event.reason);
  49. });
  50.  
  51. // 兼容性检查
  52. const browserSupport = {
  53. hasGM: typeof GM !== 'undefined',
  54. hasGMFunctions: typeof GM_getValue !== 'undefined',
  55. hasLocalStorage: (function() {
  56. try {
  57. localStorage.setItem('test', 'test');
  58. localStorage.removeItem('test');
  59. return true;
  60. } catch (e) {
  61. return false;
  62. }
  63. })(),
  64. hasBackdropFilter: (function() {
  65. const el = document.createElement('div');
  66. return typeof el.style.backdropFilter !== 'undefined' ||
  67. typeof el.style.webkitBackdropFilter !== 'undefined';
  68. })(),
  69. isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
  70. isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
  71. };
  72.  
  73. // 兼容性处理层
  74. const scriptHandler = {
  75. // 存储值
  76. setValue: async function(key, value) {
  77. try {
  78. if (browserSupport.hasGMFunctions) {
  79. GM_setValue(key, value);
  80. return true;
  81. } else if (browserSupport.hasGM && GM.setValue) {
  82. await GM.setValue(key, value);
  83. return true;
  84. } else if (browserSupport.hasLocalStorage) {
  85. localStorage.setItem('ws_' + key, JSON.stringify(value));
  86. return true;
  87. }
  88. return false;
  89. } catch (error) {
  90. console.error('存储值失败:', error);
  91. return false;
  92. }
  93. },
  94. // 获取值
  95. getValue: async function(key, defaultValue) {
  96. try {
  97. if (browserSupport.hasGMFunctions) {
  98. return GM_getValue(key, defaultValue);
  99. } else if (browserSupport.hasGM && GM.getValue) {
  100. return await GM.getValue(key, defaultValue);
  101. } else if (browserSupport.hasLocalStorage) {
  102. const value = localStorage.getItem('ws_' + key);
  103. return value ? JSON.parse(value) : defaultValue;
  104. }
  105. return defaultValue;
  106. } catch (error) {
  107. console.error('获取值失败:', error);
  108. return defaultValue;
  109. }
  110. },
  111. // HTTP请求
  112. xmlHttpRequest: function(details) {
  113. return new Promise((resolve, reject) => {
  114. const handleResponse = (response) => {
  115. resolve(response);
  116. };
  117.  
  118. const handleError = (error) => {
  119. reject(new Error('请求错误: ' + error.message));
  120. };
  121.  
  122. if (browserSupport.hasGMFunctions && typeof GM_xmlhttpRequest !== 'undefined') {
  123. GM_xmlhttpRequest({
  124. ...details,
  125. onload: handleResponse,
  126. onerror: handleError,
  127. ontimeout: details.ontimeout
  128. });
  129. } else if (browserSupport.hasGM && typeof GM !== 'undefined' && GM.xmlHttpRequest) {
  130. GM.xmlHttpRequest({
  131. ...details,
  132. onload: handleResponse,
  133. onerror: handleError,
  134. ontimeout: details.ontimeout
  135. });
  136. } else {
  137. fetch(details.url, {
  138. method: details.method,
  139. headers: details.headers,
  140. body: details.data,
  141. mode: 'cors',
  142. credentials: 'omit'
  143. })
  144. .then(async response => {
  145. const text = await response.text();
  146. handleResponse({
  147. status: response.status,
  148. responseText: text,
  149. responseHeaders: [...response.headers].join('\n')
  150. });
  151. })
  152. .catch(handleError);
  153. }
  154. }).then(response => {
  155. if (details.onload) {
  156. details.onload(response);
  157. }
  158. return response;
  159. }).catch(error => {
  160. if (details.onerror) {
  161. details.onerror(error);
  162. }
  163. throw error;
  164. });
  165. },
  166. // 注册(不可用)菜单命令
  167. registerMenuCommand: function(name, fn) {
  168. try {
  169. if (browserSupport.hasGMFunctions) {
  170. GM_registerMenuCommand(name, fn);
  171. return true;
  172. } else if (browserSupport.hasGM && GM.registerMenuCommand) {
  173. GM.registerMenuCommand(name, fn);
  174. return true;
  175. }
  176. return false;
  177. } catch (error) {
  178. console.log('注册(不可用)菜单命令失败:', error);
  179. return false;
  180. }
  181. },
  182. // 添加样式
  183. addStyle: function(css) {
  184. try {
  185. if (browserSupport.hasGMFunctions) {
  186. GM_addStyle(css);
  187. return true;
  188. } else if (browserSupport.hasGM && GM.addStyle) {
  189. GM.addStyle(css);
  190. return true;
  191. } else {
  192. const style = document.createElement('style');
  193. style.textContent = css;
  194. document.head.appendChild(style);
  195. return true;
  196. }
  197. } catch (error) {
  198. console.error('添加样式失败:', error);
  199. return false;
  200. }
  201. }
  202. };
  203.  
  204. // 配置项
  205. let config = {
  206. apiUrl: 'https://api.openai.com/v1/chat/completions',
  207. apiKey: '',
  208. model: 'gpt-3.5-turbo',
  209. theme: 'light',
  210. prompt: `You are a professional content summarizer in chinese. Your task is to create a clear, concise, and well-structured summary of the webpage content. Follow these guidelines:
  211.  
  212. 1. Output Format:
  213. - Use ## for main sections
  214. - Use bullet points (•) for key points and details
  215. - Use bold for important terms
  216. - Use blockquotes for notable quotes
  217.  
  218. 2. Content Structure:
  219. ## 核心观点
  220. Key points here...
  221.  
  222. ## 关键信息
  223. Important details here...
  224.  
  225. ## 市场情绪
  226. Market sentiment here...
  227.  
  228. ## 专家观点
  229. Expert opinions here...
  230.  
  231. ## 总结
  232. Final summary here...
  233.  
  234. 3. Writing Style:
  235. - Clear and concise language
  236. - Professional tone
  237. - Logical flow
  238. - Easy to understand
  239. - Focus on essential information
  240.  
  241. 4. Important Rules:
  242. - DO NOT show your reasoning process
  243. - DO NOT include meta-commentary
  244. - DO NOT explain your methodology
  245. - DO NOT use phrases like "this summary shows" or "the content indicates"
  246. - Start directly with the content summary
  247. - Make sure bullet points (•) are in the same line with text
  248. - Use ## for main section headers
  249.  
  250. Remember: Focus on delivering the information directly without any meta-analysis or explanation of your process.`,
  251. iconPosition: { y: 20 },
  252. shortcut: 'option+a',
  253. summaryWindowPosition: null // 用于存储摘要窗口的位置 {left, top}
  254. };
  255.  
  256. // 初始化配置
  257. async function initConfig() {
  258. config.apiUrl = await scriptHandler.getValue('apiUrl', config.apiUrl);
  259. config.apiKey = await scriptHandler.getValue('apiKey', config.apiKey);
  260. config.model = await scriptHandler.getValue('model', config.model);
  261. config.prompt = await scriptHandler.getValue('prompt', config.prompt);
  262. config.iconPosition = await scriptHandler.getValue('iconPosition', config.iconPosition || { y: 20 });
  263. config.shortcut = await scriptHandler.getValue('shortcut', config.shortcut);
  264. config.theme = await scriptHandler.getValue('theme', config.theme);
  265. config.summaryWindowPosition = await scriptHandler.getValue('summaryWindowPosition', null);
  266. console.log('加载的图标位置配置:', config.iconPosition);
  267. if (config.summaryWindowPosition) {
  268. console.log('加载的摘要窗口位置配置:', config.summaryWindowPosition);
  269. }
  270. }
  271.  
  272. // DOM 元素引用
  273. const elements = {
  274. icon: null,
  275. container: null,
  276. settings: null,
  277. backdrop: null
  278. };
  279.  
  280. // 全局变量用于判断是否已经监听了键盘事件
  281. let keyboardListenerActive = false;
  282.  
  283. // 显示提示消息
  284. function showToast(message) {
  285. const toast = document.createElement('div');
  286. toast.textContent = message;
  287. const baseStyle = `
  288. position: fixed;
  289. left: 50%;
  290. transform: translateX(-50%);
  291. background: #4CAF50;
  292. color: white;
  293. padding: ${browserSupport.isMobile ? '12px 24px' : '10px 20px'};
  294. border-radius: 4px;
  295. z-index: 1000001;
  296. font-size: ${browserSupport.isMobile ? '16px' : '14px'};
  297. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  298. text-align: center;
  299. max-width: ${browserSupport.isMobile ? '90%' : '300px'};
  300. word-break: break-word;
  301. `;
  302. // 在移动设备上显示在底部,否则显示在顶部
  303. const position = browserSupport.isMobile ?
  304. 'bottom: 80px;' :
  305. 'top: 20px;';
  306. toast.style.cssText = baseStyle + position;
  307. document.body.appendChild(toast);
  308. setTimeout(() => {
  309. toast.style.opacity = '0';
  310. toast.style.transition = 'opacity 0.3s ease';
  311. setTimeout(() => toast.remove(), 300);
  312. }, 2000);
  313. }
  314.  
  315. // 快捷键处理
  316. const keyManager = {
  317. setup() {
  318. try {
  319. // 移除旧的监听器
  320. if (keyboardListenerActive) {
  321. document.removeEventListener('keydown', this._handleKeyDown);
  322. }
  323.  
  324. // 添加新的监听器
  325. this._handleKeyDown = (e) => {
  326. // 忽略输入框中的按键
  327. if (e.target.tagName === 'INPUT' ||
  328. e.target.tagName === 'TEXTAREA' ||
  329. e.target.isContentEditable ||
  330. e.target.getAttribute('role') === 'textbox') {
  331. return;
  332. }
  333.  
  334. // 解析配置的快捷键
  335. const shortcutParts = config.shortcut.toLowerCase().split('+');
  336. // 获取主键(非修饰键)
  337. const mainKey = shortcutParts.filter(part =>
  338. !['alt', 'option', 'ctrl', 'control', 'shift', 'cmd', 'command', 'meta']
  339. .includes(part)
  340. )[0] || 'a';
  341. // 检查所需的修饰键
  342. const needAlt = shortcutParts.some(p => p === 'alt' || p === 'option');
  343. const needCtrl = shortcutParts.some(p => p === 'ctrl' || p === 'control');
  344. const needShift = shortcutParts.some(p => p === 'shift');
  345. const needMeta = shortcutParts.some(p => p === 'cmd' || p === 'command' || p === 'meta');
  346. // 检查按键是否匹配
  347. const isMainKeyMatched =
  348. e.key.toLowerCase() === mainKey ||
  349. e.code.toLowerCase() === 'key' + mainKey ||
  350. e.keyCode === mainKey.toUpperCase().charCodeAt(0);
  351. // 检查修饰键是否匹配
  352. const modifiersMatch =
  353. e.altKey === needAlt &&
  354. e.ctrlKey === needCtrl &&
  355. e.shiftKey === needShift &&
  356. e.metaKey === needMeta;
  357. if (isMainKeyMatched && modifiersMatch) {
  358. console.log('快捷键触发成功:', config.shortcut);
  359. e.preventDefault();
  360. e.stopPropagation();
  361. showSummary();
  362. return false;
  363. }
  364. };
  365. // 使用捕获阶段来确保我们能先捕获到事件
  366. document.addEventListener('keydown', this._handleKeyDown, true);
  367. keyboardListenerActive = true;
  368. // 设置全局访问方法
  369. window.activateSummary = showSummary;
  370. console.log('快捷键已设置:', config.shortcut);
  371. return true;
  372. } catch (error) {
  373. console.error('设置快捷键失败:', error);
  374. return false;
  375. }
  376. }
  377. };
  378.  
  379. // 等待页面加载完成
  380. function waitForPageLoad() {
  381. if (document.readyState === 'loading') {
  382. document.addEventListener('DOMContentLoaded', initializeScript);
  383. } else {
  384. initializeScript();
  385. }
  386. }
  387.  
  388. // 保存配置数据
  389. async function saveConfig() {
  390. try {
  391. await scriptHandler.setValue('apiUrl', config.apiUrl);
  392. await scriptHandler.setValue('apiKey', config.apiKey);
  393. await scriptHandler.setValue('model', config.model);
  394. await scriptHandler.setValue('prompt', config.prompt);
  395. await scriptHandler.setValue('iconPosition', config.iconPosition);
  396. await scriptHandler.setValue('shortcut', config.shortcut);
  397. await scriptHandler.setValue('theme', config.theme);
  398. console.log('配置已保存');
  399. return true;
  400. } catch (error) {
  401. console.error('保存配置失败:', error);
  402. return false;
  403. }
  404. }
  405.  
  406. // 为Safari创建专用存储对象
  407. function createSafariStorage() {
  408. // 内存缓存
  409. const memoryCache = {};
  410. return {
  411. getValue: async function(key, defaultValue) {
  412. try {
  413. // 优先从localStorage获取
  414. if (browserSupport.hasLocalStorage) {
  415. const storedValue = localStorage.getItem('ws_' + key);
  416. if (storedValue !== null) {
  417. return JSON.parse(storedValue);
  418. }
  419. }
  420. // 返回内存缓存或默认值
  421. return key in memoryCache ? memoryCache[key] : defaultValue;
  422. } catch (error) {
  423. console.error(`Safari存储读取失败 [${key}]:`, error);
  424. return defaultValue;
  425. }
  426. },
  427. setValue: async function(key, value) {
  428. try {
  429. // 尝试写入localStorage
  430. if (browserSupport.hasLocalStorage) {
  431. localStorage.setItem('ws_' + key, JSON.stringify(value));
  432. }
  433. // 同时写入内存缓存
  434. memoryCache[key] = value;
  435. return true;
  436. } catch (error) {
  437. console.error(`Safari存储写入失败 [${key}]:`, error);
  438. // 仅写入内存缓存
  439. memoryCache[key] = value;
  440. return false;
  441. }
  442. }
  443. };
  444. }
  445.  
  446. // 修复Safari的拖拽和显示问题
  447. function fixSafariIssues() {
  448. if (!browserSupport.isSafari) return;
  449. console.log('应用Safari兼容性修复');
  450. // 为Safari添加特定CSS
  451. const safariCSS = `
  452. #website-summary-icon,
  453. #website-summary-container,
  454. #website-summary-settings {
  455. -webkit-user-select: none !important;
  456. user-select: none !important;
  457. -webkit-touch-callout: none !important;
  458. touch-action: none !important;
  459. }
  460. #website-summary-content {
  461. -webkit-user-select: text !important;
  462. user-select: text !important;
  463. touch-action: auto !important;
  464. }
  465. `;
  466. scriptHandler.addStyle(safariCSS);
  467. }
  468.  
  469. // 初始化脚本处理程序
  470. function initScriptHandler() {
  471. // 检测Safari浏览器
  472. if (browserSupport.isSafari) {
  473. console.log('检测到Safari浏览器,应用特殊兼容');
  474. // 创建Safari特定存储
  475. const safariStorage = createSafariStorage();
  476. // 修改scriptHandler中的存储方法
  477. const originalGetValue = scriptHandler.getValue;
  478. const originalSetValue = scriptHandler.setValue;
  479. // 覆盖getValue方法
  480. scriptHandler.getValue = async function(key, defaultValue) {
  481. try {
  482. // 先尝试原有方法
  483. const result = await originalGetValue.call(scriptHandler, key, defaultValue);
  484. // 如果获取失败或返回undefined,使用Safari存储
  485. if (result === undefined || result === null) {
  486. console.log(`标准存储获取失败,使用Safari存储 [${key}]`);
  487. return await safariStorage.getValue(key, defaultValue);
  488. }
  489. return result;
  490. } catch (error) {
  491. console.error(`getValue失败 [${key}]:`, error);
  492. return await safariStorage.getValue(key, defaultValue);
  493. }
  494. };
  495. // 覆盖setValue方法
  496. scriptHandler.setValue = async function(key, value) {
  497. try {
  498. // 同时尝试原有方法和Safari存储
  499. const originalResult = await originalSetValue.call(scriptHandler, key, value);
  500. const safariResult = await safariStorage.setValue(key, value);
  501. // 只要有一个成功就返回成功
  502. return originalResult || safariResult;
  503. } catch (error) {
  504. console.error(`setValue失败 [${key}]:`, error);
  505. // 尝试使用Safari存储作为后备
  506. return await safariStorage.setValue(key, value);
  507. }
  508. };
  509. // 应用Safari特定修复
  510. fixSafariIssues();
  511. }
  512. }
  513.  
  514. // 初始化脚本
  515. async function initializeScript() {
  516. try {
  517. // 初始化ScriptHandler
  518. initScriptHandler();
  519. // 等待marked库加载
  520. await waitForMarked();
  521. // 初始化配置
  522. await initConfig();
  523. // 添加全局样式
  524. addGlobalStyles();
  525. // 创建图标
  526. createIcon();
  527. // 设置快捷键
  528. keyManager.setup();
  529. // 注册(不可用)菜单命令
  530. registerMenuCommands();
  531. console.log('AI Page Summarizer Pro 初始化完成');
  532. } catch (error) {
  533. console.error('初始化失败:', error);
  534. }
  535. }
  536.  
  537. // 等待marked库加载
  538. function waitForMarked() {
  539. return new Promise((resolve) => {
  540. if (window.marked) {
  541. window.marked.setOptions({ breaks: true, gfm: true });
  542. resolve();
  543. } else {
  544. const checkMarked = setInterval(() => {
  545. if (window.marked) {
  546. clearInterval(checkMarked);
  547. window.marked.setOptions({ breaks: true, gfm: true });
  548. resolve();
  549. }
  550. }, 100);
  551. // 10秒后超时
  552. setTimeout(() => {
  553. clearInterval(checkMarked);
  554. console.warn('marked库加载超时,继续初始化');
  555. resolve();
  556. }, 10000);
  557. }
  558. });
  559. }
  560.  
  561. // 添加全局样式
  562. function addGlobalStyles() {
  563. const css = `
  564. #website-summary-icon * {
  565. box-sizing: border-box !important;
  566. margin: 0 !important;
  567. padding: 0 !important;
  568. }
  569. #website-summary-icon span {
  570. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
  571. line-height: 1 !important;
  572. }
  573. `;
  574. scriptHandler.addStyle(css);
  575. }
  576.  
  577. // 创建图标
  578. function createIcon() {
  579. // 检查是否已存在图标
  580. const existingIcon = document.getElementById('website-summary-icon');
  581. if (existingIcon) {
  582. existingIcon.remove();
  583. }
  584.  
  585. // 创建图标元素
  586. const icon = document.createElement('div');
  587. icon.id = 'website-summary-icon';
  588. icon.innerHTML = '💡';
  589. // 从配置中获取保存的位置
  590. const savedPosition = config.iconPosition || {};
  591. const hasValidPosition = typeof savedPosition.x === 'number' && typeof savedPosition.y === 'number';
  592. // 计算位置样式
  593. let positionStyle = '';
  594. if (hasValidPosition) {
  595. // 使用保存的精确位置
  596. positionStyle = `
  597. top: ${savedPosition.y}px !important;
  598. left: ${savedPosition.x}px !important;
  599. right: auto !important;
  600. bottom: auto !important;
  601. `;
  602. } else {
  603. // 使用默认位置
  604. positionStyle = `
  605. bottom: 20px !important;
  606. right: 20px !important;
  607. `;
  608. }
  609. // 设置图标样式
  610. icon.style.cssText = `
  611. position: fixed;
  612. z-index: 2147483647 !important;
  613. ${positionStyle}
  614. width: auto !important;
  615. height: auto !important;
  616. padding: 8px !important;
  617. font-size: ${browserSupport.isMobile ? '20px' : '24px'} !important;
  618. line-height: 1 !important;
  619. cursor: pointer !important;
  620. user-select: none !important;
  621. -webkit-user-select: none !important;
  622. visibility: visible !important;
  623. opacity: 0.8;
  624. transition: opacity 0.3s ease !important;
  625. border-radius: 8px !important;
  626. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1) !important;
  627. `;
  628.  
  629. // 添加鼠标悬停效果
  630. icon.addEventListener('mouseover', () => {
  631. icon.style.opacity = '1';
  632. });
  633.  
  634. icon.addEventListener('mouseout', () => {
  635. icon.style.opacity = '0.8';
  636. });
  637.  
  638. // 添加点击事件
  639. icon.addEventListener('click', async (e) => {
  640. e.preventDefault();
  641. e.stopPropagation();
  642. await showSummary();
  643. });
  644.  
  645. // 修改右键菜单处理方式
  646. icon.addEventListener('contextmenu', (e) => {
  647. e.preventDefault();
  648. e.stopPropagation();
  649. showSettings();
  650. });
  651.  
  652. // 支持双击打开设置(为Safari增加额外的交互方式)
  653. let lastClickTime = 0;
  654. icon.addEventListener('click', (e) => {
  655. const currentTime = new Date().getTime();
  656. if (currentTime - lastClickTime < 300) { // 双击间隔300ms
  657. e.preventDefault();
  658. e.stopPropagation();
  659. showSettings();
  660. }
  661. lastClickTime = currentTime;
  662. });
  663.  
  664. // 添加优化的拖动功能
  665. makeIconDraggable(icon);
  666.  
  667. // 确保 body 存在后再添加图标
  668. if (document.body) {
  669. document.body.appendChild(icon);
  670. } else {
  671. document.addEventListener('DOMContentLoaded', () => {
  672. document.body.appendChild(icon);
  673. });
  674. }
  675.  
  676. // 将图标引用存储到elements对象中
  677. elements.icon = icon;
  678. }
  679. // 专门为图标设计的拖动函数
  680. function makeIconDraggable(icon) {
  681. let isDragging = false;
  682. let startX, startY, startLeft, startTop;
  683. // 鼠标/触摸开始事件
  684. function handleStart(e) {
  685. isDragging = true;
  686. // 记录初始位置
  687. const rect = icon.getBoundingClientRect();
  688. startLeft = rect.left;
  689. startTop = rect.top;
  690. // 记录鼠标/触摸起始位置
  691. if (e.type === 'touchstart') {
  692. startX = e.touches[0].clientX;
  693. startY = e.touches[0].clientY;
  694. } else {
  695. startX = e.clientX;
  696. startY = e.clientY;
  697. e.preventDefault(); // 防止选中文本
  698. }
  699. // 设置拖动时的样式
  700. icon.style.transition = 'none';
  701. icon.style.opacity = '1';
  702. // 添加移动和结束事件监听
  703. if (e.type === 'touchstart') {
  704. document.addEventListener('touchmove', handleMove, { passive: false });
  705. document.addEventListener('touchend', handleEnd);
  706. } else {
  707. document.addEventListener('mousemove', handleMove);
  708. document.addEventListener('mouseup', handleEnd);
  709. }
  710. }
  711. // 鼠标/触摸移动事件
  712. function handleMove(e) {
  713. if (!isDragging) return;
  714. let moveX, moveY;
  715. if (e.type === 'touchmove') {
  716. moveX = e.touches[0].clientX - startX;
  717. moveY = e.touches[0].clientY - startY;
  718. e.preventDefault(); // 防止页面滚动
  719. } else {
  720. moveX = e.clientX - startX;
  721. moveY = e.clientY - startY;
  722. }
  723. // 计算新位置
  724. let newLeft = startLeft + moveX;
  725. let newTop = startTop + moveY;
  726. // 边界检查
  727. newLeft = Math.max(0, Math.min(window.innerWidth - icon.offsetWidth, newLeft));
  728. newTop = Math.max(0, Math.min(window.innerHeight - icon.offsetHeight, newTop));
  729. // 更新位置
  730. icon.style.left = `${newLeft}px`;
  731. icon.style.top = `${newTop}px`;
  732. icon.style.right = 'auto';
  733. icon.style.bottom = 'auto';
  734. }
  735. // 鼠标/触摸结束事件
  736. function handleEnd() {
  737. if (!isDragging) return;
  738. isDragging = false;
  739. // 移除事件监听
  740. document.removeEventListener('mousemove', handleMove);
  741. document.removeEventListener('mouseup', handleEnd);
  742. document.removeEventListener('touchmove', handleMove);
  743. document.removeEventListener('touchend', handleEnd);
  744. // 保存新位置
  745. const rect = icon.getBoundingClientRect();
  746. config.iconPosition = {
  747. x: rect.left,
  748. y: rect.top
  749. };
  750. // 持久化保存位置
  751. saveIconPosition();
  752. // 恢复透明度过渡效果
  753. icon.style.transition = 'opacity 0.3s ease';
  754. if (!icon.matches(':hover')) {
  755. icon.style.opacity = '0.8';
  756. }
  757. }
  758. // 添加事件监听
  759. icon.addEventListener('mousedown', handleStart);
  760. icon.addEventListener('touchstart', handleStart, { passive: false });
  761. // 处理窗口大小变化
  762. window.addEventListener('resize', () => {
  763. const rect = icon.getBoundingClientRect();
  764. // 如果图标超出视口范围,调整位置
  765. if (rect.right > window.innerWidth) {
  766. icon.style.left = `${window.innerWidth - icon.offsetWidth}px`;
  767. }
  768. if (rect.bottom > window.innerHeight) {
  769. icon.style.top = `${window.innerHeight - icon.offsetHeight}px`;
  770. }
  771. // 更新保存的位置
  772. config.iconPosition = {
  773. x: parseInt(icon.style.left),
  774. y: parseInt(icon.style.top)
  775. };
  776. // 持久化保存位置
  777. saveIconPosition();
  778. });
  779. }
  780.  
  781. // 保存图标位置
  782. function saveIconPosition() {
  783. scriptHandler.setValue('iconPosition', config.iconPosition);
  784. console.log('图标位置已保存:', config.iconPosition);
  785. }
  786.  
  787. // 显示设置界面
  788. function showSettings() {
  789. try {
  790. const settings = elements.settings || createSettingsUI();
  791. settings.style.display = 'block';
  792. showBackdrop();
  793. setTimeout(() => settings.style.opacity = '1', 10);
  794. } catch (error) {
  795. console.error('显示设置界面失败:', error);
  796. alert('无法显示设置界面,请检查控制台以获取详细信息');
  797. }
  798. }
  799.  
  800. // 显示摘要
  801. async function showSummary() {
  802. const container = elements.container || createSummaryUI();
  803. const content = container.querySelector('#website-summary-content');
  804. // 如果容器有自定义位置,保持原位置;否则重置到屏幕中心
  805. const hasCustomPosition = container.hasAttribute('data-positioned');
  806. if (!hasCustomPosition) {
  807. container.style.left = '50%';
  808. container.style.top = '50%';
  809. container.style.transform = 'translate(-50%, -50%)';
  810. }
  811. // 显示容器和背景
  812. showBackdrop();
  813. container.style.display = 'block';
  814. setTimeout(() => container.style.opacity = '1', 10);
  815. // 显示加载中
  816. content.innerHTML = `<p style="text-align: center; color: ${config.theme === 'dark' ? '#bdc1c6' : '#666'};">正在获取总结...</p>`;
  817. try {
  818. // 获取页面内容
  819. const pageContent = getPageContent();
  820. if (!pageContent || pageContent.trim().length === 0) {
  821. throw new Error('无法获取页面内容');
  822. }
  823. console.log('页面内容长度:', pageContent.length);
  824. console.log('API配置:', {
  825. url: config.apiUrl,
  826. model: config.model,
  827. contentLength: pageContent.length
  828. });
  829. // 获取总结
  830. const summary = await getSummary(pageContent);
  831. if (!summary || summary.trim().length === 0) {
  832. throw new Error('API返回内容为空');
  833. }
  834. // 添加样式并渲染内容
  835. addMarkdownStyles();
  836. await renderContent(summary);
  837. } catch (error) {
  838. console.error('总结失败:', error);
  839. content.innerHTML = `
  840. <p style="text-align: center; color: #ff4444;">
  841. 获取总结失败:${error.message}<br>
  842. <small style="color: ${config.theme === 'dark' ? '#bdc1c6' : '#666'};">
  843. 请检查控制台以获取详细错误信息
  844. </small>
  845. </p>`;
  846. }
  847. }
  848.  
  849. // 创建/显示背景
  850. function showBackdrop() {
  851. if (!elements.backdrop) {
  852. const backdrop = document.createElement('div');
  853. backdrop.id = 'website-summary-backdrop';
  854. const isDark = config.theme === 'dark';
  855. backdrop.style.cssText = `
  856. position: fixed;
  857. top: 0;
  858. left: 0;
  859. width: 100%;
  860. height: 100%;
  861. background-color: ${isDark ? 'rgba(32, 33, 36, 0.75)' : 'rgba(250, 250, 252, 0.75)'};
  862. backdrop-filter: blur(5px);
  863. z-index: 999997;
  864. display: none;
  865. opacity: 0;
  866. transition: opacity 0.3s ease;
  867. `;
  868. backdrop.addEventListener('click', (e) => {
  869. if (e.target === backdrop) {
  870. hideUI();
  871. }
  872. });
  873. document.body.appendChild(backdrop);
  874. elements.backdrop = backdrop;
  875. } else {
  876. // 更新背景颜色以匹配当前主题
  877. const isDark = config.theme === 'dark';
  878. elements.backdrop.style.backgroundColor = isDark ? 'rgba(32, 33, 36, 0.75)' : 'rgba(250, 250, 252, 0.75)';
  879. }
  880. elements.backdrop.style.display = 'block';
  881. setTimeout(() => elements.backdrop.style.opacity = '1', 10);
  882. }
  883.  
  884. // 隐藏UI
  885. function hideUI() {
  886. // 隐藏背景
  887. if (elements.backdrop) {
  888. elements.backdrop.style.opacity = '0';
  889. setTimeout(() => elements.backdrop.style.display = 'none', 300);
  890. }
  891. // 隐藏摘要容器
  892. if (elements.container) {
  893. elements.container.style.opacity = '0';
  894. setTimeout(() => elements.container.style.display = 'none', 300);
  895. }
  896. }
  897.  
  898. // 创建摘要UI
  899. function createSummaryUI() {
  900. const container = document.createElement('div');
  901. container.id = 'website-summary-container';
  902. const isDark = config.theme === 'dark';
  903. let styles = `
  904. position: fixed;
  905. z-index: 999998;
  906. background: ${isDark ? darkColors.containerBg : 'rgba(255, 255, 255, 0.98)'};
  907. color: ${isDark ? darkColors.text : '#333'};
  908. border-radius: ${browserSupport.isMobile ? '8px' : '12px'};
  909. box-shadow: 0 8px 32px ${isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(0, 0, 0, 0.08)'};
  910. padding: ${browserSupport.isMobile ? '12px' : '16px'};
  911. width: ${browserSupport.isMobile ? '92%' : '80%'};
  912. max-width: ${browserSupport.isMobile ? '100%' : '800px'};
  913. max-height: ${browserSupport.isMobile ? '85vh' : '80vh'};
  914. font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Roboto, sans-serif;
  915. display: none;
  916. left: 50%;
  917. top: 50%;
  918. transform: translate(-50%, -50%);
  919. overflow: hidden;
  920. opacity: 0;
  921. transition: opacity 0.3s ease;
  922. will-change: transform;
  923. -webkit-backface-visibility: hidden;
  924. backface-visibility: hidden;
  925. `;
  926.  
  927. // 添加backdrop-filter(如果支持)
  928. if (browserSupport.hasBackdropFilter) {
  929. styles += 'backdrop-filter: blur(10px);';
  930. styles += '-webkit-backdrop-filter: blur(10px);';
  931. }
  932.  
  933. container.style.cssText = styles;
  934.  
  935. // 标题栏
  936. const header = document.createElement('div');
  937. header.style.cssText = `
  938. display: flex;
  939. justify-content: space-between;
  940. align-items: center;
  941. margin-bottom: 12px;
  942. cursor: move;
  943. padding-bottom: 8px;
  944. border-bottom: 1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};
  945. user-select: none;
  946. -webkit-user-select: none;
  947. `;
  948.  
  949. // 标题
  950. const title = document.createElement('h3');
  951. // 获取当前页面标题并截断(如果过长)
  952. const pageTitle = document.title;
  953. const maxTitleLength = browserSupport.isMobile ? 30 : 50;
  954. title.textContent = pageTitle.length > maxTitleLength ?
  955. pageTitle.substring(0, maxTitleLength) + '...' :
  956. pageTitle;
  957. title.style.cssText = `
  958. margin: 0;
  959. font-size: 16px;
  960. color: ${isDark ? '#e8eaed' : '#333'};
  961. pointer-events: none;
  962. white-space: nowrap;
  963. overflow: hidden;
  964. text-overflow: ellipsis;
  965. max-width: ${browserSupport.isMobile ? '160px' : '350px'};
  966. font-weight: 500;
  967. `;
  968.  
  969. // 按钮容器
  970. const buttonContainer = document.createElement('div');
  971. buttonContainer.style.cssText = 'display: flex; gap: 12px; align-items: center;';
  972.  
  973. // 复制按钮 - Mac风格SVG图标
  974. const copyBtn = document.createElement('button');
  975. copyBtn.title = '复制内容';
  976. copyBtn.style.cssText = `
  977. background: transparent;
  978. border: none;
  979. cursor: pointer;
  980. display: flex;
  981. align-items: center;
  982. justify-content: center;
  983. padding: 5px;
  984. width: 28px;
  985. height: 28px;
  986. border-radius: 6px;
  987. transition: background-color 0.2s;
  988. color: ${isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)'};
  989. `;
  990.  
  991. // Mac风格的复制图标SVG
  992. copyBtn.innerHTML = `
  993. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  994. <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
  995. <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
  996. </svg>
  997. `;
  998.  
  999. copyBtn.addEventListener('mouseover', () => {
  1000. copyBtn.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)';
  1001. });
  1002. copyBtn.addEventListener('mouseout', () => {
  1003. copyBtn.style.backgroundColor = 'transparent';
  1004. });
  1005.  
  1006. copyBtn.addEventListener('click', (e) => {
  1007. e.preventDefault();
  1008. e.stopPropagation();
  1009. const content = document.getElementById('website-summary-content').innerText;
  1010. navigator.clipboard.writeText(content).then(() => {
  1011. // 显示复制成功状态
  1012. copyBtn.innerHTML = `
  1013. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1014. <path d="M20 6L9 17l-5-5"></path>
  1015. </svg>
  1016. `;
  1017. copyBtn.style.color = isDark ? '#8ab4f8' : '#34c759';
  1018. setTimeout(() => {
  1019. // 恢复原始图标
  1020. copyBtn.innerHTML = `
  1021. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1022. <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
  1023. <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
  1024. </svg>
  1025. `;
  1026. copyBtn.style.color = isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)';
  1027. }, 1500);
  1028. });
  1029. });
  1030.  
  1031. // 设置按钮 - Mac风格SVG图标
  1032. const settingsBtn = document.createElement('button');
  1033. settingsBtn.title = '设置';
  1034. settingsBtn.style.cssText = `
  1035. background: transparent;
  1036. border: none;
  1037. cursor: pointer;
  1038. display: flex;
  1039. align-items: center;
  1040. justify-content: center;
  1041. padding: 5px;
  1042. width: 28px;
  1043. height: 28px;
  1044. border-radius: 6px;
  1045. transition: background-color 0.2s;
  1046. color: ${isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)'};
  1047. `;
  1048.  
  1049. // Mac风格的设置图标SVG
  1050. settingsBtn.innerHTML = `
  1051. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1052. <circle cx="12" cy="12" r="3"></circle>
  1053. <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V15a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51-1z"></path>
  1054. </svg>
  1055. `;
  1056.  
  1057. settingsBtn.addEventListener('mouseover', () => {
  1058. settingsBtn.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)';
  1059. });
  1060.  
  1061. settingsBtn.addEventListener('mouseout', () => {
  1062. settingsBtn.style.backgroundColor = 'transparent';
  1063. });
  1064.  
  1065. settingsBtn.addEventListener('click', (e) => {
  1066. e.preventDefault();
  1067. e.stopPropagation();
  1068. showSettings(); // 调用显示设置界面的函数
  1069. });
  1070.  
  1071. // 关闭按钮 - Mac风格SVG图标
  1072. const closeBtn = document.createElement('button');
  1073. closeBtn.title = '关闭';
  1074. closeBtn.style.cssText = `
  1075. background: transparent;
  1076. border: none;
  1077. cursor: pointer;
  1078. display: flex;
  1079. align-items: center;
  1080. justify-content: center;
  1081. padding: 5px;
  1082. width: 28px;
  1083. height: 28px;
  1084. border-radius: 6px;
  1085. transition: background-color 0.2s, color 0.2s;
  1086. color: ${isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)'};
  1087. `;
  1088.  
  1089. // Mac风格的关闭图标SVG
  1090. closeBtn.innerHTML = `
  1091. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1092. <line x1="18" y1="6" x2="6" y2="18"></line>
  1093. <line x1="6" y1="6" x2="18" y2="18"></line>
  1094. </svg>
  1095. `;
  1096.  
  1097. closeBtn.addEventListener('mouseover', () => {
  1098. closeBtn.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)';
  1099. closeBtn.style.color = isDark ? '#ff4444' : '#ff3b30';
  1100. });
  1101. closeBtn.addEventListener('mouseout', () => {
  1102. closeBtn.style.backgroundColor = 'transparent';
  1103. closeBtn.style.color = isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)';
  1104. });
  1105.  
  1106. closeBtn.addEventListener('click', (e) => {
  1107. e.preventDefault();
  1108. e.stopPropagation();
  1109. hideUI();
  1110. });
  1111.  
  1112. // 内容区域
  1113. const content = document.createElement('div');
  1114. content.id = 'website-summary-content';
  1115. content.style.cssText = `
  1116. max-height: calc(80vh - 60px);
  1117. overflow-y: auto;
  1118. font-size: 14px;
  1119. line-height: 1.6;
  1120. padding: 8px 0;
  1121. color: ${isDark ? '#e8eaed' : '#333'};
  1122. -webkit-overflow-scrolling: touch;
  1123. overscroll-behavior: contain;
  1124. `;
  1125.  
  1126. // 防止内容区域的滚动触发容器拖动
  1127. content.addEventListener('mousedown', (e) => {
  1128. e.stopPropagation();
  1129. });
  1130. content.addEventListener('touchstart', (e) => {
  1131. e.stopPropagation();
  1132. }, { passive: true });
  1133.  
  1134. // 组装界面
  1135. buttonContainer.appendChild(settingsBtn); // 添加设置按钮
  1136. buttonContainer.appendChild(copyBtn);
  1137. buttonContainer.appendChild(closeBtn);
  1138. header.appendChild(title);
  1139. header.appendChild(buttonContainer);
  1140. container.appendChild(header);
  1141. container.appendChild(content);
  1142. document.body.appendChild(container);
  1143. elements.container = container; // 必须在 makeDraggableByHeader 之前赋值
  1144.  
  1145. // 恢复窗口位置(如果已保存)
  1146. if (config.summaryWindowPosition) {
  1147. container.style.left = config.summaryWindowPosition.left + 'px';
  1148. container.style.top = config.summaryWindowPosition.top + 'px';
  1149. container.style.transform = 'none'; // 清除默认的transform居中
  1150. container.setAttribute('data-positioned', 'true');
  1151. } else {
  1152. // 确保初次显示时居中
  1153. container.style.left = '50%';
  1154. container.style.top = '50%';
  1155. container.style.transform = 'translate(-50%, -50%)';
  1156. }
  1157. // 专门使用标题栏拖动
  1158. makeDraggableByHeader(container, header);
  1159. return container;
  1160. }
  1161.  
  1162. // 专门用于通过标题栏拖动的函数
  1163. function makeDraggableByHeader(element, handle) {
  1164. let isDragging = false;
  1165. let startX, startY, startLeft, startTop;
  1166. // 鼠标/触摸开始事件
  1167. function handleStart(e) {
  1168. isDragging = true;
  1169. // 记录初始位置
  1170. const rect = element.getBoundingClientRect();
  1171. // 如果使用了transform-translate,则切换到绝对定位
  1172. if (element.style.transform && element.style.transform.includes('translate')) {
  1173. element.style.transform = 'none';
  1174. element.style.left = rect.left + 'px';
  1175. element.style.top = rect.top + 'px';
  1176. // 标记元素已被手动定位
  1177. element.setAttribute('data-positioned', 'true');
  1178. }
  1179. startLeft = rect.left;
  1180. startTop = rect.top;
  1181. // 记录鼠标/触摸起始位置
  1182. if (e.type === 'touchstart') {
  1183. startX = e.touches[0].clientX;
  1184. startY = e.touches[0].clientY;
  1185. // 阻止默认行为只在触摸时需要
  1186. e.preventDefault();
  1187. } else {
  1188. startX = e.clientX;
  1189. startY = e.clientY;
  1190. e.preventDefault();
  1191. }
  1192. // 移除过渡效果
  1193. element.style.transition = 'none';
  1194. // 添加移动和结束事件监听
  1195. if (e.type === 'touchstart') {
  1196. document.addEventListener('touchmove', handleMove, { passive: false });
  1197. document.addEventListener('touchend', handleEnd);
  1198. document.addEventListener('touchcancel', handleEnd);
  1199. } else {
  1200. document.addEventListener('mousemove', handleMove);
  1201. document.addEventListener('mouseup', handleEnd);
  1202. }
  1203. }
  1204. // 鼠标/触摸移动事件
  1205. function handleMove(e) {
  1206. if (!isDragging) return;
  1207. let moveX, moveY;
  1208. if (e.type === 'touchmove') {
  1209. moveX = e.touches[0].clientX - startX;
  1210. moveY = e.touches[0].clientY - startY;
  1211. // 阻止默认滚动
  1212. e.preventDefault();
  1213. } else {
  1214. moveX = e.clientX - startX;
  1215. moveY = e.clientY - startY;
  1216. }
  1217. // 计算新位置
  1218. const newLeft = startLeft + moveX;
  1219. const newTop = startTop + moveY;
  1220. // 边界检查
  1221. const maxLeft = window.innerWidth - element.offsetWidth;
  1222. const maxTop = window.innerHeight - element.offsetHeight;
  1223. // 应用新位置
  1224. element.style.left = Math.max(0, Math.min(newLeft, maxLeft)) + 'px';
  1225. element.style.top = Math.max(0, Math.min(newTop, maxTop)) + 'px';
  1226. // 标记元素已被手动定位
  1227. element.setAttribute('data-positioned', 'true');
  1228. }
  1229. // 鼠标/触摸结束事件
  1230. function handleEnd() {
  1231. if (!isDragging) return;
  1232. isDragging = false;
  1233. // 移除事件监听
  1234. document.removeEventListener('mousemove', handleMove);
  1235. document.removeEventListener('mouseup', handleEnd);
  1236. document.removeEventListener('touchmove', handleMove);
  1237. document.removeEventListener('touchend', handleEnd);
  1238. document.removeEventListener('touchcancel', handleEnd);
  1239. // 恢复过渡效果
  1240. element.style.transition = 'opacity 0.3s ease';
  1241. // 保存位置状态
  1242. saveWindowPosition(element);
  1243. }
  1244. // 保存窗口位置
  1245. function saveWindowPosition(element) {
  1246. if (element.id === 'website-summary-container' || element.id === 'website-summary-settings') {
  1247. const rect = element.getBoundingClientRect();
  1248. const position = { left: rect.left, top: rect.top };
  1249. if (element.id === 'website-summary-container') {
  1250. config.summaryWindowPosition = position;
  1251. scriptHandler.setValue('summaryWindowPosition', position);
  1252. } else if (element.id === 'website-summary-settings') {
  1253. // 注意:设置窗口目前没有独立的配置项来保存位置,如果需要,可以添加
  1254. // config.settingsWindowPosition = position;
  1255. // scriptHandler.setValue('settingsWindowPosition', position);
  1256. }
  1257. element.setAttribute('data-positioned', 'true'); // 标记已手动定位
  1258. }
  1259. }
  1260. // 仅在指定的标题栏上添加事件监听
  1261. handle.addEventListener('mousedown', handleStart);
  1262. handle.addEventListener('touchstart', handleStart, { passive: false });
  1263. // 处理窗口变化
  1264. window.addEventListener('resize', () => {
  1265. if (element.hasAttribute('data-positioned')) {
  1266. const rect = element.getBoundingClientRect();
  1267. let newLeft = rect.left;
  1268. let newTop = rect.top;
  1269. let positionChanged = false;
  1270.  
  1271. // 如果窗口超出视口边界,调整位置
  1272. if (rect.right > window.innerWidth) {
  1273. newLeft = Math.max(0, window.innerWidth - element.offsetWidth);
  1274. positionChanged = true;
  1275. }
  1276. if (rect.bottom > window.innerHeight) {
  1277. newTop = Math.max(0, window.innerHeight - element.offsetHeight);
  1278. positionChanged = true;
  1279. }
  1280.  
  1281. if (rect.left < 0) {
  1282. newLeft = 0;
  1283. positionChanged = true;
  1284. }
  1285.  
  1286. if (rect.top < 0) {
  1287. newTop = 0;
  1288. positionChanged = true;
  1289. }
  1290.  
  1291. if (positionChanged) {
  1292. element.style.left = newLeft + 'px';
  1293. element.style.top = newTop + 'px';
  1294. saveWindowPosition(element); // 保存调整后的位置
  1295. }
  1296. }
  1297. });
  1298. // 如果用户离开窗口,确保释放拖动状态
  1299. window.addEventListener('blur', () => {
  1300. if (isDragging) {
  1301. handleEnd();
  1302. }
  1303. });
  1304. // 检查是否应该恢复自定义位置
  1305. if (element.id === 'website-summary-container' && config.summaryWindowPosition) {
  1306. element.style.left = config.summaryWindowPosition.left + 'px';
  1307. element.style.top = config.summaryWindowPosition.top + 'px';
  1308. element.style.transform = 'none'; // 清除默认的transform居中
  1309. element.setAttribute('data-positioned', 'true');
  1310. }
  1311. // 对于设置窗口,如果也需要位置恢复,可以添加类似逻辑
  1312. // else if (element.id === 'website-summary-settings' && config.settingsWindowPosition) {
  1313. // element.style.left = config.settingsWindowPosition.left + 'px';
  1314. // element.style.top = config.settingsWindowPosition.top + 'px';
  1315. // element.style.transform = 'none';
  1316. // element.setAttribute('data-positioned', 'true');
  1317. // }
  1318. }
  1319.  
  1320. // 创建设置界面
  1321. function createSettingsUI() {
  1322. const settingsContainer = document.createElement('div');
  1323. settingsContainer.id = 'website-summary-settings';
  1324. // 基础样式
  1325. const isDark = config.theme === 'dark';
  1326. settingsContainer.style.cssText = `
  1327. position: fixed;
  1328. z-index: 1000000;
  1329. background: ${isDark ? 'rgba(32, 33, 36, 0.98)' : 'rgba(255, 255, 255, 0.98)'};
  1330. color: ${isDark ? '#e8eaed' : '#333'};
  1331. border-radius: 12px;
  1332. box-shadow: 0 4px 20px ${isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.15)'};
  1333. padding: 20px;
  1334. width: 400px;
  1335. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  1336. display: none;
  1337. left: 50%;
  1338. top: 50%;
  1339. transform: translate(-50%, -50%);
  1340. will-change: transform;
  1341. -webkit-backface-visibility: hidden;
  1342. backface-visibility: hidden;
  1343. `;
  1344.  
  1345. if (browserSupport.hasBackdropFilter) {
  1346. settingsContainer.style.backdropFilter = 'blur(10px)';
  1347. settingsContainer.style.webkitBackdropFilter = 'blur(10px)';
  1348. }
  1349.  
  1350. // 标题栏
  1351. const header = document.createElement('div');
  1352. header.style.cssText = `
  1353. display: flex;
  1354. justify-content: space-between;
  1355. align-items: center;
  1356. margin-bottom: 20px;
  1357. cursor: move;
  1358. user-select: none;
  1359. -webkit-user-select: none;
  1360. `;
  1361.  
  1362. const title = document.createElement('h3');
  1363. title.textContent = '设置';
  1364. title.style.cssText = `
  1365. margin: 0;
  1366. color: ${isDark ? '#e8eaed' : '#333'};
  1367. pointer-events: none;
  1368. `;
  1369.  
  1370. const closeBtn = document.createElement('button');
  1371. closeBtn.textContent = '×';
  1372. closeBtn.style.cssText = `
  1373. background: none;
  1374. border: none;
  1375. font-size: 24px;
  1376. cursor: pointer;
  1377. padding: 0 8px;
  1378. color: ${isDark ? '#e8eaed' : '#666'};
  1379. `;
  1380. closeBtn.addEventListener('click', (e) => {
  1381. e.preventDefault();
  1382. e.stopPropagation();
  1383. settingsContainer.style.display = 'none';
  1384. if (elements.backdrop) {
  1385. elements.backdrop.style.opacity = '0';
  1386. setTimeout(() => elements.backdrop.style.display = 'none', 300);
  1387. }
  1388. });
  1389.  
  1390. // 表单
  1391. const form = document.createElement('form');
  1392. form.style.cssText = 'display: flex; flex-direction: column; gap: 16px;';
  1393. // 创建输入字段函数
  1394. function createField(id, label, value, type = 'text', placeholder = '') {
  1395. const container = document.createElement('div');
  1396. container.style.cssText = 'display: flex; flex-direction: column; gap: 4px;';
  1397. const labelElem = document.createElement('label');
  1398. labelElem.textContent = label;
  1399. labelElem.style.cssText = `font-size: 14px; color: ${isDark ? '#e8eaed' : '#333'}; font-weight: 500;`;
  1400. const input = document.createElement(type === 'textarea' ? 'textarea' : 'input');
  1401. if (type !== 'textarea') input.type = type;
  1402. input.id = id;
  1403. input.value = value;
  1404. input.placeholder = placeholder;
  1405. input.autocomplete = 'off';
  1406. input.setAttribute('data-form-type', 'other');
  1407. const baseStyle = `
  1408. width: 100%;
  1409. padding: 8px;
  1410. border: 1px solid ${isDark ? '#555' : '#ddd'};
  1411. border-radius: 6px;
  1412. font-family: inherit;
  1413. background: ${isDark ? '#202124' : '#fff'};
  1414. color: ${isDark ? '#e8eaed' : '#333'};
  1415. `;
  1416. input.style.cssText = type === 'textarea' ? baseStyle + 'height: 100px; resize: vertical;' : baseStyle;
  1417. container.appendChild(labelElem);
  1418. container.appendChild(input);
  1419. return { container, input };
  1420. }
  1421.  
  1422. // 创建主题切换
  1423. function createThemeSwitch() {
  1424. const container = document.createElement('div');
  1425. container.style.cssText = 'display: flex; align-items: center; gap: 12px; margin-bottom: 16px;';
  1426. const label = document.createElement('label');
  1427. label.textContent = '主题模式:';
  1428. label.style.cssText = `font-size: 14px; color: ${isDark ? '#e8eaed' : '#333'}; font-weight: 500;`;
  1429. const themeSwitch = document.createElement('div');
  1430. themeSwitch.style.cssText = 'display: flex; gap: 8px;';
  1431. const createThemeButton = (themeName, text) => {
  1432. const btn = document.createElement('button');
  1433. btn.textContent = text;
  1434. btn.type = 'button';
  1435. const isActive = config.theme === themeName;
  1436. btn.style.cssText = `
  1437. padding: 6px 12px;
  1438. border-radius: 4px;
  1439. border: 1px solid ${isDark ? '#555' : '#ddd'};
  1440. background: ${isActive ? (isDark ? '#555' : '#007AFF') : 'transparent'};
  1441. color: ${isActive ? '#fff' : (isDark ? '#e8eaed' : '#333')};
  1442. cursor: pointer;
  1443. transition: all 0.2s;
  1444. `;
  1445. btn.addEventListener('click', async () => {
  1446. config.theme = themeName;
  1447. await scriptHandler.setValue('theme', themeName);
  1448. // 重新创建设置界面而不是移除
  1449. const oldSettings = elements.settings;
  1450. elements.settings = null;
  1451. showSettings();
  1452. if (oldSettings) {
  1453. oldSettings.remove();
  1454. }
  1455. });
  1456. return btn;
  1457. };
  1458. const lightBtn = createThemeButton('light', '浅色');
  1459. const darkBtn = createThemeButton('dark', '深色');
  1460. themeSwitch.appendChild(lightBtn);
  1461. themeSwitch.appendChild(darkBtn);
  1462. container.appendChild(label);
  1463. container.appendChild(themeSwitch);
  1464. return container;
  1465. }
  1466. // 创建字段
  1467. const apiUrlField = createField('apiUrl', 'API URL', config.apiUrl, 'text', '输入API URL');
  1468. const apiKeyField = createField('apiKey', 'API Key', config.apiKey, 'text', '输入API Key');
  1469. const modelField = createField('model', 'AI 模型', config.model, 'text', '输入AI模型名称');
  1470. const shortcutField = createField('shortcut', '快捷键', config.shortcut, 'text', '例如: option+a, ctrl+shift+s');
  1471. const promptField = createField('prompt', '提示词', config.prompt, 'textarea', '输入提示词');
  1472. // 添加主题切换
  1473. form.appendChild(createThemeSwitch());
  1474. // 添加字段到表单
  1475. form.appendChild(apiUrlField.container);
  1476. form.appendChild(apiKeyField.container);
  1477. form.appendChild(modelField.container);
  1478. form.appendChild(shortcutField.container);
  1479. form.appendChild(promptField.container);
  1480. // 保存按钮
  1481. const saveBtn = document.createElement('button');
  1482. saveBtn.textContent = '保存设置';
  1483. saveBtn.type = 'button';
  1484. saveBtn.style.cssText = `
  1485. background: ${isDark ? '#8ab4f8' : '#007AFF'};
  1486. color: ${isDark ? '#202124' : 'white'};
  1487. border: none;
  1488. padding: 10px;
  1489. border-radius: 6px;
  1490. cursor: pointer;
  1491. font-size: 14px;
  1492. font-weight: 500;
  1493. transition: background-color 0.2s;
  1494. `;
  1495.  
  1496. saveBtn.addEventListener('mouseover', () => {
  1497. saveBtn.style.backgroundColor = isDark ? '#aecbfa' : '#0056b3';
  1498. });
  1499. saveBtn.addEventListener('mouseout', () => {
  1500. saveBtn.style.backgroundColor = isDark ? '#8ab4f8' : '#007AFF';
  1501. });
  1502. // 保存逻辑
  1503. saveBtn.addEventListener('click', async (e) => {
  1504. e.preventDefault();
  1505. // 获取并验证表单值
  1506. const newApiUrl = apiUrlField.input.value.trim();
  1507. const newApiKey = apiKeyField.input.value.trim();
  1508. const newModel = modelField.input.value.trim();
  1509. const newPrompt = promptField.input.value.trim();
  1510. const newShortcut = shortcutField.input.value.trim();
  1511. if (!newApiUrl || !newApiKey) {
  1512. alert('请至少填写API URL和API Key');
  1513. return;
  1514. }
  1515.  
  1516. try {
  1517. // 使用scriptHandler保存设置
  1518. await scriptHandler.setValue('apiUrl', newApiUrl);
  1519. await scriptHandler.setValue('apiKey', newApiKey);
  1520. await scriptHandler.setValue('model', newModel);
  1521. await scriptHandler.setValue('prompt', newPrompt);
  1522. await scriptHandler.setValue('shortcut', newShortcut);
  1523. await scriptHandler.setValue('theme', config.theme);
  1524.  
  1525. // 更新内存配置
  1526. config.apiUrl = newApiUrl;
  1527. config.apiKey = newApiKey;
  1528. config.model = newModel;
  1529. config.prompt = newPrompt;
  1530. config.shortcut = newShortcut;
  1531. // 更新快捷键
  1532. keyManager.setup();
  1533.  
  1534. // 显示成功提示
  1535. showToast('设置已保存');
  1536. // 关闭设置
  1537. settingsContainer.style.display = 'none';
  1538. } catch (error) {
  1539. console.error('保存设置失败:', error);
  1540. showToast('保存设置失败,请重试');
  1541. }
  1542. });
  1543.  
  1544. // 组装界面
  1545. header.appendChild(title);
  1546. header.appendChild(closeBtn);
  1547. form.appendChild(saveBtn);
  1548. settingsContainer.appendChild(header);
  1549. settingsContainer.appendChild(form);
  1550. document.body.appendChild(settingsContainer);
  1551. elements.settings = settingsContainer; // 必须在 makeDraggableByHeader 之前赋值
  1552.  
  1553. // 恢复设置窗口位置(如果已保存)- 此处假设 settingsWindowPosition 已在 config 中定义并加载
  1554. // 注意:目前脚本没有为设置窗口单独保存位置的逻辑,以下代码为示例,如果需要此功能,
  1555. // 需要在 config, initConfig, 和 makeDraggableByHeader 的 saveWindowPosition 中添加相应处理。
  1556. // if (config.settingsWindowPosition) {
  1557. // settingsContainer.style.left = config.settingsWindowPosition.left + 'px';
  1558. // settingsContainer.style.top = config.settingsWindowPosition.top + 'px';
  1559. // settingsContainer.style.transform = 'none';
  1560. // settingsContainer.setAttribute('data-positioned', 'true');
  1561. // } else {
  1562. // // 确保初次显示时居中 (如果未实现位置保存,则总是居中)
  1563. // settingsContainer.style.left = '50%';
  1564. // settingsContainer.style.top = '50%';
  1565. // settingsContainer.style.transform = 'translate(-50%, -50%)';
  1566. // }
  1567.  
  1568. // 使用优化的拖拽功能,只允许通过标题栏拖动
  1569. makeDraggableByHeader(settingsContainer, header);
  1570. return settingsContainer;
  1571. }
  1572.  
  1573. // 获取页面内容
  1574. function getPageContent() {
  1575. try {
  1576. const clone = document.body.cloneNode(true);
  1577. const elementsToRemove = clone.querySelectorAll('script, style, iframe, nav, header, footer, .ad, .advertisement, .social-share, .comment, .related-content');
  1578. elementsToRemove.forEach(el => el.remove());
  1579. return clone.innerText.replace(/\s+/g, ' ').trim().slice(0, 5000);
  1580. } catch (error) {
  1581. return document.body.innerText.slice(0, 5000);
  1582. }
  1583. }
  1584.  
  1585. // 修改深色模式颜色方案
  1586. const darkColors = {
  1587. background: '#242526', // 更柔和的深色背景
  1588. containerBg: '#2d2d30', // 容器背景色
  1589. text: '#e4e6eb', // 更柔和的文字颜色
  1590. secondaryText: '#b0b3b8', // 次要文字颜色
  1591. border: '#3e4042', // 边框颜色
  1592. codeBackground: '#3a3b3c', // 代码块背景
  1593. blockquoteBorder: '#4a4b4d', // 引用块边框
  1594. blockquoteText: '#cacbcc', // 引用块文字
  1595. linkColor: '#4e89e8' // 链接颜色
  1596. };
  1597.  
  1598. // 修改 API 调用函数
  1599. function getSummary(content) {
  1600. return new Promise((resolve, reject) => {
  1601. const apiKey = config.apiKey.trim();
  1602. if (!apiKey) {
  1603. reject(new Error('请先设置API Key'));
  1604. return;
  1605. }
  1606.  
  1607. const requestData = {
  1608. model: config.model,
  1609. messages: [
  1610. {
  1611. role: 'system',
  1612. content: '你是一个专业的网页内容总结助手,善于使用markdown格式来组织信息。'
  1613. },
  1614. {
  1615. role: 'user',
  1616. content: config.prompt + '\n\n' + content
  1617. }
  1618. ],
  1619. temperature: 0.7,
  1620. stream: false
  1621. };
  1622.  
  1623. // 处理 URL
  1624. let apiUrl = config.apiUrl.trim();
  1625. if (!apiUrl.startsWith('http://') && !apiUrl.startsWith('https://')) {
  1626. apiUrl = 'https://' + apiUrl;
  1627. }
  1628.  
  1629. // 打印请求信息用于调试
  1630. console.log('发送请求到:', apiUrl);
  1631. console.log('请求数据:', JSON.stringify(requestData, null, 2));
  1632.  
  1633. // 发送请求
  1634. const xhr = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest);
  1635. if (!xhr) {
  1636. reject(new Error('不支持的环境:无法发送跨域请求'));
  1637. return;
  1638. }
  1639.  
  1640. xhr({
  1641. method: 'POST',
  1642. url: apiUrl,
  1643. headers: {
  1644. 'Content-Type': 'application/json',
  1645. 'Authorization': `Bearer ${apiKey}`,
  1646. 'Accept': 'application/json'
  1647. },
  1648. data: JSON.stringify(requestData),
  1649. timeout: 30000,
  1650. onload: function(response) {
  1651. try {
  1652. console.log('收到响应:', response.status);
  1653. console.log('响应头:', response.responseHeaders);
  1654. console.log('响应内容:', response.responseText);
  1655.  
  1656. if (response.status === 429) {
  1657. reject(new Error('API请求过于频繁,请稍后再试'));
  1658. return;
  1659. }
  1660.  
  1661. if (response.status !== 200) {
  1662. reject(new Error(`API请求失败: HTTP ${response.status}`));
  1663. return;
  1664. }
  1665.  
  1666. let data;
  1667. try {
  1668. data = JSON.parse(response.responseText);
  1669. } catch (e) {
  1670. console.error('JSON解析失败:', e);
  1671. reject(new Error('API响应格式错误'));
  1672. return;
  1673. }
  1674.  
  1675. if (data.error) {
  1676. reject(new Error('API错误: ' + (data.error.message || JSON.stringify(data.error))));
  1677. return;
  1678. }
  1679.  
  1680. // 提取内容
  1681. let content = null;
  1682. if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
  1683. const choice = data.choices[0];
  1684. if (choice.message && choice.message.content) {
  1685. content = choice.message.content;
  1686. } else if (choice.text) {
  1687. content = choice.text;
  1688. }
  1689. }
  1690.  
  1691. if (!content && data.response) {
  1692. content = typeof data.response === 'string' ? data.response : JSON.stringify(data.response);
  1693. }
  1694.  
  1695. if (!content && data.content) {
  1696. content = data.content;
  1697. }
  1698.  
  1699. if (content) {
  1700. resolve(content.trim());
  1701. } else {
  1702. reject(new Error('无法从API响应中提取内容'));
  1703. }
  1704. } catch (error) {
  1705. console.error('处理响应时出错:', error);
  1706. reject(new Error('处理响应失败: ' + error.message));
  1707. }
  1708. },
  1709. onerror: function(error) {
  1710. console.error('请求错误:', error);
  1711. reject(new Error('请求失败: ' + (error.message || '网络错误')));
  1712. },
  1713. ontimeout: function() {
  1714. reject(new Error('请求超时'));
  1715. }
  1716. });
  1717. });
  1718. }
  1719.  
  1720. // 配置 Marked 渲染器
  1721. function configureMarked() {
  1722. if (typeof marked === 'undefined') return;
  1723.  
  1724. // 配置 marked 选项
  1725. marked.setOptions({
  1726. gfm: true,
  1727. breaks: true,
  1728. headerIds: true,
  1729. mangle: false,
  1730. smartLists: true,
  1731. smartypants: true,
  1732. highlight: function(code, lang) {
  1733. return code;
  1734. }
  1735. });
  1736.  
  1737. // 自定义渲染器
  1738. const renderer = new marked.Renderer();
  1739.  
  1740. // 自定义标题渲染 - 移除 ## 前缀
  1741. renderer.heading = function(text, level) {
  1742. return `<h${level}>${text}</h${level}>`;
  1743. };
  1744.  
  1745. // 自定义列表项渲染
  1746. renderer.listitem = function(text) {
  1747. return `<li><span class="bullet">•</span><span class="text">${text}</span></li>`;
  1748. };
  1749.  
  1750. // 自定义段落渲染
  1751. renderer.paragraph = function(text) {
  1752. return `<p>${text}</p>`;
  1753. };
  1754.  
  1755. // 自定义代码块渲染
  1756. renderer.code = function(code, language) {
  1757. return `<pre><code class="language-${language}">${code}</code></pre>`;
  1758. };
  1759.  
  1760. // 自定义引用块渲染
  1761. renderer.blockquote = function(quote) {
  1762. return `<blockquote>${quote}</blockquote>`;
  1763. };
  1764.  
  1765. // 设置渲染器
  1766. marked.setOptions({ renderer });
  1767. }
  1768.  
  1769. // 修改 Markdown 样式
  1770. function addMarkdownStyles() {
  1771. const styleId = 'website-summary-markdown-styles';
  1772. if (document.getElementById(styleId)) return;
  1773.  
  1774. const isDark = config.theme === 'dark';
  1775. const style = document.createElement('style');
  1776. style.id = styleId;
  1777. // 定义颜色变量
  1778. const colors = {
  1779. light: {
  1780. text: '#2c3e50',
  1781. background: '#ffffff',
  1782. border: '#e2e8f0',
  1783. link: '#2563eb',
  1784. linkHover: '#1d4ed8',
  1785. code: '#f8fafc',
  1786. codeBorder: '#e2e8f0',
  1787. blockquote: '#f8fafc',
  1788. blockquoteBorder: '#3b82f6',
  1789. heading: '#1e293b',
  1790. hr: '#e2e8f0',
  1791. marker: '#64748b'
  1792. },
  1793. dark: {
  1794. text: '#e2e8f0',
  1795. background: '#1e293b',
  1796. border: '#334155',
  1797. link: '#60a5fa',
  1798. linkHover: '#93c5fd',
  1799. code: '#1e293b',
  1800. codeBorder: '#334155',
  1801. blockquote: '#1e293b',
  1802. blockquoteBorder: '#60a5fa',
  1803. heading: '#f1f5f9',
  1804. hr: '#334155',
  1805. marker: '#94a3b8'
  1806. }
  1807. };
  1808.  
  1809. const c = isDark ? colors.dark : colors.light;
  1810.  
  1811. style.textContent = `
  1812. #website-summary-content {
  1813. font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  1814. line-height: 1.7;
  1815. color: ${c.text};
  1816. font-size: 15px;
  1817. padding: 20px;
  1818. max-width: 800px;
  1819. margin: 0 auto;
  1820. }
  1821.  
  1822. #website-summary-content h2 {
  1823. font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
  1824. font-weight: 600;
  1825. line-height: 1.3;
  1826. margin: 1.8em 0 1em;
  1827. color: ${c.heading};
  1828. font-size: 1.6em;
  1829. letter-spacing: -0.01em;
  1830. }
  1831.  
  1832. #website-summary-content h3 {
  1833. font-size: 1.3em;
  1834. margin: 1.5em 0 0.8em;
  1835. color: ${c.heading};
  1836. font-weight: 600;
  1837. line-height: 1.4;
  1838. }
  1839.  
  1840. #website-summary-content p {
  1841. margin: 0.8em 0;
  1842. line-height: 1.75;
  1843. letter-spacing: 0.01em;
  1844. }
  1845.  
  1846. #website-summary-content ul {
  1847. margin: 0.6em 0;
  1848. padding-left: 0.5em;
  1849. list-style: none;
  1850. }
  1851.  
  1852. #website-summary-content ul li {
  1853. display: flex;
  1854. align-items: baseline;
  1855. margin: 0.4em 0;
  1856. line-height: 1.6;
  1857. letter-spacing: 0.01em;
  1858. }
  1859.  
  1860. #website-summary-content ul li .bullet {
  1861. color: ${c.marker};
  1862. margin-right: 0.7em;
  1863. font-weight: normal;
  1864. flex-shrink: 0;
  1865. }
  1866.  
  1867. #website-summary-content ul li .text {
  1868. flex: 1;
  1869. }
  1870.  
  1871. #website-summary-content blockquote {
  1872. margin: 1.2em 0;
  1873. padding: 0.8em 1.2em;
  1874. background: ${c.blockquote};
  1875. border-left: 4px solid ${c.blockquoteBorder};
  1876. border-radius: 6px;
  1877. color: ${isDark ? '#cbd5e1' : '#475569'};
  1878. font-style: italic;
  1879. }
  1880.  
  1881. #website-summary-content blockquote p {
  1882. margin: 0.4em 0;
  1883. }
  1884.  
  1885. #website-summary-content code {
  1886. font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;
  1887. font-size: 0.9em;
  1888. background: ${c.code};
  1889. border: 1px solid ${c.codeBorder};
  1890. border-radius: 4px;
  1891. padding: 0.2em 0.4em;
  1892. }
  1893.  
  1894. #website-summary-content pre {
  1895. background: ${c.code};
  1896. border: 1px solid ${c.codeBorder};
  1897. border-radius: 8px;
  1898. padding: 1.2em;
  1899. overflow-x: auto;
  1900. margin: 1.2em 0;
  1901. }
  1902.  
  1903. #website-summary-content pre code {
  1904. background: none;
  1905. border: none;
  1906. padding: 0;
  1907. font-size: 0.9em;
  1908. line-height: 1.6;
  1909. }
  1910.  
  1911. #website-summary-content strong {
  1912. font-weight: 600;
  1913. color: ${isDark ? '#f1f5f9' : '#1e293b'};
  1914. }
  1915.  
  1916. #website-summary-content em {
  1917. font-style: italic;
  1918. color: ${isDark ? '#cbd5e1' : '#475569'};
  1919. }
  1920.  
  1921. #website-summary-content hr {
  1922. margin: 2em 0;
  1923. border: none;
  1924. border-top: 1px solid ${c.hr};
  1925. }
  1926.  
  1927. #website-summary-content table {
  1928. width: 100%;
  1929. border-collapse: collapse;
  1930. margin: 1.2em 0;
  1931. font-size: 0.95em;
  1932. }
  1933.  
  1934. #website-summary-content th,
  1935. #website-summary-content td {
  1936. padding: 0.8em;
  1937. border: 1px solid ${c.border};
  1938. text-align: left;
  1939. }
  1940.  
  1941. #website-summary-content th {
  1942. background: ${c.code};
  1943. font-weight: 600;
  1944. }
  1945.  
  1946. #website-summary-content img {
  1947. max-width: 100%;
  1948. height: auto;
  1949. border-radius: 8px;
  1950. margin: 1em 0;
  1951. }
  1952.  
  1953. @media (max-width: 768px) {
  1954. #website-summary-content {
  1955. font-size: 14px;
  1956. padding: 16px;
  1957. }
  1958.  
  1959. #website-summary-content h2 {
  1960. font-size: 1.4em;
  1961. }
  1962.  
  1963. #website-summary-content h3 {
  1964. font-size: 1.2em;
  1965. }
  1966. }
  1967. `;
  1968.  
  1969. document.head.appendChild(style);
  1970. }
  1971.  
  1972. // 修复打字机效果后内容消失的问题
  1973. async function renderContent(content) {
  1974. const container = document.getElementById('website-summary-content');
  1975. if (!container) return;
  1976. try {
  1977. if (!content || content.trim().length === 0) {
  1978. throw new Error('内容为空');
  1979. }
  1980.  
  1981. // 确保 marked 已加载并配置
  1982. if (typeof marked === 'undefined') {
  1983. throw new Error('Markdown 渲染器未加载');
  1984. }
  1985. // 配置 marked
  1986. configureMarked();
  1987.  
  1988. // 渲染 Markdown
  1989. const html = marked.parse(content);
  1990. // 清空容器
  1991. container.innerHTML = '';
  1992. // 创建临时容器
  1993. const temp = document.createElement('div');
  1994. temp.innerHTML = html;
  1995. // 始终启用打字机效果
  1996. const backupContent = temp.cloneNode(true);
  1997. try {
  1998. // 真实的逐字符打字机效果
  1999. const typeWriter = async () => {
  2000. // 首先添加所有元素到DOM,但设置为不可见
  2001. const fragments = Array.from(temp.children);
  2002. const allElementsWithText = [];
  2003. // 添加所有HTML元素结构,但内容为空
  2004. for (let fragment of fragments) {
  2005. // 克隆元素,但清空文本内容
  2006. const emptyElement = fragment.cloneNode(true);
  2007. // 递归查找所有文本节点并收集信息
  2008. const collectTextNodes = (node, parentElement) => {
  2009. if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
  2010. // 保存文本节点信息
  2011. allElementsWithText.push({
  2012. element: parentElement,
  2013. originalText: node.textContent,
  2014. currentPosition: 0
  2015. });
  2016. // 清空文本
  2017. node.textContent = '';
  2018. } else if (node.nodeType === Node.ELEMENT_NODE) {
  2019. // 处理子元素中的文本节点
  2020. for (const child of Array.from(node.childNodes)) {
  2021. collectTextNodes(child, node);
  2022. }
  2023. }
  2024. };
  2025. collectTextNodes(fragment, emptyElement);
  2026. container.appendChild(emptyElement);
  2027. }
  2028. // 打字速度调整 - 根据总字符数动态调整
  2029. const totalChars = allElementsWithText.reduce((sum, item) => sum + item.originalText.length, 0);
  2030. // 对于长内容,加快打字速度
  2031. const baseCharDelay = totalChars > 1000 ? 3 : 5; // 每个字符的基础延迟(毫秒)
  2032. // 复制原始DOM结构,用于最终替换(避免打字过程中的可能问题)
  2033. const finalContent = backupContent.cloneNode(true);
  2034. // 开始打字
  2035. let typedChars = 0;
  2036. const startTime = performance.now();
  2037. let lastScrollTime = 0;
  2038. while (typedChars < totalChars) {
  2039. // 随机选择一个还有字符要显示的元素
  2040. const pendingElements = allElementsWithText.filter(item =>
  2041. item.currentPosition < item.originalText.length);
  2042. if (pendingElements.length === 0) break;
  2043. // 随机选择一个待处理元素
  2044. const randomIndex = Math.floor(Math.random() * pendingElements.length);
  2045. const selectedItem = pendingElements[randomIndex];
  2046. // 添加下一个字符
  2047. const char = selectedItem.originalText[selectedItem.currentPosition];
  2048. selectedItem.currentPosition++;
  2049. typedChars++;
  2050. // 更新DOM (查找元素中的第一个文本节点并添加字符)
  2051. const updateTextNode = (node) => {
  2052. if (node.nodeType === Node.TEXT_NODE) {
  2053. node.textContent += char;
  2054. return true;
  2055. } else if (node.nodeType === Node.ELEMENT_NODE) {
  2056. for (const child of Array.from(node.childNodes)) {
  2057. if (updateTextNode(child)) {
  2058. return true;
  2059. }
  2060. }
  2061. }
  2062. return false;
  2063. };
  2064. updateTextNode(selectedItem.element);
  2065. // 智能滚动:每处理30个字符滚动一次,并加入时间限制,避免滚动过于频繁
  2066. const currentTime = performance.now();
  2067. if (typedChars % 30 === 0 && currentTime - lastScrollTime > 200) {
  2068. container.scrollTop = container.scrollHeight;
  2069. lastScrollTime = currentTime;
  2070. }
  2071. // 动态调整延迟,以获得更自然的打字感觉
  2072. const progress = typedChars / totalChars;
  2073. let adjustedDelay = baseCharDelay;
  2074. // 开始更快,中间变慢,结束再次加速
  2075. if (progress < 0.2) {
  2076. adjustedDelay = baseCharDelay * 0.5; // 开始阶段更快
  2077. } else if (progress > 0.8) {
  2078. adjustedDelay = baseCharDelay * 0.7; // 结束阶段也较快
  2079. }
  2080. // 有时候添加一个随机的短暂停顿,模拟真人打字节奏(减少概率,避免过慢)
  2081. if (Math.random() < 0.03) {
  2082. adjustedDelay = baseCharDelay * 4; // 偶尔的停顿
  2083. }
  2084. await new Promise(resolve => setTimeout(resolve, adjustedDelay));
  2085. // 检查是否超时(超过6秒),如果超时就直接显示全部内容
  2086. if (performance.now() - startTime > 6000) {
  2087. console.log('打字机效果超时,直接显示全部内容');
  2088. break;
  2089. }
  2090. }
  2091. // 打字完成或超时后,确保显示完整内容
  2092. return finalContent;
  2093. };
  2094. // 开始打字效果
  2095. const completedContent = await typeWriter();
  2096. // 使用单独的 try-catch 确保内容不丢失
  2097. try {
  2098. // 确保内容完整显示 - 使用替换节点而不是直接操作innerHTML
  2099. if (completedContent) {
  2100. // 先替换内容,再移除原来的内容
  2101. const tempDiv = document.createElement('div');
  2102. while (completedContent.firstChild) {
  2103. tempDiv.appendChild(completedContent.firstChild);
  2104. }
  2105. // 清除旧内容
  2106. while (container.firstChild) {
  2107. container.removeChild(container.firstChild);
  2108. }
  2109. // 添加新内容
  2110. while (tempDiv.firstChild) {
  2111. container.appendChild(tempDiv.firstChild);
  2112. }
  2113. }
  2114. } catch (finalError) {
  2115. console.error('最终内容替换失败:', finalError);
  2116. // 如果替换失败,确保使用备份内容显示
  2117. container.innerHTML = '';
  2118. // 再次尝试添加原始备份内容
  2119. try {
  2120. Array.from(backupContent.children).forEach(child => {
  2121. container.appendChild(child.cloneNode(true));
  2122. });
  2123. } catch (lastError) {
  2124. // 最终失败,直接使用原始HTML
  2125. container.innerHTML = html;
  2126. }
  2127. }
  2128. } catch (typewriterError) {
  2129. console.error('打字机效果失败:', typewriterError);
  2130. // 确保内容显示即使打字机效果失败
  2131. container.innerHTML = '';
  2132. while (backupContent.firstChild) {
  2133. container.appendChild(backupContent.firstChild);
  2134. }
  2135. }
  2136. // 确保内容显示后滚动到顶部
  2137. setTimeout(() => {
  2138. container.scrollTop = 0;
  2139. }, 100);
  2140. } catch (error) {
  2141. console.error('渲染内容失败:', error);
  2142. container.innerHTML = `
  2143. <p style="text-align: center; color: #ff4444;">
  2144. 渲染内容失败:${error.message}<br>
  2145. <small style="color: ${config.theme === 'dark' ? '#bdc1c6' : '#666'};">
  2146. 请刷新页面重试
  2147. </small>
  2148. </p>`;
  2149. }
  2150. }
  2151.  
  2152. // 添加菜单命令
  2153. function registerMenuCommands() {
  2154. scriptHandler.registerMenuCommand('显示网页总结 (快捷键: ' + config.shortcut + ')', showSummary);
  2155. scriptHandler.registerMenuCommand('打开设置', showSettings);
  2156. }
  2157.  
  2158. // 启动脚本
  2159. waitForPageLoad();
  2160. })();

QingJ © 2025

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