AI Image Description Generator

使用AI生成网页图片描述

目前为 2024-12-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name AI Image Description Generator
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.7
  5. // @description 使用AI生成网页图片描述
  6. // @author AlphaCat
  7. // @match *://*/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_setClipboard
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // 全局变量
  21. let isSelectionMode = false;
  22. // 定义支持的视觉模型列表
  23. const supportedVLModels = [
  24. 'Qwen/Qwen2-VL-72B-Instruct',
  25. 'Pro/Qwen/Qwen2-VL-7B-Instruct',
  26. 'OpenGVLab/InternVL2-Llama3-76B',
  27. 'OpenGVLab/InternVL2-26B',
  28. 'Pro/OpenGVLab/InternVL2-8B'
  29. ];
  30.  
  31. // 定义GLM-4V系列模型
  32. const glm4vModels = [
  33. 'glm-4v',
  34. 'glm-4v-flash'
  35. ];
  36.  
  37. // 添加样式
  38. GM_addStyle(`
  39. .ai-config-modal {
  40. position: fixed;
  41. top: 50%;
  42. left: 50%;
  43. transform: translate(-50%, -50%);
  44. background: white;
  45. padding: 20px;
  46. border-radius: 8px;
  47. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  48. z-index: 10000;
  49. min-width: 500px;
  50. height: auto;
  51. }
  52. .ai-config-modal h3 {
  53. margin: 0 0 15px 0;
  54. font-size: 14px;
  55. font-weight: bold;
  56. color: #333;
  57. }
  58. .ai-config-modal label {
  59. display: inline-block;
  60. font-size: 12px;
  61. font-weight: bold;
  62. color: #333;
  63. margin: 0;
  64. line-height: normal;
  65. height: auto;
  66. }
  67. .ai-config-modal .input-wrapper {
  68. position: relative;
  69. display: flex;
  70. align-items: center;
  71. }
  72. .ai-config-modal input {
  73. display: block;
  74. width: 100%;
  75. padding: 2px 24px 2px 2px;
  76. margin: 2px;
  77. border: 1px solid #ddd;
  78. border-radius: 4px;
  79. font-size: 13px;
  80. line-height: normal;
  81. height: auto;
  82. box-sizing: border-box;
  83. }
  84. .ai-config-modal .input-icon {
  85. position: absolute;
  86. right: 4px;
  87. width: 16px;
  88. height: 16px;
  89. cursor: pointer;
  90. display: flex;
  91. align-items: center;
  92. justify-content: center;
  93. color: #666;
  94. font-size: 12px;
  95. user-select: none;
  96. }
  97. .ai-config-modal .clear-icon {
  98. right: 24px;
  99. }
  100. .ai-config-modal .toggle-password {
  101. right: 4px;
  102. }
  103. .ai-config-modal .input-icon:hover {
  104. color: #333;
  105. }
  106. .ai-config-modal .input-group {
  107. margin-bottom: 12px;
  108. height: auto;
  109. display: flex;
  110. flex-direction: column;
  111. }
  112. .ai-config-modal .button-row {
  113. display: flex;
  114. gap: 10px;
  115. align-items: center;
  116. margin-top: 5px;
  117. }
  118. .ai-config-modal .check-button {
  119. padding: 4px 8px;
  120. border: none;
  121. border-radius: 4px;
  122. background: #007bff;
  123. color: white;
  124. cursor: pointer;
  125. font-size: 12px;
  126. }
  127. .ai-config-modal .check-button:hover {
  128. background: #0056b3;
  129. }
  130. .ai-config-modal .check-button:disabled {
  131. background: #cccccc;
  132. cursor: not-allowed;
  133. }
  134. .ai-config-modal select {
  135. width: 100%;
  136. padding: 4px;
  137. border: 1px solid #ddd;
  138. border-radius: 4px;
  139. font-size: 13px;
  140. margin-top: 2px;
  141. }
  142. .ai-config-modal .status-text {
  143. font-size: 12px;
  144. margin-left: 10px;
  145. }
  146. .ai-config-modal .status-success {
  147. color: #28a745;
  148. }
  149. .ai-config-modal .status-error {
  150. color: #dc3545;
  151. }
  152. .ai-config-modal button {
  153. margin: 10px 5px;
  154. padding: 8px 15px;
  155. border: none;
  156. border-radius: 4px;
  157. cursor: pointer;
  158. font-size: 14px;
  159. }
  160. .ai-config-modal button#ai-save-config {
  161. background: #4CAF50;
  162. color: white;
  163. }
  164. .ai-config-modal button#ai-cancel-config {
  165. background: #dc3545;
  166. color: white;
  167. }
  168. .ai-config-modal button:hover {
  169. opacity: 0.9;
  170. }
  171. .ai-floating-btn {
  172. position: fixed;
  173. width: 32px;
  174. height: 32px;
  175. background: #4CAF50;
  176. color: white;
  177. border-radius: 50%;
  178. cursor: move;
  179. z-index: 9999;
  180. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  181. display: flex;
  182. align-items: center;
  183. justify-content: center;
  184. user-select: none;
  185. transition: background-color 0.3s;
  186. }
  187. .ai-floating-btn:hover {
  188. background: #45a049;
  189. }
  190. .ai-floating-btn svg {
  191. width: 20px;
  192. height: 20px;
  193. fill: white;
  194. }
  195. .ai-menu {
  196. position: absolute;
  197. background: white;
  198. border-radius: 5px;
  199. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  200. padding: 8px;
  201. z-index: 10000;
  202. display: flex;
  203. gap: 8px;
  204. }
  205. .ai-menu-item {
  206. width: 32px;
  207. height: 32px;
  208. padding: 6px;
  209. cursor: pointer;
  210. border-radius: 50%;
  211. display: flex;
  212. align-items: center;
  213. justify-content: center;
  214. transition: background-color 0.3s;
  215. }
  216. .ai-menu-item:hover {
  217. background: #f5f5f5;
  218. }
  219. .ai-menu-item svg {
  220. width: 20px;
  221. height: 20px;
  222. fill: #666;
  223. }
  224. .ai-menu-item:hover svg {
  225. fill: #4CAF50;
  226. }
  227. .ai-image-options {
  228. display: flex;
  229. flex-direction: column;
  230. gap: 10px;
  231. margin: 15px 0;
  232. }
  233. .ai-image-options button {
  234. padding: 8px 15px;
  235. border: none;
  236. border-radius: 4px;
  237. background: #4CAF50;
  238. color: white;
  239. cursor: pointer;
  240. transition: background-color 0.3s;
  241. font-size: 14px;
  242. }
  243. .ai-image-options button:hover {
  244. background: #45a049;
  245. }
  246. #ai-cancel {
  247. background: #dc3545;
  248. color: white;
  249. }
  250. #ai-cancel:hover {
  251. opacity: 0.9;
  252. }
  253. .ai-toast {
  254. position: fixed;
  255. top: 20px;
  256. left: 50%;
  257. transform: translateX(-50%);
  258. padding: 10px 20px;
  259. background: rgba(0, 0, 0, 0.8);
  260. color: white;
  261. border-radius: 4px;
  262. font-size: 14px;
  263. z-index: 10000;
  264. animation: fadeInOut 3s ease;
  265. pointer-events: none;
  266. white-space: pre-line;
  267. text-align: center;
  268. max-width: 80%;
  269. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  270. }
  271. @keyframes fadeInOut {
  272. 0% { opacity: 0; transform: translate(-50%, 10px); }
  273. 10% { opacity: 1; transform: translate(-50%, 0); }
  274. 90% { opacity: 1; transform: translate(-50%, 0); }
  275. 100% { opacity: 0; transform: translate(-50%, -10px); }
  276. }
  277. .ai-config-modal .button-group {
  278. display: flex;
  279. justify-content: flex-end;
  280. gap: 10px;
  281. margin-top: 20px;
  282. }
  283. .ai-config-modal .button-group button {
  284. padding: 6px 16px;
  285. border: none;
  286. border-radius: 4px;
  287. cursor: pointer;
  288. font-size: 14px;
  289. transition: background-color 0.2s;
  290. }
  291. .ai-config-modal .save-button {
  292. background: #007bff;
  293. color: white;
  294. }
  295. .ai-config-modal .save-button:hover {
  296. background: #0056b3;
  297. }
  298. .ai-config-modal .save-button:disabled {
  299. background: #cccccc;
  300. cursor: not-allowed;
  301. }
  302. .ai-config-modal .cancel-button {
  303. background: #f8f9fa;
  304. color: #333;
  305. }
  306. .ai-config-modal .cancel-button:hover {
  307. background: #e2e6ea;
  308. }
  309. .ai-selecting-image {
  310. cursor: crosshair !important;
  311. }
  312. .ai-selecting-image * {
  313. cursor: crosshair !important;
  314. }
  315. .ai-image-description {
  316. position: fixed;
  317. background: rgba(0, 0, 0, 0.8);
  318. color: white;
  319. padding: 8px 12px;
  320. border-radius: 4px;
  321. font-size: 14px;
  322. line-height: 1.4;
  323. max-width: 300px;
  324. text-align: center;
  325. word-wrap: break-word;
  326. z-index: 10000;
  327. pointer-events: none;
  328. animation: fadeIn 0.3s ease;
  329. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  330. }
  331. @keyframes fadeIn {
  332. from { opacity: 0; }
  333. to { opacity: 1; }
  334. }
  335. .ai-modal-overlay {
  336. position: fixed;
  337. top: 0;
  338. left: 0;
  339. width: 100%;
  340. height: 100%;
  341. background: rgba(0, 0, 0, 0.5);
  342. display: flex;
  343. justify-content: center;
  344. align-items: center;
  345. z-index: 9999;
  346. }
  347. .ai-result-modal {
  348. background: white;
  349. padding: 20px;
  350. border-radius: 8px;
  351. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  352. position: relative;
  353. min-width: 300px;
  354. max-width: 1000px;
  355. max-height: 540px;
  356. overflow-y: auto;
  357. width: 90%;
  358. }
  359. .ai-result-modal h3 {
  360. margin: 0 0 10px 0;
  361. font-size: 14px;
  362. color: #333;
  363. }
  364. .ai-result-modal .description-code {
  365. background: #1e1e1e;
  366. color: #ffffff;
  367. padding: 12px;
  368. border-radius: 4px;
  369. margin: 5px 0;
  370. cursor: pointer;
  371. white-space: pre-wrap;
  372. word-wrap: break-word;
  373. font-family: monospace;
  374. border: 1px solid #333;
  375. position: relative;
  376. max-height: 500px;
  377. overflow-y: auto;
  378. font-size: 12px;
  379. line-height: 1.4;
  380. }
  381. .ai-result-modal .description-code * {
  382. color: #ffffff !important;
  383. }
  384. .ai-result-modal .description-code code {
  385. color: #ffffff;
  386. display: block;
  387. width: 100%;
  388. }
  389. .ai-result-modal .description-code:hover {
  390. background: #2d2d2d;
  391. color: #ffffff;
  392. }
  393. .ai-result-modal .copy-hint {
  394. font-size: 11px;
  395. color: #666;
  396. text-align: center;
  397. margin: 2px 0;
  398. }
  399. .ai-result-modal .close-button {
  400. position: absolute;
  401. top: 8px;
  402. right: 8px;
  403. background: none;
  404. border: none;
  405. font-size: 18px;
  406. cursor: pointer;
  407. color: #666;
  408. padding: 2px 6px;
  409. line-height: 1;
  410. }
  411. .ai-result-modal .close-button:hover {
  412. color: #333;
  413. }
  414. .ai-progress-modal {
  415. position: fixed;
  416. top: 50%;
  417. left: 50%;
  418. transform: translate(-50%, -50%);
  419. background: white;
  420. padding: 20px;
  421. border-radius: 8px;
  422. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  423. z-index: 10001;
  424. min-width: 500px;
  425. height: auto;
  426. }
  427. .ai-progress-modal .progress-bar {
  428. width: 100%;
  429. height: 20px;
  430. background-color: #f3f3f3;
  431. }
  432. .ai-selection-overlay {
  433. position: fixed;
  434. top: 0;
  435. left: 0;
  436. width: 100%;
  437. height: 100%;
  438. background: rgba(0, 0, 0, 0.5);
  439. z-index: 9998;
  440. cursor: crosshair;
  441. pointer-events: none;
  442. }
  443. .ai-selecting-image img {
  444. position: relative;
  445. z-index: 9999;
  446. cursor: pointer !important;
  447. transition: outline 0.2s ease;
  448. }
  449. .ai-selecting-image img:hover {
  450. outline: 2px solid white;
  451. outline-offset: 2px;
  452. }
  453. .ai-result-modal .balance-info {
  454. font-size: 9px;
  455. color: #666;
  456. text-align: right;
  457. margin-top: 3px;
  458. padding-top: 3px;
  459. border-top: 1px solid #eee;
  460. }
  461. /* 移动端样式优化 */
  462. @media (max-width: 768px) {
  463. .ai-floating-btn {
  464. width: 40px;
  465. height: 40px;
  466. touch-action: none; /* 防止触屏滚动 */
  467. }
  468. .ai-floating-btn svg {
  469. width: 24px;
  470. height: 24px;
  471. }
  472. .ai-config-modal {
  473. width: 90%;
  474. min-width: auto;
  475. max-width: 400px;
  476. padding: 15px;
  477. margin: 10px;
  478. box-sizing: border-box;
  479. }
  480. .ai-config-modal .button-group {
  481. margin-top: 15px;
  482. flex-direction: row;
  483. justify-content: space-between;
  484. gap: 10px;
  485. }
  486.  
  487. .ai-config-modal .button-group button {
  488. flex: 1;
  489. min-height: 44px; /* 增加按钮高度,更容易点击 */
  490. font-size: 16px;
  491. padding: 10px;
  492. margin: 0;
  493. }
  494. .ai-result-modal {
  495. width: 95%;
  496. min-width: auto;
  497. max-width: 90%;
  498. margin: 10px;
  499. padding: 15px;
  500. }
  501.  
  502. .ai-modal-overlay {
  503. padding: 10px;
  504. box-sizing: border-box;
  505. }
  506.  
  507. /* 确保模态框内的所有可点击元素都有足够的点击区域 */
  508. .ai-config-modal button,
  509. .ai-config-modal .input-icon,
  510. .ai-config-modal select,
  511. .ai-config-modal input {
  512. min-height: 44px;
  513. padding: 10px;
  514. font-size: 16px;
  515. }
  516.  
  517. .ai-config-modal textarea {
  518. min-height: 100px;
  519. font-size: 16px;
  520. padding: 10px;
  521. }
  522.  
  523. .ai-config-modal .input-icon {
  524. width: 44px;
  525. height: 44px;
  526. font-size: 20px;
  527. }
  528.  
  529. /* 修复移动端的滚动问题 */
  530. .ai-config-modal {
  531. max-height: 90vh;
  532. overflow-y: auto;
  533. -webkit-overflow-scrolling: touch;
  534. }
  535. }
  536. `);
  537.  
  538. // 密码显示切换功能
  539. function togglePassword(element) {
  540. const input = element.parentElement.querySelector('input');
  541. if (input.type === 'password') {
  542. input.type = 'text';
  543. element.textContent = '👁️🗨️';
  544. } else {
  545. input.type = 'password';
  546. element.textContent = '👁️';
  547. }
  548. }
  549.  
  550. // 检查API配置并获取可用模型
  551. async function checkApiAndGetModels(apiEndpoint, apiKey) {
  552. try {
  553. const response = await fetch(`${apiEndpoint}/v1/models`, {
  554. method: 'GET',
  555. headers: {
  556. 'Authorization': `Bearer ${apiKey}`,
  557. 'Content-Type': 'application/json'
  558. }
  559. });
  560.  
  561. if (!response.ok) {
  562. throw new Error(`HTTP error! status: ${response.status}`);
  563. }
  564.  
  565. const result = await response.json();
  566. if (result.data && Array.isArray(result.data)) {
  567. // 过滤出多模态模型
  568. const multimodalModels = result.data
  569. .filter(model => model.id.includes('vision') || model.id.includes('gpt-4-v'))
  570. .map(model => ({
  571. id: model.id,
  572. name: model.id
  573. }));
  574. return multimodalModels;
  575. } else {
  576. throw new Error('Invalid response format');
  577. }
  578. } catch (error) {
  579. console.error('Error fetching models:', error);
  580. throw error;
  581. }
  582. }
  583.  
  584. // 检查API配置
  585. async function checkApiConfig() {
  586. const apiEndpoint = GM_getValue('apiEndpoint', '').trim();
  587. const apiKey = GM_getValue('apiKey', '').trim();
  588. const selectedModel = GM_getValue('selectedModel', '').trim();
  589.  
  590. if (!apiEndpoint || !apiKey || !selectedModel) {
  591. alert('请先配置API Endpoint、API Key和模型');
  592. showConfigModal();
  593. return false;
  594. }
  595.  
  596. try {
  597. // 如果是智谱AI的endpoint,跳过API检查
  598. if(apiEndpoint.includes('bigmodel.cn')) {
  599. return true;
  600. }
  601.  
  602. // 其他endpoint进行API检查
  603. const models = await checkApiAndGetModels(apiEndpoint, apiKey);
  604. if (models.length === 0) {
  605. alert('无法获取可用模型列表,请检查API配置是否正确');
  606. return false;
  607. }
  608. return true;
  609. } catch (error) {
  610. console.error('Error checking API config:', error);
  611. alert('API配置验证失败,请检查配置是否正确');
  612. return false;
  613. }
  614. }
  615.  
  616. // 获取图片的Base64内容
  617. async function getImageBase64(imageUrl) {
  618. console.log('[Debug] Starting image to Base64 conversion for:', imageUrl);
  619. // 尝试将HTTP URL换为HTTPS
  620. if (imageUrl.startsWith('http:')) {
  621. imageUrl = imageUrl.replace('http:', 'https:');
  622. console.log('[Debug] Converted to HTTPS URL:', imageUrl);
  623. }
  624.  
  625. // 获取图片的多种方法
  626. async function tryFetchImage(method) {
  627. return new Promise((resolve, reject) => {
  628. switch(method) {
  629. case 'direct':
  630. // 直接请求
  631. GM_xmlhttpRequest({
  632. method: 'GET',
  633. url: imageUrl,
  634. responseType: 'blob',
  635. headers: {
  636. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  637. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  638. 'Cache-Control': 'no-cache',
  639. 'Pragma': 'no-cache',
  640. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  641. },
  642. anonymous: true,
  643. onload: response => resolve(response),
  644. onerror: error => reject(error)
  645. });
  646. break;
  647.  
  648. case 'withReferer':
  649. // 带原始Referer的请求
  650. GM_xmlhttpRequest({
  651. method: 'GET',
  652. url: imageUrl,
  653. responseType: 'blob',
  654. headers: {
  655. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  656. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  657. 'Cache-Control': 'no-cache',
  658. 'Pragma': 'no-cache',
  659. 'Referer': new URL(imageUrl).origin,
  660. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  661. },
  662. anonymous: true,
  663. onload: response => resolve(response),
  664. onerror: error => reject(error)
  665. });
  666. break;
  667.  
  668. case 'proxy':
  669. // 通过代理服务获取
  670. const proxyUrl = `https://images.weserv.nl/?url=${encodeURIComponent(imageUrl)}`;
  671. GM_xmlhttpRequest({
  672. method: 'GET',
  673. url: proxyUrl,
  674. responseType: 'blob',
  675. headers: {
  676. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  677. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  678. },
  679. anonymous: true,
  680. onload: response => resolve(response),
  681. onerror: error => reject(error)
  682. });
  683. break;
  684.  
  685. case 'corsProxy':
  686. // 通过CORS代理获取
  687. const corsProxyUrl = `https://corsproxy.io/?${encodeURIComponent(imageUrl)}`;
  688. GM_xmlhttpRequest({
  689. method: 'GET',
  690. url: corsProxyUrl,
  691. responseType: 'blob',
  692. headers: {
  693. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  694. 'Origin': window.location.origin
  695. },
  696. anonymous: true,
  697. onload: response => resolve(response),
  698. onerror: error => reject(error)
  699. });
  700. break;
  701. }
  702. });
  703. }
  704.  
  705. // 处理响应
  706. async function handleResponse(response) {
  707. if (response.status === 200) {
  708. const blob = response.response;
  709. console.log('[Debug] Image blob size:', blob.size, 'bytes');
  710. return new Promise((resolve, reject) => {
  711. const reader = new FileReader();
  712. reader.onloadend = () => {
  713. const base64 = reader.result.split(',')[1];
  714. console.log('[Debug] Base64 conversion completed, length:', base64.length);
  715. resolve(base64);
  716. };
  717. reader.onerror = error => reject(error);
  718. reader.readAsDataURL(blob);
  719. });
  720. }
  721. throw new Error(`Failed with status: ${response.status}`);
  722. }
  723.  
  724. // 依次尝试不同的方法
  725. const methods = ['direct', 'withReferer', 'proxy', 'corsProxy'];
  726. for (const method of methods) {
  727. try {
  728. console.log(`[Debug] Trying method: ${method}`);
  729. const response = await tryFetchImage(method);
  730. if (response.status === 200) {
  731. return await handleResponse(response);
  732. }
  733. console.log(`[Debug] Method ${method} failed with status:`, response.status);
  734. } catch (error) {
  735. console.log(`[Debug] Method ${method} failed:`, error);
  736. }
  737. }
  738.  
  739. throw new Error('All methods to fetch image failed');
  740. }
  741.  
  742. // 调用API获取图片描述
  743. async function getImageDescription(imageUrl, apiEndpoint, apiKey, selectedModel) {
  744. console.log('[Debug] Starting image description request:', {
  745. apiEndpoint,
  746. selectedModel,
  747. imageUrl,
  748. timestamp: new Date().toISOString()
  749. });
  750.  
  751. try {
  752. // 获取所有API Keys
  753. const apiKeys = apiKey.split('\n').filter(key => key.trim() !== '');
  754. if (apiKeys.length === 0) {
  755. throw new Error('No valid API keys available');
  756. }
  757.  
  758. // 使用第一个key
  759. const currentKey = apiKeys[0];
  760.  
  761. const base64Image = await getImageBase64(imageUrl);
  762. console.log('[Debug] Image converted to base64, length:', base64Image.length);
  763.  
  764. // 退出选择图片模式
  765. exitImageSelectionMode();
  766. const timeout = 30000; // 30秒超时
  767. const controller = new AbortController();
  768. const timeoutId = setTimeout(() => controller.abort(), timeout);
  769. const imageSize = base64Image.length * 0.75; // 转换为字节数
  770. // 获取当前余额
  771. const userInfo = await checkUserInfo(apiEndpoint, currentKey);
  772. const currentBalance = userInfo.totalBalance;
  773. // 计算每次调用的预估花费(根据图片大小和模型)
  774. const costPerCall = calculateCost(imageSize, selectedModel);
  775. // 计算可识别的剩余图片量
  776. const remainingImages = Math.floor(currentBalance / costPerCall);
  777.  
  778. // 根据不同的API构建不同的请求体和endpoint
  779. let requestBody;
  780. let finalEndpoint;
  781.  
  782. if(selectedModel.startsWith('glm-')) {
  783. // GLM系列模型的请求格式
  784. requestBody = {
  785. model: selectedModel,
  786. messages: [{
  787. role: "user",
  788. content: [{
  789. type: "text",
  790. text: "请描述这张图片的主要内容。如果是人物图片,请至少用15个字描述人物。"
  791. }, {
  792. type: "image_url",
  793. image_url: {
  794. url: `data:image/jpeg;base64,${base64Image}`
  795. }
  796. }]
  797. }],
  798. stream: true
  799. };
  800. finalEndpoint = 'https://open.bigmodel.cn/api/paas/v4/chat/completions';
  801. } else {
  802. // 原有模型的请求格式
  803. requestBody = {
  804. model: selectedModel,
  805. messages: [{
  806. role: "user",
  807. content: [
  808. {
  809. type: "image_url",
  810. image_url: {
  811. url: `data:image/jpeg;base64,${base64Image}`
  812. }
  813. },
  814. {
  815. type: "text",
  816. text: "Describe the main content of the image. If it is a person, provide a description of the person with at least 15 words. Answer in Chinese."
  817. }
  818. ]
  819. }],
  820. stream: true
  821. };
  822. finalEndpoint = `${apiEndpoint}/chat/completions`;
  823. }
  824.  
  825. console.log('[Debug] API Request body:', JSON.stringify(requestBody, null, 2));
  826.  
  827. console.log('[Debug] Sending request to:', finalEndpoint);
  828. console.log('[Debug] Request headers:', {
  829. 'Authorization': 'Bearer ***' + currentKey.slice(-4),
  830. 'Content-Type': 'application/json'
  831. });
  832. console.log('[Debug] Request body:', requestBody);
  833.  
  834. return new Promise((resolve, reject) => {
  835. GM_xmlhttpRequest({
  836. method: 'POST',
  837. url: finalEndpoint,
  838. headers: {
  839. 'Authorization': `Bearer ${currentKey}`,
  840. 'Content-Type': 'application/json'
  841. },
  842. data: JSON.stringify(requestBody),
  843. onload: async function(response) {
  844. console.log('[Debug] Response received:', {
  845. status: response.status,
  846. statusText: response.statusText,
  847. headers: response.responseHeaders
  848. });
  849.  
  850. if (response.status === 200) {
  851. try {
  852. let description = '';
  853. const lines = response.responseText.split('\n').filter(line => line.trim() !== '');
  854. for (const line of lines) {
  855. if (line.startsWith('data: ')) {
  856. const jsonStr = line.slice(6);
  857. if (jsonStr === '[DONE]') continue;
  858. try {
  859. const jsonData = JSON.parse(jsonStr);
  860. console.log('[Debug] Parsed chunk:', jsonData);
  861. const content = jsonData.choices[0]?.delta?.content;
  862. if (content) {
  863. description += content;
  864. console.log('[Debug] Current description:', description);
  865. }
  866. } catch (e) {
  867. console.error('[Debug] Error parsing chunk JSON:', e);
  868. }
  869. }
  870. }
  871.  
  872. console.log('[Debug] Final description:', description);
  873. removeDescriptionTooltip();
  874. const balanceInfo = `剩余额度为:${currentBalance.toFixed(4)},大约还可以识别 ${remainingImages} 张图片`;
  875. showDescriptionModal(description, balanceInfo);
  876. resolve(description);
  877. } catch (error) {
  878. console.error('[Debug] Error processing response:', error);
  879. reject(error);
  880. }
  881. } else {
  882. console.error('[Debug] Error response:', {
  883. status: response.status,
  884. statusText: response.statusText,
  885. response: response.responseText
  886. });
  887.  
  888. // 检查是否是余额不足错误
  889. try {
  890. const errorResponse = JSON.parse(response.responseText);
  891. if (errorResponse.code === 30001 ||
  892. (errorResponse.message && errorResponse.message.includes('insufficient'))) {
  893. showToast('当前key余额不足,正在检测其他key...');
  894. // 自动运行一次key检测
  895. await checkAndUpdateKeys();
  896. // 重新获取更新后的key
  897. const newApiKeys = GM_getValue('apiKey', '').split('\n').filter(key => key.trim() !== '');
  898. if (newApiKeys.length > 0) {
  899. // 使用新的key重试
  900. getImageDescription(imageUrl, apiEndpoint, newApiKeys.join('\n'), selectedModel)
  901. .then(resolve)
  902. .catch(reject);
  903. return;
  904. }
  905. }
  906. } catch (e) {
  907. console.error('[Debug] Error parsing error response:', e);
  908. }
  909. reject(new Error(`Request failed with status ${response.status}`));
  910. }
  911. },
  912. onerror: function(error) {
  913. console.error('[Debug] Request error:', error);
  914. reject(error);
  915. },
  916. onprogress: function(progress) {
  917. // 用于处理流式响应的进度
  918. console.log('[Debug] Progress:', progress);
  919. try {
  920. const lines = progress.responseText.split('\n').filter(line => line.trim() !== '');
  921. let latestContent = '';
  922. for (const line of lines) {
  923. if (line.startsWith('data: ')) {
  924. const jsonStr = line.slice(6);
  925. if (jsonStr === '[DONE]') continue;
  926. try {
  927. const jsonData = JSON.parse(jsonStr);
  928. const content = jsonData.choices[0]?.delta?.content;
  929. if (content) {
  930. latestContent += content;
  931. }
  932. } catch (e) {
  933. console.error('[Debug] Error parsing progress JSON:', e);
  934. }
  935. }
  936. }
  937. if (latestContent) {
  938. updateDescriptionTooltip('正在生成描述: ' + latestContent);
  939. }
  940. } catch (error) {
  941. console.error('[Debug] Error processing progress:', error);
  942. }
  943. }
  944. });
  945. });
  946. } catch (error) {
  947. if (error.name === 'AbortError') {
  948. showToast('请求超时,请重试');
  949. }
  950. removeDescriptionTooltip();
  951. console.error('[Debug] Error in getImageDescription:', {
  952. error,
  953. stack: error.stack,
  954. timestamp: new Date().toISOString()
  955. });
  956. throw error;
  957. }
  958. }
  959.  
  960. // 显示描述tooltip
  961. function showDescriptionTooltip(description) {
  962. const tooltip = document.createElement('div');
  963. tooltip.className = 'ai-image-description';
  964. tooltip.textContent = description;
  965. // 获取视口宽度
  966. const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  967. // 计算tooltip位置(水平居中,距顶部20px)
  968. const tooltipX = Math.max(0, (viewportWidth - 300) / 2); // 300是tooltip的max-width
  969. tooltip.style.position = 'fixed';
  970. tooltip.style.left = `${tooltipX}px`;
  971. tooltip.style.top = '20px';
  972. document.body.appendChild(tooltip);
  973.  
  974. // 添加动态点的动画
  975. let dots = 1;
  976. const updateInterval = setInterval(() => {
  977. if (!document.body.contains(tooltip)) {
  978. clearInterval(updateInterval);
  979. return;
  980. }
  981. dots = dots % 6 + 1;
  982. tooltip.textContent = '正在生成描述' + '.'.repeat(dots);
  983. }, 500); // 每500ms更新一次
  984.  
  985. return tooltip;
  986. }
  987.  
  988. // 更新描述tooltip内容
  989. function updateDescriptionTooltip(description) {
  990. const tooltip = document.querySelector('.ai-image-description');
  991. if (tooltip) {
  992. tooltip.textContent = description;
  993. }
  994. }
  995.  
  996. // 移除描述tooltip
  997. function removeDescriptionTooltip() {
  998. const tooltip = document.querySelector('.ai-image-description');
  999. if (tooltip) {
  1000. tooltip.remove();
  1001. }
  1002. }
  1003.  
  1004. // 进入图片选择模式
  1005. function enterImageSelectionMode() {
  1006. console.log('[Debug] Entering image selection mode');
  1007. if(isSelectionMode) return; // 防止重复进入选择模式
  1008. isSelectionMode = true;
  1009.  
  1010. // 隐藏悬浮按钮
  1011. const floatingBtn = document.querySelector('.ai-floating-btn');
  1012. if(floatingBtn) {
  1013. floatingBtn.style.display = 'none';
  1014. }
  1015.  
  1016. // 创建遮罩层
  1017. const overlay = document.createElement('div');
  1018. overlay.className = 'ai-selection-overlay';
  1019. document.body.appendChild(overlay);
  1020. // 添加选择状态的类名
  1021. document.body.classList.add('ai-selecting-image');
  1022.  
  1023. // 创建点击事件处理函数
  1024. const clickHandler = async function(e) {
  1025. if (!isSelectionMode) return;
  1026.  
  1027. if (e.target.tagName === 'IMG') {
  1028. console.log('[Debug] Image clicked:', e.target.src);
  1029. e.preventDefault();
  1030. e.stopPropagation();
  1031. // 获取配置
  1032. const endpoint = GM_getValue('apiEndpoint', '');
  1033. const apiKey = GM_getValue('apiKey', '');
  1034. const selectedModel = GM_getValue('selectedModel', '');
  1035.  
  1036. console.log('[Debug] Current configuration:', {
  1037. endpoint,
  1038. selectedModel,
  1039. hasApiKey: !!apiKey
  1040. });
  1041.  
  1042. if (!endpoint || !apiKey || !selectedModel) {
  1043. showToast('请先配置API配置');
  1044. exitImageSelectionMode();
  1045. return;
  1046. }
  1047.  
  1048. // 显示加载中的tooltip
  1049. showDescriptionTooltip('正在生成描述...');
  1050.  
  1051. try {
  1052. await getImageDescription(e.target.src, endpoint, apiKey, selectedModel);
  1053. } catch (error) {
  1054. console.error('[Debug] Description generation failed:', error);
  1055. removeDescriptionTooltip();
  1056. showToast('生成描述失败: ' + error.message);
  1057. }
  1058. }
  1059. };
  1060.  
  1061. // 添加点击事件监听器
  1062. document.addEventListener('click', clickHandler, true);
  1063. // ESC键退选择模式
  1064. const escHandler = (e) => {
  1065. if (e.key === 'Escape') {
  1066. exitImageSelectionMode();
  1067. }
  1068. };
  1069. document.addEventListener('keydown', escHandler);
  1070.  
  1071. // 保存事件理函数以便后续移除
  1072. window._imageSelectionHandlers = {
  1073. click: clickHandler,
  1074. keydown: escHandler
  1075. };
  1076. }
  1077.  
  1078. // 退出图片选择模式
  1079. function exitImageSelectionMode() {
  1080. console.log('[Debug] Exiting image selection mode');
  1081. isSelectionMode = false;
  1082.  
  1083. // 显示悬浮按钮
  1084. const floatingBtn = document.querySelector('.ai-floating-btn');
  1085. if(floatingBtn) {
  1086. floatingBtn.style.display = 'flex';
  1087. }
  1088.  
  1089. // 移除遮罩层
  1090. const overlay = document.querySelector('.ai-selection-overlay');
  1091. if (overlay) {
  1092. overlay.remove();
  1093. }
  1094.  
  1095. // 移除选择状态的类名
  1096. document.body.classList.remove('ai-selecting-image');
  1097.  
  1098. // 移除所有事件监听器
  1099. if (window._imageSelectionHandlers) {
  1100. document.removeEventListener('click', window._imageSelectionHandlers.click, true);
  1101. document.removeEventListener('keydown', window._imageSelectionHandlers.keydown);
  1102. window._imageSelectionHandlers = null;
  1103. }
  1104. }
  1105.  
  1106. // 显示toast提示
  1107. function showToast(message, duration = 3000) {
  1108. const toast = document.createElement('div');
  1109. toast.className = 'ai-toast';
  1110. toast.textContent = message;
  1111. document.body.appendChild(toast);
  1112. setTimeout(() => {
  1113. toast.remove();
  1114. }, duration);
  1115. }
  1116.  
  1117. // 检查用户信息
  1118. async function checkUserInfo(apiEndpoint, apiKey) {
  1119. try {
  1120. // 对智谱AI的endpoint返回默认值
  1121. if(apiEndpoint.includes('bigmodel.cn')) {
  1122. const defaultUserData = {
  1123. name: 'GLM User',
  1124. balance: 1000, // 默认余额
  1125. chargeBalance: 0,
  1126. totalBalance: 1000
  1127. };
  1128. console.log('[Debug] Using default user data for GLM:', defaultUserData);
  1129. return defaultUserData;
  1130. }
  1131.  
  1132. // 其他endpoint使用原有逻辑
  1133. return new Promise((resolve, reject) => {
  1134. console.log('[Debug] Sending user info request to:', `${apiEndpoint}/v1/user/info`);
  1135. GM_xmlhttpRequest({
  1136. method: 'GET',
  1137. url: `${apiEndpoint}/v1/user/info`,
  1138. headers: {
  1139. 'Authorization': `Bearer ${apiKey}`,
  1140. 'Content-Type': 'application/json'
  1141. },
  1142. onload: function(response) {
  1143. console.log('[Debug] User Info Raw Response:', {
  1144. status: response.status,
  1145. statusText: response.statusText,
  1146. responseText: response.responseText,
  1147. headers: response.responseHeaders
  1148. });
  1149.  
  1150. if (response.status === 200) {
  1151. try {
  1152. const result = JSON.parse(response.responseText);
  1153. console.log('[Debug] User Info Parsed Response:', result);
  1154. if (result.code === 20000 && result.status && result.data) {
  1155. const { name, balance, chargeBalance, totalBalance } = result.data;
  1156. resolve({
  1157. name,
  1158. balance: parseFloat(balance),
  1159. chargeBalance: parseFloat(chargeBalance),
  1160. totalBalance: parseFloat(totalBalance)
  1161. });
  1162. } else {
  1163. throw new Error(result.message || 'Invalid response format');
  1164. }
  1165. } catch (error) {
  1166. console.error('[Debug] JSON Parse Error:', error);
  1167. reject(error);
  1168. }
  1169. } else {
  1170. console.error('[Debug] HTTP Error Response:', {
  1171. status: response.status,
  1172. statusText: response.statusText,
  1173. response: response.responseText
  1174. });
  1175. reject(new Error(`HTTP error! status: ${response.status}`));
  1176. }
  1177. },
  1178. onerror: function(error) {
  1179. console.error('[Debug] Request Error:', error);
  1180. reject(error);
  1181. }
  1182. });
  1183. });
  1184. } catch (error) {
  1185. console.error('[Debug] User Info Error:', error);
  1186. throw error;
  1187. }
  1188. }
  1189.  
  1190. // 获取可用模型列表
  1191. async function getAvailableModels(apiEndpoint, apiKey) {
  1192. console.log('[Debug] Getting available models from:', apiEndpoint);
  1193.  
  1194. try {
  1195. // 如果是智谱AI的endpoint,直接返回GLM模型列表
  1196. if(apiEndpoint.includes('bigmodel.cn')) {
  1197. const glmModels = [
  1198. {
  1199. id: 'glm-4',
  1200. name: 'GLM-4'
  1201. },
  1202. {
  1203. id: 'glm-4v',
  1204. name: 'GLM-4V'
  1205. },
  1206. {
  1207. id: 'glm-4v-flash',
  1208. name: 'GLM-4V-Flash'
  1209. }
  1210. ];
  1211. console.log('[Debug] Available GLM models:', glmModels);
  1212. return glmModels;
  1213. }
  1214.  
  1215. // 其他endpoint使用原有逻辑
  1216. return new Promise((resolve, reject) => {
  1217. console.log('[Debug] Sending models request to:', `${apiEndpoint}/v1/models`);
  1218. GM_xmlhttpRequest({
  1219. method: 'GET',
  1220. url: `${apiEndpoint}/v1/models`,
  1221. headers: {
  1222. 'Authorization': `Bearer ${apiKey}`,
  1223. 'Content-Type': 'application/json'
  1224. },
  1225. onload: function(response) {
  1226. console.log('[Debug] Models API Raw Response:', {
  1227. status: response.status,
  1228. statusText: response.statusText,
  1229. responseText: response.responseText,
  1230. headers: response.responseHeaders
  1231. });
  1232.  
  1233. if (response.status === 200) {
  1234. try {
  1235. const result = JSON.parse(response.responseText);
  1236. console.log('[Debug] Models API Parsed Response:', result);
  1237. if (result.object === 'list' && Array.isArray(result.data)) {
  1238. const models = result.data
  1239. .filter(model => supportedVLModels.includes(model.id))
  1240. .map(model => ({
  1241. id: model.id,
  1242. name: model.id.split('/').pop()
  1243. .replace('Qwen2-VL-', 'Qwen2-')
  1244. .replace('InternVL2-Llama3-', 'InternVL2-')
  1245. .replace('-Instruct', '')
  1246. }));
  1247. console.log('[Debug] Filtered and processed models:', models);
  1248. resolve(models);
  1249. } else {
  1250. console.error('[Debug] Invalid models response format:', result);
  1251. reject(new Error('Invalid models response format'));
  1252. }
  1253. } catch (error) {
  1254. console.error('[Debug] JSON Parse Error:', error);
  1255. reject(error);
  1256. }
  1257. } else {
  1258. console.error('[Debug] HTTP Error Response:', {
  1259. status: response.status,
  1260. statusText: response.statusText,
  1261. response: response.responseText
  1262. });
  1263. reject(new Error(`HTTP error! status: ${response.status}`));
  1264. }
  1265. },
  1266. onerror: function(error) {
  1267. console.error('[Debug] Models API Request Error:', error);
  1268. reject(error);
  1269. }
  1270. });
  1271. });
  1272. } catch (error) {
  1273. console.error('[Debug] Models API Error:', error);
  1274. throw error;
  1275. }
  1276. }
  1277.  
  1278. // 更新模型下拉菜单
  1279. function updateModelSelect(selectElement, models) {
  1280. if (models.length === 0) {
  1281. selectElement.innerHTML = '<option value="">未找到可用的视觉模型</option>';
  1282. selectElement.disabled = true;
  1283. return;
  1284. }
  1285.  
  1286. selectElement.innerHTML = '<option value="">请选择视觉模型</option>' +
  1287. models.map(model =>
  1288. `<option value="${model.id}" title="${model.id}">${model.name}</option>`
  1289. ).join('');
  1290. selectElement.disabled = false;
  1291. }
  1292.  
  1293. // 保存模型列表到GM存储
  1294. function saveModelList(models) {
  1295. GM_setValue('availableModels', models);
  1296. }
  1297.  
  1298. // 从GM存储获取模型列表
  1299. function getStoredModelList() {
  1300. return GM_getValue('availableModels', []);
  1301. }
  1302.  
  1303. // 创建悬浮按钮
  1304. function createFloatingButton() {
  1305. const btn = document.createElement('div');
  1306. btn.className = 'ai-floating-btn';
  1307. btn.innerHTML = `
  1308. <svg viewBox="0 0 24 24">
  1309. <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"/>
  1310. </svg>
  1311. `;
  1312.  
  1313. // 设置初始位置
  1314. const savedPos = JSON.parse(GM_getValue('btnPosition', '{"x": 20, "y": 20}'));
  1315. btn.style.left = (savedPos.x || 20) + 'px';
  1316. btn.style.top = (savedPos.y || 20) + 'px';
  1317. btn.style.right = 'auto';
  1318. btn.style.bottom = 'auto';
  1319.  
  1320. // 自动检测key的可用性
  1321. setTimeout(async () => {
  1322. await checkAndUpdateKeys();
  1323. }, 1000);
  1324.  
  1325. let isDragging = false;
  1326. let hasMoved = false;
  1327. let startX, startY;
  1328. let initialLeft, initialTop;
  1329. let longPressTimer;
  1330. let touchStartTime;
  1331.  
  1332. // 触屏事件处理
  1333. btn.addEventListener('touchstart', function(e) {
  1334. e.preventDefault();
  1335. touchStartTime = Date.now();
  1336. // 设置长按定时器
  1337. longPressTimer = setTimeout(() => {
  1338. exitImageSelectionMode();
  1339. createConfigUI();
  1340. }, 500); // 500ms长按触发
  1341.  
  1342. const touch = e.touches[0];
  1343. startX = touch.clientX;
  1344. startY = touch.clientY;
  1345. const rect = btn.getBoundingClientRect();
  1346. initialLeft = rect.left;
  1347. initialTop = rect.top;
  1348. });
  1349.  
  1350. btn.addEventListener('touchmove', function(e) {
  1351. e.preventDefault();
  1352. clearTimeout(longPressTimer); // 移动时取消长按
  1353.  
  1354. const touch = e.touches[0];
  1355. const deltaX = touch.clientX - startX;
  1356. const deltaY = touch.clientY - startY;
  1357. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  1358. hasMoved = true;
  1359. }
  1360. const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
  1361. const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
  1362. btn.style.left = newLeft + 'px';
  1363. btn.style.top = newTop + 'px';
  1364. });
  1365.  
  1366. btn.addEventListener('touchend', function(e) {
  1367. e.preventDefault();
  1368. clearTimeout(longPressTimer);
  1369. const touchDuration = Date.now() - touchStartTime;
  1370. if (!hasMoved && touchDuration < 500) {
  1371. // 短按进入图片选择模式
  1372. enterImageSelectionMode();
  1373. }
  1374. if (hasMoved) {
  1375. // 保存新位置
  1376. const rect = btn.getBoundingClientRect();
  1377. GM_setValue('btnPosition', JSON.stringify({
  1378. x: rect.left,
  1379. y: rect.top
  1380. }));
  1381. }
  1382. hasMoved = false;
  1383. });
  1384.  
  1385. // 保留原有的鼠标事件处理
  1386. btn.addEventListener('click', function(e) {
  1387. if (e.button === 0 && !hasMoved) { // 左键点击且没有移动
  1388. enterImageSelectionMode();
  1389. e.stopPropagation();
  1390. }
  1391. hasMoved = false;
  1392. });
  1393.  
  1394. btn.addEventListener('contextmenu', function(e) {
  1395. e.preventDefault();
  1396. exitImageSelectionMode();
  1397. createConfigUI();
  1398. });
  1399.  
  1400. // 拖拽相关事件
  1401. function dragStart(e) {
  1402. if (e.target === btn || btn.contains(e.target)) {
  1403. isDragging = true;
  1404. hasMoved = false;
  1405. const rect = btn.getBoundingClientRect();
  1406. startX = e.clientX;
  1407. startY = e.clientY;
  1408. initialLeft = rect.left;
  1409. initialTop = rect.top;
  1410. e.preventDefault();
  1411. }
  1412. }
  1413.  
  1414. function drag(e) {
  1415. if (isDragging) {
  1416. e.preventDefault();
  1417. const deltaX = e.clientX - startX;
  1418. const deltaY = e.clientY - startY;
  1419. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  1420. hasMoved = true;
  1421. }
  1422. const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
  1423. const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
  1424. btn.style.left = newLeft + 'px';
  1425. btn.style.top = newTop + 'px';
  1426. }
  1427. }
  1428.  
  1429. function dragEnd(e) {
  1430. if (isDragging) {
  1431. isDragging = false;
  1432. const rect = btn.getBoundingClientRect();
  1433. GM_setValue('btnPosition', JSON.stringify({
  1434. x: rect.left,
  1435. y: rect.top
  1436. }));
  1437. }
  1438. }
  1439.  
  1440. btn.addEventListener('mousedown', dragStart);
  1441. document.addEventListener('mousemove', drag);
  1442. document.addEventListener('mouseup', dragEnd);
  1443.  
  1444. // 将按钮添加到文档中
  1445. document.body.appendChild(btn);
  1446. return btn;
  1447. }
  1448.  
  1449. // 检查并更新key列表
  1450. async function checkAndUpdateKeys() {
  1451. const endpoint = GM_getValue('apiEndpoint', '');
  1452. const apiKeys = GM_getValue('apiKey', '').split('\n').filter(key => key.trim() !== '');
  1453. if (endpoint && apiKeys.length > 0) {
  1454. const validKeys = [];
  1455. const keyBalances = new Map();
  1456.  
  1457. for (const apiKey of apiKeys) {
  1458. try {
  1459. const userInfo = await checkUserInfo(endpoint, apiKey);
  1460. if (userInfo.totalBalance > 0) {
  1461. validKeys.push(apiKey);
  1462. keyBalances.set(apiKey, userInfo.totalBalance);
  1463. } else {
  1464. showToast(`${apiKey.slice(0, 8)}...可用余额为0,被移除。`);
  1465. }
  1466. } catch (error) {
  1467. console.error('Key check failed:', error);
  1468. }
  1469. }
  1470.  
  1471. // 按余额从小到大排序
  1472. validKeys.sort((a, b) => keyBalances.get(a) - keyBalances.get(b));
  1473.  
  1474. // 更新存储的key
  1475. if (validKeys.length > 0) {
  1476. GM_setValue('apiKey', validKeys.join('\n'));
  1477. showToast(`自动检测完成,${validKeys.length}个有效key`);
  1478. } else {
  1479. showToast('没有可用的API Key,请更新配置');
  1480. }
  1481. }
  1482. }
  1483.  
  1484. // 创建配置界面
  1485. function createConfigUI() {
  1486. // 如果已经存在配置界面,先移除
  1487. const existingModal = document.querySelector('.ai-modal-overlay');
  1488. if (existingModal) {
  1489. existingModal.remove();
  1490. }
  1491.  
  1492. const overlay = document.createElement('div');
  1493. overlay.className = 'ai-modal-overlay';
  1494. const modal = document.createElement('div');
  1495. modal.className = 'ai-config-modal';
  1496. modal.innerHTML = `
  1497. <h3>AI图像描述配置</h3>
  1498. <div class="input-group">
  1499. <label>API Endpoint:</label>
  1500. <div class="input-wrapper">
  1501. <input type="text" id="ai-endpoint" placeholder="https://api.openai.com" value="${GM_getValue('apiEndpoint', '')}">
  1502. <span class="input-icon clear-icon" title="清空">✕</span>
  1503. </div>
  1504. </div>
  1505. <div class="input-group">
  1506. <label>API Key (每行一个):</label>
  1507. <div class="input-wrapper">
  1508. <textarea id="ai-apikey" rows="5" style="width: 100%; resize: vertical;">${GM_getValue('apiKey', '')}</textarea>
  1509. <span class="input-icon clear-icon" title="清空">✕</span>
  1510. </div>
  1511. <div class="button-row">
  1512. <button class="check-button" id="check-api">检测可用性</button>
  1513. </div>
  1514. </div>
  1515. <div class="input-group">
  1516. <label>可用模型:</label>
  1517. <select id="ai-model">
  1518. <option value="">加载中...</option>
  1519. </select>
  1520. </div>
  1521. <div class="button-group">
  1522. <button type="button" class="cancel-button" id="ai-cancel-config">取消</button>
  1523. <button type="button" class="save-button" id="ai-save-config">保存</button>
  1524. </div>
  1525. `;
  1526.  
  1527. overlay.appendChild(modal);
  1528. document.body.appendChild(overlay);
  1529.  
  1530. // 初始化模型下拉菜单
  1531. const modelSelect = modal.querySelector('#ai-model');
  1532. const storedModels = getStoredModelList();
  1533. const selectedModel = GM_getValue('selectedModel', '');
  1534. if (storedModels.length > 0) {
  1535. updateModelSelect(modelSelect, storedModels);
  1536. if (selectedModel) {
  1537. modelSelect.value = selectedModel;
  1538. }
  1539. } else {
  1540. modelSelect.innerHTML = '<option value="">请先检测API可用性</option>';
  1541. modelSelect.disabled = true;
  1542. }
  1543.  
  1544. // 添加清空按钮事件
  1545. const clearButtons = modal.querySelectorAll('.clear-icon');
  1546. clearButtons.forEach(button => {
  1547. button.addEventListener('click', function(e) {
  1548. const input = this.parentElement.querySelector('input, textarea');
  1549. if (input) {
  1550. input.value = '';
  1551. input.focus();
  1552. }
  1553. });
  1554. });
  1555.  
  1556. // 检测API可用性
  1557. const checkButton = modal.querySelector('#check-api');
  1558. if (checkButton) {
  1559. checkButton.addEventListener('click', async function() {
  1560. const endpoint = modal.querySelector('#ai-endpoint')?.value?.trim() || '';
  1561. const apiKeys = modal.querySelector('#ai-apikey')?.value?.trim().split('\n').filter(key => key.trim() !== '') || [];
  1562.  
  1563. if (!endpoint || apiKeys.length === 0) {
  1564. showToast('请先填写API Endpoint和至少一个API Key');
  1565. return;
  1566. }
  1567.  
  1568. checkButton.disabled = true;
  1569. modelSelect.disabled = true;
  1570. modelSelect.innerHTML = '<option value="">检测中...</option>';
  1571.  
  1572. try {
  1573. // 检查每个key的可用性
  1574. const validKeys = [];
  1575. const keyBalances = new Map();
  1576.  
  1577. for (const apiKey of apiKeys) {
  1578. try {
  1579. const userInfo = await checkUserInfo(endpoint, apiKey);
  1580. if (userInfo.totalBalance > 0) {
  1581. validKeys.push(apiKey);
  1582. keyBalances.set(apiKey, userInfo.totalBalance);
  1583. } else {
  1584. showToast(`${apiKey.slice(0, 8)}...可用余额为0,被移除。`);
  1585. }
  1586. } catch (error) {
  1587. console.error('Key check failed:', error);
  1588. showToast(`${apiKey.slice(0, 8)}...验证失败,被移除。`);
  1589. }
  1590. }
  1591.  
  1592. // 按余额从小到大排序
  1593. validKeys.sort((a, b) => keyBalances.get(a) - keyBalances.get(b));
  1594.  
  1595. // 更新输入框中的key
  1596. const apiKeyInput = modal.querySelector('#ai-apikey');
  1597. if (apiKeyInput) {
  1598. apiKeyInput.value = validKeys.join('\n');
  1599. }
  1600.  
  1601. // 获取可用模型列表(使用第一个有效的key)
  1602. if (validKeys.length > 0) {
  1603. const models = await getAvailableModels(endpoint, validKeys[0]);
  1604. saveModelList(models);
  1605. updateModelSelect(modelSelect, models);
  1606. showToast(`检测完成,${validKeys.length}个有效key`);
  1607. } else {
  1608. showToast('没有可用的API Key');
  1609. modelSelect.innerHTML = '<option value="">无可用API Key</option>';
  1610. modelSelect.disabled = true;
  1611. }
  1612. } catch (error) {
  1613. showToast('API检测失败:' + error.message);
  1614. modelSelect.innerHTML = '<option value="">获取模型列表失败</option>';
  1615. modelSelect.disabled = true;
  1616. } finally {
  1617. checkButton.disabled = false;
  1618. }
  1619. });
  1620. }
  1621.  
  1622. // 保存配置
  1623. const saveButton = modal.querySelector('#ai-save-config');
  1624. if (saveButton) {
  1625. saveButton.addEventListener('click', function(e) {
  1626. e.preventDefault();
  1627. e.stopPropagation();
  1628. const endpoint = modal.querySelector('#ai-endpoint')?.value?.trim() || '';
  1629. const apiKeys = modal.querySelector('#ai-apikey')?.value?.trim() || '';
  1630. const selectedModel = modelSelect?.value || '';
  1631.  
  1632. if (!endpoint || !apiKeys) {
  1633. showToast('请填写API Endpoint和至少一个API Key');
  1634. return;
  1635. }
  1636.  
  1637. if (!selectedModel) {
  1638. showToast('请选择一个视觉模型');
  1639. return;
  1640. }
  1641.  
  1642. GM_setValue('apiEndpoint', endpoint);
  1643. GM_setValue('apiKey', apiKeys);
  1644. GM_setValue('selectedModel', selectedModel);
  1645. showToast('配置已保存');
  1646. if (overlay && overlay.parentNode) {
  1647. overlay.parentNode.removeChild(overlay);
  1648. }
  1649. });
  1650. }
  1651.  
  1652. // 取消配置
  1653. const cancelButton = modal.querySelector('#ai-cancel-config');
  1654. if (cancelButton) {
  1655. cancelButton.addEventListener('click', function(e) {
  1656. e.preventDefault();
  1657. e.stopPropagation();
  1658. if (overlay && overlay.parentNode) {
  1659. overlay.parentNode.removeChild(overlay);
  1660. }
  1661. });
  1662. }
  1663.  
  1664. // 点击遮罩层关闭
  1665. overlay.addEventListener('click', function(e) {
  1666. if (e.target === overlay) {
  1667. if (overlay.parentNode) {
  1668. overlay.parentNode.removeChild(overlay);
  1669. }
  1670. }
  1671. });
  1672.  
  1673. // 阻止模态框内的点击事件冒泡
  1674. modal.addEventListener('click', function(e) {
  1675. e.stopPropagation();
  1676. });
  1677. }
  1678.  
  1679. // 显示图像选择面
  1680. function showImageSelectionModal() {
  1681. const overlay = document.createElement('div');
  1682. overlay.className = 'ai-modal-overlay';
  1683. const modal = document.createElement('div');
  1684. modal.className = 'ai-config-modal';
  1685. modal.innerHTML = `
  1686. <h3>选择要识别的图像</h3>
  1687. <div class="ai-image-options">
  1688. <button id="ai-all-images">识别所有图片</button>
  1689. <button id="ai-visible-images">仅识别可见图片</button>
  1690. </div>
  1691. <button id="ai-cancel">取消</button>
  1692. `;
  1693.  
  1694. overlay.appendChild(modal);
  1695. document.body.appendChild(overlay);
  1696.  
  1697. // 添加事件监听
  1698. modal.querySelector('#ai-all-images').onclick = () => {
  1699. if (checkApiConfig()) {
  1700. describeAllImages();
  1701. overlay.remove();
  1702. }
  1703. };
  1704.  
  1705. modal.querySelector('#ai-visible-images').onclick = () => {
  1706. if (checkApiConfig()) {
  1707. describeVisibleImages();
  1708. overlay.remove();
  1709. }
  1710. };
  1711.  
  1712. modal.querySelector('#ai-cancel').onclick = () => {
  1713. overlay.remove();
  1714. };
  1715.  
  1716. // 点击遮罩层关闭
  1717. overlay.addEventListener('click', (e) => {
  1718. if (e.target === overlay) {
  1719. overlay.remove();
  1720. }
  1721. });
  1722. }
  1723.  
  1724. function showDescriptionModal(description, balanceInfo) {
  1725. const overlay = document.createElement('div');
  1726. overlay.className = 'ai-modal-overlay';
  1727. const modal = document.createElement('div');
  1728. modal.className = 'ai-result-modal';
  1729. modal.innerHTML = `
  1730. <h3>图片描述结果</h3>
  1731. <pre class="description-code"><code>${description}</code></pre>
  1732. <div class="copy-hint">点击上方代码块复制内容</div>
  1733. ${balanceInfo ? `<div class="balance-info">${balanceInfo}</div>` : ''}
  1734. <button class="close-button">&times;</button>
  1735. `;
  1736. // 添加复制功能
  1737. const codeBlock = modal.querySelector('.description-code');
  1738. codeBlock.addEventListener('click', async () => {
  1739. try {
  1740. await navigator.clipboard.writeText(description);
  1741. showToast('已复制描述');
  1742. } catch (err) {
  1743. console.error('[Debug] Copy failed:', err);
  1744. showToast('复制失败,请手动复制');
  1745. }
  1746. });
  1747. // 添加闭按钮功能
  1748. const closeButton = modal.querySelector('.close-button');
  1749. closeButton.addEventListener('click', () => {
  1750. overlay.remove();
  1751. });
  1752. // 点击遮罩层关闭
  1753. overlay.addEventListener('click', (e) => {
  1754. if (e.target === overlay) {
  1755. overlay.remove();
  1756. }
  1757. });
  1758. // ESC键关闭
  1759. const escHandler = (e) => {
  1760. if (e.key === 'Escape') {
  1761. overlay.remove();
  1762. document.removeEventListener('keydown', escHandler);
  1763. }
  1764. };
  1765. document.addEventListener('keydown', escHandler);
  1766. overlay.appendChild(modal);
  1767. document.body.appendChild(overlay);
  1768. }
  1769.  
  1770. // 添加计算成本的函数
  1771. function calculateCost(imageSize, modelName) {
  1772. let baseCost;
  1773. switch (modelName) {
  1774. case 'glm-4v':
  1775. baseCost = 0.015; // GLM-4V的基础成本
  1776. break;
  1777. case 'glm-4v-flash':
  1778. baseCost = 0.002; // GLM-4V-Flash的基础成本
  1779. break;
  1780. case 'Qwen/Qwen2-VL-72B-Instruct':
  1781. baseCost = 0.015;
  1782. break;
  1783. case 'Pro/Qwen/Qwen2-VL-7B-Instruct':
  1784. baseCost = 0.005;
  1785. break;
  1786. case 'OpenGVLab/InternVL2-Llama3-76B':
  1787. baseCost = 0.015;
  1788. break;
  1789. case 'OpenGVLab/InternVL2-26B':
  1790. baseCost = 0.008;
  1791. break;
  1792. case 'Pro/OpenGVLab/InternVL2-8B':
  1793. baseCost = 0.003;
  1794. break;
  1795. default:
  1796. baseCost = 0.01;
  1797. }
  1798.  
  1799. // 图片大小影响因子(每MB增加一定成本)
  1800. const imageSizeMB = imageSize / (1024 * 1024);
  1801. const sizeMultiplier = 1 + (imageSizeMB * 0.1); // 每MB增加10%成本
  1802.  
  1803. return baseCost * sizeMultiplier;
  1804. }
  1805.  
  1806. // 初始化
  1807. function initialize() {
  1808. // 确保DOM加载成后再创建按钮
  1809. if (document.readyState === 'loading') {
  1810. document.addEventListener('DOMContentLoaded', () => {
  1811. createFloatingButton();
  1812. });
  1813. } else {
  1814. createFloatingButton();
  1815. }
  1816. }
  1817.  
  1818. // 启动脚本
  1819. initialize();
  1820. })();