// ==UserScript== // @name 真免费-智慧中小学全自动学习助手 // @namespace https://github.com/berain/smart-edu-assistant // @version 1.0 // @description 智慧中小学平台全自动学习助手,免费体验1次,加群即可在获取一次密钥。8.8元/3次超值刷课支持效率/狂暴双模式,自动完成课程视频学习。全新ui,傻瓜式操作. // @author berain // @match https://basic.smartedu.cn/* // @match https://teacher.ykt.eduyun.cn/* // @icon https://img.icons8.com/color/96/000000/books.png // @grant GM_addStyle // @antifeature payment // @antifeature membership // @antifeature tracking // @tag 智慧中小学 // @tag 暑期研修 // @tag 6.8元刷3次 // @tag 寒假研修 // @tag 免费学习 // @tag 教师研修 // @license LGPL-3.0 // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ============================================================ // 🎨 样式(完整) // ============================================================ GM_addStyle(` #sl3-panel{position:fixed;right:12px;top:60px;width:370px;height:660px;background:#fffef8;color:#333;z-index:99999;font:13px/1.5 "Microsoft YaHei",sans-serif;border-radius:10px;box-shadow:0 6px 30px rgba(0,0,0,.25);overflow:hidden;display:flex;flex-direction:column;transition:width .3s,height .3s,border-radius .3s} #sl3-panel.sl3-minimized{width:50px;height:50px;border-radius:50%;cursor:pointer} #sl3-panel.sl3-minimized>*{display:none} #sl3-book-icon{display:none;position:absolute;top:0;left:0;width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg,#ff9a44,#fc6076);align-items:center;justify-content:center;font-size:22px;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3);z-index:10} #sl3-panel.sl3-minimized #sl3-book-icon{display:flex} @media (max-width:768px){#sl3-panel{right:5%;top:5%;width:90%;height:90vh;max-height:600px;min-width:0;min-height:0}} #sl3-panel .hd{background:linear-gradient(135deg,#ff9a44,#fc6076);padding:10px 14px;display:flex;justify-content:space-between;align-items:center;border-radius:10px 10px 0 0;cursor:move;user-select:none} #sl3-panel .hd .hd-left{display:flex;align-items:center;gap:8px} #sl3-panel .hd .hd-icon{width:24px;height:24px;background:rgba(255,255,255,.25);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#fff} #sl3-panel .hd .hd-title{font-size:15px;font-weight:bold;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.15)} #sl3-panel .hd .hd-ver{font-size:11px;color:rgba(255,255,255,.8);margin-left:4px;font-weight:normal} #sl3-panel .hd .hd-min{width:27px;height:27px;border:0;background:rgba(255,255,255,.26);border-radius:50%;color:#fff;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1} #sl3-panel .hd .hd-min:hover{background:rgba(255,255,255,.45)} #sl3-panel .status-bar{padding:4px 10px;background:#fafafa;border-bottom:1px solid #eee;flex-shrink:0} #sl3-panel .sb-row{display:flex;align-items:center;gap:6px;flex-wrap:nowrap} #sl3-panel .sb-label{font-size:11px;color:#555;font-weight:bold;display:flex;align-items:center;gap:3px;white-space:nowrap;flex-shrink:0} #sl3-panel .sb-dot{width:7px;height:7px;border-radius:50%;background:#4caf50;display:inline-block;flex-shrink:0} #sl3-panel .sb-dot.running{background:#ff9800;animation:pulse 1s infinite} #sl3-panel .sb-dot.stopped{background:#999} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} #sl3-panel .sb-stat{font-size:11px;color:#555;white-space:nowrap;flex-shrink:0} #sl3-panel .sb-pct{font-size:14px;font-weight:bold;color:#ff6f00;margin-left:auto;white-space:nowrap;flex-shrink:0} #sl3-panel .sb-sub{font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0} #sl3-panel .ctrl-bar{padding:5px 10px;background:#fafafa;border-bottom:1px solid #eee;display:flex;flex-direction:column;gap:5px;flex-shrink:0} #sl3-panel .cb-row{display:flex;align-items:center;gap:5px;flex-wrap:nowrap} #sl3-panel .speed-row{width:100%;display:flex;align-items:center;gap:5px} #sl3-panel .speed-box{display:flex;align-items:center;gap:4px;font-size:11px;color:#555;font-weight:bold;flex:1;width:100%} #sl3-panel .speed-slider{flex:1;width:100%;min-width:0;accent-color:#ff6f00;cursor:pointer} #sl3-panel .mode-sel{padding:3px 7px;border:2px solid #ff9800;border-radius:6px;background:#fff;color:#ff6f00;font-size:13px;font-weight:bold;cursor:pointer;outline:none;flex-shrink:0} #sl3-panel .mode-sel option{color:#333} #sl3-panel .ctrl-input{width:38px;padding:3px 5px;border:1px solid #ddd;border-radius:5px;font-size:11px;text-align:center;outline:none} #sl3-panel .btn-go{padding:4px 10px;border:0;border-radius:6px;cursor:pointer;font-size:12px;font-weight:bold;background:linear-gradient(135deg,#ff6f00,#ff9a44);color:#fff;box-shadow:0 2px 8px rgba(255,111,0,.35);flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #sl3-panel .btn-go:hover{box-shadow:0 4px 12px rgba(252,96,118,.45);transform:translateY(-1px)} #sl3-panel .btn-go:active{transform:translateY(0)} #sl3-panel .btn-stop{padding:4px 12px;border:1px solid #bbb;border-radius:6px;cursor:pointer;font-size:12px;font-weight:bold;background:#f5f5f5;color:#666;flex:1 1 auto;white-space:nowrap} #sl3-panel .btn-stop:not(:disabled){border-color:#ff9800;color:#ff6f00;background:#fff} #sl3-panel .btn-stop:hover{background:#e0e0e0} #sl3-panel .btn-go:disabled,.btn-stop:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none} #sl3-panel .tabs{display:flex;padding:0 14px;background:#fafafa;border-bottom:2px solid #eee;flex-shrink:0;gap:3px;flex-wrap:nowrap;overflow:hidden} #sl3-panel .tab{padding:6px 10px;cursor:pointer;font-size:11px;color:#666;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .2s;white-space:nowrap;flex-shrink:1} #sl3-panel .tab:hover{color:#ff6f00} #sl3-panel .tab.active{color:#ff6f00;border-bottom-color:#ff6f00;font-weight:bold} @media (max-width:370px){#sl3-panel .tab{padding:4px 6px;font-size:10px}} #sl3-panel .action-row{padding:5px 10px;display:flex;align-items:center;gap:4px;flex-wrap:nowrap;overflow:hidden;border-bottom:1px solid #f0f0f0;flex-shrink:0} #sl3-panel .btn-xs{padding:3px 8px;border:1px solid #ddd;border-radius:5px;cursor:pointer;font-size:11px;background:#fff;color:#555;white-space:nowrap} #sl3-panel .btn-xs:hover{border-color:#ff9800;color:#ff6f00} #sl3-panel .btn-xs.primary{background:#fff3e0;border-color:#ffb74d;color:#e65100} #sl3-panel .btn-xs.goal{background:#ff7a45;border-color:#ff7a45;color:#fff;font-weight:bold} #sl3-panel .btn-xs.goal:hover{background:#ff5722} #sl3-panel .list-area{flex:1;overflow-y:auto;padding:4px 0;display:flex;flex-direction:column;gap:4px} #sl3-panel .group-container{border:1px solid #f0e8dc;border-radius:8px;overflow:hidden;background:#fffcf5;display:flex;flex-direction:column} #sl3-panel .group-container.req-group{flex-shrink:0} #sl3-panel .group-container.elec-group{flex:1;min-height:0;border-color:#e8f0dc;background:#f5fff8} #sl3-panel .group-body{overflow-y:auto;min-height:0} #sl3-panel .course-row{display:flex;align-items:center;padding:6px 12px;gap:6px;border-bottom:1px solid #f5f5f5;transition:background .15s} #sl3-panel .course-row:hover{background:#fff8e1} #sl3-panel .course-row.cr-current{background:#fff3e0} #sl3-panel .cchk{width:16px;height:16px;flex-shrink:0;cursor:pointer;accent-color:#ff6f00} #sl3-panel .cexp{width:20px;height:20px;border:0;background:transparent;color:#ff9800;cursor:pointer;font-size:11px;flex-shrink:0;line-height:20px;text-align:center;border-radius:4px} #sl3-panel .cexp:hover{background:#fff3e0} #sl3-panel .cinfo{flex:1;min-width:0;display:flex;align-items:baseline;gap:6px} #sl3-panel .ctitle{flex:1;min-width:0;font-size:13px;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #sl3-panel .cmeta{font-size:11px;color:#999;white-space:nowrap;flex-shrink:0} #sl3-panel .cbadge{font-size:10px;padding:2px 8px;border-radius:8px;flex-shrink:0;font-weight:bold;white-space:nowrap} #sl3-panel .cbadge.done{background:#e8f5e9;color:#2e7d32} #sl3-panel .cbadge.fail{background:#ffebee;color:#c62828} #sl3-panel .cbadge.cur{background:#fff3e0;color:#e65100} #sl3-panel .cbadge.skip{background:#f5f5f5;color:#999} #sl3-panel .cbadge.pend{background:#f5f5f5;color:#888} #sl3-panel .group-head{padding:5px 12px;font-size:12px;font-weight:bold;color:#e65100;background:#fff3e0;border-bottom:1px solid #ffe0b2;flex-shrink:0;position:sticky;top:0;z-index:2;display:flex;align-items:center} #sl3-panel .vlist{background:#fafafa;padding:2px 0 4px 32px} #sl3-panel .vrow{display:flex;align-items:center;gap:5px;padding:3px 10px;font-size:11px;border-bottom:1px dashed #f0f0f0} #sl3-panel .vrow:hover{background:#fff} #sl3-panel .vchk{width:13px;height:13px;flex-shrink:0;cursor:pointer;accent-color:#ff9800} #sl3-panel .vname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#444} #sl3-panel .vmeta{color:#aaa;font-size:10px;flex-shrink:0} #sl3-panel .vrow.learned{opacity:.5} #sl3-panel .vrow.learned .vname{color:#bbb;text-decoration:line-through} #sl3-panel .vrow.watching .vname{color:#e65100} #sl3-panel .vbar{display:inline-block;width:36px;height:5px;background:#eee;border-radius:3px;overflow:hidden;flex-shrink:0;vertical-align:middle;margin-left:3px} #sl3-panel .vbar i{display:block;height:100%;background:linear-gradient(90deg,#4caf50,#81c784);transition:width .3s} #sl3-panel .vrow.learned .vbar i{background:#4caf50} #sl3-panel .vpct{font-size:9px;color:#999;flex-shrink:0;margin-left:2px} #sl3-panel .log-area{flex:1;min-height:0;overflow-y:auto;padding:6px 14px;font-size:11px;border-top:1px solid #eee;background:#fafafa} #sl3-panel .log-entry{padding:2px 0;border-bottom:1px dotted #eee;line-height:1.5} #sl3-panel .log-entry.ok{color:#2e7d32} #sl3-panel .log-entry.err{color:#c62828} #sl3-panel .log-entry.info{color:#1565c0} #sl3-panel .log-entry.warn{color:#e65100} #sl3-panel .log-ts{color:#bbb;margin-right:5px} /* ★★★ 底部两行 ★★★ */ #sl3-panel .ft { padding: 6px 10px; background: #fafafa; border-top: 1px solid #eee; display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; font-size: 11px; color: #888; } #sl3-panel .ft .ft-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; gap: 4px; min-height: 22px; } #sl3-panel .ft .ft-row .ft-left { display: flex; align-items: center; gap: 6px; flex-shrink: 1; min-width: 0; } #sl3-panel .ft .ft-row .ft-right { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } #sl3-panel .ft .ft-label { font-size: 10px; color: #555; white-space: nowrap; flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; } #sl3-panel .ft .btn-ft { padding: 1px 6px; border: 1px solid #ddd; border-radius: 3px; cursor: pointer; font-size: 10px; background: #fff; color: #555; white-space: nowrap; } #sl3-panel .ft .btn-ft:hover { border-color: #bbb; background: #f0f0f0; } #sl3-panel .ft .btn-qq { padding: 1px 6px; border: 1px solid #ff9800; border-radius: 3px; cursor: pointer; font-size: 10px; background: #fff3e0; color: #e65100; text-decoration: none; display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; } #sl3-panel .ft .btn-qq:hover { background: #ffe0b2; } #sl3-panel .ft .btn-qq-copy { padding: 1px 6px; border: 1px solid #1976d2; border-radius: 3px; cursor: pointer; font-size: 10px; background: #e3f2fd; color: #0d47a1; display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; } #sl3-panel .ft .btn-qq-copy:hover { background: #bbdefb; } #sl3-panel .ft #sl3-userinfo { font-size: 10px; color: #888; white-space: nowrap; } /* ★★★ 进度卡片 ★★★ */ #sl3-panel .prog-card{background:linear-gradient(135deg,#fff8e1,#fffde7);border-radius:8px;padding:6px 12px;margin:4px 14px;display:flex;align-items:center;gap:10px;border:1px solid #ffe082;flex-shrink:0} #sl3-panel .prog-ring{width:36px;height:36px;flex-shrink:0;position:relative} #sl3-panel .prog-ring svg{transform:rotate(-90deg)} #sl3-panel .ring-bg{fill:none;stroke:#ffe082;stroke-width:3.5} #sl3-panel .ring-fg{fill:none;stroke:#ff6f00;stroke-width:3.5;stroke-linecap:round;transition:stroke-dashoffset .6s ease} #sl3-panel .pct-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:10px;font-weight:bold;color:#e65100} #sl3-panel .prog-info{flex:1;min-width:0} #sl3-panel .pi-row{display:flex;justify-content:space-between;font-size:10px;line-height:1.7;color:#666} #sl3-panel .pi-val{font-weight:bold;color:#e65100} #sl3-panel .pi-label{color:#999} #sl3-panel .prog-auth { margin-left: auto; text-align: right; font-size: 12px; color: #666; flex-shrink: 0; padding-left: 10px; border-left: 1px solid #e0d5c8; white-space: normal; line-height: 1.4; } #sl3-panel .prog-auth .auth-remaining { font-weight: bold; color: #e65100; } #sl3-panel .prog-auth .auth-sub { font-size: 11px; color: #999; } #sl3-panel .prog-auth .auth-locked { color: #c62828; } #sl3-panel .pane{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0} #sl3-panel .vd-top{padding:8px 14px;background:#fff3e0;font-size:12px;color:#e65100;border-bottom:1px solid #ffe0b2;flex-shrink:0} #sl3-panel .vd-count{color:#999;margin-left:6px;font-weight:normal} #sl3-panel .vd-list-area{flex:1;overflow-y:auto;padding:4px 0;min-height:0} #sl3-panel .vd-row{display:flex;align-items:center;gap:6px;padding:5px 12px;border-bottom:1px solid #f5f5f5;font-size:11px} #sl3-panel .vd-idx{width:22px;color:#bbb;flex-shrink:0;text-align:right} #sl3-panel .vd-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#444} #sl3-panel .vd-dur{color:#aaa;flex-shrink:0} #sl3-panel .vd-bar{display:inline-block;width:60px;height:6px;background:#eee;border-radius:3px;overflow:hidden;flex-shrink:0} #sl3-panel .vd-bar i{display:block;height:100%;transition:width .3s} #sl3-panel .vd-pct{width:34px;text-align:right;color:#999;flex-shrink:0} #sl3-panel .vd-empty{padding:40px;text-align:center;color:#ccc;font-size:12px} #sl3-panel .vd-course-label{padding:6px 12px;background:#fff8e1;font-size:11px;font-weight:bold;color:#e65100;border-bottom:1px solid #ffe0b2;position:sticky;top:0;z-index:2} #sl3-panel .vd-course-meta{font-weight:normal;color:#999;margin-left:4px} #sl3-panel .vd-course-item{border-bottom:1px solid #f0f0f0} #sl3-panel .vd-course-header{display:flex;align-items:center;gap:6px;padding:7px 12px;cursor:pointer;font-size:11px;background:#fafafa;border-bottom:1px solid #f0f0f0;transition:background .15s} #sl3-panel .vd-course-header:hover{background:#fff8e1} #sl3-panel .vd-course-arrow{color:#ff9800;width:14px;text-align:center;flex-shrink:0;font-size:10px} #sl3-panel .vd-course-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:bold;color:#444} #sl3-panel .vd-course-videos{background:#fff} #sl3-panel .order-group{display:inline-flex;gap:4px;margin-left:6px} #sl3-panel .btn-order{padding:2px 8px;border:1px solid #ddd;border-radius:5px;background:#fff;color:#888;font-size:11px;cursor:pointer;outline:none} #sl3-panel .btn-order:hover{border-color:#ffb74d} #sl3-panel .btn-order.active{background:#ff9800;border-color:#ff9800;color:#fff;font-weight:bold} #sl3-panel .speed-box b{color:#ff6f00;min-width:32px} /* ★★★ 激活授权面板 ★★★ */ #sl3-license-pane { padding: 20px; display: flex; flex-direction: column; gap: 12px; } #sl3-license-pane .license-status-display { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f5f5f5; border-radius: 6px; border-left: 4px solid #ff6f00; font-size: 13px; } #sl3-license-pane .license-status-display .license-status-label { color: #666; } #sl3-license-pane .license-status-display .license-status-value { font-weight: bold; color: #c62828; } #sl3-license-pane .license-status-display .license-status-value.ok { color: #2e7d32; } #sl3-license-pane .license-input-row { display: flex; gap: 8px; flex-wrap: wrap; } #sl3-license-pane .license-input-row input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; min-width: 150px; } #sl3-license-pane .license-input-row input:focus { border-color: #ff6f00; outline: none; } #sl3-license-pane .license-input-row button { padding: 8px 20px; background: #ff6f00; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 13px; transition: 0.2s; } #sl3-license-pane .license-input-row button:hover { background: #e65c00; } #sl3-license-pane .btn-buy-key { display: inline-block; padding: 8px 20px; background: #4caf50; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: bold; text-decoration: none; text-align: center; transition: 0.2s; width: 100%; } #sl3-license-pane .btn-buy-key:hover { background: #388e3c; } #sl3-license-pane .license-helper { text-align: center; font-size: 12px; color: #999; margin-top: 2px; line-height: 1.5; } #sl3-license-pane .license-status { margin-top: 4px; font-size: 13px; color: #333; text-align: center; } #sl3-license-pane .license-status .success { color: #2e7d32; } #sl3-license-pane .license-status .error { color: #c62828; } /* ====== 次数用完弹窗 ====== */ #sl3-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:999999;justify-content:center;align-items:center} #sl3-overlay.active{display:flex} #sl3-credits-modal{background:#fff;border-radius:12px;padding:30px 30px 24px;max-width:400px;width:90%;box-shadow:0 10px 40px rgba(0,0,0,0.3);text-align:center;position:relative} #sl3-credits-modal .modal-icon{font-size:48px;margin-bottom:8px} #sl3-credits-modal .modal-title{font-size:20px;font-weight:bold;color:#c62828;margin:8px 0} #sl3-credits-modal .modal-desc{font-size:14px;color:#555;margin:8px 0 16px;line-height:1.6} #sl3-credits-modal .modal-qq-group{font-size:16px;font-weight:bold;color:#1976d2;margin:8px 0} #sl3-credits-modal .modal-actions{display:flex;flex-direction:column;gap:8px;margin:12px 0} #sl3-credits-modal .modal-actions .btn-qq-large{display:block;padding:10px;border-radius:6px;font-size:14px;font-weight:bold;text-decoration:none;transition:0.2s;cursor:pointer;border:none} #sl3-credits-modal .modal-actions .btn-qq-large.primary{background:#ff6f00;color:#fff} #sl3-credits-modal .modal-actions .btn-qq-large.primary:hover{background:#e65c00} #sl3-credits-modal .modal-actions .btn-qq-large.secondary{background:#1976d2;color:#fff} #sl3-credits-modal .modal-actions .btn-qq-large.secondary:hover{background:#1565c0} #sl3-credits-modal .modal-close{display:inline-block;margin-top:12px;padding:8px 24px;border:1px solid #ddd;border-radius:6px;background:#fff;color:#666;font-size:13px;cursor:pointer;transition:0.2s} #sl3-credits-modal .modal-close:hover{background:#f5f5f5;border-color:#bbb} #sl3-credits-modal .modal-note{font-size:12px;color:#999;margin-top:10px} /* ====== 其他 ====== */ #sl3-lock-banner{background:#ffebee;border-bottom:1px solid #ffcdd2;padding:6px 12px;font-size:12px;color:#c62828;display:none;align-items:center;gap:8px;flex-shrink:0} #sl3-lock-banner .lock-icon{font-size:16px} #sl3-lock-banner .lock-msg{flex:1} #sl3-lock-banner .lock-close{cursor:pointer;opacity:0.6} #sl3-lock-banner .lock-close:hover{opacity:1} #sl3-announcement{background:#fff8e1;border-bottom:1px solid #ffe082;padding:6px 12px;font-size:12px;color:#e65100;display:flex;align-items:center;gap:8px;flex-shrink:0} #sl3-announcement .ann-icon{font-size:16px} #sl3-announcement .ann-content{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #sl3-announcement .ann-close{cursor:pointer;opacity:0.6;font-size:14px} #sl3-announcement .ann-close:hover{opacity:1} `); // ============================================================ // ⚙️ 配置 // ============================================================ const CLOUD_FUNCTION_URL = 'https://1300815573-5c1s2m3wok.ap-shanghai.tencentscf.com'; const TRAIN_ID = 'dc6d78f2-bad8-4d09-b8da-0d758803dbe4'; const LIBRARY_ID = 'bb042e69-9a11-49a1-af22-0c3fab2e92b9'; const APP_ID = 'e5649925-441d-4a53-b525-51a2f1c4e0a8'; const ELECTIVE_CREDIT_CAP_H = 3; const REQUIRED_CREDIT_TOTAL = 10; const ANNOUNCEMENT_INTERVAL = 60000; const REMINDER_DURATION = 3600000; const QQ_GROUP_LINK = 'https://qm.qq.com/q/EBLDisQRBm'; const QQ_GROUP_NUMBER = '1041375393'; const QQ_AUTHOR = '3352639207'; const LOG_STORAGE_KEY = 'sl3_persistent_logs'; const MAX_LOG_ENTRIES = 500; const SCRIPT_VERSION = 'v1.0 berain'; // ============================================================ // 📦 全局状态 // ============================================================ let auth = null; let isRunning = false; let currentIdx = -1; let runMode = 'efficiency'; let videoOrderMode = 'random'; let speedMultiplier = 20; let coreLoaded = false; let authInfo = { remaining: 0, expireDate: '', isFreeTrialAvailable: false, locked: false }; let deviceFingerprint = ''; let announcementShown = false; let panelMinimized = false; let creditsModalShown = false; let creditsModalShownThisSession = false; // ============================================================ // 🔧 工具函数 // ============================================================ function loadAuth() { if (auth) return auth; var key = Object.keys(localStorage).find(function(k) { return k.indexOf('ND_UC_AUTH') === 0 && k.indexOf('&token') > -1; }); if (!key) return null; try { var raw = JSON.parse(localStorage.getItem(key)); var td = JSON.parse(raw.value); var user = JSON.parse(localStorage.getItem('X-EDU-WEB-USER') || '{}'); auth = { accessToken: td.access_token, macKey: td.mac_key, diff: td.diff || 0, userId: user.user_id, userName: user.real_name }; return auth; } catch(e) { return null; } } function getBrowserInfo() { try { const nav = navigator; return { userAgent: nav.userAgent, language: nav.language, platform: nav.platform, hardwareConcurrency: nav.hardwareConcurrency || 0, deviceMemory: nav.deviceMemory || 0, screenWidth: screen.width, screenHeight: screen.height, colorDepth: screen.colorDepth, timezoneOffset: new Date().getTimezoneOffset(), vendor: nav.vendor || '', productSub: nav.productSub || '' }; } catch(e) { return { error: 'Failed to collect browser info' }; } } function getDeviceId() { let id = localStorage.getItem('sl3_device_id'); if (!id) { id = 'did_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); localStorage.setItem('sl3_device_id', id); } return id; } // ★★★ 设备指纹固定(不含时间戳后缀)★★★ function getDeviceFingerprint() { let stored = localStorage.getItem('sl3_device_fingerprint'); if (stored) { return stored; } try { const nav = navigator; const components = [ nav.userAgent, nav.language, nav.platform, nav.hardwareConcurrency || 0, nav.deviceMemory || 0, screen.width, screen.height, screen.colorDepth, new Date().getTimezoneOffset(), (function() { const canvas = document.createElement('canvas'); canvas.width = 200; canvas.height = 50; const ctx = canvas.getContext('2d'); ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillStyle = '#f60'; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = '#069'; ctx.fillText('智慧中小学', 2, 15); ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; ctx.fillText('静默学习', 4, 17); return canvas.toDataURL().substring(0, 100); })() ]; const raw = components.join('||'); let hash = 0, i, chr; for (i = 0; i < raw.length; i++) { chr = raw.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; } const fp = 'fp_' + Math.abs(hash).toString(36); localStorage.setItem('sl3_device_fingerprint', fp); return fp; } catch(e) { let fallback = localStorage.getItem('sl3_device_fingerprint_fallback'); if (!fallback) { fallback = 'fp_fallback_' + Date.now().toString(36); localStorage.setItem('sl3_device_fingerprint_fallback', fallback); } localStorage.setItem('sl3_device_fingerprint', fallback); return fallback; } } function callCloudFunction(action, payload, retries = 2, timeout = 30000) { return new Promise((resolve, reject) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); fetch(CLOUD_FUNCTION_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action, payload: { ...payload, deviceFingerprint: deviceFingerprint } }), signal: controller.signal }) .then(resp => { clearTimeout(timer); if (!resp.ok) throw new Error('HTTP ' + resp.status); return resp.json(); }) .then(data => { if (data.code === 0) resolve(data.data); else reject(new Error(data.message || '云函数返回错误')); }) .catch(err => { clearTimeout(timer); if (retries > 0) { console.warn('[callCloud] 重试 (剩余' + retries + '次): ' + action, err.message); return callCloudFunction(action, payload, retries - 1, timeout); } reject(err); }); }); } // ★★★ 日志函数(过滤请求体 + 永久存储)★★★ function log(msg, cl) { if (typeof msg === 'string' && msg.indexOf('📤 请求体') === 0) { return; } var area = document.getElementById('sl3-log-area'); var entry = { time: new Date().toLocaleTimeString(), msg: String(msg), cl: cl || 'info' }; var logs = getStoredLogs(); logs.push(entry); if (logs.length > MAX_LOG_ENTRIES) { logs = logs.slice(-MAX_LOG_ENTRIES); } try { localStorage.setItem(LOG_STORAGE_KEY, JSON.stringify(logs)); } catch(e) {} if (area) { var d = document.createElement('div'); d.className = 'log-entry ' + entry.cl; d.innerHTML = '' + entry.time + '' + entry.msg.replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); area.appendChild(d); if (document.getElementById('sl3-auto-scroll')?.checked !== false) { area.scrollTop = area.scrollHeight; } } updateSubStat(msg); } function getStoredLogs() { try { var data = localStorage.getItem(LOG_STORAGE_KEY); return data ? JSON.parse(data) : []; } catch(e) { return []; } } function loadLogsFromStorage() { var area = document.getElementById('sl3-log-area'); if (!area) return; var logs = getStoredLogs(); if (logs.length === 0) return; area.innerHTML = ''; logs.forEach(function(entry) { var d = document.createElement('div'); d.className = 'log-entry ' + entry.cl; d.innerHTML = '' + entry.time + '' + entry.msg.replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); area.appendChild(d); }); area.scrollTop = area.scrollHeight; } function clearLogs() { localStorage.removeItem(LOG_STORAGE_KEY); var area = document.getElementById('sl3-log-area'); if (area) area.innerHTML = ''; } function updateSubStat(msg) { var el = document.getElementById('sl3-substat'); if (el) { const short = msg.length > 40 ? msg.substring(0, 40) + '...' : msg; el.textContent = short; } } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function shuffle(arr) { var a = arr.slice(); for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var tmp = a[i]; a[i] = a[j]; a[j] = tmp; } return a; } function delay(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } // ============================================================ // 📥 从云函数加载核心代码 // ============================================================ async function loadCoreCode() { try { const data = await callCloudFunction('getCoreCode', {}); if (data && data.coreCode) { eval(data.coreCode); coreLoaded = true; log('✅ 核心代码加载成功', 'ok'); if (typeof window.loadCourseList === 'function') { await window.loadCourseList(); } else { log('⚠️ 未暴露 loadCourseList', 'warn'); } return true; } log('❌ 核心代码为空', 'err'); return false; } catch(e) { log('❌ 加载核心代码失败: ' + e.message, 'err'); return false; } } // ============================================================ // 🔑 授权相关 // ============================================================ function saveLicenseKey(key) { if (key) { window.__sl3.licenseKey = key; localStorage.setItem('sl3_license_key', key); } else { localStorage.removeItem('sl3_license_key'); if (window.__sl3) delete window.__sl3.licenseKey; } } function getSavedLicenseKey() { return localStorage.getItem('sl3_license_key') || (window.__sl3 && window.__sl3.licenseKey); } function saveFreeTrialState(available) { localStorage.setItem('sl3_free_trial_available', available ? '1' : '0'); } function getFreeTrialState() { const avail = localStorage.getItem('sl3_free_trial_available'); return { available: avail === '1', used: false }; } async function updateAuthInfo() { try { const licenseKey = getSavedLicenseKey(); let payload = { deviceFingerprint: deviceFingerprint }; if (licenseKey) { payload.licenseKey = licenseKey; } const data = await callCloudFunction('authCheck', payload); if (data) { authInfo.remaining = data.remaining || 0; authInfo.expireDate = data.expireDate || ''; authInfo.isFreeTrialAvailable = data.isFreeTrialAvailable || false; authInfo.locked = data.locked || false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: authInfo.isFreeTrialAvailable, locked: authInfo.locked }; saveFreeTrialState(authInfo.isFreeTrialAvailable); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); updateLockBanner(); // ★★★ 检测次数用完弹窗(仅本次会话弹一次)★★★ var isLoggedIn = !!(window.__sl3 && window.__sl3.auth && window.__sl3.auth.userId); var hasLicense = !!getSavedLicenseKey(); var isOutOfCredits = isLoggedIn && !hasLicense && authInfo.remaining <= 0 && !authInfo.isFreeTrialAvailable; var isLocked = authInfo.locked === true; if (isOutOfCredits && !isLocked && !creditsModalShownThisSession) { setTimeout(function() { showCreditsModal(); }, 2000); } log('授权状态已更新: 剩余 ' + authInfo.remaining + ' 次' + (authInfo.isFreeTrialAvailable ? ' (可免费体验)' : '') + (authInfo.locked ? ' (设备已被锁定)' : ''), 'info'); } } catch(e) { log('查询授权状态失败: ' + e.message, 'err'); } } async function autoActivateFreeTrial() { if (authInfo.isFreeTrialAvailable) { log('🎁 检测到免费体验可用,自动激活...', 'info'); try { const data = await callCloudFunction('useFreeTrial', { deviceFingerprint: deviceFingerprint }); if (data && data.remaining !== undefined) { authInfo.remaining = data.remaining; authInfo.isFreeTrialAvailable = false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: false, locked: authInfo.locked }; saveFreeTrialState(false); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); log('✅ 免费体验自动激活成功,剩余 ' + data.remaining + ' 次', 'ok'); return true; } else { log('⚠️ 免费体验激活失败,请手动点击按钮', 'warn'); return false; } } catch (e) { log('❌ 免费体验自动激活异常: ' + e.message, 'err'); return false; } } return false; } async function verifyLicense(key) { try { const data = await callCloudFunction('verifyLicense', { licenseKey: key }); if (data && data.message === '激活成功') { authInfo.remaining = data.remaining; authInfo.expireDate = data.expireDate || ''; authInfo.locked = false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: false, locked: false }; saveLicenseKey(key); saveFreeTrialState(false); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); updateLockBanner(); log('✅ 激活成功,剩余 ' + data.remaining + ' 次', 'ok'); if (window.__sl3) window.__sl3.licenseKey = key; return true; } else { log('❌ 激活失败: ' + (data ? data.message : '未知错误'), 'err'); return false; } } catch(e) { log('❌ 激活异常: ' + e.message, 'err'); return false; } } async function useFreeTrial() { try { const data = await callCloudFunction('useFreeTrial', { deviceFingerprint: deviceFingerprint }); if (data && data.remaining !== undefined) { authInfo.remaining = data.remaining; authInfo.isFreeTrialAvailable = false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: false, locked: authInfo.locked }; saveFreeTrialState(false); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); log('✅ 免费体验成功,剩余 ' + data.remaining + ' 次', 'ok'); if (typeof window.startLearning === 'function') { window.startLearning(); } return true; } else { log('❌ 免费体验失败: ' + (data ? data.message : '未知错误'), 'err'); return false; } } catch(e) { log('❌ 免费体验异常: ' + e.message, 'err'); return false; } } // ============================================================ // 📢 公告系统 // ============================================================ async function fetchAnnouncement() { const lastClose = localStorage.getItem('sl3_announcement_close_time'); if (lastClose) { const diff = Date.now() - parseInt(lastClose); if (diff < REMINDER_DURATION) { return; } } try { const data = await callCloudFunction('getAnnouncement', {}); if (data && data.content) { showAnnouncement(data.content, data.type); } } catch(e) { console.warn('公告获取失败:', e.message); } } function showAnnouncement(content, type) { if (announcementShown) return; const panel = document.getElementById('sl3-panel'); if (!panel) return; const old = document.getElementById('sl3-announcement'); if (old) old.remove(); const div = document.createElement('div'); div.id = 'sl3-announcement'; const iconMap = { info: 'ℹ️', warning: '⚠️', success: '✅' }; div.innerHTML = ` ${esc(content)} 暂缓1小时 ✕ `; const statusBar = document.getElementById('sl3-statusbar'); if (statusBar) { statusBar.parentNode.insertBefore(div, statusBar.nextSibling); } else { panel.insertBefore(div, panel.firstChild); } document.getElementById('ann-close').addEventListener('click', function() { div.remove(); announcementShown = true; }); document.getElementById('ann-snooze').addEventListener('click', function() { localStorage.setItem('sl3_announcement_close_time', Date.now().toString()); div.remove(); announcementShown = true; log('公告已暂缓1小时', 'info'); }); announcementShown = true; } // ============================================================ // 🖥️ 渲染函数 // ============================================================ function effectiveLearnedHours(c) { var plat = (c.learnedHours || 0); var localSec = 0; (c.videos || []).forEach(function(v) { if (v.vstatus === 2 || v.learned) localSec += (v.dur || 0); }); return Math.max(plat, localSec / 3600); } function courseReachedCap(c) { var cap = c.capped ? parseFloat(c.maxPeriodRaw) : ELECTIVE_CREDIT_CAP_H; return effectiveLearnedHours(c) >= cap; } function electiveGroupMet() { var courses = window.__sl3.courses || []; var total = 0, any = false; courses.forEach(function(c) { if (c.kind !== 'elective') return; total += effectiveLearnedHours(c); var cap = c.capped ? parseFloat(c.maxPeriodRaw) : ELECTIVE_CREDIT_CAP_H; if (effectiveLearnedHours(c) >= cap) any = true; }); return any || total >= ELECTIVE_CREDIT_CAP_H; } function render() { var l = document.getElementById('sl3-list'); if (!l) return; var courses = window.__sl3.courses || []; if (courses.length === 0) { l.innerHTML = '