AI Image Description Generator

使用AI生成网页图片描述

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