【无水印下载神器】豆包|即梦|美间生成图片视频一键无水印

实现豆包&即梦生成的图片视频免费无水印下载

  1. // ==UserScript==
  2. // @name 【无水印下载神器】豆包|即梦|美间生成图片视频一键无水印
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.1.3
  5. // @description 实现豆包&即梦生成的图片视频免费无水印下载
  6. // @author 微信11208596
  7. // @license UNLICENSED
  8. // @match https://www.doubao.com/*
  9. // @match https://jimeng.jianying.com/ai-tool/*
  10. // @match https://www.meijian.com/mj-box/*
  11. // @match https://www.meijian.com/ai/*
  12. // @match https://meijian.com/mj-box/*
  13. // @match https://meijian.com/ai/*
  14. // @grant GM_download
  15. // @grant GM_xmlhttpRequest
  16. // @connect feishu.cn
  17. // @connect weserv.nl
  18. // @connect doubao.com
  19. // @connect jianying.com
  20. // @connect meijian.com
  21. // @connect vlabvod.com
  22. // @connect byteimg.com
  23. // @connect self
  24. // @connect *
  25. // @run-at document-start
  26. // ==/UserScript==
  27.  
  28. (function () {
  29. 'use strict';
  30.  
  31. // 尝试自动允许所有跨域请求(重要提示,添加在最前面)
  32. // 这段代码会尝试拦截Tampermonkey的请求确认对话框并自动点击"总是允许全部域名"按钮
  33. function autoAcceptCrossOriginRequests() {
  34. // 创建一个MutationObserver来监视DOM变化
  35. const observer = new MutationObserver((mutations) => {
  36. for (const mutation of mutations) {
  37. for (const node of mutation.addedNodes) {
  38. if (node.nodeType === Node.ELEMENT_NODE) {
  39. // 检查是否是Tampermonkey的跨域请求对话框
  40. if (node.textContent && node.textContent.includes('跨源资源请求') ||
  41. node.textContent && node.textContent.includes('cross-origin')) {
  42.  
  43. console.log('[自动允许] 检测到跨域请求对话框,尝试自动点击"总是允许全部域名"按钮');
  44.  
  45. // 尝试查找"总是允许全部域名"按钮并点击
  46. setTimeout(() => {
  47. // 通过文本内容定位按钮
  48. const buttons = document.querySelectorAll('button');
  49. for (const button of buttons) {
  50. if (button.textContent && (
  51. button.textContent.includes('总是允许全部域名') ||
  52. button.textContent.includes('Always allow all domains'))) {
  53. console.log('[自动允许] 找到"总是允许全部域名"按钮,自动点击');
  54. button.click();
  55. return;
  56. }
  57. }
  58.  
  59. // 如果没找到按钮,尝试通过其他方式定位
  60. const allButtons = document.querySelectorAll('button, div[role="button"]');
  61. for (const btn of allButtons) {
  62. // 寻找可能是"允许全部域名"的按钮
  63. if (btn.textContent && (btn.textContent.includes('允许全部') ||
  64. btn.textContent.includes('Allow all'))) {
  65. console.log('[自动允许] 找到可能的"允许全部"按钮,自动点击');
  66. btn.click();
  67. return;
  68. }
  69. }
  70.  
  71. console.log('[自动允许] 未找到"总是允许全部域名"按钮,尝试通过ID定位');
  72. // 通过已知ID尝试
  73. const allowBtn = document.querySelector('[id*="allow"], [class*="allow"]');
  74. if (allowBtn) {
  75. console.log('[自动允许] 通过ID找到可能的允许按钮,自动点击');
  76. allowBtn.click();
  77. }
  78. }, 500);
  79. }
  80. }
  81. }
  82. }
  83. });
  84.  
  85. // 开始观察整个文档
  86. observer.observe(document.documentElement, {
  87. childList: true,
  88. subtree: true
  89. });
  90.  
  91. console.log('[自动允许] 已启动跨域请求自动允许功能');
  92. }
  93.  
  94. // 立即运行自动允许函数
  95. autoAcceptCrossOriginRequests();
  96.  
  97. // 添加激活码相关功能
  98. const ACTIVATION_KEY = 'doubao_activation_status';
  99. const SECRET_KEY = 'db94xy20240322'; // 使用一个固定的密钥值
  100. const VALID_DAYS = 30; // 激活码有效期(天)
  101.  
  102. // 添加共享激活状态的常量
  103. const SHARED_ACTIVATION_KEY = 'ai_platform_activation_status';
  104. const SHARED_EXPIRE_KEY = 'ai_platform_expire_time';
  105. const SHARED_RECORD_KEY = 'ai_platform_record_id';
  106. const SHARED_CODE_KEY = 'ai_platform_activation_code';
  107.  
  108. // 飞书多维表格配置
  109. const FEISHU_CONFIG = {
  110. APP_ID: 'cli_a7317a5d6afd901c',
  111. APP_SECRET: 'cdGf1f5n5xY0tI6F07xKkcU1iPoFVdPD',
  112. BASE_ID: 'T1M4bzmLLarNLhs5jcEcwAcRn8Q', // 多维表格 base ID
  113. TABLE_ID: 'tbliBckxa87pskV8', // 数据表 ID
  114. API_URL: 'https://open.feishu.cn/open-apis',
  115. TOKEN: null
  116. };
  117.  
  118. // 添加设备指纹生成函数
  119. function generateDeviceFingerprint() {
  120. const components = [
  121. navigator.userAgent,
  122. navigator.language,
  123. navigator.platform,
  124. new Date().getTimezoneOffset(),
  125. screen.colorDepth,
  126. screen.width + 'x' + screen.height,
  127. navigator.hardwareConcurrency,
  128. navigator.deviceMemory,
  129. navigator.vendor
  130. ].join('|');
  131.  
  132. // 使用更稳定的哈希算法
  133. let hash = 0;
  134. for (let i = 0; i < components.length; i++) {
  135. const char = components.charCodeAt(i);
  136. hash = ((hash << 5) - hash) + char;
  137. hash = hash & hash;
  138. }
  139.  
  140. // 转换为固定长度的字符串
  141. return Math.abs(hash).toString(36).substring(0, 8);
  142. }
  143.  
  144. // 修改设备ID获取逻辑
  145. function getOrCreateDeviceId() {
  146. // 使用固定的存储键名,确保两个平台使用相同的键
  147. const DEVICE_ID_KEY = 'ai_platform_device_id';
  148. let deviceId = localStorage.getItem(DEVICE_ID_KEY);
  149.  
  150. if (!deviceId) {
  151. // 生成新的设备ID,结合设备指纹和随机数
  152. const fingerprint = generateDeviceFingerprint();
  153. const randomPart = Math.random().toString(36).substring(2, 6);
  154. deviceId = `${fingerprint}${randomPart}`;
  155.  
  156. // 保存到localStorage
  157. localStorage.setItem(DEVICE_ID_KEY, deviceId);
  158.  
  159. // 同时保存到原来的键名,保持兼容性
  160. localStorage.setItem('deviceId', deviceId);
  161. } else {
  162. // 确保两个键名下的值一致
  163. localStorage.setItem('deviceId', deviceId);
  164. }
  165.  
  166. return deviceId;
  167. }
  168.  
  169. // 修改获取访问令牌的函数
  170. async function getFeishuAccessToken() {
  171. return new Promise((resolve, reject) => {
  172. GM_xmlhttpRequest({
  173. method: 'POST',
  174. url: 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
  175. headers: {
  176. 'Content-Type': 'application/json; charset=utf-8'
  177. },
  178. data: JSON.stringify({
  179. "app_id": FEISHU_CONFIG.APP_ID,
  180. "app_secret": FEISHU_CONFIG.APP_SECRET
  181. }),
  182. onload: function(response) {
  183. try {
  184. const data = JSON.parse(response.responseText);
  185. console.log('访问令牌响应:', data);
  186.  
  187. if (data.code === 0 && data.tenant_access_token) {
  188. FEISHU_CONFIG.TOKEN = data.tenant_access_token;
  189. resolve(data.tenant_access_token);
  190. } else {
  191. console.error('获取访问令牌失败:', data);
  192. reject(new Error(`获取访问令牌失败: ${data.msg || '未知错误'}`));
  193. }
  194. } catch (e) {
  195. console.error('解析响应失败:', e);
  196. reject(e);
  197. }
  198. },
  199. onerror: function(error) {
  200. console.error('请求失败:', error);
  201. reject(error);
  202. }
  203. });
  204. });
  205. }
  206.  
  207. // 修改生成激活码的函数
  208. async function generateActivationCode() {
  209. try {
  210. if (!FEISHU_CONFIG.TOKEN) {
  211. await getFeishuAccessToken();
  212. }
  213.  
  214. // 生成随机部分
  215. const randomPart = Math.random().toString(36).substring(2, 10);
  216. // 时间戳部分
  217. const timestampPart = Date.now().toString(36);
  218. // 校验部分
  219. const checkPart = generateCheckPart(randomPart + timestampPart);
  220. // 组合激活码
  221. const code = `${randomPart}-${timestampPart}-${checkPart}`;
  222.  
  223. // 计算过期时间
  224. const expireTime = new Date();
  225. expireTime.setDate(expireTime.getDate() + VALID_DAYS);
  226.  
  227. // 创建新记录
  228. const response = await fetch(`${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records`, {
  229. method: 'POST',
  230. headers: {
  231. 'Authorization': `Bearer ${FEISHU_CONFIG.TOKEN}`,
  232. 'Content-Type': 'application/json'
  233. },
  234. body: JSON.stringify({
  235. fields: {
  236. '激活码': code,
  237. '状态': '正常',
  238. '过期时间': expireTime.toISOString(),
  239. '创建时间': new Date().toISOString()
  240. }
  241. })
  242. });
  243.  
  244. const data = await response.json();
  245. if (data.code === 0) {
  246. return code;
  247. } else {
  248. console.error('创建激活码失败:', data);
  249. return null;
  250. }
  251. } catch (e) {
  252. console.error('生成激活码出错:', e);
  253. return null;
  254. }
  255. }
  256.  
  257. // 生成校验部分
  258. function generateCheckPart(str) {
  259. let hash = 0;
  260. for (let i = 0; i < str.length; i++) {
  261. const char = str.charCodeAt(i);
  262. hash = ((hash << 5) - hash) + char;
  263. hash = hash & hash; // Convert to 32-bit integer
  264. }
  265. return Math.abs(hash).toString(36).substring(0, 4);
  266. }
  267.  
  268. // 添加获取字段信息的函数
  269. async function getTableFields() {
  270. const token = await getFeishuAccessToken();
  271. return new Promise((resolve, reject) => {
  272. const url = `${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/fields`;
  273.  
  274. GM_xmlhttpRequest({
  275. method: 'GET',
  276. url: url,
  277. headers: {
  278. 'Authorization': `Bearer ${token}`,
  279. 'Content-Type': 'application/json; charset=utf-8'
  280. },
  281. onload: function(response) {
  282. try {
  283. const data = JSON.parse(response.responseText);
  284. console.log('字段信息:', data);
  285. if (data.code === 0) {
  286. resolve(data.data.items);
  287. } else {
  288. reject(new Error(data.msg));
  289. }
  290. } catch (e) {
  291. reject(e);
  292. }
  293. },
  294. onerror: reject
  295. });
  296. });
  297. }
  298.  
  299. // 添加缓存机制
  300. const ACTIVATION_CACHE = {
  301. status: null,
  302. timestamp: 0,
  303. CACHE_TTL: 60000 // 缓存有效期,毫秒
  304. };
  305.  
  306. // 修改 checkActivation 函数
  307. async function checkActivation() {
  308. // 首先检查缓存
  309. const now = Date.now();
  310. if (ACTIVATION_CACHE.status !== null && (now - ACTIVATION_CACHE.timestamp) < ACTIVATION_CACHE.CACHE_TTL) {
  311. return ACTIVATION_CACHE.status;
  312. }
  313.  
  314. const activationStatus = localStorage.getItem(ACTIVATION_KEY);
  315. const deviceId = localStorage.getItem('deviceId');
  316. const activationCode = localStorage.getItem('activation_code');
  317. const recordId = localStorage.getItem('record_id');
  318. const expireTime = localStorage.getItem('expire_time');
  319.  
  320. if (!deviceId || !activationStatus || !activationCode || !recordId || !expireTime) {
  321. ACTIVATION_CACHE.status = false;
  322. ACTIVATION_CACHE.timestamp = now;
  323. return false;
  324. }
  325.  
  326. // 检查本地过期时间
  327. if (new Date() > new Date(expireTime)) {
  328. localStorage.removeItem(ACTIVATION_KEY);
  329. localStorage.removeItem('activation_code');
  330. localStorage.removeItem('record_id');
  331. localStorage.removeItem('expire_time');
  332. showFloatingTip('激活码已过期,请重新激活');
  333. ACTIVATION_CACHE.status = false;
  334. ACTIVATION_CACHE.timestamp = now;
  335. return false;
  336. }
  337.  
  338. // 设置缓存
  339. ACTIVATION_CACHE.status = true;
  340. ACTIVATION_CACHE.timestamp = now;
  341. return true;
  342.  
  343. // 注意:我们将远程检查改为定时任务,而不是在每次右键点击时执行
  344. }
  345.  
  346. // 修改验证激活码的函数
  347. async function verifyActivationCode(deviceId, code) {
  348. try {
  349. const token = await getFeishuAccessToken();
  350. console.log('获取到的访问令牌:', token);
  351.  
  352. return new Promise((resolve, reject) => {
  353. GM_xmlhttpRequest({
  354. method: 'POST',
  355. url: `${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records/search`,
  356. headers: {
  357. 'Authorization': `Bearer ${token}`,
  358. 'Content-Type': 'application/json'
  359. },
  360. data: JSON.stringify({
  361. page_size: 1,
  362. filter: {
  363. conditions: [
  364. {
  365. field_name: "激活码1",
  366. operator: "is",
  367. value: [code]
  368. },
  369. {
  370. field_name: "状态",
  371. operator: "is",
  372. value: ["正常"]
  373. }
  374. ],
  375. conjunction: "and"
  376. }
  377. }),
  378. onload: function(response) {
  379. try {
  380. if (!response.responseText) {
  381. console.error('空响应');
  382. resolve(false);
  383. return;
  384. }
  385.  
  386. let data;
  387. try {
  388. data = JSON.parse(response.responseText);
  389. console.log('验证响应数据:', data);
  390. } catch (e) {
  391. console.error('JSON解析失败:', response.responseText);
  392. resolve(false);
  393. return;
  394. }
  395.  
  396. if (!data || data.code !== 0 || !data.data?.items?.length) {
  397. console.log('激活码验证失败: 未找到匹配记录');
  398. resolve(false);
  399. return;
  400. }
  401.  
  402. const record = data.data.items[0];
  403. const fields = record.fields;
  404. console.log('找到记录:', fields);
  405.  
  406. // 验证激活码状态和过期时间
  407. const now = new Date().getTime();
  408. if (fields.状态 !== '正常' || now > fields.过期时间) {
  409. console.log('激活码状态不正常或已过期');
  410. resolve(false);
  411. return;
  412. }
  413.  
  414. // 验证设备ID - 处理文本格式
  415. if (fields.设备ID) {
  416. const existingDeviceId = Array.isArray(fields.设备ID) ?
  417. fields.设备ID[0]?.text : fields.设备ID;
  418.  
  419. if (existingDeviceId && existingDeviceId !== deviceId) {
  420. console.log('设备ID不匹配:', {existing: existingDeviceId, current: deviceId});
  421. resolve(false);
  422. return;
  423. }
  424. }
  425.  
  426. // 更新记录 - 使用文本格式
  427. const updatedFields = {
  428. ...fields,
  429. 设备ID: deviceId,
  430. 激活时间: new Date().toISOString()
  431. };
  432.  
  433. // 使用 Promise 处理更新记录
  434. updateActivationRecord(record.record_id, updatedFields)
  435. .then(() => {
  436. // 保存到共享存储
  437. localStorage.setItem(SHARED_ACTIVATION_KEY, 'activated');
  438. localStorage.setItem(SHARED_CODE_KEY, code);
  439. localStorage.setItem(SHARED_RECORD_KEY, record.record_id);
  440. localStorage.setItem(SHARED_EXPIRE_KEY, fields.过期时间);
  441.  
  442. // 同时保存到原有键名,保持兼容性
  443. localStorage.setItem(ACTIVATION_KEY, 'activated');
  444. localStorage.setItem('activation_code', code);
  445. localStorage.setItem('record_id', record.record_id);
  446. localStorage.setItem('expire_time', fields.过期时间);
  447.  
  448. showActivationStatus();
  449. console.log('激活成功');
  450. resolve(true);
  451.  
  452. setTimeout(() => {
  453. window.location.reload();
  454. }, 1500);
  455. })
  456. .catch((error) => {
  457. console.error('更新记录失败:', error);
  458. resolve(false);
  459. });
  460.  
  461. } catch (e) {
  462. console.error('处理验证响应失败:', e);
  463. resolve(false);
  464. }
  465. },
  466. onerror: function(error) {
  467. console.error('验证请求失败:', error);
  468. resolve(false);
  469. }
  470. });
  471. });
  472. } catch (e) {
  473. console.error('验证过程出错:', e);
  474. return false;
  475. }
  476. }
  477.  
  478. // 修改更新激活记录的函数
  479. async function updateActivationRecord(recordId, fields) {
  480. try {
  481. const token = await getFeishuAccessToken();
  482.  
  483. // 格式化日期时间
  484. const formatDateTime = (dateStr) => {
  485. if (!dateStr) return null;
  486. const date = new Date(dateStr);
  487. return date.getTime(); // 转换为时间戳
  488. };
  489.  
  490. return new Promise((resolve, reject) => {
  491. GM_xmlhttpRequest({
  492. method: 'PUT',
  493. url: `${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records/${recordId}`,
  494. headers: {
  495. 'Authorization': `Bearer ${token}`,
  496. 'Content-Type': 'application/json'
  497. },
  498. data: JSON.stringify({
  499. fields: {
  500. 设备ID: fields.设备ID,
  501. 激活时间: formatDateTime(fields.激活时间), // 转换为时间戳
  502. 状态: fields.状态,
  503. 过期时间: formatDateTime(fields.过期时间) // 转换为时间戳
  504. }
  505. }),
  506. onload: function(response) {
  507. try {
  508. if (!response.responseText) {
  509. console.error('更新记录时收到空响应');
  510. reject(new Error('更新记录失败: 空响应'));
  511. return;
  512. }
  513.  
  514. const data = JSON.parse(response.responseText);
  515. console.log('更新记录响应:', data);
  516.  
  517. if (data.code === 0) {
  518. resolve(data);
  519. } else {
  520. console.error('更新记录失败:', data);
  521. reject(new Error(`更新记录失败: ${data.msg || '未知错误'}`));
  522. }
  523. } catch (e) {
  524. console.error('处理更新响应失败:', e);
  525. reject(e);
  526. }
  527. },
  528. onerror: function(error) {
  529. console.error('更新请求失败:', error);
  530. reject(new Error('更新记录失败: 网络错误'));
  531. }
  532. });
  533. });
  534. } catch (e) {
  535. console.error('更新记录过程出错:', e);
  536. throw e;
  537. }
  538. }
  539.  
  540. // 修改激活对话框样式
  541. function createActivationDialog() {
  542. const overlay = document.createElement('div');
  543. overlay.className = 'download-confirm-overlay';
  544.  
  545. const dialog = document.createElement('div');
  546. dialog.className = 'download-confirm-dialog';
  547.  
  548. // 使用新的设备ID获取函数
  549. const deviceId = getOrCreateDeviceId();
  550.  
  551. dialog.innerHTML = `
  552. <h3 style="font-size: 17px; margin-bottom: 4px;">软件激活</h3>
  553. <p style="color: #999; font-size: 14px; margin: 0 0 20px;">请输入激活码以继续使用</p>
  554. <div class="input-container" style="margin-bottom: 12px;">
  555. <label style="color: #333; font-size: 14px; display: block; margin-bottom: 8px;">设备ID</label>
  556. <input type="text"
  557. id="deviceId"
  558. value="${deviceId}"
  559. readonly
  560. style="width: 100%;
  561. padding: 12px;
  562. border: 1px solid #e5e5e5;
  563. border-radius: 8px;
  564. font-size: 14px;
  565. background: #f5f5f5;">
  566. <div class="tip" style="font-size: 12px; color: #999; margin-top: 4px;">
  567. 请复制设备ID并联系微信<span class="copyable-text" style="cursor: pointer; color: #007AFF;">(11208596)</span>获取激活码
  568. </div>
  569. </div>
  570. <div class="input-container" style="margin-bottom: 20px;">
  571. <label style="color: #333; font-size: 14px; display: block; margin-bottom: 8px;">激活码</label>
  572. <input type="text"
  573. id="activationCode"
  574. placeholder="请输入激活码"
  575. style="width: 100%;
  576. padding: 12px;
  577. border: 1px solid #e5e5e5;
  578. border-radius: 8px;
  579. font-size: 14px;">
  580. </div>
  581. <div class="buttons" style="display: flex; gap: 12px;">
  582. <button class="cancel-btn"
  583. style="flex: 1;
  584. padding: 12px;
  585. border: none;
  586. border-radius: 8px;
  587. font-size: 14px;
  588. background: #f5f5f5;
  589. color: #333;
  590. cursor: pointer;">
  591. 取消
  592. </button>
  593. <button class="confirm-btn"
  594. style="flex: 1;
  595. padding: 12px;
  596. border: none;
  597. border-radius: 8px;
  598. font-size: 14px;
  599. background: #007AFF;
  600. color: white;
  601. cursor: pointer;">
  602. 激活
  603. </button>
  604. </div>
  605. `;
  606.  
  607. document.body.appendChild(overlay);
  608. document.body.appendChild(dialog);
  609.  
  610. // 添加样式
  611. const style = document.createElement('style');
  612. style.textContent = `
  613. .download-confirm-overlay {
  614. position: fixed;
  615. top: 0;
  616. left: 0;
  617. right: 0;
  618. bottom: 0;
  619. background: rgba(0, 0, 0, 0.4);
  620. backdrop-filter: blur(8px);
  621. z-index: 9999;
  622. }
  623.  
  624. .download-confirm-dialog {
  625. position: fixed;
  626. top: 50%;
  627. left: 50%;
  628. transform: translate(-50%, -50%);
  629. background: white;
  630. padding: 24px;
  631. border-radius: 12px;
  632. width: 90%;
  633. max-width: 360px;
  634. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
  635. z-index: 10000;
  636. }
  637.  
  638. .confirm-btn:hover {
  639. background: #0066DD !important;
  640. }
  641.  
  642. .cancel-btn:hover {
  643. background: #eee !important;
  644. }
  645.  
  646. .confirm-btn:active {
  647. transform: scale(0.98);
  648. }
  649.  
  650. .cancel-btn:active {
  651. transform: scale(0.98);
  652. }
  653. `;
  654. document.head.appendChild(style);
  655.  
  656. const confirmBtn = dialog.querySelector('.confirm-btn');
  657. const cancelBtn = dialog.querySelector('.cancel-btn');
  658. const activationInput = dialog.querySelector('#activationCode');
  659. const deviceIdInput = dialog.querySelector('#deviceId');
  660. const wechatElement = dialog.querySelector('.copyable-text');
  661.  
  662. // 复制设备ID功能
  663. deviceIdInput.addEventListener('click', () => {
  664. deviceIdInput.select();
  665. document.execCommand('copy');
  666. showFloatingTip('设备ID已复制到剪贴板');
  667. });
  668.  
  669. // 添加复制微信号功能
  670. wechatElement.addEventListener('click', () => {
  671. const tempInput = document.createElement('input');
  672. tempInput.value = '11208596';
  673. document.body.appendChild(tempInput);
  674. tempInput.select();
  675. document.execCommand('copy');
  676. document.body.removeChild(tempInput);
  677. showFloatingTip('微信号已复制到剪贴板');
  678. });
  679.  
  680. function closeDialog() {
  681. document.body.removeChild(overlay);
  682. document.body.removeChild(dialog);
  683. }
  684.  
  685. confirmBtn.addEventListener('click', async () => {
  686. const code = activationInput.value.trim();
  687. if (!code) {
  688. showFloatingTip('请输入激活码');
  689. return;
  690. }
  691.  
  692. confirmBtn.disabled = true;
  693. confirmBtn.textContent = '验证中...';
  694. confirmBtn.style.opacity = '0.7';
  695.  
  696. try {
  697. const result = await verifyActivationCode(deviceId, code);
  698. if (result) {
  699. showFloatingTip('激活成功');
  700. setTimeout(() => {
  701. closeDialog();
  702. window.location.reload();
  703. }, 1500);
  704. } else {
  705. showFloatingTip('激活失败,请联系作者11208596');
  706. confirmBtn.disabled = false;
  707. confirmBtn.textContent = '激活';
  708. confirmBtn.style.opacity = '1';
  709. }
  710. } catch (e) {
  711. console.error('验证过程出错:', e);
  712. showFloatingTip('验证出错,请联系作者11208596');
  713. confirmBtn.disabled = false;
  714. confirmBtn.textContent = '激活';
  715. confirmBtn.style.opacity = '1';
  716. }
  717. });
  718.  
  719. cancelBtn.addEventListener('click', closeDialog);
  720.  
  721. // 聚焦到文件名输入框,方便用户直接修改
  722. setTimeout(() => activationInput.focus(), 50);
  723. }
  724.  
  725. // 添加一个带远程验证的检查函数
  726. async function checkActivationWithRemote() {
  727. // 首先检查本地状态
  728. const localActivated = await checkActivation();
  729. if (!localActivated) return false;
  730.  
  731. // 如果本地状态正常,再检查远程状态
  732. try {
  733. const activationCode = localStorage.getItem('activation_code');
  734. const recordId = localStorage.getItem('record_id');
  735. const deviceId = localStorage.getItem('deviceId');
  736.  
  737. if (!activationCode || !recordId || !deviceId) return false;
  738.  
  739. if (!FEISHU_CONFIG.TOKEN) {
  740. await getFeishuAccessToken();
  741. }
  742.  
  743. // 检查飞书表格中的状态
  744. const response = await new Promise((resolve, reject) => {
  745. GM_xmlhttpRequest({
  746. method: 'GET',
  747. url: `${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records/${recordId}`,
  748. headers: {
  749. 'Authorization': `Bearer ${FEISHU_CONFIG.TOKEN}`,
  750. 'Content-Type': 'application/json'
  751. },
  752. onload: resolve,
  753. onerror: reject
  754. });
  755. });
  756.  
  757. const data = JSON.parse(response.responseText);
  758. if (data.code === 0) {
  759. const record = data.data.record;
  760. const now = new Date().getTime();
  761.  
  762. // 检查状态和过期时间
  763. if (record.fields.状态 !== '正常' || now > record.fields.过期时间) {
  764. clearActivationInfo();
  765. showFloatingTip('激活码已过期或失效,请重新激活');
  766. return false;
  767. }
  768.  
  769. // 计算剩余时间
  770. const expireTime = new Date(record.fields.过期时间);
  771. const remainingTime = Math.ceil((expireTime - now) / (1000 * 60 * 60 * 24)); // 剩余天数
  772. localStorage.setItem('remaining_time', remainingTime); // 存储剩余时间
  773.  
  774. // 检查设备ID
  775. const recordDeviceId = Array.isArray(record.fields.设备ID) ?
  776. record.fields.设备ID[0]?.text :
  777. record.fields.设备ID;
  778.  
  779. if (recordDeviceId !== deviceId) {
  780. clearActivationInfo();
  781. showFloatingTip('设备ID不匹配,请重新激活');
  782. return false;
  783. }
  784.  
  785. return true;
  786. }
  787. } catch (e) {
  788. console.error('远程验证失败:', e);
  789. return false;
  790. }
  791.  
  792. return false;
  793. }
  794.  
  795. // 添加一个激活码生成工具函数(仅供开发使用)
  796. function generateActivationCodeForDevice(deviceId) {
  797. return generateActivationCode(deviceId);
  798. }
  799.  
  800. // 显示浮动提示
  801. function showFloatingTip(message) {
  802. const tip = document.createElement('div');
  803. tip.className = 'floating-tip';
  804. tip.innerHTML = `
  805. <div class="icon">i</div>
  806. <span>${message}</span>
  807. `;
  808.  
  809. document.body.appendChild(tip);
  810.  
  811. setTimeout(() => {
  812. tip.classList.add('show');
  813. }, 100);
  814.  
  815. setTimeout(() => {
  816. tip.classList.remove('show');
  817. setTimeout(() => {
  818. document.body.removeChild(tip);
  819. }, 300);
  820. }, 3000);
  821. }
  822.  
  823. // 修改查询激活码状态的函数
  824. window.queryActivationCode = async function() {
  825. const code = document.getElementById('queryCode').value.trim();
  826. const resultDiv = document.getElementById('queryResult');
  827.  
  828. if (!code) {
  829. showFloatingTip('请输入激活码');
  830. return;
  831. }
  832.  
  833. try {
  834. const response = await fetch(`${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records/query`, {
  835. method: 'POST',
  836. headers: {
  837. 'Authorization': `Bearer ${FEISHU_CONFIG.TOKEN}`,
  838. 'Content-Type': 'application/json'
  839. },
  840. body: JSON.stringify({
  841. filter: `CurrentValue.[激活码] = "${code}"`
  842. })
  843. });
  844.  
  845. const data = await response.json();
  846.  
  847. if (!data.data.records || data.data.records.length === 0) {
  848. resultDiv.innerHTML = '<div style="color: #ff3b30;">激活码不存在</div>';
  849. return;
  850. }
  851.  
  852. const record = data.data.records[0].fields;
  853. const expireTime = new Date(record.过期时间);
  854. const now = new Date();
  855. const isExpired = now > expireTime;
  856.  
  857. resultDiv.innerHTML = `
  858. <div style="background: #f5f5f7; padding: 15px; border-radius: 8px;">
  859. <div><strong>设备ID:</strong> ${record.设备ID || '未使用'}</div>
  860. <div><strong>到期时间:</strong> ${expireTime.toLocaleString()}</div>
  861. <div><strong>状态:</strong>
  862. <span style="color: ${record.状态 === '正常' && !isExpired ? '#00c853' : '#ff3b30'}">
  863. ${record.状态 === '正常' ? (isExpired ? '已过期' : '有效') : '已禁用'}
  864. </span>
  865. </div>
  866. ${record.状态 === '正常' && !isExpired ? `
  867. <div style="margin-top: 15px;">
  868. <button onclick="deactivateCode('${code}')"
  869. style="background: #ff3b30; padding: 8px 16px; font-size: 13px;">
  870. 取消此激活码
  871. </button>
  872. </div>
  873. ` : ''}
  874. </div>
  875. `;
  876. } catch (e) {
  877. resultDiv.innerHTML = '<div style="color: #ff3b30;">查询失败,请稍后重试</div>';
  878. console.error('查询失败:', e);
  879. }
  880. };
  881.  
  882. // 修改取消激活码的函数
  883. window.deactivateCode = async function(code) {
  884. if (!confirm('确定要取消此激活码吗?取消后此激活码将无法继续使用。')) {
  885. return;
  886. }
  887.  
  888. try {
  889. // 查询激活码记录
  890. const response = await fetch(`${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records/query`, {
  891. method: 'POST',
  892. headers: {
  893. 'Authorization': `Bearer ${FEISHU_CONFIG.TOKEN}`,
  894. 'Content-Type': 'application/json'
  895. },
  896. body: JSON.stringify({
  897. filter: `CurrentValue.[激活码] = "${code}"`
  898. })
  899. });
  900.  
  901. const data = await response.json();
  902. if (!data.data.records || data.data.records.length === 0) {
  903. showFloatingTip('激活码不存在');
  904. return;
  905. }
  906.  
  907. // 更新状态为已禁用
  908. await updateActivationRecord(data.data.records[0].record_id, {
  909. 状态: '已禁用',
  910. 禁用时间: new Date().toISOString()
  911. });
  912.  
  913. // 如果当前用户正在使用这个激活码,立即取消激活
  914. const currentCode = localStorage.getItem('activation_code');
  915. if (currentCode === code) {
  916. localStorage.removeItem(ACTIVATION_KEY);
  917. localStorage.removeItem('activation_code');
  918. }
  919.  
  920. showFloatingTip('激活码已成功取消');
  921.  
  922. // 更新查询结果显示
  923. const resultDiv = document.getElementById('queryResult');
  924. resultDiv.innerHTML = `
  925. <div style="background: #f5f5f7; padding: 15px; border-radius: 8px;">
  926. <div style="color: #ff3b30;">此激活码已被禁用,无法继续使用</div>
  927. </div>
  928. `;
  929.  
  930. // 强制刷新页面以确保状态更新
  931. setTimeout(() => {
  932. window.location.reload();
  933. }, 1500);
  934. } catch (e) {
  935. console.error('取消激活码失败:', e);
  936. showFloatingTip('取消激活码失败');
  937. }
  938. };
  939.  
  940. // 修改定期检查机制
  941. function startBlacklistCheck() {
  942. setInterval(async () => {
  943. const activationCode = localStorage.getItem('activation_code');
  944. if (!activationCode) return;
  945.  
  946. try {
  947. const token = await getFeishuAccessToken();
  948.  
  949. GM_xmlhttpRequest({
  950. method: 'POST',
  951. url: `${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records/search`,
  952. headers: {
  953. 'Authorization': `Bearer ${token}`,
  954. 'Content-Type': 'application/json'
  955. },
  956. data: JSON.stringify({
  957. page_size: 1,
  958. filter: {
  959. conditions: [
  960. {
  961. field_name: "激活码1", // 修改为检查激活码1字段
  962. operator: "is",
  963. value: [activationCode]
  964. },
  965. {
  966. field_name: "状态",
  967. operator: "is",
  968. value: ["正常"]
  969. }
  970. ],
  971. conjunction: "and"
  972. }
  973. }),
  974. onload: function(response) {
  975. try {
  976. // 添加响应内容类型检查
  977. if (!response.responseText) {
  978. console.error('空响应');
  979. return;
  980. }
  981.  
  982. // 尝试解析响应
  983. let data;
  984. try {
  985. data = JSON.parse(response.responseText);
  986. } catch (e) {
  987. console.error('JSON解析失败:', response.responseText);
  988. return;
  989. }
  990.  
  991. // 检查响应状态
  992. if (response.status !== 200) {
  993. console.error('HTTP状态码错误:', response.status);
  994. return;
  995. }
  996.  
  997. // 验证数据结构
  998. if (!data || typeof data !== 'object') {
  999. console.error('无效的响应数据格式');
  1000. return;
  1001. }
  1002.  
  1003. // 处理响应数据
  1004. if (data.code === 0 && data.data && data.data.items) {
  1005. const record = data.data.items[0];
  1006. if (!record || !record.fields) {
  1007. console.error('记录数据无效');
  1008. return;
  1009. }
  1010.  
  1011. const fields = record.fields;
  1012. if (fields.状态 === '已禁用' || new Date() > new Date(fields.过期时间)) {
  1013. localStorage.removeItem(ACTIVATION_KEY);
  1014. localStorage.removeItem('activation_code');
  1015. showFloatingTip('激活码已失效,请重新激活');
  1016. window.location.reload();
  1017. }
  1018. }
  1019. } catch (e) {
  1020. console.error('处理响应时出错:', e);
  1021. }
  1022. },
  1023. onerror: function(error) {
  1024. console.error('请求失败:', error);
  1025. }
  1026. });
  1027. } catch (e) {
  1028. console.error('检查激活状态失败:', e);
  1029. }
  1030. }, 30000); // 每30秒检查一次
  1031. }
  1032.  
  1033. // 修改下载验证函数
  1034. async function downloadWithActivationCheck(mediaUrl, mediaType, downloadFunction) {
  1035. if (!checkActivation()) {
  1036. createActivationDialog();
  1037. return;
  1038. }
  1039. createConfirmDialog(mediaUrl, mediaType, downloadFunction);
  1040. }
  1041.  
  1042. // 修改确认对话框样式
  1043. const style = document.createElement('style');
  1044. style.textContent = `
  1045. @keyframes dialogShow {
  1046. from {
  1047. opacity: 0;
  1048. transform: translate(-50%, -48%) scale(0.96);
  1049. }
  1050. to {
  1051. opacity: 1;
  1052. transform: translate(-50%, -50%) scale(1);
  1053. }
  1054. }
  1055.  
  1056. @keyframes overlayShow {
  1057. from { opacity: 0; }
  1058. to { opacity: 1; }
  1059. }
  1060.  
  1061. .download-confirm-dialog {
  1062. position: fixed;
  1063. top: 50%;
  1064. left: 50%;
  1065. transform: translate(-50%, -50%);
  1066. background: rgba(255, 255, 255, 0.8);
  1067. backdrop-filter: blur(20px) saturate(180%);
  1068. -webkit-backdrop-filter: blur(20px) saturate(180%);
  1069. padding: 28px 24px;
  1070. border-radius: 14px;
  1071. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
  1072. 0 0 0 1px rgba(0, 0, 0, 0.05);
  1073. z-index: 10000;
  1074. min-width: 320px;
  1075. max-width: 400px;
  1076. text-align: center;
  1077. font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  1078. animation: dialogShow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1079. }
  1080.  
  1081. .download-confirm-dialog h3 {
  1082. margin: 0 0 8px 0;
  1083. color: #1d1d1f;
  1084. font-size: 19px;
  1085. font-weight: 600;
  1086. letter-spacing: -0.022em;
  1087. }
  1088.  
  1089. .download-confirm-dialog p {
  1090. margin: 0 0 20px 0;
  1091. color: #86868b;
  1092. font-size: 14px;
  1093. line-height: 1.4;
  1094. letter-spacing: -0.016em;
  1095. }
  1096.  
  1097. .download-confirm-dialog .input-container {
  1098. margin: 20px 0;
  1099. text-align: left;
  1100. }
  1101.  
  1102. .download-confirm-dialog label {
  1103. display: block;
  1104. margin-bottom: 8px;
  1105. color: #1d1d1f;
  1106. font-size: 13px;
  1107. font-weight: 500;
  1108. letter-spacing: -0.016em;
  1109. }
  1110.  
  1111. .download-confirm-dialog input {
  1112. width: 100%;
  1113. padding: 12px 16px;
  1114. border: 1px solid rgba(0, 0, 0, 0.1);
  1115. border-radius: 10px;
  1116. font-size: 15px;
  1117. color: #1d1d1f;
  1118. background: rgba(255, 255, 255, 0.8);
  1119. transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  1120. box-sizing: border-box;
  1121. }
  1122.  
  1123. .download-confirm-dialog input:focus {
  1124. outline: none;
  1125. border-color: #0071e3;
  1126. box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.15);
  1127. background: #ffffff;
  1128. }
  1129.  
  1130. .download-confirm-dialog .buttons {
  1131. margin-top: 28px;
  1132. display: flex;
  1133. gap: 12px;
  1134. justify-content: center;
  1135. }
  1136.  
  1137. .download-confirm-dialog button {
  1138. min-width: 128px;
  1139. padding: 12px 24px;
  1140. border: none;
  1141. border-radius: 10px;
  1142. cursor: pointer;
  1143. font-size: 14px;
  1144. font-weight: 500;
  1145. letter-spacing: -0.016em;
  1146. transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  1147. }
  1148.  
  1149. .download-confirm-dialog .confirm-btn {
  1150. background: #0071e3;
  1151. color: white;
  1152. transform: scale(1);
  1153. }
  1154.  
  1155. .download-confirm-dialog .confirm-btn:hover {
  1156. background: #0077ED;
  1157. transform: scale(1.02);
  1158. }
  1159.  
  1160. .download-confirm-dialog .confirm-btn:active {
  1161. transform: scale(0.98);
  1162. }
  1163.  
  1164. .download-confirm-dialog .confirm-btn:disabled {
  1165. background: #999999;
  1166. cursor: not-allowed;
  1167. opacity: 0.7;
  1168. transform: scale(1);
  1169. }
  1170.  
  1171. .download-confirm-dialog .cancel-btn {
  1172. background: rgba(0, 0, 0, 0.05);
  1173. color: #1d1d1f;
  1174. backdrop-filter: blur(20px);
  1175. -webkit-backdrop-filter: blur(20px);
  1176. }
  1177.  
  1178. .download-confirm-dialog .cancel-btn:hover {
  1179. background: rgba(0, 0, 0, 0.1);
  1180. }
  1181.  
  1182. .download-confirm-dialog .cancel-btn:active {
  1183. background: rgba(0, 0, 0, 0.15);
  1184. }
  1185.  
  1186. .download-confirm-overlay {
  1187. position: fixed;
  1188. top: 0;
  1189. left: 0;
  1190. right: 0;
  1191. bottom: 0;
  1192. background: rgba(0, 0, 0, 0.4);
  1193. backdrop-filter: blur(5px);
  1194. -webkit-backdrop-filter: blur(5px);
  1195. z-index: 9999;
  1196. animation: overlayShow 0.3s ease-out;
  1197. }
  1198.  
  1199. @media (prefers-color-scheme: dark) {
  1200. .download-confirm-dialog {
  1201. background: rgba(40, 40, 45, 0.8);
  1202. }
  1203.  
  1204. .download-confirm-dialog h3 {
  1205. color: #ffffff;
  1206. }
  1207.  
  1208. .download-confirm-dialog p {
  1209. color: #98989d;
  1210. }
  1211.  
  1212. .download-confirm-dialog label {
  1213. color: #ffffff;
  1214. }
  1215.  
  1216. .download-confirm-dialog input {
  1217. background: rgba(60, 60, 65, 0.8);
  1218. border-color: rgba(255, 255, 255, 0.1);
  1219. color: #ffffff;
  1220. }
  1221.  
  1222. .download-confirm-dialog input:focus {
  1223. background: rgba(70, 70, 75, 0.8);
  1224. }
  1225.  
  1226. .download-confirm-dialog .cancel-btn {
  1227. background: rgba(255, 255, 255, 0.1);
  1228. color: #ffffff;
  1229. }
  1230.  
  1231. .download-confirm-dialog .cancel-btn:hover {
  1232. background: rgba(255, 255, 255, 0.15);
  1233. }
  1234. }
  1235.  
  1236. .download-confirm-dialog .tip {
  1237. font-size: 12px;
  1238. color: #86868b;
  1239. margin-top: 6px;
  1240. text-align: left;
  1241. }
  1242.  
  1243. .download-confirm-dialog .progress-text {
  1244. margin-top: 12px;
  1245. font-size: 13px;
  1246. color: #1d1d1f;
  1247. letter-spacing: -0.016em;
  1248. }
  1249.  
  1250. .download-confirm-dialog .success-icon {
  1251. display: inline-block;
  1252. width: 16px;
  1253. height: 16px;
  1254. border-radius: 50%;
  1255. background: #00c853;
  1256. position: relative;
  1257. margin-right: 6px;
  1258. transform: scale(0);
  1259. transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  1260. }
  1261.  
  1262. .download-confirm-dialog .success-icon:after {
  1263. content: '';
  1264. position: absolute;
  1265. width: 8px;
  1266. height: 4px;
  1267. border: 2px solid white;
  1268. border-top: 0;
  1269. border-right: 0;
  1270. transform: rotate(-45deg);
  1271. top: 4px;
  1272. left: 4px;
  1273. }
  1274.  
  1275. .download-confirm-dialog .success-icon.show {
  1276. transform: scale(1);
  1277. }
  1278.  
  1279. @media (prefers-color-scheme: dark) {
  1280. .download-confirm-dialog .tip {
  1281. color: #98989d;
  1282. }
  1283. .download-confirm-dialog .progress-text {
  1284. color: #ffffff;
  1285. }
  1286. }
  1287.  
  1288. .floating-tip {
  1289. position: fixed;
  1290. bottom: 20px;
  1291. left: 50%;
  1292. transform: translateX(-50%) translateY(100px);
  1293. background: rgba(0, 0, 0, 0.8);
  1294. backdrop-filter: blur(10px);
  1295. -webkit-backdrop-filter: blur(10px);
  1296. padding: 12px 20px;
  1297. border-radius: 10px;
  1298. color: white;
  1299. font-size: 14px;
  1300. z-index: 9999;
  1301. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1302. opacity: 0;
  1303. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1304. font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  1305. display: flex;
  1306. align-items: center;
  1307. gap: 8px;
  1308. pointer-events: none;
  1309. }
  1310.  
  1311. .floating-tip.show {
  1312. transform: translateX(-50%) translateY(0);
  1313. opacity: 1;
  1314. }
  1315.  
  1316. .floating-tip .icon {
  1317. width: 18px;
  1318. height: 18px;
  1319. background: #fff;
  1320. border-radius: 50%;
  1321. display: flex;
  1322. align-items: center;
  1323. justify-content: center;
  1324. font-weight: bold;
  1325. font-size: 12px;
  1326. color: #000;
  1327. }
  1328.  
  1329. @media (prefers-color-scheme: dark) {
  1330. .floating-tip {
  1331. background: rgba(255, 255, 255, 0.9);
  1332. color: #1d1d1f;
  1333. }
  1334. .floating-tip .icon {
  1335. background: #1d1d1f;
  1336. color: #fff;
  1337. }
  1338. }
  1339.  
  1340. .usage-tip {
  1341. position: fixed;
  1342. top: 20px;
  1343. left: 50%;
  1344. transform: translateX(-50%) translateY(-100px);
  1345. background: rgba(0, 0, 0, 0.9);
  1346. backdrop-filter: blur(10px);
  1347. -webkit-backdrop-filter: blur(10px);
  1348. padding: 16px 24px;
  1349. border-radius: 12px;
  1350. color: white;
  1351. font-size: 15px;
  1352. line-height: 1.4;
  1353. z-index: 9999;
  1354. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
  1355. opacity: 0;
  1356. transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
  1357. font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  1358. display: flex;
  1359. align-items: center;
  1360. gap: 12px;
  1361. cursor: pointer;
  1362. max-width: 90%;
  1363. width: auto;
  1364. border: 1px solid rgba(255, 255, 255, 0.1);
  1365. }
  1366.  
  1367. .usage-tip.show {
  1368. transform: translateX(-50%) translateY(0);
  1369. opacity: 1;
  1370. }
  1371.  
  1372. .usage-tip .icon {
  1373. font-size: 24px;
  1374. flex-shrink: 0;
  1375. }
  1376.  
  1377. .usage-tip .content {
  1378. display: flex;
  1379. flex-direction: column;
  1380. gap: 4px;
  1381. }
  1382.  
  1383. .usage-tip .main-text {
  1384. font-weight: 500;
  1385. }
  1386.  
  1387. .usage-tip .contact {
  1388. font-size: 13px;
  1389. color: rgba(255, 255, 255, 0.8);
  1390. }
  1391.  
  1392. @media (prefers-color-scheme: dark) {
  1393. .usage-tip {
  1394. background: rgba(255, 255, 255, 0.95);
  1395. color: #1d1d1f;
  1396. border: 1px solid rgba(0, 0, 0, 0.1);
  1397. }
  1398. .usage-tip .contact {
  1399. color: rgba(0, 0, 0, 0.6);
  1400. }
  1401. }
  1402.  
  1403. .remaining-time {
  1404. background: rgba(0, 0, 0, 0.03);
  1405. padding: 8px 12px;
  1406. border-radius: 8px;
  1407. text-align: center;
  1408. }
  1409.  
  1410. @media (prefers-color-scheme: dark) {
  1411. .remaining-time {
  1412. background: rgba(255, 255, 255, 0.05);
  1413. color: #98989d;
  1414. }
  1415. }
  1416. `;
  1417. document.head.appendChild(style);
  1418.  
  1419. // 获取当前网站域名
  1420. const currentDomain = window.location.hostname;
  1421.  
  1422. // 修改createConfirmDialog函数,补充被省略的代码部分
  1423. function createConfirmDialog(mediaUrl, mediaType, downloadFunction) {
  1424. // 使用预创建的元素或创建新元素
  1425. const overlay = window._preCreatedOverlay || document.createElement('div');
  1426. const dialog = window._preCreatedDialog || document.createElement('div');
  1427.  
  1428. if (!window._preCreatedOverlay) {
  1429. overlay.className = 'download-confirm-overlay';
  1430. dialog.className = 'download-confirm-dialog';
  1431. }
  1432.  
  1433. // 显示元素
  1434. overlay.style.display = 'block';
  1435. dialog.style.display = 'block';
  1436.  
  1437. // 获取当前日期时间作为默认文件名的备选
  1438. const now = new Date();
  1439. const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}`;
  1440.  
  1441. // 简化提示词获取逻辑
  1442. let promptText = '';
  1443. try {
  1444. // 预定义可能的选择器列表
  1445. const selectors = [
  1446. 'span[class*="promptText-"]',
  1447. '.message-text-aF_36u[data-testid="message_text_content"]'
  1448. ];
  1449.  
  1450. for (const selector of selectors) {
  1451. const element = document.querySelector(selector);
  1452. if (element) {
  1453. promptText = element.textContent.trim()
  1454. .replace('帮我生成图片:', '')
  1455. .replace(/\s+/g, ' ')
  1456. .replace(/^[""]-|[""]$/g, '')
  1457. .replace(/[\\/:*?"<>|]/g, '_')
  1458. .substring(0, 100);
  1459. break;
  1460. }
  1461. }
  1462. } catch(e) {
  1463. console.error('获取提示词失败:', e);
  1464. }
  1465.  
  1466. // 默认文件名使用提示词,如果没有提示词则使用日期
  1467. const defaultFileName = promptText || dateStr;
  1468.  
  1469. // 获取用户默认格式设置
  1470. const defaultFormat = localStorage.getItem('default_image_format') || 'png';
  1471.  
  1472. // 设置对话框内容,添加格式选择选项
  1473. dialog.innerHTML = `
  1474. <h3>下载${mediaType === 'video' ? '视频' : '图片'}</h3>
  1475. <p>请确认下载信息</p>
  1476. <div class="input-container">
  1477. <label for="fileName">文件名称</label>
  1478. <input type="text"
  1479. id="fileName"
  1480. value="${defaultFileName}"
  1481. placeholder="请输入文件名称"
  1482. spellcheck="false"
  1483. autocomplete="off">
  1484. ${mediaType === 'image' ? `
  1485. <div class="format-selection" style="margin-top: 12px;">
  1486. <label style="display: block; margin-bottom: 8px;">图片格式</label>
  1487. <div style="display: flex; gap: 10px;">
  1488. <label style="display: flex; align-items: center; cursor: pointer;">
  1489. <input type="radio" name="imageFormat" value="png" ${defaultFormat === 'png' ? 'checked' : ''} style="margin-right: 5px;"> PNG格式
  1490. </label>
  1491. <label style="display: flex; align-items: center; cursor: pointer;">
  1492. <input type="radio" name="imageFormat" value="jpg" ${defaultFormat === 'jpg' ? 'checked' : ''} style="margin-right: 5px;"> JPG格式
  1493. </label>
  1494. </div>
  1495. </div>
  1496. ` : ''}
  1497. <div class="tip">提示:右键点击${mediaType === 'video' ? '视频' : '图片'}即可下载,文件名将自动使用AI提示词</div>
  1498. <div class="tip" style="margin-top: 6px;">有问题联系微信:<span class="copyable-wechat" style="cursor: pointer; color: #007AFF;">11208596</span></div>
  1499. </div>
  1500. <div class="progress-text" style="display: none;">
  1501. <span class="success-icon"></span>
  1502. <span class="status-text"></span>
  1503. </div>
  1504. <div class="buttons">
  1505. <button class="cancel-btn">取消</button>
  1506. <button class="confirm-btn">下载</button>
  1507. </div>
  1508. `;
  1509.  
  1510. // 获取元素引用
  1511. const confirmBtn = dialog.querySelector('.confirm-btn');
  1512. const cancelBtn = dialog.querySelector('.cancel-btn');
  1513. const fileNameInput = dialog.querySelector('#fileName');
  1514. const progressText = dialog.querySelector('.progress-text');
  1515. const statusText = dialog.querySelector('.status-text');
  1516. const successIcon = dialog.querySelector('.success-icon');
  1517. const wechatElement = dialog.querySelector('.copyable-wechat');
  1518.  
  1519. // 添加复制微信号功能
  1520. wechatElement.addEventListener('click', () => {
  1521. const tempInput = document.createElement('input');
  1522. tempInput.value = '11208596';
  1523. document.body.appendChild(tempInput);
  1524. tempInput.select();
  1525. document.execCommand('copy');
  1526. document.body.removeChild(tempInput);
  1527. showFloatingTip('微信号已复制到剪贴板');
  1528. });
  1529.  
  1530. function closeDialog() {
  1531. overlay.style.display = 'none';
  1532. dialog.style.display = 'none';
  1533. }
  1534.  
  1535. function handleDownloadProgress(percent) {
  1536. if (percent) {
  1537. progressText.style.display = 'block';
  1538. statusText.textContent = `正在下载...${percent}%`;
  1539. }
  1540. }
  1541.  
  1542. function handleDownloadSuccess() {
  1543. confirmBtn.style.display = 'none';
  1544. progressText.style.display = 'block';
  1545. successIcon.classList.add('show');
  1546. statusText.textContent = '下载完成';
  1547. setTimeout(() => {
  1548. closeDialog();
  1549. }, 1500);
  1550. }
  1551.  
  1552. function handleDownloadError(error) {
  1553. progressText.style.display = 'block';
  1554. statusText.textContent = `下载失败: ${error}`;
  1555. statusText.style.color = '#ff3b30';
  1556. confirmBtn.disabled = false;
  1557. confirmBtn.textContent = '重试';
  1558. }
  1559.  
  1560. confirmBtn.addEventListener('click', async () => {
  1561. // 点击下载按钮时再次验证激活状态
  1562. const isActivated = await checkActivationWithRemote();
  1563. if (!isActivated) {
  1564. closeDialog();
  1565. createActivationDialog();
  1566. return;
  1567. }
  1568.  
  1569. confirmBtn.disabled = true;
  1570. confirmBtn.textContent = '准备下载...';
  1571. const customFileName = fileNameInput.value.trim();
  1572.  
  1573. // 获取用户选择的图片格式
  1574. let selectedFormat = '';
  1575. if (mediaType === 'image') {
  1576. const formatRadios = dialog.querySelectorAll('input[name="imageFormat"]');
  1577. for (const radio of formatRadios) {
  1578. if (radio.checked) {
  1579. selectedFormat = radio.value;
  1580. // 保存用户的选择到localStorage作为默认设置
  1581. localStorage.setItem('default_image_format', selectedFormat);
  1582. break;
  1583. }
  1584. }
  1585. }
  1586.  
  1587. if (downloadFunction) {
  1588. downloadFunction(
  1589. mediaUrl,
  1590. handleDownloadSuccess,
  1591. customFileName,
  1592. handleDownloadProgress,
  1593. handleDownloadError,
  1594. selectedFormat // 传递用户选择的格式
  1595. );
  1596. } else {
  1597. // 兼容旧的调用方式
  1598. if (mediaType === 'video') {
  1599. const videoUrl = await getRealVideoUrl(mediaUrl);
  1600. await downloadVideo(videoUrl, handleDownloadSuccess, customFileName, handleDownloadProgress, handleDownloadError);
  1601. } else {
  1602. await downloadImage(mediaUrl, handleDownloadSuccess, customFileName, handleDownloadProgress, handleDownloadError, selectedFormat);
  1603. }
  1604. }
  1605. });
  1606.  
  1607. cancelBtn.addEventListener('click', closeDialog);
  1608.  
  1609. // 聚焦到文件名输入框,方便用户直接修改
  1610. setTimeout(() => fileNameInput.focus(), 50);
  1611. }
  1612.  
  1613. // 处理视频URL,移除水印
  1614. function processVideoUrl(url) {
  1615. try {
  1616. if (url.includes('vlabvod.com')) {
  1617. const urlObj = new URL(url);
  1618. const paramsToRemove = [
  1619. 'lr', 'watermark', 'display_watermark_busi_user',
  1620. 'cd', 'cs', 'ds', 'ft', 'btag', 'dy_q', 'feature_id'
  1621. ];
  1622.  
  1623. paramsToRemove.forEach(param => {
  1624. urlObj.searchParams.delete(param);
  1625. });
  1626.  
  1627. if (urlObj.searchParams.has('br')) {
  1628. const br = parseInt(urlObj.searchParams.get('br'));
  1629. urlObj.searchParams.set('br', Math.max(br, 6000).toString());
  1630. urlObj.searchParams.set('bt', Math.max(br, 6000).toString());
  1631. }
  1632.  
  1633. urlObj.searchParams.delete('l');
  1634. return urlObj.toString();
  1635. }
  1636. return url;
  1637. } catch (e) {
  1638. console.error('处理视频URL时出错:', e);
  1639. return url;
  1640. }
  1641. }
  1642.  
  1643. // 获取真实视频URL
  1644. async function getRealVideoUrl(videoElement) {
  1645. let videoUrl = videoElement.src;
  1646. if (!videoUrl) {
  1647. const sourceElement = videoElement.querySelector('source');
  1648. if (sourceElement) {
  1649. videoUrl = sourceElement.src;
  1650. }
  1651. }
  1652. if (!videoUrl) {
  1653. videoUrl = videoElement.getAttribute('data-src');
  1654. }
  1655. return videoUrl;
  1656. }
  1657.  
  1658. // 获取文件扩展名
  1659. function getFileExtension(url) {
  1660. // 针对美间的图片特殊处理
  1661. if (url.includes('maas-cos.kujiale.com') ||
  1662. url.includes('meijian-cos.kujiale.com') ||
  1663. url.includes('kujiale.com')) {
  1664. // 检查美间图片的命名格式,如果有后缀则使用,否则默认为png
  1665. const filenamePart = url.split('/').pop();
  1666. if (filenamePart.includes('.')) {
  1667. const extension = filenamePart.match(/\.(jpg|jpeg|png|gif|webp)($|\?)/i);
  1668. if (extension) return extension[0].replace('?', '');
  1669. }
  1670. return '.png'; // 美间大多数图片是png格式
  1671. }
  1672.  
  1673. // 原来的逻辑
  1674. const extension = url.split('?')[0].match(/\.(jpg|jpeg|png|gif|mp4|webm)$/i);
  1675. return extension ? extension[0] : '.jpg';
  1676. }
  1677.  
  1678. // 下载图片的函数
  1679. function downloadImage(imageUrl, onSuccess, customFileName, onProgress, onError, userFormat) {
  1680. // 下载前再次验证激活状态
  1681. checkActivation().then(isActivated => {
  1682. if (!isActivated) {
  1683. if (onError) onError('激活状态无效,请重新激活');
  1684. setTimeout(() => {
  1685. createActivationDialog();
  1686. }, 1000);
  1687. return;
  1688. }
  1689.  
  1690. // 处理美间网站图片URL
  1691. if (imageUrl.includes('maas-cos.kujiale.com') ||
  1692. imageUrl.includes('meijian-cos.kujiale.com') ||
  1693. imageUrl.includes('kujiale.com')) {
  1694. // 处理美间网站的图片URL,移除水印参数
  1695. imageUrl = imageUrl.split('?')[0]; // 移除查询参数
  1696. }
  1697.  
  1698. // 获取文件扩展名,优先使用用户选择的格式
  1699. let fileExtension = userFormat ? `.${userFormat}` : getFileExtension(imageUrl);
  1700. const fileName = customFileName ? `${customFileName}${fileExtension}` : getFileNameFromUrl(imageUrl);
  1701.  
  1702. // 对于微信头像等特殊图片使用代理
  1703. const finalUrl = imageUrl.includes('wx.qlogo.cn') ?
  1704. `https://images.weserv.nl/?url=${encodeURIComponent(imageUrl)}` :
  1705. imageUrl;
  1706.  
  1707. GM_xmlhttpRequest({
  1708. method: 'GET',
  1709. url: finalUrl,
  1710. responseType: 'blob',
  1711. headers: {
  1712. 'Accept': 'image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
  1713. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  1714. 'Referer': currentDomain.includes('doubao') ? 'https://www.doubao.com/' :
  1715. currentDomain.includes('jimeng') ? 'https://jimeng.jianying.com/' :
  1716. 'https://www.meijian.com/',
  1717. 'Origin': currentDomain.includes('doubao') ? 'https://www.doubao.com' :
  1718. currentDomain.includes('jimeng') ? 'https://jimeng.jianying.com' :
  1719. 'https://www.meijian.com',
  1720. 'User-Agent': navigator.userAgent
  1721. },
  1722. onprogress: function(progress) {
  1723. if (progress.lengthComputable) {
  1724. const percent = Math.round((progress.loaded / progress.total) * 100);
  1725. if (onProgress) onProgress(percent);
  1726. }
  1727. },
  1728. onload: function(response) {
  1729. try {
  1730. if (response.status === 200) {
  1731. const blob = response.response;
  1732.  
  1733. // 如果用户指定了格式且需要转换
  1734. if (userFormat && (blob.type.includes('webp') ||
  1735. (userFormat === 'png' && blob.type.includes('jpeg')) ||
  1736. (userFormat === 'jpg' && blob.type.includes('png')))) {
  1737.  
  1738. // 将图片转换为用户选择的格式
  1739. const img = new Image();
  1740. img.crossOrigin = 'anonymous';
  1741.  
  1742. img.onload = () => {
  1743. const canvas = document.createElement('canvas');
  1744. canvas.width = img.width;
  1745. canvas.height = img.height;
  1746. const ctx = canvas.getContext('2d');
  1747. ctx.drawImage(img, 0, 0);
  1748.  
  1749. // 根据用户选择的格式进行转换
  1750. canvas.toBlob((convertedBlob) => {
  1751. const url = URL.createObjectURL(convertedBlob);
  1752. const link = document.createElement('a');
  1753. link.href = url;
  1754. link.download = fileName;
  1755. document.body.appendChild(link);
  1756. link.click();
  1757. document.body.removeChild(link);
  1758. setTimeout(() => URL.revokeObjectURL(url), 100);
  1759. if (onSuccess) onSuccess();
  1760. }, userFormat === 'png' ? 'image/png' : 'image/jpeg', userFormat === 'jpg' ? 0.92 : 1);
  1761. };
  1762.  
  1763. img.onerror = (err) => {
  1764. console.error('图片格式转换失败:', err);
  1765. if (onError) onError('图片格式转换失败');
  1766. };
  1767.  
  1768. img.src = URL.createObjectURL(blob);
  1769. } else {
  1770. // 直接下载原格式
  1771. const url = URL.createObjectURL(blob);
  1772. const link = document.createElement('a');
  1773. link.href = url;
  1774. link.download = fileName;
  1775. document.body.appendChild(link);
  1776. link.click();
  1777. document.body.removeChild(link);
  1778. setTimeout(() => URL.revokeObjectURL(url), 100);
  1779. if (onSuccess) onSuccess();
  1780. }
  1781. } else {
  1782. if (onError) onError(`HTTP ${response.status}`);
  1783. }
  1784. } catch (e) {
  1785. console.error('下载图片时出错:', e);
  1786. if (onError) onError(e.message || '下载失败');
  1787. }
  1788. },
  1789. onerror: function(error) {
  1790. console.error('请求图片失败:', error);
  1791. if (onError) onError(error.message || '网络错误');
  1792. }
  1793. });
  1794. });
  1795. }
  1796.  
  1797. // 下载视频的函数
  1798. function downloadVideo(videoUrl, onSuccess, customFileName, onProgress, onError) {
  1799. // 下载前再次验证激活状态
  1800. checkActivation().then(isActivated => {
  1801. if (!isActivated) {
  1802. if (onError) onError('激活状态无效,请重新激活');
  1803. setTimeout(() => {
  1804. createActivationDialog();
  1805. }, 1000);
  1806. return;
  1807. }
  1808.  
  1809. const processedUrl = processVideoUrl(videoUrl);
  1810. const fileExtension = '.mp4';
  1811. const fileName = customFileName ? `${customFileName}${fileExtension}` : getFileNameFromUrl(processedUrl);
  1812.  
  1813. GM_xmlhttpRequest({
  1814. method: 'GET',
  1815. url: processedUrl,
  1816. responseType: 'blob',
  1817. headers: {
  1818. 'Accept': 'video/mp4,video/*;q=0.9,*/*;q=0.8',
  1819. 'Accept-Encoding': 'gzip, deflate, br',
  1820. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  1821. 'Range': 'bytes=0-',
  1822. 'Referer': currentDomain.includes('doubao') ?
  1823. 'https://www.doubao.com/' :
  1824. 'https://jimeng.jianying.com/',
  1825. 'Origin': currentDomain.includes('doubao') ?
  1826. 'https://www.doubao.com' :
  1827. 'https://jimeng.jianying.com',
  1828. 'User-Agent': navigator.userAgent
  1829. },
  1830. onprogress: function(progress) {
  1831. if (progress.lengthComputable && onProgress) {
  1832. const percent = Math.round((progress.loaded / progress.total) * 100);
  1833. onProgress(percent);
  1834. }
  1835. },
  1836. onload: function(response) {
  1837. if (response.status === 200 || response.status === 206) {
  1838. const blob = response.response;
  1839. const url = URL.createObjectURL(blob);
  1840. const link = document.createElement('a');
  1841. link.href = url;
  1842. link.download = fileName;
  1843. document.body.appendChild(link);
  1844. link.click();
  1845. document.body.removeChild(link);
  1846. setTimeout(() => URL.revokeObjectURL(url), 100);
  1847. if (onSuccess) onSuccess();
  1848. } else {
  1849. if (onError) onError(`HTTP ${response.status}`);
  1850. }
  1851. },
  1852. onerror: function(error) {
  1853. if (onError) onError(error.message || '网络错误');
  1854. }
  1855. });
  1856. });
  1857. }
  1858.  
  1859. // 从 URL 中提取文件名
  1860. function getFileNameFromUrl(url) {
  1861. url = url.split('?')[0];
  1862. const urlParts = url.split('/');
  1863. let fileName = urlParts[urlParts.length - 1];
  1864.  
  1865. if (fileName.includes('~')) {
  1866. fileName = fileName.split('~')[0];
  1867. }
  1868.  
  1869. if (!fileName.match(/\.(mp4|webm|jpg|jpeg|png)$/i)) {
  1870. fileName += url.includes('video') ? '.mp4' : '.jpeg';
  1871. }
  1872.  
  1873. return fileName;
  1874. }
  1875.  
  1876. // 修改右键菜单事件监听,增加对美间网站的支持
  1877. document.addEventListener('contextmenu', safeEventHandler(function (event) {
  1878. const target = event.target;
  1879.  
  1880. try {
  1881. // 检查是否是美间网站
  1882. const isMeijian = window.location.hostname.includes('meijian.com');
  1883.  
  1884. // 美间网站特殊处理
  1885. if (isMeijian) {
  1886. // 处理美间网站的图片
  1887. let imageUrl = null;
  1888.  
  1889. // 直接的img标签
  1890. if (target.tagName.toLowerCase() === 'img') {
  1891. imageUrl = target.src;
  1892. }
  1893. // 查找最近的img标签
  1894. else {
  1895. const closestImg = target.closest('div')?.querySelector('img');
  1896. if (closestImg) {
  1897. imageUrl = closestImg.src;
  1898. }
  1899.  
  1900. // 如果还没找到,尝试查找父元素中的图片
  1901. if (!imageUrl) {
  1902. const parentWithImg = target.closest('[style*="background-image"]');
  1903. if (parentWithImg) {
  1904. const bgImage = window.getComputedStyle(parentWithImg).backgroundImage;
  1905. if (bgImage && bgImage !== 'none') {
  1906. imageUrl = bgImage.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '');
  1907. }
  1908. }
  1909. }
  1910.  
  1911. // 查找特定的美间图片容器
  1912. if (!imageUrl) {
  1913. const meijianContainer = target.closest('.ai-matting-result') ||
  1914. target.closest('.mj-box-preview-container') ||
  1915. target.closest('.ai-design-preview');
  1916. if (meijianContainer) {
  1917. const imgElement = meijianContainer.querySelector('img');
  1918. if (imgElement) {
  1919. imageUrl = imgElement.src;
  1920. }
  1921. }
  1922. }
  1923. }
  1924.  
  1925. // 处理找到的图片URL
  1926. if (imageUrl) {
  1927. event.preventDefault();
  1928.  
  1929. // 检查URL格式,确保是完整URL
  1930. if (imageUrl.startsWith('/')) {
  1931. imageUrl = window.location.origin + imageUrl;
  1932. }
  1933.  
  1934. // 立即显示确认对话框
  1935. createConfirmDialog(imageUrl, 'image', (url, onSuccess, fileName, onProgress, onError, userFormat) => {
  1936. downloadImage(url, onSuccess, fileName, onProgress, onError, userFormat);
  1937. });
  1938.  
  1939. // 验证激活状态
  1940. setTimeout(() => {
  1941. checkActivation().then(isActivated => {
  1942. if (!isActivated) {
  1943. const existingDialog = document.querySelector('.download-confirm-dialog');
  1944. const existingOverlay = document.querySelector('.download-confirm-overlay');
  1945. if (existingDialog) existingDialog.style.display = 'none';
  1946. if (existingOverlay) existingOverlay.style.display = 'none';
  1947. createActivationDialog();
  1948. }
  1949. });
  1950. }, 100);
  1951.  
  1952. return;
  1953. }
  1954. }
  1955.  
  1956. // 处理普通的img标签(原有逻辑)
  1957. if (target.tagName.toLowerCase() === 'img') {
  1958. event.preventDefault();
  1959. const imageUrl = target.src;
  1960. if (imageUrl) {
  1961. // 立即显示确认对话框,避免任何网络请求导致的延迟
  1962. createConfirmDialog(imageUrl, 'image', (url, onSuccess, fileName, onProgress, onError, userFormat) => {
  1963. downloadImage(url, onSuccess, fileName, onProgress, onError, userFormat);
  1964. });
  1965.  
  1966. // 在对话框显示后,异步验证激活状态
  1967. setTimeout(() => {
  1968. checkActivation().then(isActivated => {
  1969. if (!isActivated) {
  1970. // 如果验证失败,关闭当前对话框并显示激活对话框
  1971. const existingDialog = document.querySelector('.download-confirm-dialog');
  1972. const existingOverlay = document.querySelector('.download-confirm-overlay');
  1973. if (existingDialog) existingDialog.style.display = 'none';
  1974. if (existingOverlay) existingOverlay.style.display = 'none';
  1975.  
  1976. createActivationDialog();
  1977. }
  1978. });
  1979. }, 100);
  1980. }
  1981. }
  1982. else if (target.tagName.toLowerCase() === 'video' || target.closest('video')) {
  1983. event.preventDefault();
  1984. const videoElement = target.tagName.toLowerCase() === 'video' ?
  1985. target : target.closest('video');
  1986.  
  1987. if (videoElement) {
  1988. // 立即显示确认对话框
  1989. const videoUrl = videoElement.src || videoElement.querySelector('source')?.src || videoElement.getAttribute('data-src');
  1990. if (videoUrl) {
  1991. createConfirmDialog(videoUrl, 'video', (url, onSuccess, fileName, onProgress, onError) => {
  1992. downloadVideo(url, onSuccess, fileName, onProgress, onError);
  1993. });
  1994.  
  1995. // 异步验证和获取完整视频URL
  1996. Promise.all([
  1997. checkActivation(),
  1998. getRealVideoUrl(videoElement)
  1999. ]).then(([isActivated, realVideoUrl]) => {
  2000. if (!isActivated) {
  2001. // 如果验证失败,关闭当前对话框并显示激活对话框
  2002. const existingDialog = document.querySelector('.download-confirm-dialog');
  2003. const existingOverlay = document.querySelector('.download-confirm-overlay');
  2004. if (existingDialog) existingDialog.style.display = 'none';
  2005. if (existingOverlay) existingOverlay.style.display = 'none';
  2006.  
  2007. createActivationDialog();
  2008. } else if (realVideoUrl && realVideoUrl !== videoUrl) {
  2009. // 如果找到更好的视频URL,更新下载函数中的URL
  2010. const confirmBtn = document.querySelector('.download-confirm-dialog .confirm-btn');
  2011. if (confirmBtn) {
  2012. confirmBtn.onclick = () => {
  2013. confirmBtn.disabled = true;
  2014. confirmBtn.textContent = '准备下载...';
  2015. const customFileName = document.getElementById('fileName').value.trim();
  2016.  
  2017. downloadVideo(
  2018. realVideoUrl,
  2019. handleDownloadSuccess,
  2020. customFileName,
  2021. handleDownloadProgress,
  2022. handleDownloadError
  2023. );
  2024. };
  2025. }
  2026. }
  2027. });
  2028. }
  2029. }
  2030. }
  2031. } catch (e) {
  2032. console.error('处理右键菜单事件时出错:', e);
  2033. }
  2034. }), true);
  2035.  
  2036. // 修改显示提示的函数
  2037. function showUsageTip() {
  2038. // 先检查激活状态
  2039. const activationStatus = localStorage.getItem(ACTIVATION_KEY);
  2040. if (activationStatus === 'activated') {
  2041. return; // 如果已激活,不显示提示
  2042. }
  2043.  
  2044. // 检查今天是否已经显示过
  2045. const today = new Date().toDateString();
  2046. const lastShownDate = localStorage.getItem('lastTipShownDate');
  2047.  
  2048. if (lastShownDate === today) {
  2049. return; // 今天已经显示过,不再显示
  2050. }
  2051.  
  2052. const tip = document.createElement('div');
  2053. tip.className = 'usage-tip';
  2054. tip.innerHTML = `
  2055. <span class="icon">💡</span>
  2056. <div class="content">
  2057. <span class="main-text">点击图片或视频,单击鼠标右键即可免费下载无水印的图片或视频</span>
  2058. <span class="contact">有问题联系微信:<span class="copyable-wechat" style="cursor: pointer; color: #007AFF;">11208596</span></span>
  2059. </div>
  2060. `;
  2061. document.body.appendChild(tip);
  2062.  
  2063. // 添加复制微信号功能
  2064. const wechatElement = tip.querySelector('.copyable-wechat');
  2065. wechatElement.addEventListener('click', (e) => {
  2066. e.stopPropagation(); // 阻止冒泡,避免触发整个提示的点击事件
  2067. const tempInput = document.createElement('input');
  2068. tempInput.value = '11208596';
  2069. document.body.appendChild(tempInput);
  2070. tempInput.select();
  2071. document.execCommand('copy');
  2072. document.body.removeChild(tempInput);
  2073. showFloatingTip('微信号已复制到剪贴板');
  2074. });
  2075.  
  2076. // 显示提示
  2077. setTimeout(() => {
  2078. tip.classList.add('show');
  2079. // 记录显示日期
  2080. localStorage.setItem('lastTipShownDate', today);
  2081. }, 500);
  2082.  
  2083. // 10秒后自动隐藏提示
  2084. setTimeout(() => {
  2085. tip.classList.remove('show');
  2086. setTimeout(() => {
  2087. document.body.removeChild(tip);
  2088. }, 600);
  2089. }, 10000);
  2090.  
  2091. // 点击可以提前关闭提示
  2092. tip.addEventListener('click', () => {
  2093. tip.classList.remove('show');
  2094. setTimeout(() => {
  2095. document.body.removeChild(tip);
  2096. }, 600);
  2097. });
  2098. }
  2099.  
  2100. // 修改页面加载时的提示逻辑
  2101. function initUsageTip() {
  2102. if (window.location.hostname.includes('doubao.com') ||
  2103. window.location.hostname.includes('jimeng.jianying.com') ||
  2104. window.location.hostname.includes('meijian.com')) {
  2105.  
  2106. // 页面加载完成后显示提示
  2107. if (document.readyState === 'complete') {
  2108. showUsageTip();
  2109. } else {
  2110. window.addEventListener('load', showUsageTip);
  2111. }
  2112.  
  2113. // 监听页面可见性变化,但仍然遵循每天显示一次的规则
  2114. document.addEventListener('visibilitychange', () => {
  2115. if (document.visibilityState === 'visible') {
  2116. showUsageTip();
  2117. }
  2118. });
  2119.  
  2120. // 监听页面焦点变化,但仍然遵循每天显示一次的规则
  2121. window.addEventListener('focus', showUsageTip);
  2122. }
  2123. }
  2124.  
  2125. // 修改测试函数
  2126. async function testFeishuAPI() {
  2127. try {
  2128. console.log('开始测试飞书API...');
  2129.  
  2130. // 1. 测试获取访问令牌
  2131. console.log('1. 测试获取访问令牌');
  2132. const tokenResponse = await fetch(`${FEISHU_CONFIG.API_URL}/auth/v3/tenant_access_token/internal`, {
  2133. method: 'POST',
  2134. headers: {
  2135. 'Content-Type': 'application/json'
  2136. },
  2137. body: JSON.stringify({
  2138. "app_id": FEISHU_CONFIG.APP_ID,
  2139. "app_secret": FEISHU_CONFIG.APP_SECRET
  2140. })
  2141. });
  2142.  
  2143. const tokenData = await tokenResponse.json();
  2144. console.log('访问令牌响应:', tokenData);
  2145.  
  2146. if (tokenData.code === 0) {
  2147. FEISHU_CONFIG.TOKEN = tokenData.tenant_access_token;
  2148.  
  2149. // 2. 测试查询表格
  2150. console.log('2. 测试查询表格');
  2151. const tableResponse = await fetch(`${FEISHU_CONFIG.API_URL}/bitable/v1/apps/${FEISHU_CONFIG.BASE_ID}/tables/${FEISHU_CONFIG.TABLE_ID}/records?page_size=1`, {
  2152. method: 'GET',
  2153. headers: {
  2154. 'Authorization': `Bearer ${FEISHU_CONFIG.TOKEN}`,
  2155. 'Content-Type': 'application/json'
  2156. }
  2157. });
  2158.  
  2159. const tableData = await tableResponse.json();
  2160. console.log('表格数据响应:', tableData);
  2161.  
  2162. if (tableData.code === 0) {
  2163. return '测试成功,API正常工作';
  2164. } else {
  2165. return `表格查询失败: ${tableData.msg}`;
  2166. }
  2167. } else {
  2168. return `获取访问令牌失败: ${tokenData.msg}`;
  2169. }
  2170. } catch (e) {
  2171. console.error('测试出错:', e);
  2172. return `测试失败: ${e.message}`;
  2173. }
  2174. }
  2175.  
  2176. // 修改测试面板的创建方式
  2177. function addTestPanel() {
  2178. // 创建测试面板
  2179. const testPanel = document.createElement('div');
  2180. testPanel.innerHTML = `
  2181. <div style="position: fixed; top: 10px; right: 10px; background: white; padding: 15px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 10000;">
  2182. <h3 style="margin: 0 0 10px 0;">激活码管理测试面板</h3>
  2183. <div style="margin-bottom: 10px;">
  2184. <button id="testAPIBtn" style="padding: 5px 10px;">测试飞书API</button>
  2185. <button id="generateCodeBtn" style="padding: 5px 10px;">生成新激活码</button>
  2186. </div>
  2187. <div style="margin-bottom: 10px;">
  2188. <input type="text" id="testCode" placeholder="输入激活码" style="padding: 5px; margin-right: 5px;">
  2189. <button id="verifyCodeBtn" style="padding: 5px 10px;">验证</button>
  2190. <button id="queryCodeBtn" style="padding: 5px 10px;">查询</button>
  2191. </div>
  2192. <div id="queryResult" style="margin-top: 10px;"></div>
  2193. <div style="margin-top: 10px; font-size: 12px; color: #666;">
  2194. 设备ID: ${localStorage.getItem('deviceId') || '未生成'}
  2195. </div>
  2196. <div style="margin-top: 10px;">
  2197. <label style="display: block; margin-bottom: 5px;">默认图片格式</label>
  2198. <select id="defaultImageFormat" style="padding: 5px; width: 100%;">
  2199. <option value="png" ${localStorage.getItem('default_image_format') === 'png' ? 'selected' : ''}>PNG格式</option>
  2200. <option value="jpg" ${localStorage.getItem('default_image_format') === 'jpg' ? 'selected' : ''}>JPG格式</option>
  2201. </select>
  2202. </div>
  2203. </div>
  2204. `;
  2205. document.body.appendChild(testPanel);
  2206.  
  2207. // 添加事件监听器
  2208. document.getElementById('testAPIBtn').addEventListener('click', async function() {
  2209. const button = this;
  2210. const originalText = button.textContent;
  2211. button.disabled = true;
  2212. button.textContent = '测试中...';
  2213.  
  2214. try {
  2215. const result = await testFeishuAPI();
  2216. showFloatingTip(result);
  2217. } catch (e) {
  2218. showFloatingTip('测试失败,请查看控制台');
  2219. } finally {
  2220. button.disabled = false;
  2221. button.textContent = originalText;
  2222. }
  2223. });
  2224.  
  2225. document.getElementById('generateCodeBtn').addEventListener('click', async function() {
  2226. try {
  2227. const code = await generateActivationCode();
  2228. if (code) {
  2229. document.getElementById('testCode').value = code;
  2230. showFloatingTip('激活码生成成功');
  2231. } else {
  2232. showFloatingTip('生成失败,请查看控制台');
  2233. }
  2234. } catch (e) {
  2235. console.error('生成测试激活码失败:', e);
  2236. showFloatingTip('生成失败');
  2237. }
  2238. });
  2239.  
  2240. document.getElementById('verifyCodeBtn').addEventListener('click', async function() {
  2241. const code = document.getElementById('testCode').value.trim();
  2242. if (!code) {
  2243. showFloatingTip('请输入激活码');
  2244. return;
  2245. }
  2246.  
  2247. const deviceId = localStorage.getItem('deviceId');
  2248. if (!deviceId) {
  2249. showFloatingTip('设备ID未生成');
  2250. return;
  2251. }
  2252.  
  2253. try {
  2254. const result = await verifyActivationCode(deviceId, code);
  2255. showFloatingTip(result ? '验证成功' : '验证失败');
  2256. if (result) {
  2257. setTimeout(() => window.location.reload(), 1500);
  2258. }
  2259. } catch (e) {
  2260. console.error('验证测试激活码失败:', e);
  2261. showFloatingTip('验证出错');
  2262. }
  2263. });
  2264.  
  2265. document.getElementById('queryCodeBtn').addEventListener('click', function() {
  2266. window.queryActivationCode();
  2267. });
  2268.  
  2269. // 增加默认图片格式保存功能
  2270. document.getElementById('defaultImageFormat').addEventListener('change', function() {
  2271. localStorage.setItem('default_image_format', this.value);
  2272. showFloatingTip(`默认图片格式已设置为 ${this.value.toUpperCase()}`);
  2273. });
  2274. }
  2275.  
  2276. // 修改测试面板显示条件
  2277. function shouldShowTestPanel() {
  2278. return false;
  2279. }
  2280.  
  2281. // 修改激活状态显示函数
  2282. function showActivationStatus() {
  2283. const activationStatus = localStorage.getItem(ACTIVATION_KEY);
  2284. const expireTime = localStorage.getItem('expire_time');
  2285. const deviceId = localStorage.getItem('deviceId');
  2286. const remainingTime = localStorage.getItem('remaining_time') || '未知'; // 获取剩余时间
  2287.  
  2288. // 创建或获取状态显示面板
  2289. let statusPanel = document.getElementById('activation-status-panel');
  2290. if (!statusPanel) {
  2291. statusPanel = document.createElement('div');
  2292. statusPanel.id = 'activation-status-panel';
  2293. statusPanel.style.cssText = `
  2294. position: fixed;
  2295. bottom: 20px;
  2296. right: 20px;
  2297. background: rgba(255, 255, 255, 0.9);
  2298. padding: 12px 16px;
  2299. border-radius: 10px;
  2300. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  2301. font-size: 13px;
  2302. z-index: 9999;
  2303. backdrop-filter: blur(10px);
  2304. border: 1px solid rgba(0,0,0,0.1);
  2305. `;
  2306. document.body.appendChild(statusPanel);
  2307. }
  2308.  
  2309. // 更新状态显示
  2310. if (activationStatus === 'activated' && expireTime) {
  2311. const now = new Date();
  2312. const expire = new Date(expireTime);
  2313.  
  2314. if (now > expire) {
  2315. clearActivationInfo();
  2316. statusPanel.innerHTML = `
  2317. <div style="color: #ff3b30;">激活已过期,请重新激活</div>
  2318. <div style="color: #666; margin-top: 4px; font-size: 12px;">
  2319. 设备ID: <span class="copyable-device-id">${deviceId || '未知'}</span>
  2320. </div>
  2321. <div style="color: #666; margin-top: 4px; font-size: 12px;">
  2322. 联系微信: <span class="copyable-wechat">11208596</span>
  2323. </div>
  2324. `;
  2325. return;
  2326. }
  2327.  
  2328. // 计算剩余时间
  2329. const diffTime = expire - now;
  2330. const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  2331.  
  2332. statusPanel.innerHTML = `
  2333. <div style="color: #00c853;">✓ 已激活</div>
  2334. <div style="color: #666; margin-top: 4px;">
  2335. 剩余 ${remainingTime}
  2336. </div>
  2337. <div style="color: #666; margin-top: 4px; font-size: 12px;">
  2338. 设备ID: <span class="copyable-device-id">${deviceId || '未知'}</span>
  2339. </div>
  2340. <div style="color: #666; margin-top: 4px; font-size: 12px;">
  2341. 联系微信: <span class="copyable-wechat">11208596</span>
  2342. </div>
  2343. `;
  2344. } else {
  2345. statusPanel.innerHTML = `
  2346. <div style="color: #ff3b30;">未激活</div>
  2347. <div style="color: #666; margin-top: 4px; font-size: 12px;">
  2348. 设备ID: <span class="copyable-device-id">${deviceId || '未知'}</span>
  2349. </div>
  2350. <div style="color: #666; margin-top: 4px; font-size: 12px;">
  2351. 联系微信: <span class="copyable-wechat">11208596</span>
  2352. </div>
  2353. `;
  2354. }
  2355.  
  2356. // 添加点击复制设备ID的功能
  2357. const deviceIdElement = statusPanel.querySelector('.copyable-device-id');
  2358. if (deviceIdElement) {
  2359. deviceIdElement.style.cursor = 'pointer';
  2360. deviceIdElement.style.color = '#007AFF';
  2361. deviceIdElement.addEventListener('click', function() {
  2362. const tempInput = document.createElement('input');
  2363. tempInput.value = deviceId;
  2364. document.body.appendChild(tempInput);
  2365. tempInput.select();
  2366. document.execCommand('copy');
  2367. document.body.removeChild(tempInput);
  2368. showFloatingTip('设备ID已复制到剪贴板');
  2369. });
  2370. }
  2371.  
  2372. // 添加点击复制微信号的功能
  2373. const wechatElement = statusPanel.querySelector('.copyable-wechat');
  2374. if (wechatElement) {
  2375. wechatElement.style.cursor = 'pointer';
  2376. wechatElement.style.color = '#007AFF';
  2377. wechatElement.addEventListener('click', function() {
  2378. const tempInput = document.createElement('input');
  2379. tempInput.value = '11208596';
  2380. document.body.appendChild(tempInput);
  2381. tempInput.select();
  2382. document.execCommand('copy');
  2383. document.body.removeChild(tempInput);
  2384. showFloatingTip('微信号已复制到剪贴板');
  2385. });
  2386. }
  2387. }
  2388.  
  2389. // 添加全局错误处理和补丁
  2390. function applyPatches() {
  2391. // 处理 markWeb undefined 错误
  2392. if (typeof window.markWeb === 'undefined') {
  2393. window.markWeb = {
  2394. markWeb: function() { return null; },
  2395. init: function() { return null; },
  2396. destroy: function() { return null; }
  2397. };
  2398. }
  2399.  
  2400. // 处理可能缺失的其他依赖
  2401. window.Slardar = window.Slardar || {
  2402. init: function() { return null; },
  2403. config: function() { return null; }
  2404. };
  2405. }
  2406.  
  2407. // 修改预加载资源的函数
  2408. function preloadResources() {
  2409. // 在页面空闲时请求飞书API令牌
  2410. if ('requestIdleCallback' in window) {
  2411. requestIdleCallback(() => {
  2412. getFeishuAccessToken().catch(err => console.log('预加载令牌失败:', err));
  2413. });
  2414. } else {
  2415. setTimeout(() => {
  2416. getFeishuAccessToken().catch(err => console.log('预加载令牌失败:', err));
  2417. }, 3000);
  2418. }
  2419.  
  2420. // 预创建DOM结构
  2421. preCreateDialogElements();
  2422.  
  2423. // 其它预加载...
  2424. }
  2425.  
  2426. // 在脚本初始化部分添加以下代码
  2427. function preCreateDialogElements() {
  2428. // 预先创建弹窗结构
  2429. window._preCreatedDialog = document.createElement('div');
  2430. window._preCreatedDialog.className = 'download-confirm-dialog';
  2431. window._preCreatedDialog.style.display = 'none';
  2432.  
  2433. window._preCreatedOverlay = document.createElement('div');
  2434. window._preCreatedOverlay.className = 'download-confirm-overlay';
  2435. window._preCreatedOverlay.style.display = 'none';
  2436.  
  2437. document.body.appendChild(window._preCreatedOverlay);
  2438. document.body.appendChild(window._preCreatedDialog);
  2439. }
  2440.  
  2441. // 添加直接下载功能按钮
  2442. function addRightClickButton() {
  2443. // 创建一个浮动按钮
  2444. const button = document.createElement('button');
  2445. button.innerHTML = '点击下载';
  2446. button.className = 'download-direct-button';
  2447. button.style.cssText = `
  2448. position: fixed;
  2449. top: 20px;
  2450. left: 50%;
  2451. transform: translateX(-50%);
  2452. z-index: 10000;
  2453. background-color: #3498db;
  2454. color: white;
  2455. border: none;
  2456. border-radius: 50px;
  2457. padding: 10px 15px;
  2458. font-size: 14px;
  2459. cursor: pointer;
  2460. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  2461. transition: all 0.3s ease;
  2462. `;
  2463.  
  2464. // 鼠标悬停效果
  2465. button.addEventListener('mouseover', () => {
  2466. button.style.backgroundColor = '#2980b9';
  2467. });
  2468.  
  2469. button.addEventListener('mouseout', () => {
  2470. button.style.backgroundColor = '#3498db';
  2471. });
  2472.  
  2473. // 查找并下载当前页面上最主要的图片或视频
  2474. function findAndDownloadMainMedia() {
  2475. console.log('尝试查找主要媒体元素');
  2476.  
  2477. // 1. 首先尝试下载鼠标悬停的媒体
  2478. const hoverImg = document.querySelector('img:hover');
  2479. const hoverVideo = document.querySelector('video:hover');
  2480.  
  2481. if (hoverImg && hoverImg.src) {
  2482. console.log('找到鼠标悬停的图片');
  2483. createConfirmDialog(hoverImg.src, 'image', (url, onSuccess, fileName, onProgress, onError, userFormat) => {
  2484. downloadImage(url, onSuccess, fileName, onProgress, onError, userFormat);
  2485. });
  2486. return true;
  2487. } else if (hoverVideo) {
  2488. console.log('找到鼠标悬停的视频');
  2489. getRealVideoUrl(hoverVideo).then(videoUrl => {
  2490. if (videoUrl) {
  2491. createConfirmDialog(videoUrl, 'video', (url, onSuccess, fileName, onProgress, onError) => {
  2492. downloadVideo(url, onSuccess, fileName, onProgress, onError);
  2493. });
  2494. }
  2495. });
  2496. return true;
  2497. }
  2498.  
  2499. // 2. 查找视口中的主要媒体元素
  2500. const visibleMedia = findVisibleMediaInViewport();
  2501. if (visibleMedia) {
  2502. if (visibleMedia.type === 'image') {
  2503. console.log('找到视口中的主要图片');
  2504. createConfirmDialog(visibleMedia.element.src, 'image', (url, onSuccess, fileName, onProgress, onError, userFormat) => {
  2505. downloadImage(url, onSuccess, fileName, onProgress, onError, userFormat);
  2506. });
  2507. return true;
  2508. } else if (visibleMedia.type === 'video') {
  2509. console.log('找到视口中的主要视频');
  2510. getRealVideoUrl(visibleMedia.element).then(videoUrl => {
  2511. if (videoUrl) {
  2512. createConfirmDialog(videoUrl, 'video', (url, onSuccess, fileName, onProgress, onError) => {
  2513. downloadVideo(url, onSuccess, fileName, onProgress, onError);
  2514. });
  2515. }
  2516. });
  2517. return true;
  2518. }
  2519. }
  2520.  
  2521. // 3. 如果没有找到主要媒体,尝试查找页面上任何可见的媒体
  2522. const allImages = Array.from(document.querySelectorAll('img'))
  2523. .filter(img => img.src && isElementVisible(img) && img.width > 100 && img.height > 100)
  2524. .sort((a, b) => (b.width * b.height) - (a.width * a.height));
  2525.  
  2526. const allVideos = Array.from(document.querySelectorAll('video'))
  2527. .filter(video => isElementVisible(video));
  2528.  
  2529. if (allImages.length > 0) {
  2530. console.log('找到页面上最大的图片');
  2531. createConfirmDialog(allImages[0].src, 'image', (url, onSuccess, fileName, onProgress, onError, userFormat) => {
  2532. downloadImage(url, onSuccess, fileName, onProgress, onError, userFormat);
  2533. });
  2534. return true;
  2535. } else if (allVideos.length > 0) {
  2536. console.log('找到页面上的视频');
  2537. getRealVideoUrl(allVideos[0]).then(videoUrl => {
  2538. if (videoUrl) {
  2539. createConfirmDialog(videoUrl, 'video', (url, onSuccess, fileName, onProgress, onError) => {
  2540. downloadVideo(url, onSuccess, fileName, onProgress, onError);
  2541. });
  2542. }
  2543. });
  2544. return true;
  2545. }
  2546.  
  2547. // 如果什么都没找到
  2548. alert('未找到可下载的图片或视频,请确保页面上有媒体内容');
  2549. return false;
  2550. }
  2551.  
  2552. // 查找视口中最主要的媒体元素
  2553. function findVisibleMediaInViewport() {
  2554. // 获取视口尺寸
  2555. const viewportWidth = window.innerWidth;
  2556. const viewportHeight = window.innerHeight;
  2557. const viewportCenterX = viewportWidth / 2;
  2558. const viewportCenterY = viewportHeight / 2;
  2559.  
  2560. // 查找所有可见的图片和视频
  2561. const visibleImages = Array.from(document.querySelectorAll('img'))
  2562. .filter(img => {
  2563. if (!img.src || !isElementVisible(img) || img.width < 100 || img.height < 100) {
  2564. return false;
  2565. }
  2566.  
  2567. const rect = img.getBoundingClientRect();
  2568. return (
  2569. rect.left < viewportWidth &&
  2570. rect.right > 0 &&
  2571. rect.top < viewportHeight &&
  2572. rect.bottom > 0
  2573. );
  2574. });
  2575.  
  2576. const visibleVideos = Array.from(document.querySelectorAll('video'))
  2577. .filter(video => {
  2578. if (!isElementVisible(video)) return false;
  2579.  
  2580. const rect = video.getBoundingClientRect();
  2581. return (
  2582. rect.left < viewportWidth &&
  2583. rect.right > 0 &&
  2584. rect.top < viewportHeight &&
  2585. rect.bottom > 0
  2586. );
  2587. });
  2588.  
  2589. // 如果没有可见媒体,返回null
  2590. if (visibleImages.length === 0 && visibleVideos.length === 0) {
  2591. return null;
  2592. }
  2593.  
  2594. // 计算每个元素的分数(基于大小和与视口中心的距离)
  2595. function calculateScore(element) {
  2596. const rect = element.getBoundingClientRect();
  2597. const area = rect.width * rect.height;
  2598.  
  2599. // 计算元素中心点
  2600. const centerX = rect.left + rect.width / 2;
  2601. const centerY = rect.top + rect.height / 2;
  2602.  
  2603. // 计算与视口中心的距离
  2604. const distanceToCenter = Math.sqrt(
  2605. Math.pow(centerX - viewportCenterX, 2) +
  2606. Math.pow(centerY - viewportCenterY, 2)
  2607. );
  2608.  
  2609. // 分数 = 面积 / (距离+1),加1避免除以0
  2610. return area / (distanceToCenter + 1);
  2611. }
  2612.  
  2613. // 为所有媒体元素计算分数
  2614. const scoredMedia = [];
  2615.  
  2616. visibleImages.forEach(img => {
  2617. scoredMedia.push({
  2618. element: img,
  2619. type: 'image',
  2620. score: calculateScore(img)
  2621. });
  2622. });
  2623.  
  2624. visibleVideos.forEach(video => {
  2625. scoredMedia.push({
  2626. element: video,
  2627. type: 'video',
  2628. score: calculateScore(video) * 1.5 // 视频权重略高
  2629. });
  2630. });
  2631.  
  2632. // 按分数排序并返回最高分的媒体
  2633. if (scoredMedia.length > 0) {
  2634. scoredMedia.sort((a, b) => b.score - a.score);
  2635. return scoredMedia[0];
  2636. }
  2637.  
  2638. return null;
  2639. }
  2640.  
  2641. // 检查元素是否可见
  2642. function isElementVisible(el) {
  2643. if (!el) return false;
  2644.  
  2645. const style = window.getComputedStyle(el);
  2646. if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
  2647. return false;
  2648. }
  2649.  
  2650. const rect = el.getBoundingClientRect();
  2651. return rect.width > 0 && rect.height > 0;
  2652. }
  2653.  
  2654. // 添加点击事件处理程序 - 直接下载当前主要媒体
  2655. button.addEventListener('click', async (event) => {
  2656. event.preventDefault();
  2657.  
  2658. try {
  2659. console.log('下载按钮被点击');
  2660.  
  2661. // 查找并下载主要媒体
  2662. findAndDownloadMainMedia();
  2663.  
  2664. } catch (err) {
  2665. console.error('下载功能触发失败:', err);
  2666. alert('下载功能初始化失败,请重试');
  2667. }
  2668. });
  2669.  
  2670. // 将按钮添加到页面
  2671. document.body.appendChild(button);
  2672. console.log('下载按钮已添加到页面');
  2673. }
  2674.  
  2675. // 在initScript函数中调用
  2676. function initScript() {
  2677. try {
  2678. // 应用补丁
  2679. applyPatches();
  2680.  
  2681. // 确保设备ID一致性
  2682. getOrCreateDeviceId();
  2683.  
  2684. // 预加载资源
  2685. preloadResources();
  2686.  
  2687. // 初始化提示
  2688. initUsageTip();
  2689.  
  2690. // 显示激活状态
  2691. showActivationStatus();
  2692.  
  2693. // 每分钟更新一次状态显示
  2694. setInterval(showActivationStatus, 60000);
  2695.  
  2696. // 如果启用测试面板
  2697. if (shouldShowTestPanel()) {
  2698. if (document.readyState === 'loading') {
  2699. document.addEventListener('DOMContentLoaded', addTestPanel);
  2700. } else {
  2701. addTestPanel();
  2702. }
  2703. }
  2704.  
  2705. // 启动黑名单检查
  2706. startBlacklistCheck();
  2707.  
  2708. // 添加全局错误处理
  2709. window.addEventListener('error', handleError, true);
  2710. window.addEventListener('unhandledrejection', handleError, true);
  2711.  
  2712. // 添加预创建弹窗
  2713. preCreateDialogElements();
  2714.  
  2715. // 添加右键模拟按钮
  2716. if (document.readyState === 'loading') {
  2717. document.addEventListener('DOMContentLoaded', addRightClickButton);
  2718. } else {
  2719. addRightClickButton();
  2720. }
  2721.  
  2722. } catch (e) {
  2723. console.error('初始化脚本时出错:', e);
  2724. }
  2725. }
  2726.  
  2727. // 修改事件监听器添加方式
  2728. if (document.readyState === 'loading') {
  2729. document.addEventListener('DOMContentLoaded', () => {
  2730. try {
  2731. initScript();
  2732. } catch (e) {
  2733. console.error('DOMContentLoaded 初始化失败:', e);
  2734. }
  2735. });
  2736. } else {
  2737. try {
  2738. initScript();
  2739. } catch (e) {
  2740. console.error('直接初始化失败:', e);
  2741. }
  2742. }
  2743.  
  2744. // 添加安全的事件处理包装器
  2745. function safeEventHandler(handler) {
  2746. return function(event) {
  2747. try {
  2748. handler.call(this, event);
  2749. } catch (e) {
  2750. console.error('事件处理出错:', e);
  2751. event.preventDefault();
  2752. return false;
  2753. }
  2754. };
  2755. }
  2756.  
  2757. // 修改错误处理函数
  2758. function handleError(event) {
  2759. // 忽略特定的错误
  2760. const errorMessage = event.error?.message || event.reason?.message || event.message || '';
  2761. const ignoredErrors = [
  2762. 'markWeb',
  2763. 'NotSameOriginAfterDefaultedToSameOriginByCoep',
  2764. 'The resource',
  2765. 'preloaded using link preload',
  2766. 'screenshot.min.js',
  2767. 'content.js',
  2768. 'async/$.3f091a3f.js',
  2769. 'async/$.3d5ca379.css'
  2770. ];
  2771.  
  2772. if (ignoredErrors.some(err => errorMessage.includes(err))) {
  2773. event.preventDefault();
  2774. event.stopPropagation();
  2775. return true;
  2776. }
  2777.  
  2778. console.error('脚本执行错误:', event.error || event.reason || event);
  2779. return false;
  2780. }
  2781.  
  2782. // 修改清除激活信息的逻辑
  2783. function clearActivationInfo() {
  2784. // 清除共享存储
  2785. localStorage.removeItem(SHARED_ACTIVATION_KEY);
  2786. localStorage.removeItem(SHARED_CODE_KEY);
  2787. localStorage.removeItem(SHARED_RECORD_KEY);
  2788. localStorage.removeItem(SHARED_EXPIRE_KEY);
  2789.  
  2790. // 清除原有存储
  2791. localStorage.removeItem(ACTIVATION_KEY);
  2792. localStorage.removeItem('activation_code');
  2793. localStorage.removeItem('record_id');
  2794. localStorage.removeItem('expire_time');
  2795. }
  2796. })();

QingJ © 2025

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