进度时间线显示评论

在班固米显示动画进度时间线的对应评论

  1. // ==UserScript==
  2. // @name 进度时间线显示评论
  3. // @namespace https://bgm.tv/group/topic/
  4. // @version 0.1.0
  5. // @description 在班固米显示动画进度时间线的对应评论
  6. // @author oov
  7. // @match https://bangumi.tv/
  8. // @match https://bgm.tv/
  9. // @match https://chii.in/
  10. // @match https://bangumi.tv/user/*/timeline*
  11. // @match https://bgm.tv/user/*/timeline*
  12. // @match https://chii.in/user/*/timeline*
  13. // @icon https://www.google.com/s2/favicons?sz=64&domain=bgm.tv
  14. // @grant unsafeWindow
  15. // @grant GM_xmlhttpRequest
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. /*
  20. * 兼容性:
  21. * - [加载更多](https://bgm.tv/dev/app/432)
  22. */
  23.  
  24. (async function() {
  25. 'use strict';
  26.  
  27. const FACE_KEY_GIF_MAPPING = {
  28. "0": "44",
  29. "140": "101",
  30. "80": "41",
  31. "54": "15",
  32. "85": "46",
  33. "104": "65",
  34. "88": "49",
  35. "62": "23",
  36. "79": "40",
  37. "53": "14",
  38. "122": "83",
  39. "92": "53",
  40. "118": "79",
  41. "141": "102",
  42. "90": "51",
  43. "76": "37",
  44. "60": "21",
  45. "128": "89",
  46. "47": "08",
  47. "68": "29",
  48. "137": "98",
  49. "132": "93"
  50. };
  51.  
  52. const dontNetabare = localStorage.getItem('incheijs_eptl_nonetabare') === 'true';
  53. const style = document.createElement('style');
  54. style.textContent = /* css */`
  55. .skeleton {
  56. background-color: #e0e0e0;
  57. border-radius: 4px;
  58. position: relative;
  59. overflow: hidden;
  60. }
  61. .skeleton::after {
  62. content: '';
  63. position: absolute;
  64. top: 0;
  65. left: 0;
  66. right: 0;
  67. bottom: 0;
  68. background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
  69. animation: shimmer 1.5s infinite;
  70. }
  71. @keyframes shimmer {
  72. 0% { transform: translateX(-100%); }
  73. 100% { transform: translateX(100%); }
  74. }
  75. html[data-theme="dark"] .skeleton {
  76. background-color: #333;
  77. }
  78. html[data-theme="dark"] .skeleton::after {
  79. background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
  80. }
  81. .comment-skeleton {
  82. max-width: 500px;
  83. height: 32.4px;
  84. margin-top: 5px;
  85. margin-bottom: 5px;
  86. border-radius: 5px;
  87. border: 1px solid transparent;
  88. }
  89.  
  90. .netabare-comment-container {
  91. max-height: 200px;
  92. overflow: auto;
  93. scrollbar-width: thin;
  94. ${ dontNetabare ? /* css */`
  95. .netabare-comment {
  96. filter: blur(4px);
  97. transition: filter 200ms cubic-bezier(1, 0, 0, 1) 100ms;
  98. img:not([smileid]) {
  99. filter: blur(3em);
  100. clip-path: inset(0);
  101. transition: filter 200ms cubic-bezier(1, 0, 0, 1) 100ms;
  102. }
  103. }` : '' }
  104. }
  105. .netabare-comment-container:hover,
  106. .netabare-comment-container:focus {
  107. ${ dontNetabare ? /* css */`
  108. .netabare-comment {
  109. filter: blur(0);
  110. img:not([smileid]) {
  111. filter: blur(0);
  112. }
  113. }` : '' }
  114. }
  115. .comment.comment-failed {
  116. opacity: .4;
  117. }
  118. .comment.comment-failed:hover,
  119. .comment.comment-failed:focus {
  120. opacity: 1;
  121. }
  122. `;
  123. document.head.appendChild(style);
  124.  
  125. class LocalStorageWithExpiry {
  126. constructor() {
  127. this.prefix = 'incheijs_eptl_';
  128. this.initialize();
  129. this.ttl = 240; // 分钟
  130. }
  131.  
  132. // 初始化时清理过期项
  133. initialize() {
  134. Object.keys(localStorage).forEach((key) => {
  135. if (key.startsWith(this.prefix)) {
  136. const item = JSON.parse(localStorage.getItem(key));
  137. if (this.isExpired(item)) localStorage.removeItem(key);
  138. }
  139. });
  140. }
  141.  
  142. isExpired(item) {
  143. return item && item.expiry && Date.now() > item.expiry;
  144. }
  145.  
  146. setItem(key, value) {
  147. const storageKey = `${this.prefix}${key}`;
  148. const expiry = Date.now() + this.ttl * 60 * 1000;
  149. const item = { value, expiry };
  150. localStorage.setItem(storageKey, JSON.stringify(item));
  151. }
  152.  
  153. getItem(key) {
  154. const storageKey = `${this.prefix}${key}`;
  155. const item = JSON.parse(localStorage.getItem(storageKey));
  156. if (this.isExpired(item)) {
  157. localStorage.removeItem(storageKey);
  158. return null;
  159. }
  160. return item ? item.value : null;
  161. }
  162.  
  163. removeItem(key) {
  164. const storageKey = `${this.prefix}${key}`;
  165. localStorage.removeItem(storageKey);
  166. }
  167. }
  168. const storage = new LocalStorageWithExpiry();
  169.  
  170. const epCommentsCache = new Map();
  171. const subjectEpIdCache = new Map();
  172.  
  173. const myUsername = document.querySelector('#dock a').href.split('/').pop();
  174. const menu = document.querySelector('#timelineTabs');
  175. const tmlContent = document.querySelector('#tmlContent');
  176.  
  177. const epExists = focused => ['tab_all', 'tab_progress'].includes(focused.id);
  178. const isEpTl = li => {
  179. const subjectOrEpLink = li.querySelector(`a.l[href^="${location.origin}/subject/"]`);
  180. return subjectOrEpLink?.href.includes('/ep/') || subjectOrEpLink?.previousSibling.textContent.trim() === '完成了'; // 主页和时光机前后空格不同
  181. }
  182. const superGetEpComments = beDistinctConcurrentRetryCached(getEpComments, { cacheMap: epCommentsCache });
  183. const superGetSubjectEpId = beDistinctConcurrentRetryCached(getSubjectEpId, { maxCacheSize: 10, cacheMap: subjectEpIdCache, genKey: (subjectId, epNum) => `${subjectId}_${epNum}` });
  184. let loading = false; // 兼容加载更多,避免连续点击导致重复
  185.  
  186. // 初始
  187. const initialTab = document.querySelector('#timelineTabs .focus');
  188. if (epExists(initialTab)) {
  189. lazyLoadLis([...tmlContent.querySelectorAll('li')].filter(isEpTl));
  190. }
  191.  
  192. // 翻页
  193. tmlContent.addEventListener('click', e => {
  194. if (loading || !e.target.classList.contains('p')) return;
  195. const text = e.target.textContent;
  196.  
  197. let toObserve, getLis;
  198. if (['下一页 ››', '‹‹上一页'].includes(text)) {
  199. superGetEpComments.abortAll();
  200. toObserve = tmlContent;
  201. getLis = addedNodes => [...addedNodes].find((node) => node.id === 'timeline')?.querySelectorAll('li');
  202. } else if (['加载更多', '再来点'].includes(text)) {
  203. // 兼容加载更多
  204. toObserve = document.querySelector('#timeline');
  205. getLis = addedNodes => [...addedNodes].filter((node) => node.tagName === 'UL').flatMap((ul) => [...ul.children]);
  206. } else {
  207. return;
  208. }
  209.  
  210. const observer = new MutationObserver(mutations => {
  211. const focused = document.querySelector('#timelineTabs .focus');
  212. if (!epExists(focused)) return;
  213. for (const mutation of mutations) {
  214. const { addedNodes } = mutation;
  215. let addedLis = getLis(addedNodes);
  216. addedLis &&= [...addedLis].filter(isEpTl);
  217. if (!addedLis || addedLis.length === 0) continue;
  218. observer.disconnect();
  219. lazyLoadLis(addedLis);
  220. loading = false;
  221. }
  222. });
  223. observer.observe(toObserve, { childList: true });
  224. loading = true;
  225. }, true);
  226.  
  227. // 切换Tab
  228. let loadedObserver, currentResolve;
  229. const loadbarRemoved = (mutations) => mutations.some(mutation => [...mutation.removedNodes].some(node => node.classList?.contains('loading')));
  230. const initLoadedObserver = () => {
  231. if (loadedObserver) return;
  232. loadedObserver = new MutationObserver(mutations => {
  233. if (loadbarRemoved(mutations)) {
  234. loadedObserver.disconnect();
  235. currentResolve();
  236. currentResolve = null;
  237. }
  238. });
  239. };
  240. menu.addEventListener('click', async (e) => {
  241. loadedObserver?.disconnect();
  242. if (e.target.tagName !== 'A' || !epExists(e.target)) return;
  243. superGetEpComments.abortAll();
  244. await (new Promise(resolve => {
  245. currentResolve = resolve;
  246. initLoadedObserver();
  247. loadedObserver.observe(tmlContent, { childList: true });
  248. }));
  249. let originalItems = [...document.querySelectorAll('#timeline li')].filter(isEpTl);
  250. lazyLoadLis(originalItems);
  251. }, true);
  252.  
  253. function lazyLoadLis(lis) {
  254. const observer = new IntersectionObserver(
  255. (entries) => {
  256. entries.forEach((entry) => {
  257. if (entry.isIntersecting) {
  258. const li = entry.target;
  259. loadComments(li);
  260. observer.unobserve(li);
  261. }
  262. });
  263. },
  264. { threshold: 0.1 }
  265. );
  266. lis.forEach((li) => observer.observe(li));
  267. }
  268.  
  269. async function loadComments(tl) {
  270. let comment = storage.getItem(tl.id);
  271. const inexist = comment?.inexist;
  272. let epA, epUrl, epId = comment?.epId;
  273. if (inexist && !epId) return;
  274.  
  275. const subjectOrEpLink = tl.querySelector(`a.l[href^="${location.origin}/subject/"]`);
  276. const card = tl.querySelector('.card');
  277. const isWcl = !!epId || !subjectOrEpLink.href.includes('/ep/');
  278.  
  279. const skeleton = document.createElement('div');
  280. skeleton.className = 'comment-skeleton skeleton';
  281.  
  282. if (isWcl) {
  283. const subjectId = subjectOrEpLink.href.split('/').pop();
  284. const progText = subjectOrEpLink.nextSibling;
  285. const progTextFrag = progText.textContent.split(' ');
  286. const epNum = +progTextFrag[1];
  287. if (isNaN(epNum)) return;
  288.  
  289. try {
  290. if (!epId) {
  291. card.before(skeleton);
  292. epId = await superGetSubjectEpId(subjectId, epNum);
  293. }
  294. epUrl = `/subject/ep/${epId}`;
  295. epA = document.createElement('a');
  296. epA.className = 'l';
  297. epA.href = epUrl;
  298. epA.textContent = epNum;
  299. const newProgText = document.createTextNode(` ${progTextFrag.slice(2).join(' ')}`);
  300. progText.replaceWith(' ', epA, newProgText);
  301.  
  302. if (inexist) {
  303. skeleton.remove();
  304. return;
  305. }
  306. } catch (error) {
  307. console.error(tl, error);
  308. card.before(makeReloadBtn(tl, '获取章节 ID 失败,点击重试'));
  309. skeleton.remove();
  310. return;
  311. }
  312. } else {
  313. epA = subjectOrEpLink;
  314. epUrl = epA.href;
  315. epId = epUrl.split('/').pop();
  316. }
  317.  
  318. const footer = tl.querySelector('.post_actions.date');
  319. const userId = tl.dataset.itemUser || location.pathname.split('/')?.[2];
  320.  
  321. card.before(skeleton);
  322.  
  323. try {
  324. if (!comment || epCommentsCache.has(epId)
  325. || Object.keys(comment).length === 1 && Object.keys(comment)[0] === 'epId') {
  326. const data = await superGetEpComments(epId);
  327. const rawComment = data.find(comment => comment.user.username === userId && comment.content);
  328. if (!rawComment) {
  329. storage.setItem(tl.id, { inexist: true, ...(isWcl && { epId }) });
  330. throw new Error('No comment found');
  331. }
  332. const { content, id, reactions } = rawComment;
  333. comment = {
  334. html: bbcodeToHtml(content),
  335. id,
  336. tietie: reactions?.length ? getDataLikesList(epId, reactions) : null,
  337. ...(isWcl && { epId })
  338. };
  339. storage.setItem(tl.id, comment);
  340. }
  341. const { html, id, tietie } = comment;
  342. card.insertAdjacentHTML('beforebegin', `<div class="comment netabare-comment-container" role="button" tabindex="0"><span class="netabare-comment">${html}</span></div>`);
  343. epA.href = `${epUrl}#post_${id}`;
  344. footer.insertAdjacentHTML('beforebegin', `<div class="likes_grid" id="likes_grid_${id}"></div>`);
  345. footer.insertAdjacentHTML('afterbegin', /* html */`
  346. <div class="action dropdown dropdown_right">
  347. <a href="javascript:void(0);" class="icon like_dropdown"
  348. data-like-type="11"
  349. data-like-main-id="${ epId }"
  350. data-like-related-id="${ id }"
  351. data-like-tpl-id="likes_reaction_menu">
  352. <span class="ico ico_like">&nbsp;</span>
  353. <span class="title">贴贴</span>
  354. </a>
  355. </div>
  356. `);
  357. unsafeWindow.chiiLib.likes.updateGridWithRelatedID(id, tietie);
  358. unsafeWindow.chiiLib.likes.init();
  359. } catch (error) {
  360. if (error.message !== 'No comment found') {
  361. console.error(tl, error);
  362. if (isWcl) storage.setItem(tl.id, { epId });
  363. card.before(makeReloadBtn(tl, '获取章节评论失败,点击重试'));
  364. } else {
  365. console.log(tl, '未找到评论');
  366. }
  367. } finally {
  368. skeleton.remove();
  369. }
  370. }
  371.  
  372. function makeReloadBtn(tl, message) {
  373. const btn = document.createElement('div');
  374. btn.className = 'comment comment-failed';
  375. btn.textContent = message;
  376. btn.style.cursor = 'pointer';
  377. btn.role = 'button';
  378. btn.tabIndex = 0;
  379. btn.onclick = () => {
  380. btn.remove();
  381. loadComments(tl);
  382. };
  383. return btn;
  384. }
  385.  
  386. async function getEpComments(episodeId) {
  387. return new Promise((resolve, reject) => {
  388. GM_xmlhttpRequest({
  389. method: 'GET',
  390. url: `https://next.bgm.tv/p1/episodes/${episodeId}/comments`,
  391. onload: function(response) {
  392. if (response.status >= 200 && response.status < 300) {
  393. resolve(JSON.parse(response.responseText));
  394. } else {
  395. reject(new Error(`请求 ${episodeId} 评论区失败,状态码: ${response.status}`));
  396. }
  397. },
  398. onerror: function(e) {
  399. reject(new Error(`请求出错: ${e.status}`));
  400. }
  401. });
  402. });
  403. }
  404.  
  405. function getSubjectEpIdFromDOM(subjectId, epNum) {
  406. if (location.pathname.includes('/user/')) return null;
  407. const epEles = [...document.querySelectorAll('.load-epinfo')];
  408. const epTlEles = [...document.querySelectorAll('.tml_item')].filter(isEpTl);
  409. return (epEles.find(epEle => {
  410. const epEleSubjectId = epEle.getAttribute('subject_id');
  411. const epEleEpNum = +epEle.textContent;
  412. return (epEleSubjectId === subjectId && epEleEpNum === epNum)
  413. }) || epTlEles.find(epTlEle => {
  414. const epLink = epTlEle.querySelector(':is(.info, .info_full) a.l:last-of-type');
  415. const epTlEleSubjectId = epTlEle.querySelector('.card a').href.split('/').pop();
  416. const epTlEleEpNum = +epLink.textContent.split(' ')[0].split('.')[1]
  417. if (isNaN(epTlEleEpNum)) return false;
  418. return (epTlEleSubjectId === subjectId && epTlEleEpNum === epNum)
  419. }))?.href.split('/').pop();
  420. }
  421.  
  422. async function getSubjectEpId(subjectId, epNum) {
  423. const epIdInDOM = getSubjectEpIdFromDOM(subjectId, epNum);
  424. if (epIdInDOM) return epIdInDOM;
  425. const response = await fetch(`https://api.bgm.tv/v0/episodes?subject_id=${subjectId}&limit=1&offset=${epNum-1}`);
  426. if (!response.ok) throw new Error(`请求 ${subjectId} ep${epNum} ID 失败,状态码: ${response.status}`);
  427. const { data } = await response.json();
  428. if (!data[0]) throw new Error(`未找到 ${subjectId} ep${epNum}`);
  429. return data[0].id;
  430. }
  431.  
  432. function bbcodeToHtml(bbcode) {
  433. // (bgm38)
  434. let html = bbcode.replace(/\(bgm(\d+)\)/g, function (_, number) {
  435. const mathNumber = parseInt(number);
  436. let imgUrl, appendix = '';
  437. if (mathNumber > 23) {
  438. const formattedNumber = (mathNumber - 23).toString().padStart(2, '0');
  439. imgUrl = `/img/smiles/tv/${formattedNumber}.gif`;
  440. appendix = 'width="21"';
  441. } else if (mathNumber > 10) {
  442. imgUrl = `/img/smiles/bgm/${number}.gif`;
  443. } else {
  444. imgUrl = `/img/smiles/bgm/${number}.png`;
  445. }
  446. return `<img src="${imgUrl}" smileid="${mathNumber + 16}" alt="(bgm${number})" ${appendix}>`;
  447. });
  448. // [url]
  449. html = html.replace(/\[url=([^\]]+)\]([^\[]+)\[\/url\]/g, '<a class="l" href="$1" target="_blank" rel="nofollow external noopener noreferrer">$2</a>');
  450. html = html.replace(/\[url\]([^[]+)\[\/url\]/g, '<a class="l" href="$1" target="_blank" rel="nofollow external noopener noreferrer">$1</a>');
  451. // [img]
  452. html = html.replace(/\[img(?:=(\d+),(\d+))?\]([^[]+)\[\/img\]/g, function (_, width, height, url) {
  453. const trimmedUrl = url.trim();
  454. return `<img class="code" src="${trimmedUrl}" rel="noreferrer" referrerpolicy="no-referrer" alt="${trimmedUrl}" loading="lazy"${width && height ? ` width="${width}" height="${height}"` : ''}>`;
  455. });
  456. // [b]
  457. html = html.replace(/\[b\]([^[]+)\[\/b\]/g, '<span style="font-weight:bold">$1</span>');
  458. // [u]
  459. html = html.replace(/\[u\]([^[]+)\[\/u\]/g, '<span style="text-decoration:underline">$1</span>');
  460. // [size]
  461. html = html.replace(/\[size=([^\]]+)\]([^[]+)\[\/size\]/g, '<span style="font-size:$1px; line-height:$1px;">$2</span>');
  462. // [mask]
  463. html = html.replace(/\[mask\]([^[]+)\[\/mask\]/g, '<span class="text_mask" style="background-color:#555;color:#555;border:1px solid #555;">$1</span>');
  464. // [s]
  465. html = html.replace(/\[s\]([^[]+)\[\/s\]/g, '<span style="text-decoration: line-through;">$1</span>');
  466. // [quote]
  467. html = html.replace(/\[quote\]([^[]+)\[\/quote\]/g, '<div class="quote"><q>$1</q></div>');
  468. // \n
  469. html = html.replace(/\n/g, '<br>');
  470.  
  471. return html;
  472. }
  473.  
  474. function getDataLikesList(mainID, reactions) {
  475. return reactions.reduce((acc, i) => {
  476. acc[i.value] = {
  477. type: 11,
  478. main_id: mainID,
  479. value: i.value,
  480. total: i.users.length,
  481. emoji: FACE_KEY_GIF_MAPPING[i.value],
  482. users: i.users,
  483. selected: i.users.some(user => user.id === unsafeWindow.CHOBITS_UID)
  484. };
  485. return acc;
  486. }, {})
  487. }
  488.  
  489. function beDistinctConcurrentRetryCached(requestFunction, options = {}) {
  490. const {
  491. maxConcurrency = 3,
  492. maxRetries = 3,
  493. retryDelay = 1000,
  494. maxCacheSize = 5,
  495. cacheMap, // ep comments 缓存会在外部调用
  496. genKey = (arg1) => arg1,
  497. } = options;
  498.  
  499. const pendingRequests = new Map();
  500. const activeRequests = new Set();
  501. const abortControllers = new Map();
  502.  
  503. const wrapped = async (...args) => {
  504. const key = genKey(...args);
  505. if (cacheMap.has(key)) {
  506. console.log(`Returning cached result for ${key}`);
  507. const result = cacheMap.get(key);
  508. cacheMap.delete(key);
  509. cacheMap.set(key, result);
  510. return result;
  511. }
  512.  
  513. if (pendingRequests.has(key)) {
  514. console.log(`Request to ${key} is already pending, waiting...`);
  515. return pendingRequests.get(key);
  516. }
  517.  
  518. while (activeRequests.size >= maxConcurrency) {
  519. console.log(`Max concurrency (${maxConcurrency}) reached, waiting...`);
  520. await Promise.race([...activeRequests]);
  521. }
  522.  
  523. try {
  524. const requestPromise = (async () => {
  525. let retries = 0;
  526. while (retries <= maxRetries) {
  527. try {
  528. const result = await requestFunction(...args);
  529. if (cacheMap.size > maxCacheSize) {
  530. const oldestKey = cacheMap.keys().next().value;
  531. cacheMap.delete(oldestKey);
  532. }
  533. cacheMap.set(key, result);
  534. return result;
  535. } catch (error) {
  536. retries++;
  537. if (retries > maxRetries) {
  538. throw new Error(`Request to ${key} failed after ${maxRetries} retries: ${error.message}`);
  539. }
  540. console.log(`Request to ${key} failed: ${error.message}, retrying (${retries}/${maxRetries})...`);
  541. await new Promise((resolve) => setTimeout(resolve, retryDelay));
  542. }
  543. }
  544. })();
  545.  
  546. const manageActiveRequests = (async () => {
  547. activeRequests.add(requestPromise);
  548. try {
  549. return await requestPromise;
  550. } finally {
  551. activeRequests.delete(requestPromise);
  552. }
  553. })();
  554.  
  555. pendingRequests.set(key, manageActiveRequests);
  556. return await manageActiveRequests;
  557. } finally {
  558. pendingRequests.delete(key);
  559. }
  560. };
  561.  
  562. wrapped.abortAll = () => {
  563. abortControllers.forEach((controller) => controller.abort());
  564. abortControllers.clear();
  565. activeRequests.clear();
  566. pendingRequests.clear();
  567. };
  568.  
  569. return wrapped;
  570. }
  571.  
  572. // 键盘操作
  573. document.addEventListener('click', e => {
  574. if (e.target.classList.contains('netabare-comment-container')) e.target.focus();
  575. }, true);
  576.  
  577. // 保存贴贴变化
  578. const originalReq = unsafeWindow.chiiLib.likes.req;
  579. unsafeWindow.chiiLib.likes.req = (ele) => {
  580. const tlId = ele.closest('.tml_item').id;
  581. const comment = storage.getItem(tlId);
  582. if (!comment) return originalReq.call(this, ele);
  583.  
  584. const id = new URLSearchParams(ele.href).get('id');
  585. const originalAjax = $.ajax;
  586. $.ajax = (options) => {
  587. const originalSuccess = options.success;
  588. options.success = function(json) {
  589. originalSuccess.call(this, json);
  590. const tietie = json.data?.[id];
  591. if (tietie) {
  592. comment.tietie = tietie;
  593. } else {
  594. const originalTietie = comment.tietie;
  595. const onlyValue = (arr, filter) => arr.length === 1 && filter(arr[0]);
  596.  
  597. // 频繁贴贴会导致返回 undefined,此时不应该清除贴贴数据
  598. if (!originalTietie || onlyValue(Object.keys(originalTietie), key => onlyValue(originalTietie[key].users, user => user.username === myUsername))) {
  599. comment.tietie = null;
  600. }
  601. };
  602. storage.setItem(tlId, comment);
  603. };
  604. const result = originalAjax.call(this, options);
  605. $.ajax = originalAjax;
  606. return result;
  607. };
  608. originalReq.call(this, ele);
  609. };
  610. })();

QingJ © 2025

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