Greasy Fork

Greasy Fork is available in English.

好医生自动观看视频/签到

仅观看视频,自动签到,自动下一小节,下载即可用 无套路

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         好医生自动观看视频/签到
// @namespace    好医生自动观看视频/签到
// @version      1.6.10
// @description  仅观看视频,自动签到,自动下一小节,下载即可用 无套路
// @match        https://www.cmechina.net/cme/study2.jsp?*
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      learn.tejiade.cn
// @license      MIT
// ==/UserScript==

(function(){
  'use strict';

  const _DocQS   = Document.prototype.querySelector;
  const _DocQSA  = Document.prototype.querySelectorAll;
  const _ById    = Document.prototype.getElementById;
  const _ByClass = Document.prototype.getElementsByClassName;
  const _ByTag   = Document.prototype.getElementsByTagName;
  const _ElQS    = Element.prototype.querySelector;
  const _ElQSA   = Element.prototype.querySelectorAll;

  const qs   = (sel, root=document) => { try { return _DocQS.call(root, sel); } catch(e){ return null; } };
  const qsa  = (sel, root=document) => { try { return Array.from(_DocQSA.call(root, sel)); } catch(e){ return []; } };
  const byId = (id, root=document)     => { try { return _ById.call(root, id); } catch(e){ return null; } };
  const byClass = (clz, root=document) => { try { return Array.from(_ByClass.call(root, clz)); } catch(e){ return []; } };
  const byTag   = (tag, root=document) => { try { return Array.from(_ByTag.call(root, tag)); } catch(e){ return []; } };
  const qsIn  = (root, sel) => { try { return _ElQS.call(root, sel); } catch(e){ return null; } };

  const sleep = (ms)=> new Promise(r=>setTimeout(r, ms));
  const CME_BASE = 'https://www.cmechina.net/cme/';
  const HELP_URL = 'https://learn.tejiade.cn/logs/haoyisheng.json';

  async function xFetch(url){
    try {
      const r = await fetch(url, { credentials: 'omit', cache: 'no-cache' });
      if (!r.ok) throw new Error('HTTP '+r.status);
      return await r.text();
    } catch(e){
      return await new Promise((resolve, reject)=>{
        if (typeof GM_xmlhttpRequest !== 'function') return reject(e);
        GM_xmlhttpRequest({
          method: 'GET',
          url,
          headers: { 'Accept': 'application/json' },
          onload: (res)=>{
            if (res.status>=200 && res.status<300) resolve(res.responseText);
            else reject(new Error('GM_http '+res.status));
          },
          onerror: ()=>reject(new Error('GM_http error'))
        });
      });
    }
  }

  function formatDuration(sec){
    sec = Math.max(0, Math.floor(sec||0));
    const h = Math.floor(sec/3600);
    const m = Math.floor((sec%3600)/60);
    const s = sec%60;
    const mm = h>0 ? String(m).padStart(2,'0') : String(m);
    const ss = String(s).padStart(2,'0');
    return h>0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`;
  }
  function getCwidFromURL(){
    try { return new URL(location.href).searchParams.get('courseware_id') || null; } catch { return null; }
  }


  function extractJumpUrlFromAnchor(a) {
    if (!a) return null;
    const onclick = a.getAttribute('onclick') || '';
    const m = onclick.match(/kjJumpTo\('([^']+)'/);
    if (m && m[1]) {
      let rel = m[1].replace(/&amp;/g, '&').trim();
      if (/^https?:\/\//i.test(rel)) return rel;
      if (rel.startsWith('/')) rel = rel.replace(/^\/+/, '');
      return CME_BASE + rel;
    }
    const href = (a.getAttribute('href') || '').replace(/&amp;/g, '&').trim();
    if (href && !/^javascript:/i.test(href)) {
      if (/^https?:\/\//i.test(href)) return href;
      let rel = href;
      if (rel.startsWith('/')) rel = rel.replace(/^\/+/, '');
      return CME_BASE + rel;
    }
    return null;
  }

  function withIsNext(url) {
    try {
      const u = new URL(url, CME_BASE);
      u.searchParams.set('isNext', Math.random().toString(36).slice(2));
      return u.toString();
    } catch {
      return url + (url.includes('?') ? '&' : '?') + 'isNext=' + Math.random();
    }
  }

  function navigate(url) {
    if (!url) return false;
    const final = withIsNext(url);
    try { window.history.scrollRestoration = 'auto'; } catch(_){}
    try { window.location.assign(final); return true; } catch(_){}
    try { window.location.href = final; return true; } catch(_){}
    try { window.top.location.href = final; return true; } catch(_){}
    try {
      const a = document.createElement('a');
      a.href = final; a.target = '_self'; a.rel = 'noopener'; a.style.display='none';
      document.body.appendChild(a); a.click(); a.remove(); return true;
    } catch(_){}
    try { window.location.replace(final); return true; } catch(_){}
    return false;
  }


  function getCourseTitle(){
    const tit = document.getElementsByClassName('study_right_tit')[0];
    const h3  = tit ? qsIn(tit, 'h3') : null;
    if (h3 && h3.textContent.trim()) return h3.textContent.trim();
    const jback = document.querySelector('.j_back .box a:nth-of-type(2)');
    if (jback && jback.textContent.trim()) return jback.textContent.trim();
    return '-';
  }
  function getLessonTitle(){
    const main = document.querySelector('.main h3');
    if (main) {
      const txt = main.textContent.replace(/\s+/g,' ').trim();
      if (txt) return txt;
    }
    const ul = document.getElementById('s_r_ml');
    if (ul) {
      const activeLi = Array.from(ul.querySelectorAll('li')).find(li => li.classList.contains('active') || li.classList.contains('cur'));
      const a = activeLi ? activeLi.querySelector('a') : null;
      if (a) {
        const t = a.textContent.replace(/\s+/g,' ').trim();
        if (t) return t;
      }
    }
    return '-';
  }


  function buildPanel(){
    const panel = document.createElement('div');
    panel.id = 'cmechina-helper-panel';
    panel.style.cssText = `
      position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
      width: 420px; max-height: 80vh; overflow: hidden; pointer-events: auto;
      background: #111; color: #eee; border-radius: 12px;
      box-shadow: 0 8px 24px rgba(0,0,0,.25);
      border: 1px solid rgba(255,255,255,.12);
      font-family: system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft YaHei",sans-serif;
    `;
    panel.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.12);">
        <div style="font-weight:700;">CME 助手</div>
        <div id="cme-status" style="opacity:.8;font-size:12px;">Idle</div>
      </div>

      <div style="padding:10px 12px;display:grid;grid-template-columns:100px 1fr;row-gap:6px;column-gap:8px;">
        <div style="opacity:.8;">课程名称</div><div id="cme-course">-</div>
        <div style="opacity:.8;">当前小节</div><div id="cme-lesson">-</div>
        <div style="opacity:.8;">课件ID</div><div id="cme-curcwid">-</div>
        <div style="opacity:.8;">预计总时长</div><div id="cme-exp">-</div>
        <div style="opacity:.8;">剩余倒计时</div><div id="cme-eta">-</div>
      </div>

      <div style="padding:8px 12px;display:flex;gap:8px;flex-wrap:wrap;">
        <button id="cme-start" style="padding:6px 10px;border-radius:8px;background:#2e7d32;color:#fff;border:none;cursor:pointer;">开始自动</button>
        <button id="cme-prev"  style="padding:6px 10px;border-radius:8px;background:#424242;color:#fff;border:none;cursor:pointer;">上一节</button>
        <button id="cme-next"  style="padding:6px 10px;border-radius:8px;background:#424242;color:#fff;border:none;cursor:pointer;">下一节</button>
        <button id="cme-mute"  style="padding:6px 10px;border-radius:8px;background:#8e24aa;color:#fff;border:none;cursor:pointer;">静音</button>
        <button id="cme-help"  style="padding:6px 10px;border-radius:8px;background:#1976d2;color:#fff;border:none;cursor:pointer;">使用手册</button>
      </div>

      <div style="padding:6px 12px 4px;opacity:.8;">全部小节</div>
      <div id="cme-sections" style="margin:0 12px 8px;border:1px solid rgba(255,255,255,.12);border-radius:8px;overflow:auto;max-height:240px;">
        <div style="padding:8px 10px;color:#aaa;">(加载中…)</div>
      </div>

      <div style="padding:6px 12px 4px;opacity:.8;">提示日志</div>
      <div id="cme-log" style="margin:0 12px 12px;border:1px solid rgba(255,255,255,.12);border-radius:8px;overflow:auto;max-height:120px;background:#0b0b0b;"></div>
    `;
    return panel;
  }

  function ensurePanel(){
    let p = document.getElementById('cmechina-helper-panel');
    if (!p) {
      p = buildPanel();
      document.body.appendChild(p);
      bindButtons();
      setCourse(getCourseTitle());
      setLesson(getLessonTitle());
      renderSections();
      updateMuteButtonLabel();
      ensureHelpModal();
    }
    const keys = ['cme-status','cme-course','cme-lesson','cme-curcwid','cme-exp','cme-eta','cme-sections','cme-log'];
    if (keys.some(id => !document.getElementById(id))) {
      p.remove();
      p = buildPanel();
      document.body.appendChild(p);
      bindButtons();
      setCourse(getCourseTitle());
      setLesson(getLessonTitle());
      renderSections();
      updateMuteButtonLabel();
      ensureHelpModal();
    }
    return p;
  }

  function setStatus(t){ ensurePanel(); const el = document.getElementById('cme-status'); if (el) el.textContent = t; }
  function setCourse(t){ ensurePanel(); const el = document.getElementById('cme-course');  if (el) el.textContent = t ?? '-'; }
  function setLesson(t){ ensurePanel(); const el = document.getElementById('cme-lesson');  if (el) el.textContent = t ?? '-'; }
  function setCurCwid(t){ ensurePanel(); const el = document.getElementById('cme-curcwid'); if (el) el.textContent = t ?? '-'; }
  function setExpected(sec){ ensurePanel(); const el = document.getElementById('cme-exp'); if (el) el.textContent = (sec!=null)? `${formatDuration(sec)}(含缓冲)` : '-'; }
  function setETA(sec){ ensurePanel(); const el = document.getElementById('cme-eta'); if (el) el.textContent = (sec!=null)? formatDuration(sec) : '-'; }


  function addLog(msg){
    ensurePanel();
    const box = document.getElementById('cme-log');
    if (!box) return;
    const line = document.createElement('div');
    line.style.cssText = 'padding:6px 8px;border-bottom:1px solid rgba(255,255,255,.06);font-size:12px;white-space:pre-wrap;word-break:break-all;';
    const time = new Date().toLocaleTimeString();
    line.textContent = `[${time}] ${msg}`;
    box.appendChild(line);
    box.scrollTop = box.scrollHeight;
    while (box.childNodes.length > 200) box.removeChild(box.firstChild);
  }

  function parseList() {
    const ul = document.getElementById('s_r_ml');
    const items = [];
    if (!ul) return { ul: null, items };
    const lis = Array.from(ul.querySelectorAll('li'));
    lis.forEach((li, idx) => {
      const a = li.querySelector('a');
      if (!a) return;
      const s = (a.getAttribute('onclick') || '') + '&' + (a.getAttribute('href') || '');
      const m = s.match(/courseware_id=([0-9A-Za-z_-]+)/);
      const cwid = m ? m[1] : null;
      const text = a.textContent.replace(/\s+/g,' ').trim();
      const url  = extractJumpUrlFromAnchor(a);
      items.push({ idx, li, a, cwid, text, url });
    });
    return { ul, items };
  }


  function renderSections(){
    ensurePanel();
    const wrap = document.getElementById('cme-sections');
    if (!wrap) return;
    const {items} = parseList();
    if (!items.length) { wrap.innerHTML = `<div style="padding:8px 10px;color:#aaa;">(未找到目录)</div>`; return; }

    const cur = getCwidFromURL();
    const html = items.map(it=>{
      const active = (it.cwid && it.cwid === cur);
      return `
        <div data-cwid="${it.cwid||''}" style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-bottom:1px solid rgba(255,255,255,.06);${active?'background:#1b1b1b;':''}">
          <div style="flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
            ${it.text || ('课件 '+(it.cwid||''))}
          </div>
            <div style="opacity:.6;font-size:12px;">${it.cwid || '-'}</div>
        </div>`;
    }).join('');
    wrap.innerHTML = html;
  }

  function findNextAnchorByCwid(cwid) {
    if (!cwid) return null;
    const ul = document.getElementById('s_r_ml');
    if (!ul) return null;

    const liCurrent = document.getElementById('li' + cwid);
    if (liCurrent) {
      let nextLi = liCurrent.nextElementSibling;
      while (nextLi && !nextLi.querySelector('a')) nextLi = nextLi.nextElementSibling;
      if (nextLi) {
        const a = nextLi.querySelector('a');
        if (a) return a;
      }
    }

    const lis = Array.from(ul.querySelectorAll('li'));
    let curIdx = -1;
    for (let i = 0; i < lis.length; i++) {
      const a = lis[i].querySelector('a');
      if (!a) continue;
      const s = (a.getAttribute('onclick') || '') + '&' + (a.getAttribute('href') || '');
      if (s.includes(`courseware_id=${cwid}`)) { curIdx = i; break; }
    }
    if (curIdx === -1) return null;

    for (let j = curIdx + 1; j < lis.length; j++) {
      const a = lis[j].querySelector('a');
      if (a) return a;
    }
    return null;
  }

  function safeNavigateToAnchor(a){
    const url = extractJumpUrlFromAnchor(a);
    if (url) return navigate(url);
    try { a && a.click && a.click(); return true; } catch(_) {}
    return false;
  }

  function goPrev(){
    const {items} = parseList();
    if (!items.length) return;
    const urlCwid = getCwidFromURL();
    let i = urlCwid ? items.findIndex(x=>x.cwid===urlCwid) : 0;
    i = Math.max(0, i-1);
    if (items[i] && (items[i].url || items[i].a)) {
      addLog(`上一节:${items[i].text || items[i].cwid || ''}`);
      if (items[i].url) navigate(items[i].url);
      else safeNavigateToAnchor(items[i].a);
    }
  }

  function goNext(){
    const cwid = getCwidFromURL();
    const a = cwid ? findNextAnchorByCwid(cwid) : null;
    if (a) {
      const url = extractJumpUrlFromAnchor(a);
      if (url) { addLog('自动进入下一节'); navigate(url); return; }
      if (safeNavigateToAnchor(a)) { addLog('自动进入下一节(click兜底)'); return; }
    }
    const ul = document.getElementById('s_r_ml');
    if (!ul) { setStatus('目录未找到'); return; }
    const lis = Array.from(ul.querySelectorAll('li'));
    if (!lis.length) { setStatus('目录为空'); return; }

    let idx = lis.findIndex(li => li.classList.contains('active') || li.classList.contains('cur'));
    if (idx < 0) idx = 0;
    let nextLi = lis[idx + 1];
    while (nextLi && !nextLi.querySelector('a')) nextLi = nextLi.nextElementSibling;
    const a2 = nextLi ? nextLi.querySelector('a') : null;
    if (a2) {
      const url = extractJumpUrlFromAnchor(a2);
      if (url) { addLog('自动进入下一节(兜底)'); navigate(url); return; }
      if (safeNavigateToAnchor(a2)) { addLog('自动进入下一节(兜底+click)'); return; }
    } else {
      setStatus('已经是最后一节');
      addLog('已是最后一节,无法再前进');
    }
  }


  async function fetchBufferedSeconds(maxWaitMs=25000){
    const start = Date.now(), step=300;
    while (Date.now()-start < maxWaitMs) {
      try{
        const W = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window;
        if (typeof W.icme_getLearningInfos === 'function') {
          const info = W.icme_getLearningInfos();
          if (info && info.totalTime != null) {
            const sec = parseInt(String(info.totalTime).trim(), 10);
            if (!Number.isNaN(sec) && sec>0) {
              const cwid = String(info.coursewareId || getCwidFromURL() || '');
              return { buffered: sec+60, cwid };
            }
          }
        }
      }catch(_){}
      await sleep(step);
    }
    return null;
  }


  let countdownTimer = null;
  let expectedTotalSec = null;
  let remainingSec = null;     // 剩余秒数
  let isPlaying = false;       // 当前播放状态(播放时 true,暂停/未播放 false)
  let hookedPlayer = false;    // 是否已挂钩站点的 play/pause 回调

  function clearCountdown(){
    if (countdownTimer){ clearInterval(countdownTimer); countdownTimer=null; }
    remainingSec = null;
  }

  function evaluateVideoPlaying(){
    const vids = document.getElementsByTagName('video');
    if (vids.length){
      const v = vids[0];
      return !!(!v.paused && !v.ended && v.readyState >= 2);
    }
    return false;
  }

  function onPlayHook(){
    isPlaying = true;
    addLog('检测到播放中 → 继续倒计时');
  }
  function onPauseHook(){
    isPlaying = false;
    addLog('检测到暂停/未播放 → 倒计时暂停');
  }

  function hookPlayerStatus(){
    if (hookedPlayer) return;
    hookedPlayer = true;
    try{
      const W = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window;

      const vids = document.getElementsByTagName('video');
      if (vids.length){
        const v = vids[0];
        v.addEventListener('play', onPlayHook, {passive:true});
        v.addEventListener('pause', onPauseHook, {passive:true});
      }


      const origPlay  = W.on_CCH5player_play;
      const origPause = W.on_CCH5player_pause;

      if (typeof origPlay === 'function'){
        W.on_CCH5player_play = function(...args){
          try { origPlay.apply(this, args); } finally { onPlayHook(); }
        };
      } else {
        const check = setInterval(()=>{
          if (typeof W.on_CCH5player_play === 'function'){
            clearInterval(check);
            const _op = W.on_CCH5player_play;
            W.on_CCH5player_play = function(...args){
              try { _op.apply(this, args); } finally { onPlayHook(); }
            };
          }
        }, 800);
      }

      if (typeof origPause === 'function'){
        W.on_CCH5player_pause = function(...args){
          try { origPause.apply(this, args); } finally { onPauseHook(); }
        };
      } else {
        const check2 = setInterval(()=>{
          if (typeof W.on_CCH5player_pause === 'function'){
            clearInterval(check2);
            const _oz = W.on_CCH5player_pause;
            W.on_CCH5player_pause = function(...args){
              try { _oz.apply(this, args); } finally { onPauseHook(); }
            };
          }
        }, 800);
      }
    }catch(_){}
  }

  function setExpectedAndETA(totalSec){
    expectedTotalSec = totalSec;
    remainingSec = totalSec;
    setExpected(totalSec);
    setETA(totalSec);
    isPlaying = evaluateVideoPlaying(); // 初始化当前状态
    if (countdownTimer){ clearInterval(countdownTimer); }
    countdownTimer = setInterval(()=>{
      if (isPlaying && remainingSec != null){
        remainingSec = Math.max(0, remainingSec - 1);
      }
      setETA(remainingSec != null ? remainingSec : null);
      if (remainingSec === 0){
        clearCountdown();
        setStatus('到达预计时长,自动进入下一节…');
        addLog('到达预计时长(剩余倒计时=0)→ 自动进入下一节');
        goNext();
      }
    }, 1000);
  }

  function startCountdown(totalSec, forCwid){
    clearCountdown();
    setExpectedAndETA(totalSec);
  }

  function bindVideoEnded(){
    const vids = document.getElementsByTagName('video');
    if (vids.length){
      const v = vids[0];
      if (!v._cmeEnded){
        v._cmeEnded = true;
        v.addEventListener('ended', ()=>{
          onPauseHook(); 
          clearCountdown();
          setStatus('检测到视频 ended → 自动下一节…');
          addLog('检测到视频 ended → 自动下一节');
          goNext();
        });
      }
    }
  }


  function isVisible(el){
    if (!el) return false;
    const cs = window.getComputedStyle ? getComputedStyle(el) : null;
    if (cs && (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0')) return false;
    if (el.style && el.style.display === 'none') return false;
    return true;
  }

  let signSilenceUntil = 0;

  function tryClickSignDialog(){
    const warps = document.querySelectorAll('.xywarp');
    if (!warps || warps.length === 0) return;
    const warp = warps[0];
    if (!isVisible(warp)) return;

    const btns = document.querySelectorAll('.xywarp .zfb_btns1 a');
    if (!btns || btns.length === 0) return;

    const btn = btns[0];
    try {
      btn.click();                       
      addLog('签到成功:已自动点击“签到”按钮'); 
      signSilenceUntil = Date.now() + 8000; 
    } catch (_) {
      
    }
  }
  setInterval(tryClickSignDialog, 10000);


  let autoMode = false;

  function updateMuteButtonLabel(){
    const btn = document.getElementById('cme-mute');
    if (!btn) return;
    const vids = document.querySelectorAll('video');
    const muted = vids.length ? vids[0].muted : false;
    btn.textContent = muted ? '取消静音' : '静音';
  }

  function ensureHelpModal(){
    if (document.getElementById('cme-help-modal')) return;

    const modal = document.createElement('div');
    modal.id = 'cme-help-modal';
    modal.style.cssText = `
      position: fixed; inset: 0; display: none; z-index: 2147483648;
      align-items: center; justify-content: center; background: rgba(0,0,0,.45);
      font-family: inherit;
    `;
    modal.innerHTML = `
      <div style="background:#1a1a1a;color:#eee;max-width:680px;width:92%;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.12);">
        <div style="padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.12);display:flex;justify-content:space-between;align-items:center;">
          <div style="font-weight:700;">使用手册</div>
          <button id="cme-help-close" style="background:transparent;border:none;color:#fff;font-size:18px;cursor:pointer;line-height:1;">×</button>
        </div>
        <div id="cme-help-body" style="padding:14px 16px;font-size:14px;line-height:1.6;">
          <div style="opacity:.8;">正在加载使用手册…</div>
        </div>
        <div style="padding:10px 16px;border-top:1px solid rgba(255,255,255,.12);text-align:right;">
          <button id="cme-help-ok" style="padding:6px 12px;border-radius:8px;background:#1976d2;color:#fff;border:none;cursor:pointer;">我知道了</button>
        </div>
      </div>
    `;
    document.body.appendChild(modal);

    const hide = ()=>{ modal.style.display = 'none'; };
    document.getElementById('cme-help-close').addEventListener('click', hide);
    document.getElementById('cme-help-ok').addEventListener('click', hide);
    modal.addEventListener('click', (e)=>{ if (e.target === modal) hide(); });
  }

  function escapeHTML(s){
    return String(s ?? '').replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&gt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m]));
  }

  let helpLoaded = false;
  async function loadHelpContent(){
    const body = document.getElementById('cme-help-body');
    if (!body) return;
    if (helpLoaded) return; // 只加载一次
    try {
      const txt = await xFetch(HELP_URL);
      let data = JSON.parse(txt);
      if (!Array.isArray(data)) throw new Error('格式错误');
      const rows = data.map(item=>{
        const func = item['功能'] ?? item['feature'] ?? '';
        const desc = item['描述'] ?? item['description'] ?? '';
        return `
          <div style="padding:8px 10px;border:1px solid rgba(255,255,255,.08);border-radius:8px;margin-bottom:8px;background:#121212;">
            <div style="font-weight:600;margin-bottom:4px;">${escapeHTML(func)}</div>
            <div style="opacity:.9;">${escapeHTML(desc)}</div>
          </div>
        `;
      }).join('');
      body.innerHTML = rows || `<div style="opacity:.8;">暂无内容</div>`;
      helpLoaded = true;
    } catch(e){
      body.innerHTML = `<div style="color:#ff8a80;">使用手册加载失败,请稍后重试。</div>`;
    }
  }

  function showHelp(){
    ensureHelpModal();
    const modal = document.getElementById('cme-help-modal');
    if (modal) modal.style.display = 'flex';
    loadHelpContent();
  }

  function bindButtons(){
    document.getElementById('cme-start')?.addEventListener('click', async ()=>{
      autoMode = true;
      setStatus('自动已开启,正在获取时长…');
      setCourse(getCourseTitle());
      setLesson(getLessonTitle());
      setCurCwid(getCwidFromURL() || '-');
      renderSections();
      if (!countdownTimer) {
        const got = await fetchBufferedSeconds();
        if (got && got.buffered) {
          setCurCwid(got.cwid || (getCwidFromURL()||'-'));
          startCountdown(got.buffered, got.cwid);
          setStatus('已获取预计时长(含缓冲)');
          addLog(`预计总时长:${formatDuration(got.buffered)}(含缓冲)`);
        } else {
          setStatus('未获取到 totalTime,稍后重试或等待 ended…');
          addLog('未获取到 totalTime,稍后自动重试');
        }
      }
    });

    document.getElementById('cme-prev')?.addEventListener('click', ()=>{
      autoMode=false; clearCountdown(); goPrev();
    });
    document.getElementById('cme-next')?.addEventListener('click', ()=>{
      autoMode=false; clearCountdown(); goNext();
    });

    document.getElementById('cme-mute')?.addEventListener('click', ()=>{
      const vids = document.querySelectorAll('video');
      if (!vids.length) return;
      const newMuted = !vids[0].muted;
      vids.forEach(v=> v.muted = newMuted);
      updateMuteButtonLabel();
      console.log(`视频已${newMuted?'静音':'取消静音'}`);
    });

    // 使用手册按钮(远程加载)
    document.getElementById('cme-help')?.addEventListener('click', showHelp);
  }


  async function init(){
    window.addEventListener('load', async ()=>{
      ensurePanel();
      setCourse(getCourseTitle());
      setLesson(getLessonTitle());
      setCurCwid(getCwidFromURL() || '-');
      renderSections();
      setStatus('页面已加载');
      updateMuteButtonLabel();
      ensureHelpModal();
      hookPlayerStatus(); // 页面加载完尝试挂钩
      isPlaying = evaluateVideoPlaying();
    });

    (async ()=>{
      const got = await fetchBufferedSeconds();
      if (got && got.buffered) {
        setCurCwid(got.cwid || (getCwidFromURL()||'-'));
        startCountdown(got.buffered, got.cwid);
        setStatus('已获取预计时长(含缓冲)');
        addLog(`预计总时长:${formatDuration(got.buffered)}(含缓冲)`);
      } else {
        setStatus('等待站点初始化…');
        addLog('等待站点初始化以获取 totalTime…');
      }
    })();

    let lastHref = location.href;
    setInterval(async ()=>{
      ensurePanel();
      bindVideoEnded();
      hookPlayerStatus(); // 持续确保已挂钩

      isPlaying = evaluateVideoPlaying() || isPlaying;

      if (location.href !== lastHref) {
        lastHref = location.href;

        if (Date.now() < signSilenceUntil) {
        
        } else {
          
          clearCountdown();
          setCourse(getCourseTitle());
          setLesson(getLessonTitle());
          setCurCwid(getCwidFromURL() || '-');
          setExpected(null);
          setETA(null);
          renderSections();
          addLog('检测到章节切换,已刷新面板信息');
          if (autoMode){
            const got = await fetchBufferedSeconds();
            if (got && got.buffered) {
              setCurCwid(got.cwid || (getCwidFromURL()||'-'));
              startCountdown(got.buffered, got.cwid);
              setStatus('已获取新课件预计时长');
              addLog(`新课件预计时长:${formatDuration(got.buffered)}(含缓冲)`);
            }
          }
        }
      }

      if (autoMode && !countdownTimer) {
        const got = await fetchBufferedSeconds(6000);
        if (got && got.buffered) {
          setCurCwid(got.cwid || (getCwidFromURL()||'-'));
          startCountdown(got.buffered, got.cwid);
          setStatus('已获取预计时长(重试)');
          addLog(`重试成功,预计总时长:${formatDuration(got.buffered)}(含缓冲)`);
        }
      }
    }, 1200);
  }


  setInterval(() => {
    const playButtons = document.querySelectorAll("#replaybtn");
    if (playButtons && playButtons.length > 0) {
      const playButton = playButtons[0];
      if (playButton.style.display == "none") return;
      if (playButton.className == "ccH5PlayBtn") {
        try {
          playButton.click();
          console.log("已自动播放");
        } catch(e){}
      }
    }
  }, 10000);
  console.log("自动播放功能已启动!");

  // 启动
  init().catch(e=>{
    console.error(e);
    try { addLog('初始化失败:' + (e && e.message ? e.message : e)); } catch(_){}
  });
})();