Greasy Fork 还支持 简体中文。

AI Image Description Generator Gimini

使用AI生成网页图片描述

目前為 2024-12-23 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name AI Image Description Generator Gimini
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.0
  5. // @description 使用AI生成网页图片描述
  6. // @author AlphaCat
  7. // @match *://*/*
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_xmlhttpRequest
  12. // @connect *
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. // 添加样式
  20. GM_addStyle(`
  21. .ai-config-modal {
  22. position: fixed;
  23. top: 50%;
  24. left: 50%;
  25. transform: translate(-50%, -50%);
  26. background: white;
  27. padding: 20px;
  28. border-radius: 8px;
  29. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  30. z-index: 10000;
  31. min-width: 500px;
  32. height: auto;
  33. }
  34. .ai-config-modal h3 {
  35. margin: 0 0 15px 0;
  36. font-size: 14px;
  37. font-weight: bold;
  38. color: #333;
  39. }
  40. .ai-config-modal label {
  41. display: inline-block;
  42. font-size: 12px;
  43. font-weight: bold;
  44. color: #333;
  45. margin: 0;
  46. line-height: normal;
  47. height: auto;
  48. }
  49. .ai-config-modal .input-wrapper {
  50. position: relative;
  51. display: flex;
  52. align-items: center;
  53. }
  54. .ai-config-modal input {
  55. display: block;
  56. width: 100%;
  57. padding: 2px 24px 2px 2px;
  58. margin: 2px;
  59. border: 1px solid #ddd;
  60. border-radius: 4px;
  61. font-size: 13px;
  62. line-height: normal;
  63. height: auto;
  64. box-sizing: border-box;
  65. }
  66. .ai-config-modal .input-icon {
  67. position: absolute;
  68. right: 4px;
  69. width: 16px;
  70. height: 16px;
  71. cursor: pointer;
  72. display: flex;
  73. align-items: center;
  74. justify-content: center;
  75. color: #666;
  76. font-size: 12px;
  77. user-select: none;
  78. }
  79. .ai-config-modal .clear-icon {
  80. right: 24px;
  81. }
  82. .ai-config-modal .toggle-password {
  83. right: 4px;
  84. }
  85. .ai-config-modal .input-icon:hover {
  86. color: #333;
  87. }
  88. .ai-config-modal .input-group {
  89. margin-bottom: 12px;
  90. height: auto;
  91. display: flex;
  92. flex-direction: column;
  93. }
  94. .ai-config-modal .button-row {
  95. display: flex;
  96. gap: 10px;
  97. align-items: center;
  98. margin-top: 5px;
  99. }
  100. .ai-config-modal .check-button {
  101. padding: 4px 8px;
  102. border: none;
  103. border-radius: 4px;
  104. background: #007bff;
  105. color: white;
  106. cursor: pointer;
  107. font-size: 12px;
  108. }
  109. .ai-config-modal .check-button:hover {
  110. background: #0056b3;
  111. }
  112. .ai-config-modal .check-button:disabled {
  113. background: #cccccc;
  114. cursor: not-allowed;
  115. }
  116. .ai-config-modal select {
  117. width: 100%;
  118. padding: 4px;
  119. border: 1px solid #ddd;
  120. border-radius: 4px;
  121. font-size: 13px;
  122. margin-top: 2px;
  123. }
  124. .ai-config-modal .status-text {
  125. font-size: 12px;
  126. margin-left: 10px;
  127. }
  128. .ai-config-modal .status-success {
  129. color: #28a745;
  130. }
  131. .ai-config-modal .status-error {
  132. color: #dc3545;
  133. }
  134. .ai-config-modal button {
  135. margin: 10px 5px;
  136. padding: 8px 15px;
  137. border: none;
  138. border-radius: 4px;
  139. cursor: pointer;
  140. font-size: 14px;
  141. }
  142. .ai-config-modal button#ai-save-config {
  143. background: #4CAF50;
  144. color: white;
  145. }
  146. .ai-config-modal button#ai-cancel-config {
  147. background: #dc3545;
  148. color: white;
  149. }
  150. .ai-config-modal button:hover {
  151. opacity: 0.9;
  152. }
  153. .ai-floating-btn {
  154. position: fixed;
  155. width: 32px;
  156. height: 32px;
  157. background: #4CAF50;
  158. color: white;
  159. border-radius: 50%;
  160. cursor: move;
  161. z-index: 9999;
  162. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  163. display: flex;
  164. align-items: center;
  165. justify-content: center;
  166. user-select: none;
  167. transition: background-color 0.3s;
  168. }
  169. .ai-floating-btn:hover {
  170. background: #45a049;
  171. }
  172. .ai-floating-btn svg {
  173. width: 20px;
  174. height: 20px;
  175. fill: white;
  176. }
  177. .ai-menu {
  178. position: absolute;
  179. background: white;
  180. border-radius: 5px;
  181. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  182. padding: 8px;
  183. z-index: 10000;
  184. display: flex;
  185. gap: 8px;
  186. }
  187. .ai-menu-item {
  188. width: 32px;
  189. height: 32px;
  190. padding: 6px;
  191. cursor: pointer;
  192. border-radius: 50%;
  193. display: flex;
  194. align-items: center;
  195. justify-content: center;
  196. transition: background-color 0.3s;
  197. }
  198. .ai-menu-item:hover {
  199. background: #f5f5f5;
  200. }
  201. .ai-menu-item svg {
  202. width: 20px;
  203. height: 20px;
  204. fill: #666;
  205. }
  206. .ai-menu-item:hover svg {
  207. fill: #4CAF50;
  208. }
  209. .ai-image-options {
  210. display: flex;
  211. flex-direction: column;
  212. gap: 10px;
  213. margin: 15px 0;
  214. }
  215. .ai-image-options button {
  216. padding: 8px 15px;
  217. border: none;
  218. border-radius: 4px;
  219. background: #4CAF50;
  220. color: white;
  221. cursor: pointer;
  222. transition: background-color 0.3s;
  223. font-size: 14px;
  224. }
  225. .ai-image-options button:hover {
  226. background: #45a049;
  227. }
  228. #ai-cancel {
  229. background: #dc3545;
  230. color: white;
  231. }
  232. #ai-cancel:hover {
  233. opacity: 0.9;
  234. }
  235. .ai-toast {
  236. position: fixed;
  237. top: 20px;
  238. left: 50%;
  239. transform: translateX(-50%);
  240. padding: 10px 20px;
  241. background: rgba(0, 0, 0, 0.8);
  242. color: white;
  243. border-radius: 4px;
  244. font-size: 14px;
  245. z-index: 10000;
  246. animation: fadeInOut 3s ease;
  247. pointer-events: none;
  248. white-space: pre-line;
  249. text-align: center;
  250. max-width: 80%;
  251. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  252. }
  253. @keyframes fadeInOut {
  254. 0% { opacity: 0; transform: translate(-50%, 10px); }
  255. 10% { opacity: 1; transform: translate(-50%, 0); }
  256. 90% { opacity: 1; transform: translate(-50%, 0); }
  257. 100% { opacity: 0; transform: translate(-50%, -10px); }
  258. }
  259. .ai-config-modal .button-group {
  260. display: flex;
  261. justify-content: flex-end;
  262. gap: 10px;
  263. margin-top: 20px;
  264. }
  265. .ai-config-modal .button-group button {
  266. padding: 6px 16px;
  267. border: none;
  268. border-radius: 4px;
  269. cursor: pointer;
  270. font-size: 14px;
  271. transition: background-color 0.2s;
  272. }
  273. .ai-config-modal .save-button {
  274. background: #007bff;
  275. color: white;
  276. }
  277. .ai-config-modal .save-button:hover {
  278. background: #0056b3;
  279. }
  280. .ai-config-modal .save-button:disabled {
  281. background: #cccccc;
  282. cursor: not-allowed;
  283. }
  284. .ai-config-modal .cancel-button {
  285. background: #f8f9fa;
  286. color: #333;
  287. }
  288. .ai-config-modal .cancel-button:hover {
  289. background: #e2e6ea;
  290. }
  291. .ai-selecting-image {
  292. cursor: crosshair !important;
  293. }
  294. .ai-selecting-image * {
  295. cursor: crosshair !important;
  296. }
  297. .ai-image-description {
  298. position: fixed;
  299. background: rgba(0, 0, 0, 0.8);
  300. color: white;
  301. padding: 8px 12px;
  302. border-radius: 4px;
  303. font-size: 14px;
  304. line-height: 1.4;
  305. max-width: 300px;
  306. text-align: center;
  307. word-wrap: break-word;
  308. z-index: 10000;
  309. pointer-events: none;
  310. animation: fadeIn 0.3s ease;
  311. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  312. }
  313. @keyframes fadeIn {
  314. from { opacity: 0; }
  315. to { opacity: 1; }
  316. }
  317. .ai-modal-overlay {
  318. position: fixed;
  319. top: 0;
  320. left: 0;
  321. width: 100%;
  322. height: 100%;
  323. background: rgba(0, 0, 0, 0.5);
  324. display: flex;
  325. justify-content: center;
  326. align-items: center;
  327. z-index: 9999;
  328. }
  329. .ai-result-modal {
  330. background: white;
  331. padding: 20px;
  332. border-radius: 8px;
  333. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  334. position: relative;
  335. min-width: 300px;
  336. max-width: 1000px;
  337. max-height: 540px;
  338. overflow-y: auto;
  339. width: 90%;
  340. }
  341. .ai-result-modal h3 {
  342. margin: 0 0 10px 0;
  343. font-size: 14px;
  344. color: #333;
  345. }
  346. .ai-result-modal .description-code {
  347. background: #1e1e1e;
  348. color: #ffffff;
  349. padding: 6px;
  350. border-radius: 4px;
  351. margin: 5px 0;
  352. cursor: pointer;
  353. white-space: pre-wrap;
  354. word-wrap: break-word;
  355. font-family: monospace;
  356. border: 1px solid #333;
  357. position: relative;
  358. max-height: 500px;
  359. overflow-y: auto;
  360. font-size: 12px;
  361. line-height: 1.4;
  362. }
  363. .ai-result-modal .description-code * {
  364. color: #ffffff !important;
  365. background: transparent !important;
  366. }
  367. .ai-result-modal .description-code code {
  368. color: #ffffff;
  369. display: block;
  370. width: 100%;
  371. background: transparent !important;
  372. padding: 0;
  373. }
  374. .ai-result-modal .description-code:hover {
  375. background: #2d2d2d;
  376. }
  377. .ai-result-modal .copy-hint {
  378. font-size: 11px;
  379. color: #666;
  380. text-align: center;
  381. margin: 2px 0;
  382. }
  383. .ai-result-modal .close-button {
  384. position: absolute;
  385. top: 8px;
  386. right: 8px;
  387. background: none;
  388. border: none;
  389. font-size: 18px;
  390. cursor: pointer;
  391. color: #666;
  392. padding: 2px 6px;
  393. line-height: 1;
  394. }
  395. .ai-result-modal .close-button:hover {
  396. color: #333;
  397. }
  398. .ai-selection-overlay {
  399. position: fixed;
  400. top: 0;
  401. left: 0;
  402. right: 0;
  403. bottom: 0;
  404. background: rgba(0, 0, 0, 0.1);
  405. z-index: 999999;
  406. cursor: crosshair;
  407. pointer-events: none;
  408. }
  409.  
  410. .ai-selecting-image img {
  411. position: relative;
  412. z-index: 9999;
  413. cursor: pointer !important;
  414. transition: outline 0.2s ease;
  415. }
  416.  
  417. .ai-selecting-image img:hover {
  418. outline: 2px solid white;
  419. outline-offset: 2px;
  420. }
  421.  
  422. /* 移动端样式优化 */
  423. @media (max-width: 768px) {
  424. .ai-floating-btn {
  425. width: 40px;
  426. height: 40px;
  427. touch-action: none;
  428. }
  429.  
  430. .ai-floating-btn svg {
  431. width: 24px;
  432. height: 24px;
  433. }
  434.  
  435. .ai-config-modal {
  436. width: 90%;
  437. min-width: auto;
  438. max-width: 400px;
  439. padding: 15px;
  440. margin: 10px;
  441. box-sizing: border-box;
  442. }
  443.  
  444. .ai-config-modal .button-group {
  445. margin-top: 15px;
  446. flex-direction: row;
  447. justify-content: space-between;
  448. gap: 10px;
  449. }
  450.  
  451. .ai-config-modal .button-group button {
  452. flex: 1;
  453. min-height: 44px;
  454. font-size: 16px;
  455. padding: 10px;
  456. margin: 0;
  457. }
  458.  
  459. .ai-result-modal {
  460. width: 95%;
  461. min-width: auto;
  462. max-width: 90%;
  463. margin: 10px;
  464. padding: 15px;
  465. }
  466.  
  467. .ai-modal-overlay {
  468. padding: 10px;
  469. box-sizing: border-box;
  470. }
  471.  
  472. .ai-config-modal button,
  473. .ai-config-modal .input-icon,
  474. .ai-config-modal select,
  475. .ai-config-modal input {
  476. min-height: 44px;
  477. padding: 10px;
  478. font-size: 16px;
  479. }
  480.  
  481. .ai-config-modal textarea {
  482. min-height: 100px;
  483. font-size: 16px;
  484. padding: 10px;
  485. }
  486.  
  487. .ai-config-modal .input-icon {
  488. width: 44px;
  489. height: 44px;
  490. font-size: 20px;
  491. }
  492.  
  493. .ai-config-modal {
  494. max-height: 90vh;
  495. overflow-y: auto;
  496. -webkit-overflow-scrolling: touch;
  497. }
  498. }
  499.  
  500. .ai-selection-overlay img,
  501. .ai-selection-overlay [style*="background-image"],
  502. .ai-selection-overlay [class*="img"],
  503. .ai-selection-overlay [class*="photo"],
  504. .ai-selection-overlay [class*="image"],
  505. .ai-selection-overlay [class*="thumb"],
  506. .ai-selection-overlay [class*="avatar"] {
  507. cursor: pointer !important;
  508. transition: outline 0.2s;
  509. pointer-events: auto;
  510. }
  511.  
  512. .ai-selection-overlay img:hover,
  513. .ai-selection-overlay [style*="background-image"]:hover,
  514. .ai-selection-overlay [class*="img"]:hover,
  515. .ai-selection-overlay [class*="photo"]:hover,
  516. .ai-selection-overlay [class*="image"]:hover,
  517. .ai-selection-overlay [class*="thumb"]:hover,
  518. .ai-selection-overlay [class*="avatar"]:hover {
  519. outline: 3px solid #4CAF50 !important;
  520. outline-offset: 2px !important;
  521. }
  522.  
  523. /* 结果框样式 */
  524. .ai-result-modal {
  525. position: fixed;
  526. top: 50%;
  527. left: 50%;
  528. transform: translate(-50%, -50%);
  529. background: white;
  530. padding: 20px;
  531. border-radius: 8px;
  532. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  533. z-index: 1000000;
  534. max-width: 80%;
  535. max-height: 80vh;
  536. overflow-y: auto;
  537. }
  538.  
  539. .ai-result-modal .result-content {
  540. position: relative;
  541. }
  542.  
  543. .ai-result-modal .description-code {
  544. background: #1e1e1e;
  545. color: #ffffff;
  546. padding: 6px;
  547. border-radius: 4px;
  548. margin: 5px 0;
  549. cursor: pointer;
  550. white-space: pre-wrap;
  551. word-wrap: break-word;
  552. font-family: monospace;
  553. border: 1px solid #333;
  554. position: relative;
  555. max-height: 500px;
  556. overflow-y: auto;
  557. font-size: 12px;
  558. line-height: 1.4;
  559. }
  560.  
  561. .ai-result-modal .description-code * {
  562. color: #ffffff !important;
  563. background: transparent !important;
  564. }
  565.  
  566. .ai-result-modal .description-code code {
  567. color: #ffffff;
  568. display: block;
  569. width: 100%;
  570. background: transparent !important;
  571. padding: 0;
  572. }
  573.  
  574. .ai-result-modal .description-code:hover {
  575. background: #2d2d2d;
  576. }
  577.  
  578. .ai-result-modal .copy-hint {
  579. font-size: 12px;
  580. color: #666;
  581. text-align: center;
  582. margin-top: 5px;
  583. }
  584.  
  585. .ai-result-modal .close-button {
  586. position: absolute;
  587. top: -10px;
  588. right: -10px;
  589. width: 24px;
  590. height: 24px;
  591. border-radius: 50%;
  592. background: #ff4444;
  593. color: white;
  594. border: none;
  595. cursor: pointer;
  596. display: flex;
  597. align-items: center;
  598. justify-content: center;
  599. font-size: 16px;
  600. line-height: 1;
  601. padding: 0;
  602. }
  603.  
  604. .ai-result-modal .close-button:hover {
  605. background: #ff6666;
  606. }
  607. `);
  608.  
  609. // 全局变量
  610. let isSelectionMode = false;
  611.  
  612. // 定义默认提示词
  613. const DEFAULT_PROMPT = "I will give you a picture, help me describe the main content of the picture. If there are people in the picture, describe their clothing, posture, and expressions, and give a simple compliment. Answer in Chinese";
  614.  
  615. // 在全局变量部分添加
  616. const DEFAULT_API_KEY = '';
  617. const DEFAULT_API_ENDPOINT = 'https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash-exp:generateContent';
  618. const DEFAULT_MODEL = 'gemini-2.0-flash-exp';
  619.  
  620. // 添加支持的图片格式
  621. const SUPPORTED_MIME_TYPES = [
  622. 'image/png',
  623. 'image/jpeg',
  624. 'image/webp',
  625. 'image/heic',
  626. 'image/heif'
  627. ];
  628.  
  629. const MAX_FILE_SIZE = 7 * 1024 * 1024; // 7MB
  630. const TARGET_FILE_SIZE = 1 * 1024 * 1024; // 1MB
  631.  
  632. // 添加日志函数
  633. function log(message, data = null) {
  634. const timestamp = new Date().toISOString();
  635. if (data) {
  636. console.log(`[Gemini] ${timestamp} ${message}:`, data);
  637. } else {
  638. console.log(`[Gemini] ${timestamp} ${message}`);
  639. }
  640. }
  641.  
  642. // 修改图片压缩函数
  643. async function compressImage(base64Image, mimeType) {
  644. log('开始压缩图片', { mimeType });
  645. return new Promise((resolve, reject) => {
  646. const img = new Image();
  647. img.onload = () => {
  648. let quality = 0.9;
  649. let canvas = document.createElement('canvas');
  650. let ctx = canvas.getContext('2d');
  651.  
  652. let width = img.width;
  653. let height = img.height;
  654. log('原始图片尺寸', { width, height });
  655.  
  656. const MAX_DIMENSION = 2048;
  657. if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
  658. const ratio = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
  659. width *= ratio;
  660. height *= ratio;
  661. log('调整后的图片尺寸', { width, height });
  662. }
  663.  
  664. canvas.width = width;
  665. canvas.height = height;
  666. ctx.drawImage(img, 0, 0, width, height);
  667.  
  668. const compress = () => {
  669. const base64 = canvas.toDataURL(mimeType, quality);
  670. const size = Math.ceil((base64.length * 3) / 4);
  671. log('当前压缩质量和大小', { quality, size: `${(size / 1024 / 1024).toFixed(2)}MB` });
  672.  
  673. if (size > TARGET_FILE_SIZE && quality > 0.1) {
  674. quality -= 0.1;
  675. compress();
  676. } else {
  677. log('压缩完成', { finalQuality: quality, finalSize: `${(size / 1024 / 1024).toFixed(2)}MB` });
  678. resolve(base64.split(',')[1]);
  679. }
  680. };
  681.  
  682. compress();
  683. };
  684. img.onerror = (error) => {
  685. log('图片加载失败', error);
  686. reject(error);
  687. };
  688. img.src = `data:${mimeType};base64,${base64Image}`;
  689. });
  690. }
  691.  
  692. // 修改图片上传函数
  693. async function uploadImageToGemini(base64Image, mimeType) {
  694. try {
  695. log('开始上传图片', { mimeType });
  696.  
  697. // 转换为二进制数据
  698. const binaryData = atob(base64Image);
  699. const bytes = new Uint8Array(binaryData.length);
  700. for (let i = 0; i < binaryData.length; i++) {
  701. bytes[i] = binaryData.charCodeAt(i);
  702. }
  703. const blob = new Blob([bytes], { type: mimeType });
  704. log('准备上传的文件大小', `${(blob.size / 1024 / 1024).toFixed(2)}MB`);
  705.  
  706. // 获取 API Key
  707. const apiKey = GM_getValue('apiKey', DEFAULT_API_KEY);
  708. if (!apiKey) {
  709. throw new Error('请先在配置中设置 API Key');
  710. }
  711.  
  712. // 第一步:发起 resumable 上传请求
  713. log('发起 resumable 上传请求');
  714. const initResponse = await fetch(`https://generativelanguage.googleapis.com/upload/v1beta/files?key=${apiKey}`, {
  715. method: 'POST',
  716. headers: {
  717. 'X-Goog-Upload-Protocol': 'resumable',
  718. 'X-Goog-Upload-Command': 'start',
  719. 'X-Goog-Upload-Header-Content-Length': blob.size.toString(),
  720. 'X-Goog-Upload-Header-Content-Type': mimeType,
  721. 'Content-Type': 'application/json'
  722. },
  723. body: JSON.stringify({
  724. file: {
  725. display_name: `image_${Date.now()}.${mimeType.split('/')[1]}`
  726. }
  727. })
  728. });
  729.  
  730. if (!initResponse.ok) {
  731. const errorData = await initResponse.text();
  732. throw new Error(`上传初始化失败: HTTP ${initResponse.status} - ${errorData}`);
  733. }
  734.  
  735. // 从响应头中获取上传 URL
  736. const uploadUrl = initResponse.headers.get('x-goog-upload-url');
  737. if (!uploadUrl) {
  738. throw new Error('未能获取上传 URL');
  739. }
  740.  
  741. // 第二步:上传实际的图片数据
  742. log('开��上传图片数据');
  743. const uploadResponse = await fetch(uploadUrl, {
  744. method: 'POST',
  745. headers: {
  746. 'Content-Length': blob.size.toString(),
  747. 'X-Goog-Upload-Offset': '0',
  748. 'X-Goog-Upload-Command': 'upload, finalize'
  749. },
  750. body: blob
  751. });
  752.  
  753. if (!uploadResponse.ok) {
  754. const errorData = await uploadResponse.text();
  755. throw new Error(`上传文件失败: HTTP ${uploadResponse.status} - ${errorData}`);
  756. }
  757.  
  758. const data = await uploadResponse.json();
  759. if (data.file && data.file.uri) {
  760. return data.file.uri;
  761. } else {
  762. throw new Error(`文件上传失败: ${JSON.stringify(data)}`);
  763. }
  764. } catch (error) {
  765. log('上传图片失败', error);
  766. throw error;
  767. }
  768. }
  769.  
  770. // 修改 fetchImageAsBase64 函数
  771. async function fetchImageAsBase64(url) {
  772. try {
  773. log('开始通过 fetch 获取图片', url);
  774.  
  775. // 直接使用 GM_xmlhttpRequest 获取图片
  776. return await new Promise((resolve, reject) => {
  777. GM_xmlhttpRequest({
  778. method: 'GET',
  779. url: url,
  780. responseType: 'blob',
  781. headers: {
  782. 'Accept': 'image/*'
  783. },
  784. onload: function(response) {
  785. if (response.status === 200) {
  786. const reader = new FileReader();
  787. reader.onloadend = () => {
  788. const base64 = reader.result.split(',')[1];
  789. resolve({
  790. base64,
  791. mimeType: response.response.type || 'image/jpeg'
  792. });
  793. };
  794. reader.onerror = reject;
  795. reader.readAsDataURL(response.response);
  796. } else {
  797. reject(new Error(`HTTP ${response.status}`));
  798. }
  799. },
  800. onerror: function(error) {
  801. log('GM_xmlhttpRequest 失败', error);
  802. reject(error);
  803. }
  804. });
  805. });
  806. } catch (error) {
  807. log('获取图片失败', error);
  808.  
  809. // 如果直接获取失败,尝试使用代理
  810. const proxyServices = [
  811. // 使用 cors-anywhere 代理
  812. `https://cors-anywhere.herokuapp.com/${url}`,
  813. // 使用 allOrigins 代理
  814. `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`,
  815. // 使用 crossorigin.me 代理
  816. `https://crossorigin.me/${url}`,
  817. // 使用 cors.bridged.cc 代理
  818. `https://cors.bridged.cc/${url}`
  819. ];
  820.  
  821. for (const proxyUrl of proxyServices) {
  822. try {
  823. log('尝试使用代理', proxyUrl);
  824. return await new Promise((resolve, reject) => {
  825. GM_xmlhttpRequest({
  826. method: 'GET',
  827. url: proxyUrl,
  828. responseType: 'blob',
  829. headers: {
  830. 'Accept': 'image/*'
  831. },
  832. onload: function(response) {
  833. if (response.status === 200) {
  834. const reader = new FileReader();
  835. reader.onloadend = () => {
  836. const base64 = reader.result.split(',')[1];
  837. resolve({
  838. base64,
  839. mimeType: response.response.type || 'image/jpeg'
  840. });
  841. };
  842. reader.onerror = reject;
  843. reader.readAsDataURL(response.response);
  844. } else {
  845. reject(new Error(`HTTP ${response.status}`));
  846. }
  847. },
  848. onerror: reject
  849. });
  850. });
  851. } catch (proxyError) {
  852. log(`代理 ${proxyUrl} 请求失败`, proxyError);
  853. continue;
  854. }
  855. }
  856.  
  857. throw new Error('无法获取图片数据: ' + error.message);
  858. }
  859. }
  860.  
  861. // 修改 imageToBase64 函数,添加更多错误检查
  862. async function imageToBase64(imgElement) {
  863. return new Promise((resolve, reject) => {
  864. try {
  865. // 检查是否是效的图片元素
  866. if (!(imgElement instanceof HTMLImageElement)) {
  867. throw new Error('无效的图片元素');
  868. }
  869.  
  870. // 检查图片是否已加载
  871. if (!imgElement.complete || !imgElement.naturalWidth) {
  872. // 如果图片未加载完成,等待加载
  873. imgElement.onload = () => processImage();
  874. imgElement.onerror = () => reject(new Error('图片加载失败'));
  875. return;
  876. }
  877.  
  878. processImage();
  879.  
  880. function processImage() {
  881. try {
  882. const canvas = document.createElement('canvas');
  883. canvas.width = imgElement.naturalWidth;
  884. canvas.height = imgElement.naturalHeight;
  885. const ctx = canvas.getContext('2d');
  886.  
  887. // 检查画布上下文是否创建成功
  888. if (!ctx) {
  889. throw new Error('无法创建 Canvas 上下文');
  890. }
  891.  
  892. // 绘制图片到 canvas
  893. ctx.drawImage(imgElement, 0, 0);
  894.  
  895. // 获取图片的实际 MIME 类型
  896. let mimeType = 'image/jpeg'; // 默认格式
  897. const src = imgElement.src;
  898.  
  899. // 检查图片源
  900. if (!src) {
  901. throw new Error('图片源无效');
  902. }
  903.  
  904. // 从 src 获取 MIME 类型
  905. if (src.startsWith('data:')) {
  906. const match = src.match(/^data:([^;]+);/);
  907. if (match) {
  908. mimeType = match[1];
  909. }
  910. } else {
  911. // 从文件扩展名获取 MIME 类型
  912. const extension = src.toLowerCase().match(/\.([^.]+)$/);
  913. if (extension) {
  914. const ext = extension[1];
  915. const mimeMap = {
  916. 'jpg': 'image/jpeg',
  917. 'jpeg': 'image/jpeg',
  918. 'png': 'image/png',
  919. 'webp': 'image/webp',
  920. 'gif': 'image/gif'
  921. };
  922. mimeType = mimeMap[ext] || 'image/jpeg';
  923. }
  924. }
  925.  
  926. log('处理图片', {
  927. width: imgElement.naturalWidth,
  928. height: imgElement.naturalHeight,
  929. src: src.substring(0, 100) + '...',
  930. mimeType: mimeType
  931. });
  932.  
  933. try {
  934. // 尝试使用原始格式
  935. const base64 = canvas.toDataURL(mimeType, 1.0).split(',')[1];
  936. if (!base64) {
  937. throw new Error('Base64 转换失败');
  938. }
  939. resolve({ base64, mimeType });
  940. } catch (e) {
  941. log('原始格式转换失败,尝试使用 JPEG', e);
  942. try {
  943. // 降级到 JPEG
  944. const base64 = canvas.toDataURL('image/jpeg', 0.9).split(',')[1];
  945. if (!base64) {
  946. throw new Error('JPEG 转换也失败了');
  947. }
  948. resolve({ base64, mimeType: 'image/jpeg' });
  949. } catch (jpegError) {
  950. // 如果 Canvas 转换都失败了,尝试直接获取图片数据
  951. log('Canvas 转换失败,尝试直接获取图片', jpegError);
  952. if (src.startsWith('data:')) {
  953. const [header, base64] = src.split(',');
  954. const mimeType = header.split(':')[1].split(';')[0];
  955. if (base64 && mimeType) {
  956. resolve({ base64, mimeType });
  957. } else {
  958. throw new Error('无法从 data URL 提取数据');
  959. }
  960. } else {
  961. // 最后尝试通过 fetch 获取
  962. fetchImageAsBase64(src).then(resolve).catch(reject);
  963. }
  964. }
  965. }
  966. } catch (error) {
  967. log('图片处理失败', error);
  968. // 尝试通过 fetch 获取
  969. fetchImageAsBase64(imgElement.src).then(resolve).catch(reject);
  970. }
  971. }
  972. } catch (error) {
  973. log('图片处理过程出错', error);
  974. reject(error);
  975. }
  976. });
  977. }
  978.  
  979. // 修改生成描述的函数
  980. async function generateImageDescription(imageBase64, prompt, mimeType) {
  981. try {
  982. log('开始生成图片描述');
  983. log('使用的提示词', prompt);
  984.  
  985. const fileUri = await uploadImageToGemini(imageBase64, mimeType);
  986. log('开始调用生成接口');
  987.  
  988. // 完全按照 demo-gemini.sh 的请求格式修改
  989. const requestBody = {
  990. contents: [{
  991. parts: [
  992. {
  993. text: prompt || DEFAULT_PROMPT
  994. },
  995. {
  996. file_data: { // 注意这里是 file_data 而不是 fileData
  997. mime_type: mimeType, // 使用下划线格式
  998. file_uri: fileUri // 使用下划线格式
  999. }
  1000. }
  1001. ]
  1002. }]
  1003. };
  1004. log('请求参数', requestBody);
  1005.  
  1006. // 修改请求 URL,使用 v1beta 版本的 API
  1007. const apiKey = GM_getValue('apiKey', DEFAULT_API_KEY);
  1008. const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, {
  1009. method: 'POST',
  1010. headers: {
  1011. 'Content-Type': 'application/json'
  1012. },
  1013. body: JSON.stringify(requestBody)
  1014. });
  1015.  
  1016. const data = await response.json();
  1017. log('生成接口响应', data);
  1018.  
  1019. // 解析响应数据
  1020. if (data.candidates && data.candidates[0] && data.candidates[0].content) {
  1021. const text = data.candidates[0].content.parts[0].text;
  1022. log('成功生成描述');
  1023. return text;
  1024. } else {
  1025. throw new Error(data.error?.message || '无法获取图片描述');
  1026. }
  1027. } catch (error) {
  1028. log('生成描述失��', error);
  1029. throw error;
  1030. }
  1031. }
  1032.  
  1033. // 修改 API 检测功能
  1034. async function checkApiKey(apiKey) {
  1035. try {
  1036. log('开始验证 API Key');
  1037. const response = await fetch(`https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash-exp`, {
  1038. headers: {
  1039. 'x-goog-api-key': apiKey
  1040. }
  1041. });
  1042.  
  1043. const data = await response.json();
  1044. log('API 验证响应', data);
  1045.  
  1046. if (data.name && data.name.includes('gemini-2.0-flash-exp')) {
  1047. log('API Key 验证成功');
  1048. return [data];
  1049. }
  1050. throw new Error('无效的 API Key 或模型不可用');
  1051. } catch (error) {
  1052. log('API 验证失败', error);
  1053. throw new Error(`API 验证失败: ${error.message}`);
  1054. }
  1055. }
  1056.  
  1057. // 显示toast提示
  1058. function showToast(message, duration = 3000) {
  1059. const toast = document.createElement('div');
  1060. toast.className = 'ai-toast';
  1061. toast.textContent = message;
  1062. document.body.appendChild(toast);
  1063.  
  1064. setTimeout(() => {
  1065. toast.remove();
  1066. }, duration);
  1067. }
  1068.  
  1069. // 修改 findImage 函数,增强懒加载图片的检测
  1070. function findImage(target) {
  1071. let img = null;
  1072. let imgSrc = null;
  1073.  
  1074. // 检查是否为图片元素
  1075. if (target.nodeName === 'IMG') {
  1076. img = target;
  1077. // 优先获取 data-src(懒加载原图)
  1078. imgSrc = target.getAttribute('data-src') ||
  1079. target.getAttribute('data-original') ||
  1080. target.getAttribute('data-actualsrc') ||
  1081. target.getAttribute('data-url') ||
  1082. target.getAttribute('data-echo') ||
  1083. target.getAttribute('data-lazy-src') ||
  1084. target.getAttribute('data-original-src') ||
  1085. target.src; // 最后才使用 src 属性
  1086. }
  1087. // 检查背景图
  1088. else if (target.style && target.style.backgroundImage) {
  1089. let bgImg = target.style.backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
  1090. if (bgImg) {
  1091. imgSrc = bgImg[1];
  1092. img = target;
  1093. }
  1094. }
  1095. // 检查父元素的背景图
  1096. else {
  1097. let parent = target.parentElement;
  1098. if (parent && parent.style && parent.style.backgroundImage) {
  1099. let bgImg = parent.style.backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
  1100. if (bgImg) {
  1101. imgSrc = bgImg[1];
  1102. img = parent;
  1103. }
  1104. }
  1105. }
  1106.  
  1107. // 检查常见的图片容器
  1108. if (!img) {
  1109. // 检查父元素是否为图片容器
  1110. let imgWrapper = target.closest('[class*="img"],[class*="photo"],[class*="image"],[class*="thumb"],[class*="avatar"],[class*="masonry"]');
  1111. if (imgWrapper) {
  1112. // 在容器中查找图片元素
  1113. let possibleImg = imgWrapper.querySelector('img');
  1114. if (possibleImg) {
  1115. img = possibleImg;
  1116. // 同样优先获取懒加载原图
  1117. imgSrc = possibleImg.getAttribute('data-src') ||
  1118. possibleImg.getAttribute('data-original') ||
  1119. possibleImg.getAttribute('data-actualsrc') ||
  1120. possibleImg.getAttribute('data-url') ||
  1121. possibleImg.getAttribute('data-echo') ||
  1122. possibleImg.getAttribute('data-lazy-src') ||
  1123. possibleImg.getAttribute('data-original-src') ||
  1124. possibleImg.src;
  1125. } else {
  1126. // 检查容器的背景图
  1127. let bgImg = getComputedStyle(imgWrapper).backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
  1128. if (bgImg) {
  1129. imgSrc = bgImg[1];
  1130. img = imgWrapper;
  1131. }
  1132. }
  1133. }
  1134. }
  1135.  
  1136. // 检查特殊情况:某些网站使用自定义属性存储真实图片地址
  1137. if (img && !imgSrc) {
  1138. // 获取元素的所有属性
  1139. const attrs = img.attributes;
  1140. for (let i = 0; i < attrs.length; i++) {
  1141. const attr = attrs[i];
  1142. // 检查属性名中���否包含关键字
  1143. if (attr.name.toLowerCase().includes('src') ||
  1144. attr.name.toLowerCase().includes('url') ||
  1145. attr.name.toLowerCase().includes('img') ||
  1146. attr.name.toLowerCase().includes('thumb') ||
  1147. attr.name.toLowerCase().includes('original') ||
  1148. attr.name.toLowerCase().includes('data')) {
  1149. const value = attr.value;
  1150. if (value && /^https?:\/\//.test(value)) {
  1151. imgSrc = value;
  1152. break;
  1153. }
  1154. }
  1155. }
  1156. }
  1157.  
  1158. // 检查父级链接
  1159. if (img && !imgSrc) {
  1160. let parentLink = img.closest('a');
  1161. if (parentLink && parentLink.href) {
  1162. if (/\.(jpe?g|png|webp|gif)$/i.test(parentLink.href)) {
  1163. imgSrc = parentLink.href;
  1164. }
  1165. }
  1166. }
  1167.  
  1168. // 如果找到了图片但没有找到有效的 URL,记录日志
  1169. if (img && !imgSrc) {
  1170. log('找到图片元素但未找到有效的图片URL', {
  1171. element: img,
  1172. attributes: Array.from(img.attributes).map(attr => `${attr.name}="${attr.value}"`).join(', ')
  1173. });
  1174. }
  1175.  
  1176. return { img, imgSrc };
  1177. }
  1178.  
  1179. // 修改点击处理函数
  1180. function clickHandler(e) {
  1181. if (!isSelectionMode) return;
  1182.  
  1183. const { img, imgSrc } = findImage(e.target);
  1184.  
  1185. if (!img || !imgSrc) return;
  1186.  
  1187. e.preventDefault();
  1188. e.stopPropagation();
  1189.  
  1190. // 检查图片是否有效
  1191. if (img instanceof HTMLImageElement) {
  1192. if (!img.complete || !img.naturalWidth) {
  1193. showToast('图片未加载完成或无效');
  1194. return;
  1195. }
  1196. if (img.naturalWidth < 10 || img.naturalHeight < 10) {
  1197. showToast('图片太小,无法处理');
  1198. return;
  1199. }
  1200. }
  1201.  
  1202. processImage(img, imgSrc);
  1203. }
  1204.  
  1205. // 进入图片选择模式
  1206. function enterImageSelectionMode() {
  1207. if (isSelectionMode) return;
  1208.  
  1209. isSelectionMode = true;
  1210.  
  1211. const floatingBtn = document.querySelector('.ai-floating-btn');
  1212. if (floatingBtn) {
  1213. floatingBtn.style.display = 'none';
  1214. }
  1215.  
  1216. const overlay = document.createElement('div');
  1217. overlay.className = 'ai-selection-overlay';
  1218. document.body.appendChild(overlay);
  1219.  
  1220. document.body.classList.add('ai-selecting-image');
  1221.  
  1222. document.addEventListener('click', clickHandler, true);
  1223.  
  1224. const escHandler = (e) => {
  1225. if (e.key === 'Escape') {
  1226. exitImageSelectionMode();
  1227. }
  1228. };
  1229. document.addEventListener('keydown', escHandler);
  1230.  
  1231. window._imageSelectionHandlers = {
  1232. click: clickHandler,
  1233. keydown: escHandler
  1234. };
  1235. }
  1236.  
  1237. // 退出图片选择模式
  1238. function exitImageSelectionMode() {
  1239. isSelectionMode = false;
  1240.  
  1241. const floatingBtn = document.querySelector('.ai-floating-btn');
  1242. if (floatingBtn) {
  1243. floatingBtn.style.display = 'flex';
  1244. }
  1245.  
  1246. const overlay = document.querySelector('.ai-selection-overlay');
  1247. if (overlay) {
  1248. overlay.remove();
  1249. }
  1250.  
  1251. document.body.classList.remove('ai-selecting-image');
  1252.  
  1253. if (window._imageSelectionHandlers) {
  1254. document.removeEventListener('click', window._imageSelectionHandlers.click, true);
  1255. document.removeEventListener('keydown', window._imageSelectionHandlers.keydown);
  1256. window._imageSelectionHandlers = null;
  1257. }
  1258. }
  1259.  
  1260. // 修改配置界面创建函数
  1261. function createConfigUI() {
  1262. const existingModal = document.querySelector('.ai-modal-overlay');
  1263. if (existingModal) {
  1264. existingModal.remove();
  1265. }
  1266.  
  1267. const overlay = document.createElement('div');
  1268. overlay.className = 'ai-modal-overlay';
  1269.  
  1270. const modal = document.createElement('div');
  1271. modal.className = 'ai-config-modal';
  1272. modal.innerHTML = `
  1273. <h3>AI图像描述配置</h3>
  1274. <div class="input-group">
  1275. <label>API Endpoint:</label>
  1276. <div class="input-wrapper">
  1277. <input type="text" id="ai-endpoint" placeholder="https://api.openai.com" value="${DEFAULT_API_ENDPOINT}" readonly>
  1278. </div>
  1279. </div>
  1280. <div class="input-group">
  1281. <label>API Key:</label>
  1282. <div class="input-wrapper">
  1283. <input type="password" id="ai-apikey" placeholder="输入你的 API Key" value="${GM_getValue('apiKey', DEFAULT_API_KEY)}">
  1284. <span class="input-icon toggle-password" title="显示/隐藏">👁️</span>
  1285. </div>
  1286. </div>
  1287. <div class="input-group">
  1288. <label>使用模型:</label>
  1289. <select id="ai-model" disabled>
  1290. <option value="${DEFAULT_MODEL}">${DEFAULT_MODEL}</option>
  1291. </select>
  1292. </div>
  1293. <div class="input-group">
  1294. <label>提示词:</label>
  1295. <div class="input-wrapper">
  1296. <textarea id="ai-prompt" rows="4" style="width: 100%; resize: vertical;">${GM_getValue('customPrompt', DEFAULT_PROMPT)}</textarea>
  1297. <span class="input-icon clear-icon" title="重置为默认值">↺</span>
  1298. </div>
  1299. </div>
  1300. <div class="button-group">
  1301. <button type="button" class="cancel-button" id="ai-cancel-config">取消</button>
  1302. <button type="button" class="save-button" id="ai-save-config">保存</button>
  1303. </div>
  1304. `;
  1305.  
  1306. overlay.appendChild(modal);
  1307. document.body.appendChild(overlay);
  1308.  
  1309. // 添加密码显示/隐藏功能
  1310. const togglePassword = modal.querySelector('.toggle-password');
  1311. const apiKeyInput = modal.querySelector('#ai-apikey');
  1312. if (togglePassword && apiKeyInput) {
  1313. togglePassword.addEventListener('click', function() {
  1314. const type = apiKeyInput.type === 'password' ? 'text' : 'password';
  1315. apiKeyInput.type = type;
  1316. this.textContent = type === 'password' ? '👁️' : '👁️‍🗨️';
  1317. });
  1318. }
  1319.  
  1320. // 保留提示词的重置功能
  1321. const clearButtons = modal.querySelectorAll('.clear-icon');
  1322. clearButtons.forEach(button => {
  1323. button.addEventListener('click', function(e) {
  1324. const input = this.parentElement.querySelector('textarea');
  1325. if (input && input.id === 'ai-prompt') {
  1326. input.value = DEFAULT_PROMPT;
  1327. input.focus();
  1328. }
  1329. });
  1330. });
  1331.  
  1332. // 修改保存按钮事件
  1333. const saveButton = modal.querySelector('#ai-save-config');
  1334. if (saveButton) {
  1335. saveButton.addEventListener('click', function(e) {
  1336. e.preventDefault();
  1337. e.stopPropagation();
  1338.  
  1339. const apiKey = modal.querySelector('#ai-apikey').value.trim();
  1340. const customPrompt = modal.querySelector('#ai-prompt').value.trim();
  1341.  
  1342. // 保存配置
  1343. if (apiKey) {
  1344. GM_setValue('apiKey', apiKey);
  1345. }
  1346. if (customPrompt) {
  1347. GM_setValue('customPrompt', customPrompt);
  1348. }
  1349.  
  1350. showToast('配置已保存');
  1351. overlay.remove();
  1352. });
  1353. }
  1354.  
  1355. // 取消配置
  1356. const cancelButton = modal.querySelector('#ai-cancel-config');
  1357. if (cancelButton) {
  1358. cancelButton.addEventListener('click', function(e) {
  1359. e.preventDefault();
  1360. e.stopPropagation();
  1361. overlay.remove();
  1362. });
  1363. }
  1364.  
  1365. // 点击遮罩层关闭
  1366. overlay.addEventListener('click', function(e) {
  1367. if (e.target === overlay) {
  1368. overlay.remove();
  1369. }
  1370. });
  1371.  
  1372. // 阻止模态框内的点击事件冒泡
  1373. modal.addEventListener('click', function(e) {
  1374. e.stopPropagation();
  1375. });
  1376. }
  1377.  
  1378. // 创建悬浮按钮
  1379. function createFloatingButton() {
  1380. const btn = document.createElement('div');
  1381. btn.className = 'ai-floating-btn';
  1382. btn.innerHTML = `
  1383. <svg viewBox="0 0 24 24">
  1384. <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-14c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
  1385. </svg>
  1386. `;
  1387.  
  1388. const savedPos = JSON.parse(GM_getValue('btnPosition', '{"x": 20, "y": 20}'));
  1389. btn.style.left = (savedPos.x || 20) + 'px';
  1390. btn.style.top = (savedPos.y || 20) + 'px';
  1391. btn.style.right = 'auto';
  1392. btn.style.bottom = 'auto';
  1393.  
  1394. let isDragging = false;
  1395. let hasMoved = false;
  1396. let startX, startY;
  1397. let initialLeft, initialTop;
  1398. let longPressTimer;
  1399. let touchStartTime;
  1400.  
  1401. // 触屏事件处理
  1402. btn.addEventListener('touchstart', function (e) {
  1403. e.preventDefault();
  1404. touchStartTime = Date.now();
  1405.  
  1406. longPressTimer = setTimeout(() => {
  1407. exitImageSelectionMode();
  1408. createConfigUI();
  1409. }, 500);
  1410.  
  1411. const touch = e.touches[0];
  1412. startX = touch.clientX;
  1413. startY = touch.clientY;
  1414. const rect = btn.getBoundingClientRect();
  1415. initialLeft = rect.left;
  1416. initialTop = rect.top;
  1417. });
  1418.  
  1419. btn.addEventListener('touchmove', function (e) {
  1420. e.preventDefault();
  1421. clearTimeout(longPressTimer);
  1422.  
  1423. const touch = e.touches[0];
  1424. const deltaX = touch.clientX - startX;
  1425. const deltaY = touch.clientY - startY;
  1426.  
  1427. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  1428. hasMoved = true;
  1429. }
  1430.  
  1431. const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
  1432. const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
  1433.  
  1434. btn.style.left = newLeft + 'px';
  1435. btn.style.top = newTop + 'px';
  1436. });
  1437.  
  1438. btn.addEventListener('touchend', function (e) {
  1439. e.preventDefault();
  1440. clearTimeout(longPressTimer);
  1441.  
  1442. const touchDuration = Date.now() - touchStartTime;
  1443.  
  1444. if (!hasMoved && touchDuration < 500) {
  1445. enterImageSelectionMode();
  1446. }
  1447.  
  1448. if (hasMoved) {
  1449. const rect = btn.getBoundingClientRect();
  1450. GM_setValue('btnPosition', JSON.stringify({
  1451. x: rect.left,
  1452. y: rect.top
  1453. }));
  1454. }
  1455.  
  1456. hasMoved = false;
  1457. });
  1458.  
  1459. // 鼠标事件处理
  1460. btn.addEventListener('click', function (e) {
  1461. if (e.button === 0 && !hasMoved) {
  1462. enterImageSelectionMode();
  1463. e.stopPropagation();
  1464. }
  1465. hasMoved = false;
  1466. });
  1467.  
  1468. btn.addEventListener('contextmenu', function (e) {
  1469. e.preventDefault();
  1470. exitImageSelectionMode();
  1471. createConfigUI();
  1472. });
  1473.  
  1474. // 拖拽相关事件
  1475. function dragStart(e) {
  1476. if (e.target === btn || btn.contains(e.target)) {
  1477. isDragging = true;
  1478. hasMoved = false;
  1479. const rect = btn.getBoundingClientRect();
  1480. startX = e.clientX;
  1481. startY = e.clientY;
  1482. initialLeft = rect.left;
  1483. initialTop = rect.top;
  1484. e.preventDefault();
  1485. }
  1486. }
  1487.  
  1488. function drag(e) {
  1489. if (isDragging) {
  1490. e.preventDefault();
  1491. const deltaX = e.clientX - startX;
  1492. const deltaY = e.clientY - startY;
  1493.  
  1494. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  1495. hasMoved = true;
  1496. }
  1497.  
  1498. const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
  1499. const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
  1500.  
  1501. btn.style.left = newLeft + 'px';
  1502. btn.style.top = newTop + 'px';
  1503. }
  1504. }
  1505.  
  1506. function dragEnd(e) {
  1507. if (isDragging) {
  1508. isDragging = false;
  1509. const rect = btn.getBoundingClientRect();
  1510. GM_setValue('btnPosition', JSON.stringify({
  1511. x: rect.left,
  1512. y: rect.top
  1513. }));
  1514. }
  1515. }
  1516.  
  1517. btn.addEventListener('mousedown', dragStart);
  1518. document.addEventListener('mousemove', drag);
  1519. document.addEventListener('mouseup', dragEnd);
  1520.  
  1521. document.body.appendChild(btn);
  1522. return btn;
  1523. }
  1524.  
  1525. // 添加 processImage 函数
  1526. async function processImage(img, imgSrc) {
  1527. try {
  1528. showToast('正在处理图片...');
  1529.  
  1530. // 获取图片数据
  1531. let imgData;
  1532. if (img instanceof HTMLImageElement) {
  1533. imgData = await imageToBase64(img);
  1534. } else {
  1535. // 对于背景图等情况,直接获取图片
  1536. imgData = await fetchImageAsBase64(imgSrc);
  1537. }
  1538.  
  1539. if (!imgData || !imgData.base64) {
  1540. throw new Error('无法获取图片数据');
  1541. }
  1542.  
  1543. log('获取到图片数据', {
  1544. mimeType: imgData.mimeType,
  1545. dataLength: imgData.base64.length,
  1546. source: imgSrc
  1547. });
  1548.  
  1549. // 获取用户设置的提示词
  1550. const customPrompt = GM_getValue('customPrompt', DEFAULT_PROMPT);
  1551.  
  1552. // 调用 Gemini API 获取描述
  1553. const description = await generateImageDescription(imgData.base64, customPrompt, imgData.mimeType);
  1554.  
  1555. // 显示结果
  1556. showResult(description);
  1557.  
  1558. // 处理完成后退出选择模式
  1559. exitImageSelectionMode();
  1560. } catch (error) {
  1561. log('处理图片失败', error);
  1562. showToast(`处理失败: ${error.message}`);
  1563. }
  1564. }
  1565.  
  1566. // 添加显示结果的函数
  1567. function showResult(description) {
  1568. // 移除已存在的结果框
  1569. const existingResult = document.querySelector('.ai-result-modal');
  1570. if (existingResult) {
  1571. existingResult.remove();
  1572. }
  1573.  
  1574. // 创建结果框
  1575. const resultModal = document.createElement('div');
  1576. resultModal.className = 'ai-result-modal';
  1577. resultModal.innerHTML = `
  1578. <div class="result-content">
  1579. <div class="description-code">
  1580. <code>${description}</code>
  1581. </div>
  1582. <div class="copy-hint">点击上方文本可复制</div>
  1583. <button class="close-button">×</button>
  1584. </div>
  1585. `;
  1586.  
  1587. // 添加复制功能
  1588. const codeBlock = resultModal.querySelector('.description-code');
  1589. codeBlock.addEventListener('click', () => {
  1590. const text = codeBlock.textContent;
  1591. GM_setClipboard(text);
  1592. showToast('已复制到剪贴板');
  1593. });
  1594.  
  1595. // 添加关闭功能
  1596. const closeButton = resultModal.querySelector('.close-button');
  1597. closeButton.addEventListener('click', () => {
  1598. resultModal.remove();
  1599. });
  1600.  
  1601. document.body.appendChild(resultModal);
  1602. }
  1603.  
  1604. // 初始化
  1605. function initialize() {
  1606. if (document.readyState === 'loading') {
  1607. document.addEventListener('DOMContentLoaded', () => {
  1608. createFloatingButton();
  1609. });
  1610. } else {
  1611. createFloatingButton();
  1612. }
  1613. }
  1614.  
  1615. // 启动脚本
  1616. initialize();
  1617. })();