AI Image Description Generator

使用AI生成网页图片描述

目前为 2024-12-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AI Image Description Generator
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.3
  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. }
  477. .ai-result-modal {
  478. width: 95%;
  479. min-width: auto;
  480. }
  481. }
  482. `);
  483.  
  484. // 密码显示切换功能
  485. function togglePassword(element) {
  486. const input = element.parentElement.querySelector('input');
  487. if (input.type === 'password') {
  488. input.type = 'text';
  489. element.textContent = '👁️🗨️';
  490. } else {
  491. input.type = 'password';
  492. element.textContent = '👁️';
  493. }
  494. }
  495.  
  496. // 检查API配置并获取可用模型
  497. async function checkApiAndGetModels(apiEndpoint, apiKey) {
  498. try {
  499. const response = await fetch(`${apiEndpoint}/v1/models`, {
  500. method: 'GET',
  501. headers: {
  502. 'Authorization': `Bearer ${apiKey}`,
  503. 'Content-Type': 'application/json'
  504. }
  505. });
  506.  
  507. if (!response.ok) {
  508. throw new Error(`HTTP error! status: ${response.status}`);
  509. }
  510.  
  511. const result = await response.json();
  512. if (result.data && Array.isArray(result.data)) {
  513. // 过滤出多模态模型
  514. const multimodalModels = result.data
  515. .filter(model => model.id.includes('vision') || model.id.includes('gpt-4-v'))
  516. .map(model => ({
  517. id: model.id,
  518. name: model.id
  519. }));
  520. return multimodalModels;
  521. } else {
  522. throw new Error('Invalid response format');
  523. }
  524. } catch (error) {
  525. console.error('Error fetching models:', error);
  526. throw error;
  527. }
  528. }
  529.  
  530. // 检查API配置
  531. async function checkApiConfig() {
  532. const apiEndpoint = GM_getValue('apiEndpoint', '').trim();
  533. const apiKey = GM_getValue('apiKey', '').trim();
  534. const selectedModel = GM_getValue('selectedModel', '').trim();
  535.  
  536. if (!apiEndpoint || !apiKey || !selectedModel) {
  537. alert('请先配置API Endpoint、API Key和模型');
  538. showConfigModal();
  539. return false;
  540. }
  541.  
  542. try {
  543. // 如果是智谱AI的endpoint,跳过API检查
  544. if(apiEndpoint.includes('bigmodel.cn')) {
  545. return true;
  546. }
  547.  
  548. // 其他endpoint进行API检查
  549. const models = await checkApiAndGetModels(apiEndpoint, apiKey);
  550. if (models.length === 0) {
  551. alert('无法获取可用模型列表,请检查API配置是否正确');
  552. return false;
  553. }
  554. return true;
  555. } catch (error) {
  556. console.error('Error checking API config:', error);
  557. alert('API配置验证失败,请检查配置是否正确');
  558. return false;
  559. }
  560. }
  561.  
  562. // 获取图片的Base64内容
  563. async function getImageBase64(imageUrl) {
  564. console.log('[Debug] Starting image to Base64 conversion for:', imageUrl);
  565. // 尝试将HTTP URL换为HTTPS
  566. if (imageUrl.startsWith('http:')) {
  567. imageUrl = imageUrl.replace('http:', 'https:');
  568. console.log('[Debug] Converted to HTTPS URL:', imageUrl);
  569. }
  570.  
  571. // 获取图片的多种方法
  572. async function tryFetchImage(method) {
  573. return new Promise((resolve, reject) => {
  574. switch(method) {
  575. case 'direct':
  576. // 直接请求
  577. GM_xmlhttpRequest({
  578. method: 'GET',
  579. url: imageUrl,
  580. responseType: 'blob',
  581. headers: {
  582. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  583. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  584. 'Cache-Control': 'no-cache',
  585. 'Pragma': 'no-cache',
  586. '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'
  587. },
  588. anonymous: true,
  589. onload: response => resolve(response),
  590. onerror: error => reject(error)
  591. });
  592. break;
  593.  
  594. case 'withReferer':
  595. // 带原始Referer的请求
  596. GM_xmlhttpRequest({
  597. method: 'GET',
  598. url: imageUrl,
  599. responseType: 'blob',
  600. headers: {
  601. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  602. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  603. 'Cache-Control': 'no-cache',
  604. 'Pragma': 'no-cache',
  605. 'Referer': new URL(imageUrl).origin,
  606. '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'
  607. },
  608. anonymous: true,
  609. onload: response => resolve(response),
  610. onerror: error => reject(error)
  611. });
  612. break;
  613.  
  614. case 'proxy':
  615. // 通过代理服务获取
  616. const proxyUrl = `https://images.weserv.nl/?url=${encodeURIComponent(imageUrl)}`;
  617. GM_xmlhttpRequest({
  618. method: 'GET',
  619. url: proxyUrl,
  620. responseType: 'blob',
  621. headers: {
  622. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  623. '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'
  624. },
  625. anonymous: true,
  626. onload: response => resolve(response),
  627. onerror: error => reject(error)
  628. });
  629. break;
  630.  
  631. case 'corsProxy':
  632. // 通过CORS代理获取
  633. const corsProxyUrl = `https://corsproxy.io/?${encodeURIComponent(imageUrl)}`;
  634. GM_xmlhttpRequest({
  635. method: 'GET',
  636. url: corsProxyUrl,
  637. responseType: 'blob',
  638. headers: {
  639. 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  640. 'Origin': window.location.origin
  641. },
  642. anonymous: true,
  643. onload: response => resolve(response),
  644. onerror: error => reject(error)
  645. });
  646. break;
  647. }
  648. });
  649. }
  650.  
  651. // 处理响应
  652. async function handleResponse(response) {
  653. if (response.status === 200) {
  654. const blob = response.response;
  655. console.log('[Debug] Image blob size:', blob.size, 'bytes');
  656. return new Promise((resolve, reject) => {
  657. const reader = new FileReader();
  658. reader.onloadend = () => {
  659. const base64 = reader.result.split(',')[1];
  660. console.log('[Debug] Base64 conversion completed, length:', base64.length);
  661. resolve(base64);
  662. };
  663. reader.onerror = error => reject(error);
  664. reader.readAsDataURL(blob);
  665. });
  666. }
  667. throw new Error(`Failed with status: ${response.status}`);
  668. }
  669.  
  670. // 依次尝试不同的方法
  671. const methods = ['direct', 'withReferer', 'proxy', 'corsProxy'];
  672. for (const method of methods) {
  673. try {
  674. console.log(`[Debug] Trying method: ${method}`);
  675. const response = await tryFetchImage(method);
  676. if (response.status === 200) {
  677. return await handleResponse(response);
  678. }
  679. console.log(`[Debug] Method ${method} failed with status:`, response.status);
  680. } catch (error) {
  681. console.log(`[Debug] Method ${method} failed:`, error);
  682. }
  683. }
  684.  
  685. throw new Error('All methods to fetch image failed');
  686. }
  687.  
  688. // 调用API获取图片描述
  689. async function getImageDescription(imageUrl, apiEndpoint, apiKey, selectedModel) {
  690. console.log('[Debug] Starting image description request:', {
  691. apiEndpoint,
  692. selectedModel,
  693. imageUrl,
  694. timestamp: new Date().toISOString()
  695. });
  696.  
  697. try {
  698. const base64Image = await getImageBase64(imageUrl);
  699. console.log('[Debug] Image converted to base64, length:', base64Image.length);
  700.  
  701. // 退出选择图片模式
  702. exitImageSelectionMode();
  703. const timeout = 30000; // 30秒超时
  704. const controller = new AbortController();
  705. const timeoutId = setTimeout(() => controller.abort(), timeout);
  706. const imageSize = base64Image.length * 0.75; // 转换为字节数
  707. // 获取当前余额
  708. const userInfo = await checkUserInfo(apiEndpoint, apiKey);
  709. const currentBalance = userInfo.totalBalance;
  710. // 计算每次调用的预估花费(根据图片大小和模型)
  711. const costPerCall = calculateCost(imageSize, selectedModel);
  712. // 计算可识别的剩余图片量
  713. const remainingImages = Math.floor(currentBalance / costPerCall);
  714.  
  715. // 根据不同的API构建不同的请求体和endpoint
  716. let requestBody;
  717. let finalEndpoint;
  718.  
  719. if(selectedModel.startsWith('glm-')) {
  720. // GLM系列模型的请求格式
  721. requestBody = {
  722. model: selectedModel,
  723. messages: [{
  724. role: "user",
  725. content: [{
  726. type: "text",
  727. text: "请描述这张图片的主要内容。如果是人物图片,请至少用15个字描述人物。"
  728. }, {
  729. type: "image_url",
  730. image_url: {
  731. url: `data:image/jpeg;base64,${base64Image}`
  732. }
  733. }]
  734. }],
  735. stream: true
  736. };
  737. finalEndpoint = 'https://open.bigmodel.cn/api/paas/v4/chat/completions';
  738. } else {
  739. // 原有模型的请求格式
  740. requestBody = {
  741. model: selectedModel,
  742. messages: [{
  743. role: "user",
  744. content: [
  745. {
  746. type: "image_url",
  747. image_url: {
  748. url: `data:image/jpeg;base64,${base64Image}`
  749. }
  750. },
  751. {
  752. type: "text",
  753. 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."
  754. }
  755. ]
  756. }],
  757. stream: true
  758. };
  759. finalEndpoint = `${apiEndpoint}/chat/completions`;
  760. }
  761.  
  762. console.log('[Debug] API Request body:', JSON.stringify(requestBody, null, 2));
  763.  
  764. console.log('[Debug] Sending request to:', finalEndpoint);
  765. console.log('[Debug] Request headers:', {
  766. 'Authorization': 'Bearer ***' + apiKey.slice(-4),
  767. 'Content-Type': 'application/json'
  768. });
  769. console.log('[Debug] Request body:', requestBody);
  770.  
  771. return new Promise((resolve, reject) => {
  772. GM_xmlhttpRequest({
  773. method: 'POST',
  774. url: finalEndpoint,
  775. headers: {
  776. 'Authorization': `Bearer ${apiKey}`,
  777. 'Content-Type': 'application/json'
  778. },
  779. data: JSON.stringify(requestBody),
  780. onload: function(response) {
  781. console.log('[Debug] Response received:', {
  782. status: response.status,
  783. statusText: response.statusText,
  784. headers: response.responseHeaders
  785. });
  786.  
  787. if (response.status === 200) {
  788. try {
  789. let description = '';
  790. const lines = response.responseText.split('\n').filter(line => line.trim() !== '');
  791. for (const line of lines) {
  792. if (line.startsWith('data: ')) {
  793. const jsonStr = line.slice(6);
  794. if (jsonStr === '[DONE]') continue;
  795. try {
  796. const jsonData = JSON.parse(jsonStr);
  797. console.log('[Debug] Parsed chunk:', jsonData);
  798. const content = jsonData.choices[0]?.delta?.content;
  799. if (content) {
  800. description += content;
  801. console.log('[Debug] Current description:', description);
  802. }
  803. } catch (e) {
  804. console.error('[Debug] Error parsing chunk JSON:', e);
  805. }
  806. }
  807. }
  808.  
  809. console.log('[Debug] Final description:', description);
  810. removeDescriptionTooltip();
  811. const balanceInfo = `剩余额度为:${currentBalance.toFixed(4)},大约还可以识别 ${remainingImages} 张图片`;
  812. showDescriptionModal(description, balanceInfo);
  813. resolve(description);
  814. } catch (error) {
  815. console.error('[Debug] Error processing response:', error);
  816. reject(error);
  817. }
  818. } else {
  819. console.error('[Debug] Error response:', {
  820. status: response.status,
  821. statusText: response.statusText,
  822. response: response.responseText
  823. });
  824. reject(new Error(`Request failed with status ${response.status}`));
  825. }
  826. },
  827. onerror: function(error) {
  828. console.error('[Debug] Request error:', error);
  829. reject(error);
  830. },
  831. onprogress: function(progress) {
  832. // 用于处理流式响应的进度
  833. console.log('[Debug] Progress:', progress);
  834. try {
  835. const lines = progress.responseText.split('\n').filter(line => line.trim() !== '');
  836. let latestContent = '';
  837. for (const line of lines) {
  838. if (line.startsWith('data: ')) {
  839. const jsonStr = line.slice(6);
  840. if (jsonStr === '[DONE]') continue;
  841. try {
  842. const jsonData = JSON.parse(jsonStr);
  843. const content = jsonData.choices[0]?.delta?.content;
  844. if (content) {
  845. latestContent += content;
  846. }
  847. } catch (e) {
  848. console.error('[Debug] Error parsing progress JSON:', e);
  849. }
  850. }
  851. }
  852. if (latestContent) {
  853. updateDescriptionTooltip('正在生成描述: ' + latestContent);
  854. }
  855. } catch (error) {
  856. console.error('[Debug] Error processing progress:', error);
  857. }
  858. }
  859. });
  860. });
  861. } catch (error) {
  862. if (error.name === 'AbortError') {
  863. showToast('请求超时,请重试');
  864. }
  865. removeDescriptionTooltip();
  866. console.error('[Debug] Error in getImageDescription:', {
  867. error,
  868. stack: error.stack,
  869. timestamp: new Date().toISOString()
  870. });
  871. throw error;
  872. }
  873. }
  874.  
  875. // 显示描述tooltip
  876. function showDescriptionTooltip(description) {
  877. const tooltip = document.createElement('div');
  878. tooltip.className = 'ai-image-description';
  879. tooltip.textContent = description;
  880. // 获取视口宽度
  881. const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  882. // 计算tooltip位置(水平居中,距顶部20px)
  883. const tooltipX = Math.max(0, (viewportWidth - 300) / 2); // 300是tooltip的max-width
  884. tooltip.style.position = 'fixed';
  885. tooltip.style.left = `${tooltipX}px`;
  886. tooltip.style.top = '20px';
  887. document.body.appendChild(tooltip);
  888.  
  889. // 添加动态点的动画
  890. let dots = 1;
  891. const updateInterval = setInterval(() => {
  892. if (!document.body.contains(tooltip)) {
  893. clearInterval(updateInterval);
  894. return;
  895. }
  896. dots = dots % 6 + 1;
  897. tooltip.textContent = '正在生成描述' + '.'.repeat(dots);
  898. }, 500); // 每500ms更新一次
  899.  
  900. return tooltip;
  901. }
  902.  
  903. // 更新描述tooltip内容
  904. function updateDescriptionTooltip(description) {
  905. const tooltip = document.querySelector('.ai-image-description');
  906. if (tooltip) {
  907. tooltip.textContent = description;
  908. }
  909. }
  910.  
  911. // 移除描述tooltip
  912. function removeDescriptionTooltip() {
  913. const tooltip = document.querySelector('.ai-image-description');
  914. if (tooltip) {
  915. tooltip.remove();
  916. }
  917. }
  918.  
  919. // 进入图片选择模式
  920. function enterImageSelectionMode() {
  921. console.log('[Debug] Entering image selection mode');
  922. if(isSelectionMode) return; // 防止重复进入选择模式
  923. isSelectionMode = true;
  924.  
  925. // 隐藏悬浮按钮
  926. const floatingBtn = document.querySelector('.ai-floating-btn');
  927. if(floatingBtn) {
  928. floatingBtn.style.display = 'none';
  929. }
  930.  
  931. // 创建遮罩层
  932. const overlay = document.createElement('div');
  933. overlay.className = 'ai-selection-overlay';
  934. document.body.appendChild(overlay);
  935. // 添加选择状态的类名
  936. document.body.classList.add('ai-selecting-image');
  937.  
  938. // 创建点击事件处理函数
  939. const clickHandler = async function(e) {
  940. if (!isSelectionMode) return;
  941.  
  942. if (e.target.tagName === 'IMG') {
  943. console.log('[Debug] Image clicked:', e.target.src);
  944. e.preventDefault();
  945. e.stopPropagation();
  946. // 获取配置
  947. const endpoint = GM_getValue('apiEndpoint', '');
  948. const apiKey = GM_getValue('apiKey', '');
  949. const selectedModel = GM_getValue('selectedModel', '');
  950.  
  951. console.log('[Debug] Current configuration:', {
  952. endpoint,
  953. selectedModel,
  954. hasApiKey: !!apiKey
  955. });
  956.  
  957. if (!endpoint || !apiKey || !selectedModel) {
  958. showToast('请先配置API配置');
  959. exitImageSelectionMode();
  960. return;
  961. }
  962.  
  963. // 显示加载中的tooltip
  964. showDescriptionTooltip('正在生成描述...');
  965.  
  966. try {
  967. await getImageDescription(e.target.src, endpoint, apiKey, selectedModel);
  968. } catch (error) {
  969. console.error('[Debug] Description generation failed:', error);
  970. removeDescriptionTooltip();
  971. showToast('生成描述失败: ' + error.message);
  972. }
  973. }
  974. };
  975.  
  976. // 添加点击事件监听器
  977. document.addEventListener('click', clickHandler, true);
  978. // ESC键退选择模式
  979. const escHandler = (e) => {
  980. if (e.key === 'Escape') {
  981. exitImageSelectionMode();
  982. }
  983. };
  984. document.addEventListener('keydown', escHandler);
  985.  
  986. // 保存事件理函数以便后续移除
  987. window._imageSelectionHandlers = {
  988. click: clickHandler,
  989. keydown: escHandler
  990. };
  991. }
  992.  
  993. // 退出图片选择模式
  994. function exitImageSelectionMode() {
  995. console.log('[Debug] Exiting image selection mode');
  996. isSelectionMode = false;
  997.  
  998. // 显示悬浮按钮
  999. const floatingBtn = document.querySelector('.ai-floating-btn');
  1000. if(floatingBtn) {
  1001. floatingBtn.style.display = 'flex';
  1002. }
  1003.  
  1004. // 移除遮罩层
  1005. const overlay = document.querySelector('.ai-selection-overlay');
  1006. if (overlay) {
  1007. overlay.remove();
  1008. }
  1009.  
  1010. // 移除选择状态的类名
  1011. document.body.classList.remove('ai-selecting-image');
  1012.  
  1013. // 移除所有事件监听器
  1014. if (window._imageSelectionHandlers) {
  1015. document.removeEventListener('click', window._imageSelectionHandlers.click, true);
  1016. document.removeEventListener('keydown', window._imageSelectionHandlers.keydown);
  1017. window._imageSelectionHandlers = null;
  1018. }
  1019. }
  1020.  
  1021. // 显示toast提示
  1022. function showToast(message, duration = 3000) {
  1023. const toast = document.createElement('div');
  1024. toast.className = 'ai-toast';
  1025. toast.textContent = message;
  1026. document.body.appendChild(toast);
  1027. setTimeout(() => {
  1028. toast.remove();
  1029. }, duration);
  1030. }
  1031.  
  1032. // 检查用户信息
  1033. async function checkUserInfo(apiEndpoint, apiKey) {
  1034. try {
  1035. // 对智谱AI的endpoint返回默认值
  1036. if(apiEndpoint.includes('bigmodel.cn')) {
  1037. const defaultUserData = {
  1038. name: 'GLM User',
  1039. balance: 1000, // 默认余额
  1040. chargeBalance: 0,
  1041. totalBalance: 1000
  1042. };
  1043. console.log('[Debug] Using default user data for GLM:', defaultUserData);
  1044. return defaultUserData;
  1045. }
  1046.  
  1047. // 其他endpoint使用原有逻辑
  1048. return new Promise((resolve, reject) => {
  1049. console.log('[Debug] Sending user info request to:', `${apiEndpoint}/v1/user/info`);
  1050. GM_xmlhttpRequest({
  1051. method: 'GET',
  1052. url: `${apiEndpoint}/v1/user/info`,
  1053. headers: {
  1054. 'Authorization': `Bearer ${apiKey}`,
  1055. 'Content-Type': 'application/json'
  1056. },
  1057. onload: function(response) {
  1058. console.log('[Debug] User Info Raw Response:', {
  1059. status: response.status,
  1060. statusText: response.statusText,
  1061. responseText: response.responseText,
  1062. headers: response.responseHeaders
  1063. });
  1064.  
  1065. if (response.status === 200) {
  1066. try {
  1067. const result = JSON.parse(response.responseText);
  1068. console.log('[Debug] User Info Parsed Response:', result);
  1069. if (result.code === 20000 && result.status && result.data) {
  1070. const { name, balance, chargeBalance, totalBalance } = result.data;
  1071. resolve({
  1072. name,
  1073. balance: parseFloat(balance),
  1074. chargeBalance: parseFloat(chargeBalance),
  1075. totalBalance: parseFloat(totalBalance)
  1076. });
  1077. } else {
  1078. throw new Error(result.message || 'Invalid response format');
  1079. }
  1080. } catch (error) {
  1081. console.error('[Debug] JSON Parse Error:', error);
  1082. reject(error);
  1083. }
  1084. } else {
  1085. console.error('[Debug] HTTP Error Response:', {
  1086. status: response.status,
  1087. statusText: response.statusText,
  1088. response: response.responseText
  1089. });
  1090. reject(new Error(`HTTP error! status: ${response.status}`));
  1091. }
  1092. },
  1093. onerror: function(error) {
  1094. console.error('[Debug] Request Error:', error);
  1095. reject(error);
  1096. }
  1097. });
  1098. });
  1099. } catch (error) {
  1100. console.error('[Debug] User Info Error:', error);
  1101. throw error;
  1102. }
  1103. }
  1104.  
  1105. // 获取可用模型列表
  1106. async function getAvailableModels(apiEndpoint, apiKey) {
  1107. console.log('[Debug] Getting available models from:', apiEndpoint);
  1108.  
  1109. try {
  1110. // 如果是智谱AI的endpoint,直接返回GLM模型列表
  1111. if(apiEndpoint.includes('bigmodel.cn')) {
  1112. const glmModels = [
  1113. {
  1114. id: 'glm-4',
  1115. name: 'GLM-4'
  1116. },
  1117. {
  1118. id: 'glm-4v',
  1119. name: 'GLM-4V'
  1120. },
  1121. {
  1122. id: 'glm-4v-flash',
  1123. name: 'GLM-4V-Flash'
  1124. }
  1125. ];
  1126. console.log('[Debug] Available GLM models:', glmModels);
  1127. return glmModels;
  1128. }
  1129.  
  1130. // 其他endpoint使用原有逻辑
  1131. return new Promise((resolve, reject) => {
  1132. console.log('[Debug] Sending models request to:', `${apiEndpoint}/v1/models`);
  1133. GM_xmlhttpRequest({
  1134. method: 'GET',
  1135. url: `${apiEndpoint}/v1/models`,
  1136. headers: {
  1137. 'Authorization': `Bearer ${apiKey}`,
  1138. 'Content-Type': 'application/json'
  1139. },
  1140. onload: function(response) {
  1141. console.log('[Debug] Models API Raw Response:', {
  1142. status: response.status,
  1143. statusText: response.statusText,
  1144. responseText: response.responseText,
  1145. headers: response.responseHeaders
  1146. });
  1147.  
  1148. if (response.status === 200) {
  1149. try {
  1150. const result = JSON.parse(response.responseText);
  1151. console.log('[Debug] Models API Parsed Response:', result);
  1152. if (result.object === 'list' && Array.isArray(result.data)) {
  1153. const models = result.data
  1154. .filter(model => supportedVLModels.includes(model.id))
  1155. .map(model => ({
  1156. id: model.id,
  1157. name: model.id.split('/').pop()
  1158. .replace('Qwen2-VL-', 'Qwen2-')
  1159. .replace('InternVL2-Llama3-', 'InternVL2-')
  1160. .replace('-Instruct', '')
  1161. }));
  1162. console.log('[Debug] Filtered and processed models:', models);
  1163. resolve(models);
  1164. } else {
  1165. console.error('[Debug] Invalid models response format:', result);
  1166. reject(new Error('Invalid models response format'));
  1167. }
  1168. } catch (error) {
  1169. console.error('[Debug] JSON Parse Error:', error);
  1170. reject(error);
  1171. }
  1172. } else {
  1173. console.error('[Debug] HTTP Error Response:', {
  1174. status: response.status,
  1175. statusText: response.statusText,
  1176. response: response.responseText
  1177. });
  1178. reject(new Error(`HTTP error! status: ${response.status}`));
  1179. }
  1180. },
  1181. onerror: function(error) {
  1182. console.error('[Debug] Models API Request Error:', error);
  1183. reject(error);
  1184. }
  1185. });
  1186. });
  1187. } catch (error) {
  1188. console.error('[Debug] Models API Error:', error);
  1189. throw error;
  1190. }
  1191. }
  1192.  
  1193. // 更新模型下拉菜单
  1194. function updateModelSelect(selectElement, models) {
  1195. if (models.length === 0) {
  1196. selectElement.innerHTML = '<option value="">未找到可用的视觉模型</option>';
  1197. selectElement.disabled = true;
  1198. return;
  1199. }
  1200.  
  1201. selectElement.innerHTML = '<option value="">请选择视觉模型</option>' +
  1202. models.map(model =>
  1203. `<option value="${model.id}" title="${model.id}">${model.name}</option>`
  1204. ).join('');
  1205. selectElement.disabled = false;
  1206. }
  1207.  
  1208. // 保存模型列表到GM存储
  1209. function saveModelList(models) {
  1210. GM_setValue('availableModels', models);
  1211. }
  1212.  
  1213. // 从GM存储获取模型列表
  1214. function getStoredModelList() {
  1215. return GM_getValue('availableModels', []);
  1216. }
  1217.  
  1218. // 创建悬浮按钮
  1219. function createFloatingButton() {
  1220. const btn = document.createElement('div');
  1221. btn.className = 'ai-floating-btn';
  1222. btn.innerHTML = `
  1223. <svg viewBox="0 0 24 24">
  1224. <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"/>
  1225. </svg>
  1226. `;
  1227.  
  1228. // 设置初始位置
  1229. const savedPos = JSON.parse(GM_getValue('btnPosition', '{"x": 20, "y": 20}'));
  1230. btn.style.left = (savedPos.x || 20) + 'px';
  1231. btn.style.top = (savedPos.y || 20) + 'px';
  1232. btn.style.right = 'auto';
  1233. btn.style.bottom = 'auto';
  1234.  
  1235. let isDragging = false;
  1236. let hasMoved = false;
  1237. let startX, startY;
  1238. let initialLeft, initialTop;
  1239. let longPressTimer;
  1240. let touchStartTime;
  1241.  
  1242. // 触屏事件处理
  1243. btn.addEventListener('touchstart', function(e) {
  1244. e.preventDefault();
  1245. touchStartTime = Date.now();
  1246. // 设置长按定时器
  1247. longPressTimer = setTimeout(() => {
  1248. exitImageSelectionMode();
  1249. createConfigUI();
  1250. }, 500); // 500ms长按触发
  1251.  
  1252. const touch = e.touches[0];
  1253. startX = touch.clientX;
  1254. startY = touch.clientY;
  1255. const rect = btn.getBoundingClientRect();
  1256. initialLeft = rect.left;
  1257. initialTop = rect.top;
  1258. });
  1259.  
  1260. btn.addEventListener('touchmove', function(e) {
  1261. e.preventDefault();
  1262. clearTimeout(longPressTimer); // 移动时取消长按
  1263.  
  1264. const touch = e.touches[0];
  1265. const deltaX = touch.clientX - startX;
  1266. const deltaY = touch.clientY - startY;
  1267. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  1268. hasMoved = true;
  1269. }
  1270. const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
  1271. const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
  1272. btn.style.left = newLeft + 'px';
  1273. btn.style.top = newTop + 'px';
  1274. });
  1275.  
  1276. btn.addEventListener('touchend', function(e) {
  1277. e.preventDefault();
  1278. clearTimeout(longPressTimer);
  1279. const touchDuration = Date.now() - touchStartTime;
  1280. if (!hasMoved && touchDuration < 500) {
  1281. // 短按进入图片选择模式
  1282. enterImageSelectionMode();
  1283. }
  1284. if (hasMoved) {
  1285. // 保存新位置
  1286. const rect = btn.getBoundingClientRect();
  1287. GM_setValue('btnPosition', JSON.stringify({
  1288. x: rect.left,
  1289. y: rect.top
  1290. }));
  1291. }
  1292. hasMoved = false;
  1293. });
  1294.  
  1295. // 保留原有的鼠标事件处理
  1296. btn.addEventListener('click', function(e) {
  1297. if (e.button === 0 && !hasMoved) { // 左键点击且没有移动
  1298. enterImageSelectionMode();
  1299. e.stopPropagation();
  1300. }
  1301. hasMoved = false;
  1302. });
  1303.  
  1304. btn.addEventListener('contextmenu', function(e) {
  1305. e.preventDefault();
  1306. exitImageSelectionMode();
  1307. createConfigUI();
  1308. });
  1309.  
  1310. // 拖拽相关事件
  1311. function dragStart(e) {
  1312. if (e.target === btn || btn.contains(e.target)) {
  1313. isDragging = true;
  1314. hasMoved = false;
  1315. const rect = btn.getBoundingClientRect();
  1316. startX = e.clientX;
  1317. startY = e.clientY;
  1318. initialLeft = rect.left;
  1319. initialTop = rect.top;
  1320. e.preventDefault();
  1321. }
  1322. }
  1323.  
  1324. function drag(e) {
  1325. if (isDragging) {
  1326. e.preventDefault();
  1327. const deltaX = e.clientX - startX;
  1328. const deltaY = e.clientY - startY;
  1329. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  1330. hasMoved = true;
  1331. }
  1332. const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
  1333. const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
  1334. btn.style.left = newLeft + 'px';
  1335. btn.style.top = newTop + 'px';
  1336. }
  1337. }
  1338.  
  1339. function dragEnd(e) {
  1340. if (isDragging) {
  1341. isDragging = false;
  1342. const rect = btn.getBoundingClientRect();
  1343. GM_setValue('btnPosition', JSON.stringify({
  1344. x: rect.left,
  1345. y: rect.top
  1346. }));
  1347. }
  1348. }
  1349.  
  1350. btn.addEventListener('mousedown', dragStart);
  1351. document.addEventListener('mousemove', drag);
  1352. document.addEventListener('mouseup', dragEnd);
  1353.  
  1354. // 将按钮添加到文档中
  1355. document.body.appendChild(btn);
  1356. return btn;
  1357. }
  1358.  
  1359. // 创建配置界面
  1360. function createConfigUI() {
  1361. const overlay = document.createElement('div');
  1362. overlay.className = 'ai-modal-overlay';
  1363. const modal = document.createElement('div');
  1364. modal.className = 'ai-config-modal';
  1365. modal.innerHTML = `
  1366. <h3>AI图像描述配置</h3>
  1367. <div class="input-group">
  1368. <label>API Endpoint:</label>
  1369. <div class="input-wrapper">
  1370. <input type="text" id="ai-endpoint" placeholder="https://api.openai.com" value="${GM_getValue('apiEndpoint', '')}">
  1371. <span class="input-icon clear-icon" title="清空" onclick="this.previousElementSibling.value=''">✕</span>
  1372. </div>
  1373. </div>
  1374. <div class="input-group">
  1375. <label>API Key:</label>
  1376. <div class="input-wrapper">
  1377. <input type="password" id="ai-apikey" value="${GM_getValue('apiKey', '')}">
  1378. <span class="input-icon clear-icon" title="清空" onclick="this.previousElementSibling.value=''">✕</span>
  1379. <span class="input-icon toggle-password" title="显示/隐藏密码">👁️</span>
  1380. </div>
  1381. <div class="button-row">
  1382. <button class="check-button" id="check-api">检测可用性</button>
  1383. </div>
  1384. </div>
  1385. <div class="input-group">
  1386. <label>可用模型:</label>
  1387. <select id="ai-model">
  1388. <option value="">加载中...</option>
  1389. </select>
  1390. </div>
  1391. <div class="button-group">
  1392. <button class="cancel-button" id="ai-cancel-config">取消</button>
  1393. <button class="save-button" id="ai-save-config">保存</button>
  1394. </div>
  1395. `;
  1396.  
  1397. overlay.appendChild(modal);
  1398. document.body.appendChild(overlay);
  1399.  
  1400. // 初始化模型下拉菜单
  1401. const modelSelect = modal.querySelector('#ai-model');
  1402. const storedModels = getStoredModelList();
  1403. const selectedModel = GM_getValue('selectedModel', '');
  1404. if (storedModels.length > 0) {
  1405. updateModelSelect(modelSelect, storedModels);
  1406. if (selectedModel) {
  1407. modelSelect.value = selectedModel;
  1408. }
  1409. } else {
  1410. modelSelect.innerHTML = '<option value="">请先检测API可用性</option>';
  1411. modelSelect.disabled = true;
  1412. }
  1413.  
  1414. // 添加密码显示切换事件监听
  1415. const toggleBtn = modal.querySelector('.toggle-password');
  1416. toggleBtn.addEventListener('click', function() {
  1417. togglePassword(this);
  1418. });
  1419.  
  1420. // 自动保存配置
  1421. const inputs = modal.querySelectorAll('input');
  1422. inputs.forEach(input => {
  1423. input.addEventListener('blur', function() {
  1424. const endpoint = modal.querySelector('#ai-endpoint').value.trim();
  1425. const apiKey = modal.querySelector('#ai-apikey').value.trim();
  1426. if (endpoint && apiKey) {
  1427. GM_setValue('apiEndpoint', endpoint);
  1428. GM_setValue('apiKey', apiKey);
  1429. showToast('配置已保存');
  1430. }
  1431. });
  1432. });
  1433.  
  1434. // 检测API可用性
  1435. const checkButton = modal.querySelector('#check-api');
  1436. checkButton.addEventListener('click', async function() {
  1437. const endpoint = modal.querySelector('#ai-endpoint').value.trim();
  1438. const apiKey = modal.querySelector('#ai-apikey').value.trim();
  1439.  
  1440. if (!endpoint || !apiKey) {
  1441. showToast('请先填写API Endpoint和API Key');
  1442. return;
  1443. }
  1444.  
  1445. checkButton.disabled = true;
  1446. modelSelect.disabled = true;
  1447. modelSelect.innerHTML = '<option value="">检测中...</option>';
  1448.  
  1449. try {
  1450. // 并行请求用户信息和模型列表
  1451. const [userInfo, models] = await Promise.all([
  1452. checkUserInfo(endpoint, apiKey),
  1453. getAvailableModels(endpoint, apiKey)
  1454. ]);
  1455.  
  1456. // 保存模型列表
  1457. saveModelList(models);
  1458.  
  1459. // 更新模型下拉菜单
  1460. updateModelSelect(modelSelect, models);
  1461.  
  1462. // 显示用户信息
  1463. showToast(`检测通过,欢迎 ${userInfo.name}!\n账户余额:${userInfo.balance.toFixed(2)}\n充值余额:${userInfo.chargeBalance.toFixed(2)}\n总余额:${userInfo.totalBalance.toFixed(2)}`);
  1464.  
  1465. // 如果之前存过模型选择,恢复选择
  1466. const savedModel = GM_getValue('selectedModel', '');
  1467. if (savedModel && models.some(m => m.id === savedModel)) {
  1468. modelSelect.value = savedModel;
  1469. }
  1470. } catch (error) {
  1471. showToast('API检测失败:' + error.message);
  1472. modelSelect.innerHTML = '<option value="">获取模型列表失败</option>';
  1473. modelSelect.disabled = true;
  1474. } finally {
  1475. checkButton.disabled = false;
  1476. }
  1477. });
  1478.  
  1479. // 模型选择变更时保存
  1480. modelSelect.addEventListener('change', function() {
  1481. if (this.value) {
  1482. GM_setValue('selectedModel', this.value);
  1483. showToast('已保存模型选择');
  1484. }
  1485. });
  1486.  
  1487. // 保存配置
  1488. const saveButton = modal.querySelector('#ai-save-config');
  1489. saveButton.addEventListener('click', function() {
  1490. const endpoint = modal.querySelector('#ai-endpoint').value.trim();
  1491. const apiKey = modal.querySelector('#ai-apikey').value.trim();
  1492. const selectedModel = modelSelect.value;
  1493.  
  1494. if (!endpoint || !apiKey) {
  1495. showToast('请填写API Endpoint和API Key');
  1496. return;
  1497. }
  1498.  
  1499. if (!selectedModel) {
  1500. showToast('请选择一个视觉模型');
  1501. return;
  1502. }
  1503.  
  1504. GM_setValue('apiEndpoint', endpoint);
  1505. GM_setValue('apiKey', apiKey);
  1506. GM_setValue('selectedModel', selectedModel);
  1507. showToast('配置已保存');
  1508. overlay.remove();
  1509. });
  1510.  
  1511. // 更新保存按钮状态
  1512. function updateSaveButtonState() {
  1513. const endpoint = modal.querySelector('#ai-endpoint').value.trim();
  1514. const apiKey = modal.querySelector('#ai-apikey').value.trim();
  1515. const selectedModel = modelSelect.value;
  1516. saveButton.disabled = !endpoint || !apiKey || !selectedModel;
  1517. }
  1518.  
  1519. // 监听输入变化
  1520. modal.querySelector('#ai-endpoint').addEventListener('input', updateSaveButtonState);
  1521. modal.querySelector('#ai-apikey').addEventListener('input', updateSaveButtonState);
  1522. modelSelect.addEventListener('change', updateSaveButtonState);
  1523.  
  1524. // 初始化保存按钮状态
  1525. updateSaveButtonState();
  1526.  
  1527. // 取消配置
  1528. modal.querySelector('#ai-cancel-config').onclick = () => {
  1529. overlay.remove();
  1530. };
  1531.  
  1532. // 点击遮罩层关闭
  1533. overlay.addEventListener('click', (e) => {
  1534. if (e.target === overlay) {
  1535. overlay.remove();
  1536. }
  1537. });
  1538. }
  1539.  
  1540. // 显示图像选择面
  1541. function showImageSelectionModal() {
  1542. const overlay = document.createElement('div');
  1543. overlay.className = 'ai-modal-overlay';
  1544. const modal = document.createElement('div');
  1545. modal.className = 'ai-config-modal';
  1546. modal.innerHTML = `
  1547. <h3>选择要识别的图像</h3>
  1548. <div class="ai-image-options">
  1549. <button id="ai-all-images">识别所有图片</button>
  1550. <button id="ai-visible-images">仅识别可见图片</button>
  1551. </div>
  1552. <button id="ai-cancel">取消</button>
  1553. `;
  1554.  
  1555. overlay.appendChild(modal);
  1556. document.body.appendChild(overlay);
  1557.  
  1558. // 添加事件监听
  1559. modal.querySelector('#ai-all-images').onclick = () => {
  1560. if (checkApiConfig()) {
  1561. describeAllImages();
  1562. overlay.remove();
  1563. }
  1564. };
  1565.  
  1566. modal.querySelector('#ai-visible-images').onclick = () => {
  1567. if (checkApiConfig()) {
  1568. describeVisibleImages();
  1569. overlay.remove();
  1570. }
  1571. };
  1572.  
  1573. modal.querySelector('#ai-cancel').onclick = () => {
  1574. overlay.remove();
  1575. };
  1576.  
  1577. // 点击遮罩层关闭
  1578. overlay.addEventListener('click', (e) => {
  1579. if (e.target === overlay) {
  1580. overlay.remove();
  1581. }
  1582. });
  1583. }
  1584.  
  1585. function showDescriptionModal(description, balanceInfo) {
  1586. const overlay = document.createElement('div');
  1587. overlay.className = 'ai-modal-overlay';
  1588. const modal = document.createElement('div');
  1589. modal.className = 'ai-result-modal';
  1590. modal.innerHTML = `
  1591. <h3>图片描述结果</h3>
  1592. <pre class="description-code"><code>${description}</code></pre>
  1593. <div class="copy-hint">点击上方代码块复制内容</div>
  1594. ${balanceInfo ? `<div class="balance-info">${balanceInfo}</div>` : ''}
  1595. <button class="close-button">&times;</button>
  1596. `;
  1597. // 添加复制功能
  1598. const codeBlock = modal.querySelector('.description-code');
  1599. codeBlock.addEventListener('click', async () => {
  1600. try {
  1601. await navigator.clipboard.writeText(description);
  1602. showToast('已复制描述');
  1603. } catch (err) {
  1604. console.error('[Debug] Copy failed:', err);
  1605. showToast('复制失败,请手动复制');
  1606. }
  1607. });
  1608. // 添加闭按钮功能
  1609. const closeButton = modal.querySelector('.close-button');
  1610. closeButton.addEventListener('click', () => {
  1611. overlay.remove();
  1612. });
  1613. // 点击遮罩层关闭
  1614. overlay.addEventListener('click', (e) => {
  1615. if (e.target === overlay) {
  1616. overlay.remove();
  1617. }
  1618. });
  1619. // ESC键关闭
  1620. const escHandler = (e) => {
  1621. if (e.key === 'Escape') {
  1622. overlay.remove();
  1623. document.removeEventListener('keydown', escHandler);
  1624. }
  1625. };
  1626. document.addEventListener('keydown', escHandler);
  1627. overlay.appendChild(modal);
  1628. document.body.appendChild(overlay);
  1629. }
  1630.  
  1631. // 添加计算成本的函数
  1632. function calculateCost(imageSize, modelName) {
  1633. let baseCost;
  1634. switch (modelName) {
  1635. case 'glm-4v':
  1636. baseCost = 0.015; // GLM-4V的基础成本
  1637. break;
  1638. case 'glm-4v-flash':
  1639. baseCost = 0.002; // GLM-4V-Flash的基础成本
  1640. break;
  1641. case 'Qwen/Qwen2-VL-72B-Instruct':
  1642. baseCost = 0.015;
  1643. break;
  1644. case 'Pro/Qwen/Qwen2-VL-7B-Instruct':
  1645. baseCost = 0.005;
  1646. break;
  1647. case 'OpenGVLab/InternVL2-Llama3-76B':
  1648. baseCost = 0.015;
  1649. break;
  1650. case 'OpenGVLab/InternVL2-26B':
  1651. baseCost = 0.008;
  1652. break;
  1653. case 'Pro/OpenGVLab/InternVL2-8B':
  1654. baseCost = 0.003;
  1655. break;
  1656. default:
  1657. baseCost = 0.01;
  1658. }
  1659.  
  1660. // 图片大小影响因子(每MB增加一定成本)
  1661. const imageSizeMB = imageSize / (1024 * 1024);
  1662. const sizeMultiplier = 1 + (imageSizeMB * 0.1); // 每MB增加10%成本
  1663.  
  1664. return baseCost * sizeMultiplier;
  1665. }
  1666.  
  1667. // 初始化
  1668. function initialize() {
  1669. // 确保DOM加载成后再创建按钮
  1670. if (document.readyState === 'loading') {
  1671. document.addEventListener('DOMContentLoaded', () => {
  1672. createFloatingButton();
  1673. });
  1674. } else {
  1675. createFloatingButton();
  1676. }
  1677. }
  1678.  
  1679. // 启动脚本
  1680. initialize();
  1681. })();