// ==UserScript==
// @name 超星学习通|知到智慧树 免费网课助手 - 学神助手|自动答题|免费搜题|全平台刷课|海量题库
// @namespace IPYIWEI
// @version 6.0.8
// @author IPYIWEI
// @keywords 超星学习通,知到智慧树,学习通,智慧树,免费刷课,自动答题,免费搜题,刷课脚本,网课助手,网课答案,油猴脚本,chaoxing,zhihuishu
// @description 超星学习通、知到智慧树免费网课助手(刷课脚本):自动刷课、自动答题、视频倍速、自动切换任务点,海量题库每日更新,支持免费搜题查网课答案。同时支持青书学堂、出头科技、雨课堂、云班课、智慧职教、国家开放大学、中国大学MOOC、U校园、学堂在线、绎通云、山东专技、西财在线|DeepSeek AI 兜底作答|字体加密解析
// @description:zh-CN 超星学习通、知到智慧树免费网课助手(刷课脚本):自动刷课、自动答题、视频倍速、自动切换任务点,海量题库每日更新,支持免费搜题查网课答案。同时支持青书学堂、出头科技、雨课堂、云班课、智慧职教、国家开放大学、中国大学MOOC、U校园、学堂在线、绎通云、山东专技、西财在线|DeepSeek AI 兜底作答|字体加密解析
// @homepageURL https://xs.openget.cn/
// @supportURL https://xs.openget.cn/user.html
// @license Proprietary
// @antifeature payment 部分高级功能需付费(AI 答题额度、自有模型)
// @original-author enncy
// @original-license MIT
// @original-script https://github.com/ocsjs/ocsjs
// @changelog v6.0.8 (2026-10-31)
// @changelog 1. 与油猴版对齐版本号,无功能变化
// @changelog v6.0.7 (2026-10-31)
// @changelog 1. 按脚本猫规则补充付费功能标注(@antifeature payment)与开源引用声明(@original-*)
// @changelog 2. 精简 @keywords,去掉堆砌的关键词
// @changelog v6.0.6 (2026-10-31)
// @changelog 1. 脚本描述补充「刷课脚本、学习通刷课、免费搜题、网课答案」等搜索关键词,功能不变
// @changelog v6.0.5 (2026-10-31)
// @changelog 1. 脚本更名为「超星学习通|知到智慧树 免费网课助手」,提升学习通、超星、智慧树搜索曝光,功能不变
// @changelog v6.0.4 (2026-10-31)
// @changelog 1. 设置页、答题页的面板高度收紧,内容多时在面板里滚动,不再把窗口撑得很高
// @changelog 2. 智慧树「掌握度」答题提速:点击、等待、翻页的节奏都压紧了,同一页答完更快
// @changelog 3. 开始答题前批量取题库答案,一次最多 20 题:题库里有的直接作答,没有的照常问 AI,速度更快、准确率不变
// @changelog 4. 六套主题配色统一,首页按钮去掉紫色渐变
// @changelog v6.0.3 (2026-10-30)
// @changelog 1. 修复:脚本自动切页签时会把你正在看的「设置 / 搜题 / 记录」抢走,现在只有停在首页或日志时才自动切
// @changelog 2. 智慧树答题页按当前页面自动换内容:答题、考试、测验页显示答题,掌握度页显示掌握度,不再固定放切换按钮
// @changelog v6.0.2 (2026-10-30)
// @changelog 1. 智慧树「掌握度」合并进「答题」:答题页顶部多了「答题 / 掌握度满分」切换,菜单栏少一个重复项
// @changelog 2. 「立即更新」现在会打开脚本安装地址,油猴 / 脚本猫直接弹更新页,不再跳到一串源码
// @changelog 3. 脚本更新地址换到自己的服务器,国内访问更快,不用再连 GitHub
// @changelog 4. 智慧树 AI 随堂练习改为按题库答案作答,多选题也会全部选上(原来是随机点一个,多选永远选不全)
// @changelog v6.0.1 (2026-10-30)
// @changelog v6.0.0 (2026-10-30)
// @changelog 1. 智慧树新版作业页修复:以前答完一题得刷新页面才能接着答,现在答完自动点「下一题」、等新题出来继续
// @changelog 2. 页面按钮结构变了也能找到「下一题」(按文字找),找不到也不再中断整个答题流程
// @changelog 1. 新平台新增「自动答题」:进入作业或考试页点扫描,脚本抓题、后端给答案、自动填到选项上
// @changelog 2. 单选、多选、判断、填空、简答都能处理,题目在 iframe 里也能抓到
// @changelog 3. 只自动填答案,不替你点提交;答完自己检查再交卷
// @changelog 4. 识别不到题目时点「诊断」,把页面结构写进日志,方便按真实页面补适配
// @changelog 5. 超星 / 智慧树 / 青书 / 出头 的答题页原样保留,逻辑一行未改
// @changelog 6. 雨课堂、中国大学MOOC 按真实页面结构精确识别题目,识别不到时自动退回通用识别
// @changelog 7. 云班课、绎通云、国家开放大学、西财在线同样按真实页面结构识别;电大考试站 ouchn.edu.cn 也纳入
// @changelog 8. 脚本更名「学神助手·网课助手|全平台自动刷课」,14 个平台的名称、简称和网址都写进关键词,搜哪个平台都能出来
// @changelog v5.6.3 (2026-10-30)
// @changelog 1. 新增 10 个平台:雨课堂、云班课、智慧职教(含 AI 版 / 2 版)、国家开放大学(含 syxy 版)、中国大学 MOOC、U 校园、学堂在线、绎通云、山东专技、西财在线
// @changelog 2. 视频助手:进入课程页自动播放、播完自动点下一节、文档页自动向下滚动,播放器在 iframe 里也能找到
// @changelog 3. 新平台不显示「答题」页签(还没有做这些平台的抓题作答),「搜题」「记录」照常可用
// @changelog 4. 不再单独放「视频」页签,倍速和翻页直接复用「设置」里的「视频倍速」「自动切换章节」,新平台在设置里多一个「视频助手」开关
// @changelog 5. 设置项按平台显示:当前平台没有的功能不出现,名称里不再带「(智慧树)」「(新增平台)」这类后缀
// @changelog 6. 超星 / 智慧树 / 青书 / 出头 的逻辑一行未改,原有页面行为不变
// @changelog v5.6.2 (2026-10-29)
// @changelog 1. 新增「掌握度满分」(智慧树):按知识点逐个进入练习,答对记住答案、答错换下一组选项重答,一直刷到 100%
// @changelog 2. 掌握度答案按课程存在本地:可导出 / 导入纯文字题库,也能导出未入库清单
// @changelog 3. 智慧树新增「掌握度」面板:开始 / 继续、暂停、进度、题库导入导出、运行记录
// @changelog 4. 设置新增「掌握度满分(智慧树)」开关;关闭后掌握度页仍走原来的答题流程
// @changelog 5. 可用题库优先:先问题库和 AI,答不出来再按选项组合猜,尽量少消耗答题次数
// @changelog 6. 修复:智慧树面板里的「队列」「掌握度」页签点开是空白(面板不再依赖运行时模板编译)
// @changelog 7. 修复:导出的题库文件中文乱码(补齐 BOM)、导入题库会覆盖别的标签页进度、中途换页后重复刷同一个知识点
// @changelog 8. 以上改动只涉及智慧树,超星 / 青书 / 出头 不受影响
// @changelog v5.6.1 (2026-10-29)
// @changelog 1. 智慧树答题补齐:掌握度页可自动翻页、每题等题干渲染完再解析、题干取不到时只看可见文字、作业页也能抓到试卷接口
// @changelog 2. 智慧树弹题:弹窗出现就处理(不再等视频暂停)、遇到验证码先等、读不到页面答案时兜底作答、AI 智慧课补上弹题处理
// @changelog 3. 新增「视频助手」「答题助手」两个开关(仅智慧树),关掉后进对应页面不自动开始
// @changelog 4. 新增课程队列:在「队列」页勾选课程,按顺序一门门刷完自动切课
// @changelog 5. 以上改动只涉及智慧树,超星 / 青书 / 出头 不受影响
// @changelog 6. 移除早已没有入口的「问 AI」聊天组件,脚本内不再引用任何第三方 AI 服务
// @changelog v5.6.0 (2026-10-28)
// @changelog 1. 新增「青书学堂」支持:课件学习时长接口上报、作业自动作答(默认只保存不交卷)
// @changelog 2. 新增「出头科技」支持:章节学习时长上报、考试自动作答并逐题核对(交卷需手动)
// @changelog 3. 两个新平台均为接口直通,超星 / 智慧树 原有功能不受影响
// @changelog 4. 修复智慧树多选题答不全:题型统一归一化、按页面复选框识别多选,选项逐项点击并回读确认,不再漏选
// @changelog v5.5.2 (2026-10-27)
// @changelog 1. 适配智慧树新版前端检测:视频倍速钳制 1.8x(平台红线)、考试页 XHR hook 原生伪装,答题/视频页不再弹「检测到异常脚本」
// @changelog 1. 「手动选择」新增处理方式:等待我选择后继续 / 跳过此题(不自动提交)
// @changelog 2. 等待模式:遇到相似题停下整条答题链,你在「题目结果」里点选后自动继续答后面的题
// @changelog 3. 跳过模式:该题留空、本轮不自动提交,等你有空自己处理
// @changelog 4. 还有题等你选择时,自动提交与自动切章都会暂停,选完自动补上,不会再被刷走
// @changelog v5.5.0 (2026-10-19)
// @changelog 1. 题库「相似题」不再卡住:新增设置项「题库匹配方式」,可选自动选最优 / 手动选择
// @changelog 2. 自动选最优:按题面 6 : 选项 3 : 题型 1 加权算相似度,≥90% 才算同一题直接作答,低于阈值交给 AI 重答
// @changelog 3. 手动选择:命中相似题时不再乱答,作业入口列出候选(最多 5 条、标注相似度),点一下即填入
// @changelog 4. 手动选题不扣答题次数,只有真正拿到答案才计入额度
// @changelog 5. 修复题库回退检索:去符号后搜不到时改用原始题干再搜,减少明明有题却匹配不到
// @changelog v5.4.0 (2026-09-22)
// @changelog 1. 新增「跳过已完成章节」开关:自动切章按目录完成状态优先跳到未完成章节
// @changelog 2. 任务点完成统一确认:监听任务上报与完成图标,未确认完成时停止自动跳转并提示手动确认
// @changelog 3. 视频/音频增加播放看门狗,暂停自动恢复重试;播完等待平台确认,超时刷新只刷一次
// @changelog 4. 文档(PPT/DOC/PDF)任务点重写:完成入口、幻灯翻页、旧版PPT滚动,完成后确认
// @changelog 5. 电子书与计时阅读按要求时长保持阅读,多轮确认完成
// @changelog 6. 任务链逐条容错,同一 iframe 任务不重复处理
// @changelog v5.3.12 (2026-09-22)
// @changelog 1. 视频播完后若任务点未标记完成,立即刷新页面确认,避免干等卡住切章
// @changelog 2. 同一任务只自动刷新一次,刷新后复查完成态再继续切章
// @changelog v5.3.11 (2026-09-22)
// @changelog 1. 修复切章入口及完成状态检测,执行前复核各自动操作开关
// @changelog 2. 定时刷新按开关控制,一键关闭包含定时刷新
// @changelog 3. 接通自定义编辑框开关,保留原生划词选区;开关值原样保留
// @changelog v5.3.10 更新内容:
// @changelog 1. 倍速改用播放器真实速度,支持重复修改,播放器重置后自动恢复
// @changelog 2. 修复播放启动失败后无法重试,增加页面恢复、断网恢复后的播放检查
// @changelog 3. 定时刷新默认关闭;已有配置开启时,播放中/答题中也不会刷新页面
// @changelog 4. 多选逐项核验,漏选不计成功,重试时先清理旧选项
// @changelog 5. 图片上传保留选项与图片的对应关系,缺图时明确报错
// @changelog 6. 请求限流自动等待重试,统一前后端超时预算
// @changelog 7. 答题统计默认当天,支持按历史日期区间查询
// @changelog v5.3.9 更新内容:
// @changelog 1. 答题小结不再写入「余额不足」:欠费不是答题卡点,不进填充率/卡点榜
// @changelog 2. 拿到答案但点不上时,不再被后续余额不足盖掉真实原因
// @changelog 3. 超星选项匹配与点击加固:兼容字母前缀差异,并尝试点 input/字母链
// @changelog v5.3.8 更新内容:
// @changelog 1. 填充不再谎报:点击后回读选中态,没点上就记「没填上」,不会看着填了其实还是空的
// @changelog 2. 填空题按每个空分别填写,多余的答案不再挤进第一个空
// @changelog 3. 答案解析加固:列表形式的答案能拆开,英文单词不会被误当成选项字母
// @changelog 4. 每答完一卷回传一条小结(共几题、填上几题、哪几题没填上),后台按题型和平台定位卡点
// @changelog v5.3.7 更新内容:
// @changelog 1. 渠道专属版加固:代理商未配置店铺链接时,脚本不再露出官方购买入口,续费只走渠道发放的卡密激活
// @changelog 2. 代理商门户可自助填写店铺链接,保存后重新下载的脚本即跳该地址
// @changelog 3. 官方版功能与行为无变化
// @changelog v5.3.6 更新内容:
// @changelog 1. 修复图片题漏传:纯图题干不再被跳过,图片选项会采集并发送给模型
// @changelog 2. 智慧树 iframe 改为绑定题目 DOM,选项图/题干图都能下载
// @changelog 3. 超星图片选项按文件名勾选;填不上则跳过题库改走模型
// @changelog v5.3.5 更新内容:
// @changelog 1. 修复多选题偶发只填一项:后端不再把 A,B 截成首选项,考试/智慧树页按复选框识别多选
// @changelog 2. 智慧树 iframe 支持 ABD / A,B,D 及 X型/不定项多选填充
// @changelog v5.3.4 更新内容:
// @changelog 1. 修复无扩展名图片URL(超星部分CDN图)无法匹配填充的问题:图片比对改用URL末段哈希,兼容有无扩展名
// @changelog 2. 图片匹配加固:短哈希仅精确比对防误选,http/https与查询串差异不再影响匹配
// @changelog v5.3.3 更新内容:
// @changelog 1. 彻底修复多选题填充:新增字母答案(A/ABD)按选项序号直接映射,考试页与超星/智慧树作业页全覆盖
// @changelog 2. 后端多选答案改返回结构化数组,支持E及更多选项字母,修复5个及以上选项无法匹配的问题
// @changelog v5.3.2 更新内容:
// @changelog 1. 彻底修复多选题答案拆分问题:不再依赖分隔符切割,改用子串匹配,答案含任何标点都能正确全选
// @changelog v5.3.1 更新内容:
// @changelog 1. 修复多选题答案无分隔符(如ABCD)无法全选的问题
// @changelog 2. 修复图片选项匹配失败:改用文件名提取比较,解决URL被normalize清空导致无法匹配
// @changelog 3. 修复月卡用户答题时显示"包月权益生效"而非正确答案的问题
// @changelog v5.3.0 更新内容:
// @changelog 1. 修复含图片题目 AI 无法识别的问题:答题时自动提取题目中的图片并转 base64 发送给 AI 模型
// @changelog 2. 后端同步优化:图片下载失败时自动跳过并提示,不再因无效图片链接导致 AI 报错
// @changelog v5.2.9 更新内容:
// @changelog 1. 新增脚本自动检测更新功能,发现新版本顶部提示一键更新
// @changelog 2. 兼容 Tampermonkey / ScriptCat / Greasemonkey 三大脚本管理器
// @changelog v5.2.8 更新内容:
// @changelog 1. 修复包月用户答题菜单答案显示"包月权益生效,本题不扣点"的问题,后端返回200但无答案时正确提示"未找到答案"
// @changelog v5.2.7 更新内容:
// @changelog 1. 网页搜题未命中提示优化:后端返回 200 但无答案时,提示"未找到答案"而非"本地题库未命中"
// @changelog v5.2.6 更新内容:
// @changelog 1. 网页搜题升级:搜题结果返回题库中所有相关题目与答案(多结果展示),与后台查询一致,解决只返回单条的问题
// @changelog 2. 网页搜题免费、登录后可用、每日限 30 次(由服务端统一控制)
// @changelog v5.2.5 更新内容:
// @changelog 1. 优化 AI 答题超时处理:后端自动重试/切换模型耗时较长时,前端答题请求不再提前超时(120s→240s、180s→300s),重试成功后自动填充答案
// @changelog v5.2.4 更新内容:
// @changelog 1. 账号登录移至标题栏下方常驻,切换任意页面Tab登录状态均可见
// @changelog 2. 视频倍速支持最高16倍,可在设置中自定义,设置后即时生效无需刷新页面
// @changelog 3. 首页新增"QQ频道"入口,一键加入腾讯频道
// @changelog 4. 最小化悬浮球及旋转光晕缩小,界面更精简
// @changelog 5. 修复视频倍速设置偶发失效问题
// @changelog v5.2.3 更新内容:
// @changelog 1. 账号自动登录稳定性优化(仅 401/403 清除登录记忆,网络异常保留登录状态)
// @changelog 2. 登录 Token 有效期延长至 180 天,减少频繁重新登录
// @changelog v5.2.2 更新内容:
// @changelog 1. 优化账号登录记忆:仅在 Token 真正失效时才要求重新登录,网络波动不再误清登录状态
// @changelog 2. 后端登录 Token 有效期延长至 180 天,减少频繁重新登录
// @changelog v5.2.1 更新内容:
// @changelog 1. 新增江西开放大学继续教育平台(jxrtvu.com)域名支持
// @changelog v5.2.0 更新内容:
// @changelog 1. 首页新增"推广返利"入口,一键跳转推广赚钱页面
// @changelog 2. 账号登录区移除推广按钮,精简界面
// @changelog 3. 推广返利说明动态读取后台配置(佣金比例/冷却天数/最低提现)
// @changelog 4. 后台推广数据统计新增已提现/待审核金额卡片
// @changelog 5. 提现申请列表新增今日/已通过/待审核金额合计
// @changelog 6. 收款信息美化:支付宝/微信双卡片布局,单图替换模式
// @changelog 7. 提现申请强制校验收款二维码,未上传不可提交
// @changelog 8. 修复admin页面推广返利模块渲染异常
// @icon https://xs.openget.cn/static/icon.jpg
// @match *://*.chaoxing.com/*
// @match *://*.neauce.com/*
// @match *://*.zhihuishu.com/*
// @match *://*.edu.cn/*
// @match *://*.jxrtvu.com/*
// @match *://*.o-learn.cn/*
// @match *://*.qingshuxuetang.com/*
// @match *://*.web2.superchutou.com/*
// @match *://*.yuketang.cn/*
// @match *://*.mosoteach.cn/*
// @match *://*.icve.com.cn/*
// @match *://*.ouchn.cn/*
// @match *://*.ouchn.edu.cn/*
// @match *://*.icourse163.org/*
// @match *://*.unipus.cn/*
// @match *://*.xuetangx.com/*
// @match *://*.ytccr.com/*
// @match *://*.treewises.com/*
// @match *://*.swufe-online.com/*
// @require https://xs.openget.cn/libs/vue/3.4.31/vue.global.prod.js
// @require https://xs.openget.cn/libs/vue-demi/0.14.7/index.iife.js
// @require data:application/javascript,window.Vue%3DVue%3B
// @require https://xs.openget.cn/libs/pinia/2.1.7/pinia.iife.min.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/dayjs.min.js
// @require https://xs.openget.cn/libs/dompurify/3.1.6/purify.min.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/customParseFormat.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/weekday.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/localeData.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/weekOfYear.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/weekYear.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/advancedFormat.js
// @require https://xs.openget.cn/libs/dayjs/1.11.10/plugin/quarterOfYear.js
// @require https://xs.openget.cn/libs/ant-design-vue/4.2.6/antd.min.js
// @require https://xs.openget.cn/libs/blueimp-md5/2.19.0/js/md5.min.js
// @require https://xs.openget.cn/libs/rxjs/7.8.1/rxjs.umd.min.js
// @require https://xs.openget.cn/libs/marked/16.1.1/lib/marked.umd.min.js
// @resource ttf https://xs.openget.cn/libs/ttf/table.json
// @resource ttf2 https://xs.openget.cn/libs/ttf2/table.json
// @connect chaoxing.com
// @connect zhihuishu.com
// @connect xs.openget.cn
// @connect *.o-learn.cn
// @connect qingshuxuetang.com
// @connect web2.superchutou.com
// @connect *.webtrn.cn
// @connect 127.0.0.1
// @connect localhost
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_info
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM.openInTab
// @grant unsafeWindow
// @run-at document-start
// @tag 自动学习
// @tag AI答题
// @tag 超星学习通
// @tag 智慧树
// @tag 网课
// ==/UserScript==
(e=>{globalThis.__abcCSS=e})(' .theme-light{--t-bg:#f5f6f8;--t-surface:#fff;--t-surface2:#f0f2f5;--t-border:#e2e5ea;--t-border2:#d1d5db;--t-text:#1a1a2e;--t-text2:#4b5563;--t-text3:#6b7280;--t-primary:#2563eb;--t-primary-bg:#eff6ff;--t-primary-border:#bfdbfe;--t-primary-2:#1d4ed8;--t-accent:#1d4ed8;--t-success:#16a34a;--t-warning:#b45309;--t-danger:#dc2626;--t-info:#0891b2;--t-pink:#db2777;--t-shadow:rgba(0,0,0,.08);--t-shadow2:rgba(0,0,0,.04)}.theme-dark{--t-bg:#0d1117;--t-surface:#161b22;--t-surface2:#1c2330;--t-border:#30363d;--t-border2:#484f58;--t-text:#e6edf3;--t-text2:#9198a1;--t-text3:#6e7681;--t-primary:#3b82f6;--t-primary-bg:rgba(59,130,246,.1);--t-primary-border:rgba(59,130,246,.3);--t-primary-2:#2563eb;--t-accent:#2563eb;--t-success:#3fb950;--t-warning:#d29922;--t-danger:#f85149;--t-info:#22d3ee;--t-pink:#f472b6;--t-shadow:rgba(0,0,0,.3);--t-shadow2:rgba(0,0,0,.15)}.theme-graphite{--t-bg:#1c1c1e;--t-surface:#2c2c2e;--t-surface2:#3a3a3c;--t-border:#3d3d41;--t-border2:#545458;--t-text:#f2f2f7;--t-text2:#a1a1aa;--t-text3:#98989f;--t-primary:#0a84ff;--t-primary-bg:rgba(10,132,255,.16);--t-primary-border:rgba(10,132,255,.4);--t-primary-2:#0060df;--t-accent:#0060df;--t-success:#30d158;--t-warning:#ff9f0a;--t-danger:#ff453a;--t-info:#64d2ff;--t-pink:#ff375f;--t-shadow:rgba(0,0,0,.5);--t-shadow2:rgba(0,0,0,.3)}.theme-mist{--t-bg:#eef2f8;--t-surface:#ffffff;--t-surface2:#e7edf6;--t-border:#dbe3ee;--t-border2:#c7d2e0;--t-text:#1d2733;--t-text2:#4d5a6a;--t-text3:#64707f;--t-primary:#2f6fd0;--t-primary-bg:#e9f0fc;--t-primary-border:#c4daf6;--t-primary-2:#2459ab;--t-accent:#2459ab;--t-success:#1f9d55;--t-warning:#b26a00;--t-danger:#d02f2f;--t-info:#0f7f9e;--t-pink:#c73d7a;--t-shadow:rgba(23,42,71,.12);--t-shadow2:rgba(23,42,71,.06)}.theme-sand{--t-bg:#f6f2ea;--t-surface:#fffdf9;--t-surface2:#f2ebe0;--t-border:#e7ded0;--t-border2:#d8ccb9;--t-text:#2a251e;--t-text2:#5a5348;--t-text3:#7c7264;--t-primary:#a4632c;--t-primary-bg:#f7ede2;--t-primary-border:#e6cfb5;--t-primary-2:#8a4f1f;--t-accent:#8a4f1f;--t-success:#3f7d4e;--t-warning:#a86a00;--t-danger:#b23a2f;--t-info:#2f7f8a;--t-pink:#b04a6d;--t-shadow:rgba(74,58,36,.14);--t-shadow2:rgba(74,58,36,.07)}.theme-glass{--t-bg:rgba(248,249,251,.55);--t-surface:rgba(255,255,255,.55);--t-surface2:rgba(243,245,249,.6);--t-border:rgba(255,255,255,.6);--t-border2:rgba(0,0,0,.1);--t-text:#1d1d1f;--t-text2:#4b4b52;--t-text3:#6e6e75;--t-primary:#0071e3;--t-primary-bg:rgba(0,113,227,.1);--t-primary-border:rgba(0,113,227,.28);--t-primary-2:#0058b8;--t-accent:#0058b8;--t-success:#248a3d;--t-warning:#b45f00;--t-danger:#d70015;--t-info:#0a7ea4;--t-pink:#d6336c;--t-shadow:rgba(0,0,0,.16);--t-shadow2:rgba(0,0,0,.06)}.main-box.theme-graphite{border-radius:12px;box-shadow:0 14px 40px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.06)}.main-box.theme-mist{border-radius:12px;box-shadow:0 14px 38px rgba(23,42,71,.12),0 0 0 1px rgba(255,255,255,.9)}.main-box.theme-sand{border-radius:12px;box-shadow:0 14px 38px rgba(74,58,36,.16),0 0 0 1px rgba(255,253,247,.9)}.main-box.theme-glass{position:relative;z-index:1;background:radial-gradient(130% 90% at 0% 0%,rgba(255,255,255,.6),rgba(255,255,255,0) 62%),linear-gradient(165deg,rgba(252,254,255,.52),rgba(236,243,252,.38));-webkit-backdrop-filter:blur(34px) saturate(190%);backdrop-filter:blur(34px) saturate(190%);border-radius:14px;box-shadow:0 20px 50px rgba(0,0,0,.2),0 0 0 1px rgba(255,255,255,.55),inset 0 1px 0 rgba(255,255,255,.85)}.main-box.theme-glass:hover{box-shadow:0 24px 60px rgba(0,0,0,.24),0 0 0 1px rgba(255,255,255,.7),inset 0 1px 0 rgba(255,255,255,.9)}.main-box.theme-glass .box-header{background:rgba(255,255,255,.34);border-bottom:1px solid rgba(255,255,255,.6);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%)}.main-box.theme-glass .login-bar{background:rgba(255,255,255,.32)!important;border-bottom-color:rgba(255,255,255,.6)!important}.main-box.theme-glass .ant-tabs{background:rgba(238,244,255,.44)!important;border-right:1px solid rgba(255,255,255,.55)!important;-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%)}.main-box.theme-glass .ant-tabs-tab{border-radius:8px!important}.main-box.theme-glass .ant-tabs-tab-active{background:rgba(0,113,227,.12)!important}.main-box.theme-glass .ant-tabs-tab-active:after{display:none}.theme-menu{position:absolute;top:38px;right:8px;z-index:100002;min-width:140px;padding:6px;border-radius:12px;background:var(--t-surface);border:1px solid var(--t-border);box-shadow:0 14px 36px var(--t-shadow);-webkit-backdrop-filter:blur(24px) saturate(180%);backdrop-filter:blur(24px) saturate(180%)}.theme-menu-title{padding:4px 10px 6px;font-size:11px;color:var(--t-text3);letter-spacing:.4px}.theme-menu-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:8px;font-size:12px;color:var(--t-text);cursor:pointer;white-space:nowrap;transition:background .15s,color .15s}.theme-menu-item:hover{background:var(--t-primary-bg);color:var(--t-primary)}.theme-menu-item.active{background:var(--t-primary-bg);color:var(--t-primary);font-weight:600}.theme-menu-item .dot{width:12px;height:12px;border-radius:50%;flex:none;border:1px solid var(--t-border2)}.theme-menu-item .dot-dark{background:#0d1117;border-color:rgba(255,255,255,.35)}.theme-menu-item .dot-light{background:#fff;border-color:rgba(0,0,0,.14)}.theme-menu-item .dot-glass{background:linear-gradient(135deg,rgba(255,255,255,.95),rgba(170,200,240,.85))}.theme-menu-item .dot-graphite{background:#3c3c3e;border-color:#9a9aa0}.theme-menu-item .dot-mist{background:#dbe6f4;border-color:#c7d2e0}.theme-menu-item .dot-sand{background:#f3e7d3;border-color:#d8ccb9}.theme-glass .theme-menu{background:rgba(255,255,255,.72);border-color:rgba(255,255,255,.7);box-shadow:0 16px 40px rgba(0,0,0,.16),inset 0 1px 0 rgba(255,255,255,.9)}.main-box.theme-glass.theme-glass-flat{background:linear-gradient(165deg,#f7faff,#e4edfa);-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:0 16px 42px rgba(28,58,110,.18),0 0 0 1px rgba(255,255,255,.9),inset 0 1px 0 #fff}.main-box.theme-glass.theme-glass-flat .box-header{background:#eaf1fb;border-bottom-color:#dbe6f4}.main-box.theme-glass.theme-glass-flat .login-bar{background:#f0f5fd!important;border-bottom-color:#dbe6f4!important}.main-box.theme-glass.theme-glass-flat .ant-tabs{background:#e3ecf9!important;border-right-color:#dbe6f4!important;-webkit-backdrop-filter:none;backdrop-filter:none}.theme-glass.theme-glass-flat .theme-menu{background:#f8fbff;border-color:#e3ecf9}.glass-aura{position:absolute;inset:-10px;z-index:0;border-radius:22px;pointer-events:none;opacity:.85;filter:blur(26px);background:radial-gradient(56% 50% at 14% 6%,rgba(122,184,255,.4),rgba(122,184,255,0) 70%),radial-gradient(44% 42% at 92% 20%,rgba(168,150,255,.26),rgba(168,150,255,0) 72%),radial-gradient(58% 54% at 78% 98%,rgba(120,225,210,.3),rgba(120,225,210,0) 74%);animation:glassAuraDrift 18s ease-in-out infinite alternate}@keyframes glassAuraDrift{0%{transform:translate3d(0,0,0) scale(1)}100%{transform:translate3d(0,-8px,0) scale(1.05)}}.theme-glass-flat .glass-aura{display:none}.log-wrap{font:13px "Segoe UI",system-ui,-apple-system,sans-serif}.log-wrap{display:flex;flex-direction:column;height:100%}.log-wrap .console{flex:1;min-height:200px;max-height:calc(100vh - 120px);width:100%;box-sizing:border-box;overflow-y:scroll;background:var(--t-surface2);padding:10px 12px;color:var(--t-text2);font-size:12px;border-radius:8px;border:1px solid var(--t-border)}.log-wrap .console>div{padding:3px 0;line-height:1.6}.log-wrap .console>div>span:first-child{color:var(--t-text3);font-variant-numeric:tabular-nums}.log-wrap .console>div>span:nth-child(2){font-size:11px;letter-spacing:.2px}.log-wrap .console div span{margin:0 3px}.log-wrap .console div span .module{background:var(--t-primary);color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}.log-wrap .console .log{color:var(--t-text2)}.log-wrap .console .warning{color:var(--t-warning);font-weight:600}.home-wrap[data-v-137ea562]{font:14px "Segoe UI",system-ui,-apple-system,sans-serif;color:var(--t-text)}.home-wrap a{color:var(--t-primary);text-decoration:none}.home-wrap a:hover{text-decoration:underline}.home-wrap ol li{line-height:26px;list-style-type:decimal;list-style-position:inside;color:var(--t-text2)}.home-wrap .tips ul li span{color:var(--t-warning)}.home-wrap .hero{padding:16px 14px;margin:8px;background:linear-gradient(135deg,var(--t-primary),var(--t-primary-2));border-radius:8px;color:#fff;text-align:center}.home-wrap .hero .hero-title{font-size:18px;font-weight:700;margin-bottom:4px}.home-wrap .hero .hero-sub{font-size:12px;opacity:.85}.home-wrap .home-section{margin:8px;padding:12px 14px;background:var(--t-surface);border:1px solid var(--t-border);border-radius:8px}.home-wrap .home-section .section-title{font-size:13px;font-weight:700;color:var(--t-text);margin-bottom:8px;display:flex;align-items:center;gap:4px}.home-wrap .home-section ul{list-style:none;padding:0;margin:0}.home-wrap .home-section ul li{list-style:none;padding:4px 0;color:var(--t-text2);font-size:12px;line-height:1.6}.collapsible-section{cursor:pointer;user-select:none;transition:background .15s}.collapsible-section:hover{background:var(--t-surface2)}.collapsible-section .section-title{justify-content:space-between;gap:8px;margin-bottom:0}.collapse-icon{font-size:14px;color:var(--t-text3);font-weight:700;width:22px;height:22px;display:flex;align-items:center;justify-content:center;border-radius:4px;background:var(--t-surface2);transition:all .15s}.collapsible-section:hover .collapse-icon{background:var(--t-primary-bg);color:var(--t-primary)}.home-card-btn{display:block;margin-top:8px;padding:10px 12px;background:linear-gradient(135deg,var(--t-primary),var(--t-primary-2));border-radius:8px;color:#fff!important;text-align:center;font-size:13px;font-weight:600;cursor:pointer;text-decoration:none;transition:opacity .15s,transform .15s}.home-card-btn:hover{opacity:.9;transform:translateY(-1px);color:#fff!important}.home-wrap .login-box{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px}.home-wrap .login-box input{height:30px;width:140px;padding:0 8px;border:1px solid var(--t-border2);border-radius:4px;background:var(--t-surface);color:var(--t-text);font-size:12px;outline:none}.home-wrap .login-box input:focus{border-color:var(--t-primary)}.home-wrap .login-box button{height:30px;padding:0 12px;border:none;border-radius:4px;font-size:12px;cursor:pointer;font-weight:500;transition:opacity .15s}.home-wrap .login-box button:hover{opacity:.85}.home-wrap .login-box .btn-primary{background:var(--t-primary);color:#fff}.home-wrap .login-box .btn-ghost{background:var(--t-surface2);color:var(--t-text2);border:1px solid var(--t-border2)}.home-wrap .login-box .btn-outline{background:transparent;color:var(--t-primary);border:1px solid var(--t-primary)}.home-wrap .login-meta{display:flex;gap:8px;align-items:center;margin-top:6px;color:var(--t-text3);font-size:11px;flex-wrap:wrap}.home-wrap .login-msg{margin-top:6px;font-size:12px}.answer-wrap .search-info{-webkit-user-select:none;user-select:none;box-sizing:border-box;display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;border:1px solid var(--t-border2);margin-right:4px;font-weight:600;font-size:11px;background:var(--t-surface2);color:var(--t-text3);flex-shrink:0}.answer-wrap .search-info.default{box-shadow:none}.answer-wrap .search-info.searching{box-shadow:0 0 6px 1px var(--t-primary-border);border-color:var(--t-primary);color:var(--t-primary);animation:pulse 1.2s ease infinite}.answer-wrap .search-info.success{color:#fff;border:0;background:var(--t-success)}.answer-wrap .search-info.ai-success{color:#fff;border:0;background:var(--t-accent)}.answer-wrap .search-info.warning{color:var(--t-warning);border:1px solid var(--t-warning)}.answer-wrap .answer-tips{font-size:12px;color:var(--t-text3)}.answer-wrap .answer-tips span{margin-right:4px}.answer-wrap .answer-tips .box-tips{display:flex;align-items:center}.answer-wrap .answer-tips .box-tips div{margin:0 4px 0 0!important}.answer-wrap .answer-tips .box-tips span{margin-right:8px}.answer-wrap .answer-list{flex-wrap:wrap;color:var(--t-text);display:flex}.answer-wrap .answer-list .search-info{cursor:pointer!important}.answer-wrap .answer-list div{margin:0 8px 8px 0!important;cursor:pointer!important}.answer-wrap .answer-detail{position:relative;max-height:200px;overflow-y:scroll;scroll-behavior:smooth}.answer-wrap .answer-detail .answer-item{margin-bottom:8px}.answer-wrap .answer-detail .answer-content{margin-top:4px;padding:9px 11px;border-radius:6px;background:var(--t-surface2);font-weight:600;color:var(--t-text);border-left:2px solid var(--t-primary);font-size:13px}.answer-wrap .answer-detail .answer-content .module{padding:1px 4px;border-radius:3px;color:var(--t-text3);border:1px solid var(--t-border2);font-size:10px}.ant-tooltip,.ant-select-dropdown{z-index:99999!important}.config-wrap{min-width:420px!important;width:420px!important;max-width:480px;color:var(--t-text);font-size:13px;padding:10px 16px;box-sizing:border-box}.config-wrap .config{margin-bottom:14px;display:flex;align-items:flex-start;gap:12px;min-height:32px}.config-wrap .config .config-name{display:flex;align-items:flex-start;width:120px;flex-shrink:0;font-size:12px;color:var(--t-text2);line-height:1.5;padding-top:7px}.config-wrap .config .config-input{flex:1;display:flex;align-items:center;justify-content:flex-start;gap:6px;min-width:0;padding-top:2px}.config-wrap .config .config-input .ant-select{width:100%!important;max-width:260px}.config-wrap .config .config-input .ant-input{width:100%!important;max-width:260px}.config-wrap .config .config-input .ant-input-number{width:100px!important}.config-wrap .config .config-input .ant-slider{flex:1;max-width:180px!important;margin:0 10px 0 0}.config-wrap .config-input .ant-input,.config-wrap .config-input .ant-input-affix-wrapper,.config-wrap .config-input .ant-input-number,.config-wrap .config-input .ant-select-selector{height:30px;box-sizing:border-box}.config-wrap .config-input .ant-input-number-input{height:28px}.config-wrap .buy-btn{background:var(--t-primary);padding:5px 12px;border-radius:4px;height:28px;color:#fff;cursor:pointer;font-weight:600;border:none;font-size:12px;white-space:nowrap}.config-wrap .buy-btn a{color:#fff}.config-wrap .switch-item{display:inline-flex!important;width:calc(50% - 6px)!important;margin-right:12px;margin-bottom:12px!important;vertical-align:top;box-sizing:border-box;gap:8px}.config-wrap .switch-item .config-name{flex:1;width:auto!important;padding-top:0;line-height:1.5;word-break:break-word}.config-wrap .switch-item .config-input{width:auto!important;flex:none;padding-top:0}.config-wrap .switch-item .ant-switch{margin-top:0!important}.config-wrap .switch-item:nth-of-type(2n){margin-right:0}.ant-message{z-index:999999!important}.online-search-wrap{color:var(--t-text);width:420px;box-sizing:border-box}.online-search-wrap .config{padding:8px 10px 4px;display:flex}.online-search-wrap .config .config-name{width:150px}.online-search-wrap .config .config-input{display:flex;justify-content:right;flex:1}.online-search-wrap .searth-input-wrap{padding:8px 10px 4px;display:flex}.online-search-wrap .searth-input-wrap .title{padding-top:5px}.online-search-wrap .searth-input-wrap .searth-input{margin-left:16px;flex:1}.online-search-wrap .search-btn{padding-right:10px;display:flex;justify-content:right}.online-search-wrap .result-list{margin-top:8px;max-height:calc(100vh - 200px);overflow:auto}.online-search-wrap .result-list .result-item{margin-bottom:10px}.online-search-wrap .result-list .result-item em{font-weight:600;color:var(--t-warning)}.online-search-wrap .result-list .result-answer{margin-top:4px;padding:9px 11px;border-radius:6px;background:var(--t-surface2);font-weight:600;color:var(--t-text);border-left:2px solid var(--t-primary)}div{margin:0}.ai-wrapper{width:420px}.ai-message-role-avatar{margin:0}.ai-message-content{margin:0;padding:9px 11px;border-radius:8px;background:var(--t-surface2);font-size:14px;-webkit-user-select:text;user-select:text;word-break:break-word;border:1px solid var(--t-border);position:relative;transition:all .2s ease}.ai-message-content pre{white-space:pre-wrap;background:var(--t-bg);color:var(--t-text);padding:10px;border-radius:4px;border:1px solid var(--t-border);font-size:13px}.ai-message-container{display:flex;flex-direction:column;align-items:flex-start}.ai-message-container.user-message{align-items:flex-end}.ai-message-list{height:calc(100vh - 160px);max-height:360px;min-height:200px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.ai-message-item{width:100%}.user-avatar{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:4px;border:1px solid var(--t-border);background:var(--t-primary-bg);margin-bottom:4px;font-size:13px}.ai-input-wrapper{padding:8px;margin-top:8px;border-radius:6px;border:1px solid var(--t-border);display:flex;align-items:center;justify-content:center;background:var(--t-surface)}.ai-input-wrapper textarea{flex:1;height:46px;border:none;margin-right:8px;background:transparent;color:var(--t-text);resize:none;outline:none}.send-message-btn{display:flex;align-items:center;justify-content:center;height:34px;border-radius:4px;border:none;padding:0 12px;background:var(--t-primary);color:#fff;cursor:pointer!important;gap:4px;font-weight:600;font-size:13px}.send-message-btn:hover{opacity:.85}.loading-container{display:flex;align-items:center;justify-content:center;gap:4px}.record-wrap{padding-bottom:6px}.record-tip{display:flex;align-items:center;justify-content:space-between;padding:0 8px 6px;color:var(--t-text3);font-size:11px}.record-filter{display:flex;gap:6px;padding:0 8px 8px;flex-wrap:wrap}.record-search-wrap{padding:0 8px 8px}.record-search{display:block;width:100%;box-sizing:border-box;padding:6px 10px;font-size:12px;color:var(--t-text);background:var(--t-surface);border:1px solid var(--t-border2);border-radius:6px;outline:none;transition:border-color .15s,box-shadow .15s}.record-search::placeholder{color:var(--t-text3)}.record-search:focus{border-color:var(--t-primary);box-shadow:0 0 0 2px var(--t-primary-bg)}.filter-btn{padding:4px 10px;color:var(--t-text2);background:var(--t-surface2);border:1px solid var(--t-border);border-radius:6px;cursor:pointer;transition:all .15s ease;font-size:11px}.filter-btn:hover{color:var(--t-primary);border-color:var(--t-primary-border);background:var(--t-primary-bg)}.filter-btn.active{color:var(--t-primary);border-color:var(--t-primary);background:var(--t-primary-bg)}.record-list{display:flex;flex-direction:column;gap:8px;padding:0 8px 6px;max-height:min(460px,calc(100vh - 240px));overflow-y:auto}.clear-btn{color:var(--t-text2);border-color:var(--t-border);background:var(--t-surface2);border-radius:3px;font-size:12px}.clear-btn:hover,.clear-btn:focus{color:var(--t-danger);border-color:var(--t-danger);background:rgba(220,38,38,.06)}.record-item{padding:9px 11px;border:1px solid var(--t-border);border-radius:8px;background:var(--t-surface);transition:border-color .15s}.record-item:hover{border-color:var(--t-border2)}.record-row{display:flex;align-items:flex-start;line-height:20px;font-size:12px}.record-label{width:60px;flex-shrink:0;color:var(--t-primary);font-weight:600;font-size:11px}.record-value{flex:1;color:var(--t-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-content{flex:1!important;min-width:420px!important;width:420px!important;min-height:320px;max-width:480px;max-height:min(640px,calc(100vh - 96px));overflow-y:auto;padding:8px 12px;box-sizing:border-box}.ant-tabs-nav-operations{display:none!important}.box-content{min-width:300px;display:flex!important;flex-direction:row!important;gap:0!important}.separator{display:flex;align-items:center;padding:16px 0 10px;font-size:13px;font-weight:700;color:var(--t-text);border-top:1px solid var(--t-border);margin-top:6px}.separator:first-child{border-top:none;margin-top:0}.tips{padding:10px 12px;background:rgba(217,119,6,.06);border:1px solid rgba(217,119,6,.15);border-radius:8px;width:-webkit-fill-available;margin:4px 0 14px;line-height:22px;letter-spacing:.2px;color:var(--t-warning);font-size:12px;box-sizing:border-box}.theme-dark .tips,.theme-graphite .tips{background:rgba(255,159,10,.1);border-color:rgba(255,159,10,.26)}.theme-glass .tips{background:rgba(180,95,0,.07);border-color:rgba(180,95,0,.2)}.tips button{background:var(--t-warning)!important;border-color:var(--t-warning)!important;color:#fff!important}.tips a{color:var(--t-primary);text-decoration:underline}.backend-dropdown-overlay{z-index:100001!important}.ant-dropdown-menu{background:var(--t-surface)!important;border:1px solid var(--t-border)!important;border-radius:4px!important;box-shadow:0 4px 16px var(--t-shadow)!important;padding:4px 0!important}.ant-dropdown-menu-item{color:var(--t-text)!important;font-size:12px!important;padding:6px 12px!important}.ant-dropdown-menu-item:hover{background:var(--t-primary-bg)!important;color:var(--t-primary)!important}.ant-dropdown-menu-item-divider{background-color:var(--t-border)!important}.main-wrap{position:fixed;top:0;z-index:99999!important;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;pointer-events:auto}.message-container{margin-bottom:4px;position:absolute;bottom:100%;left:50%;width:100%;transform:translate(-50%);min-width:280px}.main-box{padding:0;font:13px "Segoe UI",system-ui,-apple-system,sans-serif;color:var(--t-text);background:var(--t-bg);box-shadow:0 8px 32px var(--t-shadow),0 0 0 1px var(--t-border);border-radius:8px;letter-spacing:0;overflow:hidden;transition:box-shadow .2s;display:flex;flex-direction:column;max-height:calc(100vh - 60px)}.main-box:hover{box-shadow:0 12px 32px var(--t-shadow),0 0 0 1px var(--t-border2)}.main-box .box-header{display:flex;justify-content:space-between;align-items:center;height:36px;cursor:move;padding:0 10px;background:var(--t-surface);border-bottom:1px solid var(--t-border);flex-shrink:0}.main-box .box-header div{margin:0}.main-box .box-header .header-left{display:flex;align-items:center;gap:6px}.main-box .box-header .script-info{-webkit-user-select:none!important;user-select:none!important;font-weight:700;color:var(--t-primary);letter-spacing:0;font-size:13px;display:flex;align-items:center;gap:4px}.main-box .box-header .script-info:before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--t-success);box-shadow:0 0 4px var(--t-success);margin-right:2px}.main-box .box-header .backend-entry{text-decoration:none;color:var(--t-text2);padding:3px 8px;border-radius:4px;cursor:pointer;-webkit-user-select:none!important;user-select:none!important;transition:all .15s ease;font-size:11px;background:var(--t-surface2);border:1px solid var(--t-border)}.main-box .box-header .backend-entry.active,.main-box .box-header .backend-entry:hover{color:var(--t-primary);background:var(--t-primary-bg);border-color:var(--t-primary-border)}.main-box .box-header .theme-toggle{width:28px;height:28px;border-radius:4px;border:1px solid var(--t-border);background:var(--t-surface2);color:var(--t-text2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .15s ease}.main-box .box-header .theme-toggle:hover{color:var(--t-primary);border-color:var(--t-primary-border);background:var(--t-primary-bg)}.main-box .box-header .shrink-btn{width:28px;height:28px;border-radius:4px;border:1px solid var(--t-border);background:var(--t-surface2);color:var(--t-text2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .15s ease}.main-box .box-header .shrink-btn:hover{color:var(--t-danger);border-color:var(--t-danger)}.main-box .box-content{display:flex;flex:1;overflow:hidden;min-height:0}.main-box .box-content.mini{display:none}.mini-ball{position:fixed;display:flex;align-items:center;gap:13px;height:64px;padding:0 30px 0 12px;border-radius:32px;background:linear-gradient(135deg,#0d1117 0%,#161b22 20%,#1a1a2e 40%,#16325c 60%,#1a1a2e 80%,#0d1117 100%);background-size:400% 400%;box-shadow:0 10px 24px rgba(0,0,0,.55),0 0 0 1.5px rgba(59,130,246,.2),0 0 10px rgba(59,130,246,.15),0 0 18px rgba(37,99,235,.09),0 0 26px rgba(59,130,246,.06),inset 0 1px 2px rgba(255,255,255,.08);cursor:pointer;z-index:9999999;user-select:none;transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s ease;animation:miniBallGradient 5s ease infinite,miniBallGlow 2.5s ease-in-out infinite}.mini-ball:hover{transform:scale(1.1) translateY(-4px);box-shadow:0 16px 44px rgba(0,0,0,.65),0 0 0 3px rgba(59,130,246,.35),0 0 20px rgba(59,130,246,.25),0 0 40px rgba(37,99,235,.2),0 0 64px rgba(59,130,246,.15),inset 0 1px 2px rgba(255,255,255,.12)}.mini-ball:active{transform:scale(.94)}.mini-ball-icon{width:48px;height:48px;border-radius:50%;background:rgba(59,130,246,.15);display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:800;letter-spacing:1px;backdrop-filter:blur(10px);box-shadow:inset 0 1px 2px rgba(255,255,255,.15),0 0 18px rgba(59,130,246,.15);animation:miniBallIconPulse 2s ease-in-out infinite;gap:1px}.mini-ball-count{font-size:8px;font-weight:700;color:#60a5fa;text-shadow:0 0 6px rgba(167,139,250,.5);line-height:1}.mini-ball:hover .mini-ball-icon{animation:miniBallIconWiggle .5s ease}.mini-ball-body{display:flex;flex-direction:column;align-items:flex-start;gap:3px}.mini-ball-text{color:#fff;font-size:15px;font-weight:800;white-space:nowrap;text-shadow:0 1px 4px rgba(0,0,0,.3);animation:miniBallTextSlide .5s cubic-bezier(.22,1,.36,1)}.mini-ball-sub{color:rgba(255,255,255,.88);font-size:11px;font-weight:500;white-space:nowrap;text-shadow:0 1px 3px rgba(0,0,0,.25);animation:miniBallTextSlide .5s .1s cubic-bezier(.22,1,.36,1) both}.mini-ball-shine{position:absolute;inset:0;border-radius:32px;overflow:hidden;pointer-events:none}.mini-ball-shine:before{content:"";position:absolute;top:0;left:-100%;width:80%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);transform:skewX(-20deg);animation:miniBallShine 3.5s ease-in-out infinite}.mini-ball-glow{position:absolute;inset:12px;border-radius:20px;background:linear-gradient(135deg,#1a1a2e,#16325c,#1e4f8f,#2f6fb5);filter:blur(5px);opacity:.1;z-index:-1;animation:miniBallGlowRotate 10s linear infinite}.mini-ball-orbits{position:absolute;inset:0;border-radius:32px;overflow:hidden;pointer-events:none}.mini-ball-orbits i{position:absolute;width:6px;height:6px;border-radius:50%;background:#60a5fa;box-shadow:0 0 10px #60a5fa;opacity:.65}.mini-ball-orbits i:first-child{top:10px;right:20px;animation:miniBallOrbit1 2.5s linear infinite}.mini-ball-orbits i:last-child{bottom:10px;left:24px;animation:miniBallOrbit2 3s linear infinite}@keyframes miniBallGradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}@keyframes miniBallGlow{0%,100%{box-shadow:0 9px 22px rgba(0,0,0,.45),0 0 0 1.5px rgba(59,130,246,.18),0 0 10px rgba(59,130,246,.12),0 0 18px rgba(37,99,235,.09),0 0 26px rgba(59,130,246,.06),inset 0 1px 2px rgba(255,255,255,.06)}50%{box-shadow:0 10px 26px rgba(0,0,0,.5),0 0 0 1.5px rgba(59,130,246,.2),0 0 14px rgba(59,130,246,.16),0 0 24px rgba(59,130,246,.1),0 0 32px rgba(37,99,235,.08),inset 0 1px 2px rgba(255,255,255,.08)}}@keyframes miniBallGlowRotate{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@keyframes miniBallIconPulse{0%,100%{box-shadow:inset 0 1px 2px rgba(255,255,255,.15),0 0 0 0 rgba(59,130,246,.2)}50%{box-shadow:inset 0 1px 2px rgba(255,255,255,.15),0 0 0 14px rgba(59,130,246,0)}}@keyframes miniBallIconWiggle{0%,100%{transform:rotate(0)}25%{transform:rotate(-12deg)}75%{transform:rotate(12deg)}}@keyframes miniBallTextSlide{0%{opacity:0;transform:translateX(-14px)}100%{opacity:1;transform:translateX(0)}}@keyframes miniBallShine{0%,100%{left:-100%}50%,70%{left:150%}}@keyframes miniBallOrbit1{0%{transform:translate(0,0) scale(1);opacity:.6}50%{transform:translate(-18px,14px) scale(.6);opacity:.2}100%{transform:translate(0,0) scale(1);opacity:.6}}@keyframes miniBallOrbit2{0%{transform:translate(0,0) scale(.6);opacity:.2}50%{transform:translate(14px,-12px) scale(1);opacity:.6}100%{transform:translate(0,0) scale(.6);opacity:.2}}.ant-btn{border-radius:6px!important;font-size:12px!important;transition:background-color .15s ease,border-color .15s ease,opacity .15s ease}.ant-btn-primary{background:var(--t-primary)!important;border-color:var(--t-primary)!important}.ant-btn-primary:hover{opacity:.85!important}.ant-input,.ant-input-affix-wrapper{background:var(--t-surface)!important;border-color:var(--t-border2)!important;border-radius:6px!important;color:var(--t-text)!important;font-size:12px!important}.ant-input:focus,.ant-input-affix-wrapper-focused{border-color:var(--t-primary)!important;box-shadow:0 0 0 2px var(--t-primary-border)!important}.ant-input:hover,.ant-input-affix-wrapper:hover{border-color:var(--t-border2)!important}.ant-input-number{background:var(--t-surface)!important;border-color:var(--t-border2)!important;border-radius:4px!important;color:var(--t-text)!important;font-size:12px!important}.ant-input-number-input{color:var(--t-text)!important;background:transparent!important}.ant-input-number-focused{border-color:var(--t-primary)!important;box-shadow:0 0 0 2px var(--t-primary-border)!important}.ant-input-number-handler-wrap{background:var(--t-surface2)!important;border-left-color:var(--t-border)!important}.ant-input-number-handler span{color:var(--t-text2)!important}.ant-select-selector{background:var(--t-surface)!important;border-color:var(--t-border2)!important;border-radius:6px!important;color:var(--t-text)!important;font-size:12px!important;min-height:30px!important}.config-wrap .ant-select{width:100%!important;max-width:220px}.ant-select-selection-item{color:var(--t-text)!important}.ant-select-arrow{color:var(--t-text3)!important}.ant-switch-checked{background:var(--t-primary)!important}.config-wrap .ant-switch{margin-top:2px}.ant-card{background:var(--t-surface)!important;border-color:var(--t-border)!important;border-radius:6px!important;color:var(--t-text)!important;font-size:12px!important}.ant-modal-content{background:var(--t-surface)!important;border:1px solid var(--t-border)!important;border-radius:6px!important}.ant-modal-header{background:var(--t-surface)!important;border-bottom:1px solid var(--t-border)!important}.ant-modal-title{color:var(--t-text)!important;font-size:14px!important}.ant-modal-close-icon{color:var(--t-text3)!important}.ant-modal-body{color:var(--t-text)!important}.ant-checkbox-checked .ant-checkbox-inner{background:var(--t-primary)!important;border-color:var(--t-primary)!important}.ant-radio-checked .ant-radio-inner{border-color:var(--t-primary)!important}.ant-radio-inner:after{background:var(--t-primary)!important}.ant-message-notice-content{background:var(--t-surface)!important;border:1px solid var(--t-border)!important;border-radius:4px!important;box-shadow:0 4px 16px var(--t-shadow)!important;color:var(--t-text)!important}.ant-tooltip-inner{background:var(--t-surface)!important;border:1px solid var(--t-border)!important;border-radius:3px!important;font-size:11px!important;color:var(--t-text)!important}.ant-alert{border-radius:6px!important;font-size:12px!important;margin-bottom:12px!important;padding:10px 12px!important}.ant-alert-message{color:var(--t-text)!important;line-height:1.8!important}.ant-alert .ant-btn-primary{background:var(--t-warning)!important;border-color:var(--t-warning)!important}.ant-tabs{display:flex!important;flex-direction:column!important;width:80px!important;flex-shrink:0;border-right:1px solid var(--t-border)!important;background:var(--t-surface)!important;margin:0!important;height:auto!important}.ant-tabs-nav{margin:0!important;width:80px!important;border:none!important}.ant-tabs-nav::before{border:none!important}.ant-tabs-nav-wrap{width:80px!important}.ant-tabs-nav-list{flex-direction:column!important;width:80px!important;gap:0!important}.ant-tabs-tab{padding:10px 4px!important;margin:0!important;justify-content:center!important;width:80px!important;font-size:12px!important;border:none!important;background:transparent!important;border-radius:0!important}.ant-tabs-tab .ant-tabs-tab-btn{font-size:12px!important;text-align:center!important;padding:0!important;color:var(--t-text)!important}.ant-tabs-tab:hover{background:var(--t-primary-bg)!important}.ant-tabs-tab-active{background:var(--t-primary-bg)!important;position:relative}.ant-tabs-tab-active:after{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--t-primary)}.ant-tabs-tab-active .ant-tabs-tab-btn{color:var(--t-primary)!important;font-weight:600!important}.ant-tabs-ink-bar{display:none!important}.ant-tabs-content-holder{display:none!important}.ant-tabs-content{display:none!important}.ant-tabs-top>.ant-tabs-nav::before{border:none!important}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}@keyframes floatPulse{0%,100%{box-shadow:0 4px 16px var(--t-shadow),0 0 0 1px rgba(255,255,255,.1)}50%{box-shadow:0 4px 24px var(--t-shadow),0 0 0 3px var(--t-primary-border)}}@keyframes floatRing{0%{opacity:.6;transform:scale(1)}100%{opacity:0;transform:scale(1.3)}}.main-box ::-webkit-scrollbar{width:6px;height:6px}.main-box ::-webkit-scrollbar-thumb{background:var(--t-border2);border-radius:3px}.main-box ::-webkit-scrollbar-thumb:hover{background:var(--t-text3)}.main-box ::-webkit-scrollbar-track{background:transparent}.main-box .console::-webkit-scrollbar{width:6px}.main-box button:focus-visible,.main-box [role=button]:focus-visible,.main-box a:focus-visible,.main-box input:focus-visible,.main-box textarea:focus-visible{outline:2px solid var(--t-primary);outline-offset:2px}@media (prefers-reduced-motion:reduce){.mini-ball,.mini-ball-icon,.mini-ball-text,.mini-ball-sub,.mini-ball-shine,.mini-ball-glow,.glass-aura,.answer-wrap .search-info.searching{animation:none!important}.main-box *,.mini-ball *{transition-duration:.01ms!important}}@media (max-width:560px){.main-box{max-width:calc(100vw - 12px)}.tab-content{min-width:0!important;width:auto!important;padding:8px!important}.config-wrap{min-width:0!important;width:auto!important;padding:6px 8px 6px 2px!important}.config-wrap .config{gap:8px;margin-bottom:12px}.config-wrap .config .config-name{width:74px!important;font-size:11px!important}.config-wrap .config .config-input{min-width:0}.config-wrap .config .config-input .ant-select,.config-wrap .config .config-input .ant-input{max-width:none!important}.config-wrap .config .config-input .ant-slider{max-width:none!important}.login-bar input{font-size:12px!important}.ant-tabs,.ant-tabs-nav,.ant-tabs-nav-wrap,.ant-tabs-nav-list{width:66px!important}.ant-tabs-tab{width:66px!important;padding:10px 2px!important}.ant-tabs-tab .ant-tabs-tab-btn{font-size:11px!important}} ');
(function (vue, Antd, pinia, marked, rxjs, md5, DOMPurify) {
'use strict';
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
function bound01(n, max) {
if (isOnePointZero(n)) {
n = "100%";
}
var isPercent = isPercentage(n);
n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
if (isPercent) {
n = parseInt(String(n * max), 10) / 100;
}
if (Math.abs(n - max) < 1e-6) {
return 1;
}
if (max === 360) {
n = (n < 0 ? n % max + max : n % max) / parseFloat(String(max));
} else {
n = n % max / parseFloat(String(max));
}
return n;
}
function isOnePointZero(n) {
return typeof n === "string" && n.indexOf(".") !== -1 && parseFloat(n) === 1;
}
function isPercentage(n) {
return typeof n === "string" && n.indexOf("%") !== -1;
}
function boundAlpha(a) {
a = parseFloat(a);
if (isNaN(a) || a < 0 || a > 1) {
a = 1;
}
return a;
}
function convertToPercentage(n) {
if (n <= 1) {
return "".concat(Number(n) * 100, "%");
}
return n;
}
function pad2(c) {
return c.length === 1 ? "0" + c : String(c);
}
function rgbToRgb(r, g, b) {
return {
r: bound01(r, 255) * 255,
g: bound01(g, 255) * 255,
b: bound01(b, 255) * 255
};
}
function hue2rgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * (6 * t);
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
function hslToRgb(h2, s, l) {
var r;
var g;
var b;
h2 = bound01(h2, 360);
s = bound01(s, 100);
l = bound01(l, 100);
if (s === 0) {
g = l;
b = l;
r = l;
} else {
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h2 + 1 / 3);
g = hue2rgb(p, q, h2);
b = hue2rgb(p, q, h2 - 1 / 3);
}
return { r: r * 255, g: g * 255, b: b * 255 };
}
function rgbToHsv(r, g, b) {
r = bound01(r, 255);
g = bound01(g, 255);
b = bound01(b, 255);
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var h2 = 0;
var v = max;
var d = max - min;
var s = max === 0 ? 0 : d / max;
if (max === min) {
h2 = 0;
} else {
switch (max) {
case r:
h2 = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h2 = (b - r) / d + 2;
break;
case b:
h2 = (r - g) / d + 4;
break;
}
h2 /= 6;
}
return { h: h2, s, v };
}
function hsvToRgb(h2, s, v) {
h2 = bound01(h2, 360) * 6;
s = bound01(s, 100);
v = bound01(v, 100);
var i = Math.floor(h2);
var f = h2 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
var mod = i % 6;
var r = [v, q, p, p, t, v][mod];
var g = [t, v, v, q, p, p][mod];
var b = [p, p, t, v, v, q][mod];
return { r: r * 255, g: g * 255, b: b * 255 };
}
function rgbToHex(r, g, b, allow3Char) {
var hex = [
pad2(Math.round(r).toString(16)),
pad2(Math.round(g).toString(16)),
pad2(Math.round(b).toString(16))
];
return hex.join("");
}
function convertHexToDecimal(h2) {
return parseIntFromHex(h2) / 255;
}
function parseIntFromHex(val) {
return parseInt(val, 16);
}
var names = {
aliceblue: "#f0f8ff",
antiquewhite: "#faebd7",
aqua: "#00ffff",
aquamarine: "#7fffd4",
azure: "#f0ffff",
beige: "#f5f5dc",
bisque: "#ffe4c4",
black: "#000000",
blanchedalmond: "#ffebcd",
blue: "#0000ff",
blueviolet: "#8a2be2",
brown: "#a52a2a",
burlywood: "#deb887",
cadetblue: "#5f9ea0",
chartreuse: "#7fff00",
chocolate: "#d2691e",
coral: "#ff7f50",
cornflowerblue: "#6495ed",
cornsilk: "#fff8dc",
crimson: "#dc143c",
cyan: "#00ffff",
darkblue: "#00008b",
darkcyan: "#008b8b",
darkgoldenrod: "#b8860b",
darkgray: "#a9a9a9",
darkgreen: "#006400",
darkgrey: "#a9a9a9",
darkkhaki: "#bdb76b",
darkmagenta: "#8b008b",
darkolivegreen: "#556b2f",
darkorange: "#ff8c00",
darkorchid: "#9932cc",
darkred: "#8b0000",
darksalmon: "#e9967a",
darkseagreen: "#8fbc8f",
darkslateblue: "#483d8b",
darkslategray: "#2f4f4f",
darkslategrey: "#2f4f4f",
darkturquoise: "#00ced1",
darkviolet: "#9400d3",
deeppink: "#ff1493",
deepskyblue: "#00bfff",
dimgray: "#696969",
dimgrey: "#696969",
dodgerblue: "#1e90ff",
firebrick: "#b22222",
floralwhite: "#fffaf0",
forestgreen: "#228b22",
fuchsia: "#ff00ff",
gainsboro: "#dcdcdc",
ghostwhite: "#f8f8ff",
goldenrod: "#daa520",
gold: "#ffd700",
gray: "#808080",
green: "#008000",
greenyellow: "#adff2f",
grey: "#808080",
honeydew: "#f0fff0",
hotpink: "#ff69b4",
indianred: "#cd5c5c",
indigo: "#4b0082",
ivory: "#fffff0",
khaki: "#f0e68c",
lavenderblush: "#fff0f5",
lavender: "#e6e6fa",
lawngreen: "#7cfc00",
lemonchiffon: "#fffacd",
lightblue: "#add8e6",
lightcoral: "#f08080",
lightcyan: "#e0ffff",
lightgoldenrodyellow: "#fafad2",
lightgray: "#d3d3d3",
lightgreen: "#90ee90",
lightgrey: "#d3d3d3",
lightpink: "#ffb6c1",
lightsalmon: "#ffa07a",
lightseagreen: "#20b2aa",
lightskyblue: "#87cefa",
lightslategray: "#778899",
lightslategrey: "#778899",
lightsteelblue: "#b0c4de",
lightyellow: "#ffffe0",
lime: "#00ff00",
limegreen: "#32cd32",
linen: "#faf0e6",
magenta: "#ff00ff",
maroon: "#800000",
mediumaquamarine: "#66cdaa",
mediumblue: "#0000cd",
mediumorchid: "#ba55d3",
mediumpurple: "#9370db",
mediumseagreen: "#3cb371",
mediumslateblue: "#7b68ee",
mediumspringgreen: "#00fa9a",
mediumturquoise: "#48d1cc",
mediumvioletred: "#c71585",
midnightblue: "#191970",
mintcream: "#f5fffa",
mistyrose: "#ffe4e1",
moccasin: "#ffe4b5",
navajowhite: "#ffdead",
navy: "#000080",
oldlace: "#fdf5e6",
olive: "#808000",
olivedrab: "#6b8e23",
orange: "#ffa500",
orangered: "#ff4500",
orchid: "#da70d6",
palegoldenrod: "#eee8aa",
palegreen: "#98fb98",
paleturquoise: "#afeeee",
palevioletred: "#db7093",
papayawhip: "#ffefd5",
peachpuff: "#ffdab9",
peru: "#cd853f",
pink: "#ffc0cb",
plum: "#dda0dd",
powderblue: "#b0e0e6",
purple: "#800080",
rebeccapurple: "#663399",
red: "#ff0000",
rosybrown: "#bc8f8f",
royalblue: "#4169e1",
saddlebrown: "#8b4513",
salmon: "#fa8072",
sandybrown: "#f4a460",
seagreen: "#2e8b57",
seashell: "#fff5ee",
sienna: "#a0522d",
silver: "#c0c0c0",
skyblue: "#87ceeb",
slateblue: "#6a5acd",
slategray: "#708090",
slategrey: "#708090",
snow: "#fffafa",
springgreen: "#00ff7f",
steelblue: "#4682b4",
tan: "#d2b48c",
teal: "#008080",
thistle: "#d8bfd8",
tomato: "#ff6347",
turquoise: "#40e0d0",
violet: "#ee82ee",
wheat: "#f5deb3",
white: "#ffffff",
whitesmoke: "#f5f5f5",
yellow: "#ffff00",
yellowgreen: "#9acd32"
};
function inputToRGB(color) {
var rgb = { r: 0, g: 0, b: 0 };
var a = 1;
var s = null;
var v = null;
var l = null;
var ok = false;
var format = false;
if (typeof color === "string") {
color = stringInputToObject(color);
}
if (typeof color === "object") {
if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
rgb = rgbToRgb(color.r, color.g, color.b);
ok = true;
format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
} else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
s = convertToPercentage(color.s);
v = convertToPercentage(color.v);
rgb = hsvToRgb(color.h, s, v);
ok = true;
format = "hsv";
} else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
s = convertToPercentage(color.s);
l = convertToPercentage(color.l);
rgb = hslToRgb(color.h, s, l);
ok = true;
format = "hsl";
}
if (Object.prototype.hasOwnProperty.call(color, "a")) {
a = color.a;
}
}
a = boundAlpha(a);
return {
ok,
format: color.format || format,
r: Math.min(255, Math.max(rgb.r, 0)),
g: Math.min(255, Math.max(rgb.g, 0)),
b: Math.min(255, Math.max(rgb.b, 0)),
a
};
}
var CSS_INTEGER = "[-\\+]?\\d+%?";
var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
var CSS_UNIT = "(?:".concat(CSS_NUMBER, ")|(?:").concat(CSS_INTEGER, ")");
var PERMISSIVE_MATCH3 = "[\\s|\\(]+(".concat(CSS_UNIT, ")[,|\\s]+(").concat(CSS_UNIT, ")[,|\\s]+(").concat(CSS_UNIT, ")\\s*\\)?");
var PERMISSIVE_MATCH4 = "[\\s|\\(]+(".concat(CSS_UNIT, ")[,|\\s]+(").concat(CSS_UNIT, ")[,|\\s]+(").concat(CSS_UNIT, ")[,|\\s]+(").concat(CSS_UNIT, ")\\s*\\)?");
var matchers = {
CSS_UNIT: new RegExp(CSS_UNIT),
rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
};
function stringInputToObject(color) {
color = color.trim().toLowerCase();
if (color.length === 0) {
return false;
}
var named = false;
if (names[color]) {
color = names[color];
named = true;
} else if (color === "transparent") {
return { r: 0, g: 0, b: 0, a: 0, format: "name" };
}
var match = matchers.rgb.exec(color);
if (match) {
return { r: match[1], g: match[2], b: match[3] };
}
match = matchers.rgba.exec(color);
if (match) {
return { r: match[1], g: match[2], b: match[3], a: match[4] };
}
match = matchers.hsl.exec(color);
if (match) {
return { h: match[1], s: match[2], l: match[3] };
}
match = matchers.hsla.exec(color);
if (match) {
return { h: match[1], s: match[2], l: match[3], a: match[4] };
}
match = matchers.hsv.exec(color);
if (match) {
return { h: match[1], s: match[2], v: match[3] };
}
match = matchers.hsva.exec(color);
if (match) {
return { h: match[1], s: match[2], v: match[3], a: match[4] };
}
match = matchers.hex8.exec(color);
if (match) {
return {
r: parseIntFromHex(match[1]),
g: parseIntFromHex(match[2]),
b: parseIntFromHex(match[3]),
a: convertHexToDecimal(match[4]),
format: named ? "name" : "hex8"
};
}
match = matchers.hex6.exec(color);
if (match) {
return {
r: parseIntFromHex(match[1]),
g: parseIntFromHex(match[2]),
b: parseIntFromHex(match[3]),
format: named ? "name" : "hex"
};
}
match = matchers.hex4.exec(color);
if (match) {
return {
r: parseIntFromHex(match[1] + match[1]),
g: parseIntFromHex(match[2] + match[2]),
b: parseIntFromHex(match[3] + match[3]),
a: convertHexToDecimal(match[4] + match[4]),
format: named ? "name" : "hex8"
};
}
match = matchers.hex3.exec(color);
if (match) {
return {
r: parseIntFromHex(match[1] + match[1]),
g: parseIntFromHex(match[2] + match[2]),
b: parseIntFromHex(match[3] + match[3]),
format: named ? "name" : "hex"
};
}
return false;
}
function isValidCSSUnit(color) {
return Boolean(matchers.CSS_UNIT.exec(String(color)));
}
var hueStep = 2;
var saturationStep = 0.16;
var saturationStep2 = 0.05;
var brightnessStep1 = 0.05;
var brightnessStep2 = 0.15;
var lightColorCount = 5;
var darkColorCount = 4;
var darkColorMap = [{
index: 7,
opacity: 0.15
}, {
index: 6,
opacity: 0.25
}, {
index: 5,
opacity: 0.3
}, {
index: 5,
opacity: 0.45
}, {
index: 5,
opacity: 0.65
}, {
index: 5,
opacity: 0.85
}, {
index: 4,
opacity: 0.9
}, {
index: 3,
opacity: 0.95
}, {
index: 2,
opacity: 0.97
}, {
index: 1,
opacity: 0.98
}];
function toHsv(_ref) {
var r = _ref.r, g = _ref.g, b = _ref.b;
var hsv = rgbToHsv(r, g, b);
return {
h: hsv.h * 360,
s: hsv.s,
v: hsv.v
};
}
function toHex(_ref2) {
var r = _ref2.r, g = _ref2.g, b = _ref2.b;
return "#".concat(rgbToHex(r, g, b));
}
function mix(rgb1, rgb2, amount) {
var p = amount / 100;
var rgb = {
r: (rgb2.r - rgb1.r) * p + rgb1.r,
g: (rgb2.g - rgb1.g) * p + rgb1.g,
b: (rgb2.b - rgb1.b) * p + rgb1.b
};
return rgb;
}
function getHue(hsv, i, light) {
var hue;
if (Math.round(hsv.h) >= 60 && Math.round(hsv.h) <= 240) {
hue = light ? Math.round(hsv.h) - hueStep * i : Math.round(hsv.h) + hueStep * i;
} else {
hue = light ? Math.round(hsv.h) + hueStep * i : Math.round(hsv.h) - hueStep * i;
}
if (hue < 0) {
hue += 360;
} else if (hue >= 360) {
hue -= 360;
}
return hue;
}
function getSaturation(hsv, i, light) {
if (hsv.h === 0 && hsv.s === 0) {
return hsv.s;
}
var saturation;
if (light) {
saturation = hsv.s - saturationStep * i;
} else if (i === darkColorCount) {
saturation = hsv.s + saturationStep;
} else {
saturation = hsv.s + saturationStep2 * i;
}
if (saturation > 1) {
saturation = 1;
}
if (light && i === lightColorCount && saturation > 0.1) {
saturation = 0.1;
}
if (saturation < 0.06) {
saturation = 0.06;
}
return Number(saturation.toFixed(2));
}
function getValue(hsv, i, light) {
var value;
if (light) {
value = hsv.v + brightnessStep1 * i;
} else {
value = hsv.v - brightnessStep2 * i;
}
if (value > 1) {
value = 1;
}
return Number(value.toFixed(2));
}
function generate$1(color) {
var opts = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
var patterns = [];
var pColor = inputToRGB(color);
for (var i = lightColorCount; i > 0; i -= 1) {
var hsv = toHsv(pColor);
var colorString = toHex(inputToRGB({
h: getHue(hsv, i, true),
s: getSaturation(hsv, i, true),
v: getValue(hsv, i, true)
}));
patterns.push(colorString);
}
patterns.push(toHex(pColor));
for (var _i = 1; _i <= darkColorCount; _i += 1) {
var _hsv = toHsv(pColor);
var _colorString = toHex(inputToRGB({
h: getHue(_hsv, _i),
s: getSaturation(_hsv, _i),
v: getValue(_hsv, _i)
}));
patterns.push(_colorString);
}
if (opts.theme === "dark") {
return darkColorMap.map(function(_ref3) {
var index = _ref3.index, opacity = _ref3.opacity;
var darkColorString = toHex(mix(inputToRGB(opts.backgroundColor || "#141414"), inputToRGB(patterns[index]), opacity * 100));
return darkColorString;
});
}
return patterns;
}
var presetPrimaryColors = {
red: "#F5222D",
volcano: "#FA541C",
orange: "#FA8C16",
gold: "#FAAD14",
yellow: "#FADB14",
lime: "#A0D911",
green: "#52C41A",
cyan: "#13C2C2",
blue: "#1890FF",
geekblue: "#2F54EB",
purple: "#722ED1",
magenta: "#EB2F96",
grey: "#666666"
};
var presetPalettes = {};
var presetDarkPalettes = {};
Object.keys(presetPrimaryColors).forEach(function(key) {
presetPalettes[key] = generate$1(presetPrimaryColors[key]);
presetPalettes[key].primary = presetPalettes[key][5];
presetDarkPalettes[key] = generate$1(presetPrimaryColors[key], {
theme: "dark",
backgroundColor: "#141414"
});
presetDarkPalettes[key].primary = presetDarkPalettes[key][5];
});
var blue = presetPalettes.blue;
var contextKey = Symbol("iconContext");
var useInjectIconContext = function useInjectIconContext2() {
return vue.inject(contextKey, {
prefixCls: vue.ref("anticon"),
rootClassName: vue.ref(""),
csp: vue.ref()
});
};
function canUseDom() {
return !!(typeof window !== "undefined" && window.document && window.document.createElement);
}
function contains(root, n) {
if (!root) {
return false;
}
if (root.contains) {
return root.contains(n);
}
return false;
}
var APPEND_ORDER = "data-vc-order";
var MARK_KEY = "vc-icon-key";
var containerCache = /* @__PURE__ */ new Map();
function getMark() {
var _ref = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}, mark = _ref.mark;
if (mark) {
return mark.startsWith("data-") ? mark : "data-".concat(mark);
}
return MARK_KEY;
}
function getContainer(option) {
if (option.attachTo) {
return option.attachTo;
}
var head = document.querySelector("head");
return head || document.body;
}
function getOrder(prepend) {
if (prepend === "queue") {
return "prependQueue";
}
return prepend ? "prepend" : "append";
}
function findStyles(container) {
return Array.from((containerCache.get(container) || container).children).filter(function(node) {
return node.tagName === "STYLE";
});
}
function injectCSS(css) {
var option = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
if (!canUseDom()) {
return null;
}
var csp = option.csp, prepend = option.prepend;
var styleNode = document.createElement("style");
styleNode.setAttribute(APPEND_ORDER, getOrder(prepend));
if (csp && csp.nonce) {
styleNode.nonce = csp.nonce;
}
styleNode.innerHTML = css;
var container = getContainer(option);
var firstChild = container.firstChild;
if (prepend) {
if (prepend === "queue") {
var existStyle = findStyles(container).filter(function(node) {
return ["prepend", "prependQueue"].includes(node.getAttribute(APPEND_ORDER));
});
if (existStyle.length) {
container.insertBefore(styleNode, existStyle[existStyle.length - 1].nextSibling);
return styleNode;
}
}
container.insertBefore(styleNode, firstChild);
} else {
container.appendChild(styleNode);
}
return styleNode;
}
function findExistNode(key) {
var option = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
var container = getContainer(option);
return findStyles(container).find(function(node) {
return node.getAttribute(getMark(option)) === key;
});
}
function syncRealContainer(container, option) {
var cachedRealContainer = containerCache.get(container);
if (!cachedRealContainer || !contains(document, cachedRealContainer)) {
var placeholderStyle = injectCSS("", option);
var parentNode = placeholderStyle.parentNode;
containerCache.set(container, parentNode);
container.removeChild(placeholderStyle);
}
}
function updateCSS(css, key) {
var option = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
var container = getContainer(option);
syncRealContainer(container, option);
var existNode = findExistNode(key, option);
if (existNode) {
if (option.csp && option.csp.nonce && existNode.nonce !== option.csp.nonce) {
existNode.nonce = option.csp.nonce;
}
if (existNode.innerHTML !== css) {
existNode.innerHTML = css;
}
return existNode;
}
var newNode = injectCSS(css, option);
newNode.setAttribute(getMark(option), key);
return newNode;
}
function _objectSpread$8(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$8(target, key, source[key]);
});
}
return target;
}
function _defineProperty$8(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
function warning(valid, message2) {
}
function isIconDefinition(target) {
return typeof target === "object" && typeof target.name === "string" && typeof target.theme === "string" && (typeof target.icon === "object" || typeof target.icon === "function");
}
function generate(node, key, rootProps) {
if (!rootProps) {
return vue.h(node.tag, _objectSpread$8({
key
}, node.attrs), (node.children || []).map(function(child, index) {
return generate(child, "".concat(key, "-").concat(node.tag, "-").concat(index));
}));
}
return vue.h(node.tag, _objectSpread$8({
key
}, rootProps, node.attrs), (node.children || []).map(function(child, index) {
return generate(child, "".concat(key, "-").concat(node.tag, "-").concat(index));
}));
}
function getSecondaryColor(primaryColor) {
return generate$1(primaryColor)[0];
}
function normalizeTwoToneColors(twoToneColor) {
if (!twoToneColor) {
return [];
}
return Array.isArray(twoToneColor) ? twoToneColor : [twoToneColor];
}
var iconStyles = "\n.anticon {\n display: inline-block;\n color: inherit;\n font-style: normal;\n line-height: 0;\n text-align: center;\n text-transform: none;\n vertical-align: -0.125em;\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n.anticon > * {\n line-height: 1;\n}\n\n.anticon svg {\n display: inline-block;\n}\n\n.anticon::before {\n display: none;\n}\n\n.anticon .anticon-icon {\n display: block;\n}\n\n.anticon[tabindex] {\n cursor: pointer;\n}\n\n.anticon-spin::before,\n.anticon-spin {\n display: inline-block;\n -webkit-animation: loadingCircle 1s infinite linear;\n animation: loadingCircle 1s infinite linear;\n}\n\n@-webkit-keyframes loadingCircle {\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes loadingCircle {\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n";
function getRoot(ele) {
return ele && ele.getRootNode && ele.getRootNode();
}
function inShadow(ele) {
if (!canUseDom()) {
return false;
}
return getRoot(ele) instanceof ShadowRoot;
}
function getShadowRoot(ele) {
return inShadow(ele) ? getRoot(ele) : null;
}
var useInsertStyles = function useInsertStyles2() {
var _useInjectIconContext = useInjectIconContext(), prefixCls = _useInjectIconContext.prefixCls, csp = _useInjectIconContext.csp;
var instance = vue.getCurrentInstance();
var mergedStyleStr = iconStyles;
if (prefixCls) {
mergedStyleStr = mergedStyleStr.replace(/anticon/g, prefixCls.value);
}
vue.nextTick(function() {
if (!canUseDom()) {
return;
}
var ele = instance.vnode.el;
var shadowRoot = getShadowRoot(ele);
updateCSS(mergedStyleStr, "@ant-design-vue-icons", {
prepend: true,
csp: csp.value,
attachTo: shadowRoot
});
});
};
var _excluded$1 = ["icon", "primaryColor", "secondaryColor"];
function _objectWithoutProperties$1(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose$1(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _objectWithoutPropertiesLoose$1(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectSpread$7(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$7(target, key, source[key]);
});
}
return target;
}
function _defineProperty$7(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var twoToneColorPalette = vue.reactive({
primaryColor: "#333",
secondaryColor: "#E6E6E6",
calculated: false
});
function setTwoToneColors(_ref) {
var primaryColor = _ref.primaryColor, secondaryColor = _ref.secondaryColor;
twoToneColorPalette.primaryColor = primaryColor;
twoToneColorPalette.secondaryColor = secondaryColor || getSecondaryColor(primaryColor);
twoToneColorPalette.calculated = !!secondaryColor;
}
function getTwoToneColors() {
return _objectSpread$7({}, twoToneColorPalette);
}
var IconBase = function IconBase2(props, context) {
var _props$context$attrs = _objectSpread$7({}, props, context.attrs), icon = _props$context$attrs.icon, primaryColor = _props$context$attrs.primaryColor, secondaryColor = _props$context$attrs.secondaryColor, restProps = _objectWithoutProperties$1(_props$context$attrs, _excluded$1);
var colors = twoToneColorPalette;
if (primaryColor) {
colors = {
primaryColor,
secondaryColor: secondaryColor || getSecondaryColor(primaryColor)
};
}
warning(isIconDefinition(icon));
if (!isIconDefinition(icon)) {
return null;
}
var target = icon;
if (target && typeof target.icon === "function") {
target = _objectSpread$7({}, target, {
icon: target.icon(colors.primaryColor, colors.secondaryColor)
});
}
return generate(target.icon, "svg-".concat(target.name), _objectSpread$7({}, restProps, {
"data-icon": target.name,
width: "1em",
height: "1em",
fill: "currentColor",
"aria-hidden": "true"
}));
};
IconBase.props = {
icon: Object,
primaryColor: String,
secondaryColor: String,
focusable: String
};
IconBase.inheritAttrs = false;
IconBase.displayName = "IconBase";
IconBase.getTwoToneColors = getTwoToneColors;
IconBase.setTwoToneColors = setTwoToneColors;
function _slicedToArray$1(arr, i) {
return _arrayWithHoles$1(arr) || _iterableToArrayLimit$1(arr, i) || _unsupportedIterableToArray$1(arr, i) || _nonIterableRest$1();
}
function _nonIterableRest$1() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _unsupportedIterableToArray$1(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray$1(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$1(o, minLen);
}
function _arrayLikeToArray$1(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
function _iterableToArrayLimit$1(arr, i) {
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
if (_i == null) return;
var _arr = [];
var _n = true;
var _d = false;
var _s, _e;
try {
for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _arrayWithHoles$1(arr) {
if (Array.isArray(arr)) return arr;
}
function setTwoToneColor(twoToneColor) {
var _normalizeTwoToneColo = normalizeTwoToneColors(twoToneColor), _normalizeTwoToneColo2 = _slicedToArray$1(_normalizeTwoToneColo, 2), primaryColor = _normalizeTwoToneColo2[0], secondaryColor = _normalizeTwoToneColo2[1];
return IconBase.setTwoToneColors({
primaryColor,
secondaryColor
});
}
function getTwoToneColor() {
var colors = IconBase.getTwoToneColors();
if (!colors.calculated) {
return colors.primaryColor;
}
return [colors.primaryColor, colors.secondaryColor];
}
var InsertStyles = vue.defineComponent({
name: "InsertStyles",
setup: function setup() {
useInsertStyles();
return function() {
return null;
};
}
});
var _excluded = ["class", "icon", "spin", "rotate", "tabindex", "twoToneColor", "onClick"];
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
function _iterableToArrayLimit(arr, i) {
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
if (_i == null) return;
var _arr = [];
var _n = true;
var _d = false;
var _s, _e;
try {
for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _objectSpread$6(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$6(target, key, source[key]);
});
}
return target;
}
function _defineProperty$6(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
setTwoToneColor(blue.primary);
var Icon = function Icon2(props, context) {
var _classObj;
var _props$context$attrs = _objectSpread$6({}, props, context.attrs), cls = _props$context$attrs["class"], icon = _props$context$attrs.icon, spin = _props$context$attrs.spin, rotate = _props$context$attrs.rotate, tabindex = _props$context$attrs.tabindex, twoToneColor = _props$context$attrs.twoToneColor, onClick = _props$context$attrs.onClick, restProps = _objectWithoutProperties(_props$context$attrs, _excluded);
var _useInjectIconContext = useInjectIconContext(), prefixCls = _useInjectIconContext.prefixCls, rootClassName = _useInjectIconContext.rootClassName;
var classObj = (_classObj = {}, _defineProperty$6(_classObj, rootClassName.value, !!rootClassName.value), _defineProperty$6(_classObj, prefixCls.value, true), _defineProperty$6(_classObj, "".concat(prefixCls.value, "-").concat(icon.name), Boolean(icon.name)), _defineProperty$6(_classObj, "".concat(prefixCls.value, "-spin"), !!spin || icon.name === "loading"), _classObj);
var iconTabIndex = tabindex;
if (iconTabIndex === void 0 && onClick) {
iconTabIndex = -1;
}
var svgStyle = rotate ? {
msTransform: "rotate(".concat(rotate, "deg)"),
transform: "rotate(".concat(rotate, "deg)")
} : void 0;
var _normalizeTwoToneColo = normalizeTwoToneColors(twoToneColor), _normalizeTwoToneColo2 = _slicedToArray(_normalizeTwoToneColo, 2), primaryColor = _normalizeTwoToneColo2[0], secondaryColor = _normalizeTwoToneColo2[1];
return vue.createVNode("span", _objectSpread$6({
"role": "img",
"aria-label": icon.name
}, restProps, {
"onClick": onClick,
"class": [classObj, cls],
"tabindex": iconTabIndex
}), [vue.createVNode(IconBase, {
"icon": icon,
"primaryColor": primaryColor,
"secondaryColor": secondaryColor,
"style": svgStyle
}, null), vue.createVNode(InsertStyles, null, null)]);
};
Icon.props = {
spin: Boolean,
rotate: Number,
icon: Object,
twoToneColor: [String, Array]
};
Icon.displayName = "AntdIcon";
Icon.inheritAttrs = false;
Icon.getTwoToneColor = getTwoToneColor;
Icon.setTwoToneColor = setTwoToneColor;
var BulbOutlined$1 = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M632 888H392c-4.4 0-8 3.6-8 8v32c0 17.7 14.3 32 32 32h192c17.7 0 32-14.3 32-32v-32c0-4.4-3.6-8-8-8zM512 64c-181.1 0-328 146.9-328 328 0 121.4 66 227.4 164 284.1V792c0 17.7 14.3 32 32 32h264c17.7 0 32-14.3 32-32V676.1c98-56.7 164-162.7 164-284.1 0-181.1-146.9-328-328-328zm127.9 549.8L604 634.6V752H420V634.6l-35.9-20.8C305.4 568.3 256 484.5 256 392c0-141.4 114.6-256 256-256s256 114.6 256 256c0 92.5-49.4 176.3-128.1 221.8z" } }] }, "name": "bulb", "theme": "outlined" };
function _objectSpread$5(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$5(target, key, source[key]);
});
}
return target;
}
function _defineProperty$5(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var BulbOutlined = function BulbOutlined2(props, context) {
var p = _objectSpread$5({}, props, context.attrs);
return vue.createVNode(Icon, _objectSpread$5({}, p, {
"icon": BulbOutlined$1
}), null);
};
BulbOutlined.displayName = "BulbOutlined";
BulbOutlined.inheritAttrs = false;
var ExpandOutlined$1 = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "defs", "attrs": {}, "children": [{ "tag": "style", "attrs": {} }] }, { "tag": "path", "attrs": { "d": "M342 88H120c-17.7 0-32 14.3-32 32v224c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16V168h174c8.8 0 16-7.2 16-16v-48c0-8.8-7.2-16-16-16zm578 576h-48c-8.8 0-16 7.2-16 16v176H682c-8.8 0-16 7.2-16 16v48c0 8.8 7.2 16 16 16h222c17.7 0 32-14.3 32-32V680c0-8.8-7.2-16-16-16zM342 856H168V680c0-8.8-7.2-16-16-16h-48c-8.8 0-16 7.2-16 16v224c0 17.7 14.3 32 32 32h222c8.8 0 16-7.2 16-16v-48c0-8.8-7.2-16-16-16zM904 88H682c-8.8 0-16 7.2-16 16v48c0 8.8 7.2 16 16 16h174v176c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16V120c0-17.7-14.3-32-32-32z" } }] }, "name": "expand", "theme": "outlined" };
function _objectSpread$4(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$4(target, key, source[key]);
});
}
return target;
}
function _defineProperty$4(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var ExpandOutlined = function ExpandOutlined2(props, context) {
var p = _objectSpread$4({}, props, context.attrs);
return vue.createVNode(Icon, _objectSpread$4({}, p, {
"icon": ExpandOutlined$1
}), null);
};
ExpandOutlined.displayName = "ExpandOutlined";
ExpandOutlined.inheritAttrs = false;
var LoadingOutlined$1 = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z" } }] }, "name": "loading", "theme": "outlined" };
function _objectSpread$3(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$3(target, key, source[key]);
});
}
return target;
}
function _defineProperty$3(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var LoadingOutlined = function LoadingOutlined2(props, context) {
var p = _objectSpread$3({}, props, context.attrs);
return vue.createVNode(Icon, _objectSpread$3({}, p, {
"icon": LoadingOutlined$1
}), null);
};
LoadingOutlined.displayName = "LoadingOutlined";
LoadingOutlined.inheritAttrs = false;
var SendOutlined$1 = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "defs", "attrs": {}, "children": [{ "tag": "style", "attrs": {} }] }, { "tag": "path", "attrs": { "d": "M931.4 498.9L94.9 79.5c-3.4-1.7-7.3-2.1-11-1.2a15.99 15.99 0 00-11.7 19.3l86.2 352.2c1.3 5.3 5.2 9.6 10.4 11.3l147.7 50.7-147.6 50.7c-5.2 1.8-9.1 6-10.3 11.3L72.2 926.5c-.9 3.7-.5 7.6 1.2 10.9 3.9 7.9 13.5 11.1 21.5 7.2l836.5-417c3.1-1.5 5.6-4.1 7.2-7.1 3.9-8 .7-17.6-7.2-21.6zM170.8 826.3l50.3-205.6 295.2-101.3c2.3-.8 4.2-2.6 5-5 1.4-4.2-.8-8.7-5-10.2L221.1 403 171 198.2l628 314.9-628.2 313.2z" } }] }, "name": "send", "theme": "outlined" };
function _objectSpread$2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$2(target, key, source[key]);
});
}
return target;
}
function _defineProperty$2(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var SendOutlined = function SendOutlined2(props, context) {
var p = _objectSpread$2({}, props, context.attrs);
return vue.createVNode(Icon, _objectSpread$2({}, p, {
"icon": SendOutlined$1
}), null);
};
SendOutlined.displayName = "SendOutlined";
SendOutlined.inheritAttrs = false;
var SettingOutlined$1 = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z" } }] }, "name": "setting", "theme": "outlined" };
function _objectSpread$1(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty$1(target, key, source[key]);
});
}
return target;
}
function _defineProperty$1(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var SettingOutlined = function SettingOutlined2(props, context) {
var p = _objectSpread$1({}, props, context.attrs);
return vue.createVNode(Icon, _objectSpread$1({}, p, {
"icon": SettingOutlined$1
}), null);
};
SettingOutlined.displayName = "SettingOutlined";
SettingOutlined.inheritAttrs = false;
var ShrinkOutlined$1 = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M881.7 187.4l-45.1-45.1a8.03 8.03 0 00-11.3 0L667.8 299.9l-54.7-54.7a7.94 7.94 0 00-13.5 4.7L576.1 439c-.6 5.2 3.7 9.5 8.9 8.9l189.2-23.5c6.6-.8 9.3-8.8 4.7-13.5l-54.7-54.7 157.6-157.6c3-3 3-8.1-.1-11.2zM439 576.1l-189.2 23.5c-6.6.8-9.3 8.9-4.7 13.5l54.7 54.7-157.5 157.5a8.03 8.03 0 000 11.3l45.1 45.1c3.1 3.1 8.2 3.1 11.3 0l157.6-157.6 54.7 54.7a7.94 7.94 0 0013.5-4.7L447.9 585a7.9 7.9 0 00-8.9-8.9z" } }] }, "name": "shrink", "theme": "outlined" };
function _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? Object(arguments[i]) : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_defineProperty(target, key, source[key]);
});
}
return target;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var ShrinkOutlined = function ShrinkOutlined2(props, context) {
var p = _objectSpread({}, props, context.attrs);
return vue.createVNode(Icon, _objectSpread({}, p, {
"icon": ShrinkOutlined$1
}), null);
};
ShrinkOutlined.displayName = "ShrinkOutlined";
ShrinkOutlined.inheritAttrs = false;
var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_info = /* @__PURE__ */ (() => typeof GM_info != "undefined" ? GM_info : void 0)();
var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
const getScriptInfo = () => {
return {
name: _GM_info.script.name,
author: _GM_info.script.author,
namespace: _GM_info.script.namespace,
version: _GM_info.script.version,
description: _GM_info.script.description
};
};
const LOCAL_USER_CENTER_URL = "https://xs.openget.cn/user?section=account";
// >>> FX-INJECT:dist-channel
// __DIST.shop stays empty in the official build, so every FX-CH site below
// falls back to the official URL. build_distributor.py replaces this block.
var __DIST = { shop: "", name: "", code: "" };
// <<< FX-INJECT:dist-channel
// 渠道版没配店铺链接时,续费类入口不许回落到官方购买页:那等于渠道自己把
// 客户送回官网下单。此时链接整体置空,只留下面板里的卡密激活。
// 官方版 __DIST.code 恒为空串,__noShop 永远是 false,行为零变化。
const __noShop = !!(__DIST.code && !__DIST.shop); // FX-CH:no-shop-guard
const __noShopTip = "该渠道未开通在线购买,续费请向渠道索取卡密,在「账号」面板输入卡密激活";
const __billHref = __DIST.shop || (__noShop ? "" : "https://xs.openget.cn/user?section=billing"); // FX-CH:no-shop-billing
const localBackendJson = (url, data, token) => {
return new Promise((resolve) => {
GM_xmlhttpRequest({
url,
method: data ? "POST" : "GET",
headers: {
"Content-Type": "application/json",
...token ? { "Authorization": "Bearer " + token } : {}
},
data: data ? JSON.stringify(data) : void 0,
timeout: 2e4,
onload: (response) => {
try {
resolve(JSON.parse(response.responseText || "{}"));
} catch (_) {
resolve({ code: 500, msg: "本地后端响应解析失败" });
}
},
onerror: () => resolve({ code: 500, msg: "无法连接本地后端" }),
ontimeout: () => resolve({ code: 500, msg: "本地后端请求超时" })
});
});
};
const sha256Text = async (text) => {
const encoder = new TextEncoder();
const hashBuf = await crypto.subtle.digest("SHA-256", encoder.encode(text || ""));
return Array.from(new Uint8Array(hashBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
};
const getRememberedLogin = () => {
try {
return JSON.parse(_GM_getValue("localBackendLogin") || "{}") || {};
} catch (_) {
return {};
}
};
const setRememberedLogin = (data) => {
try {
_GM_setValue("localBackendLogin", JSON.stringify(data || {}));
} catch (_) {
}
};
const syncLoginTokenToScriptKey = async (setting, loginToken) => {
const keyRes = await localBackendJson("https://xs.openget.cn/api/user/script-key", null, loginToken);
if (keyRes.code !== 200 || !keyRes.script_key) {
return { ok: false, msg: keyRes.msg || "脚本密钥获取失败" };
}
setting.config.basicConfig.token.value = keyRes.script_key;
try {
abcCloudApplying = true;
await fetchAndApplyAbcCloudConfig(setting, keyRes.script_key);
} catch (_) {
} finally {
abcCloudApplying = false;
}
const profileRes = await localBackendJson("https://xs.openget.cn/api/user/profile", null, keyRes.script_key);
setting.userInfo.remainCount = profileRes.code === 200 && profileRes.profile ? getProfileRemainingCount(profileRes.profile) : "已配置";
if (profileRes.code === 200 && profileRes.profile) {
setting.userInfo.activeMember = !!profileRes.profile.active_member;
setting.userInfo.memberUntil = profileRes.profile.member_until || "";
setting.userInfo.pointsBalance = Number(profileRes.profile.points_balance || 0);
}
persistSettingState(setting);
return { ok: true, msg: "已自动登录并同步密钥" };
};
const persistSettingState = (setting) => {
try {
_GM_setValue("setting", JSON.stringify(setting.$state || setting));
} catch (_) {
}
};
const serializeAbcCloudConfig = (setting) => {
const config = {};
["basicConfig", "examConfig", "cxConfig", "searchConfig"].forEach((section) => {
config[section] = {};
const group = setting.config && setting.config[section] || {};
Object.keys(group).forEach((key) => {
if (key === "token") return;
if (group[key] && Object.prototype.hasOwnProperty.call(group[key], "value")) {
config[section][key] = group[key].value;
}
});
});
return config;
};
const applyAbcCloudConfig = (setting, cloudConfig) => {
if (!cloudConfig || typeof cloudConfig !== "object") return false;
let changed = false;
["basicConfig", "examConfig", "cxConfig", "searchConfig"].forEach((section) => {
const group = setting.config && setting.config[section];
const source = cloudConfig[section] || {};
if (!group || !source) return;
Object.keys(source).forEach((key) => {
if (key === "token") return;
if (group[key] && Object.prototype.hasOwnProperty.call(group[key], "value") && group[key].value !== source[key]) {
group[key].value = source[key];
changed = true;
}
});
});
return changed;
};
const fetchAndApplyAbcCloudConfig = async (setting, token) => {
if (!token) return false;
const res = await localBackendJson("https://xs.openget.cn/api/user/settings", null, token);
if (res.code === 200 && res.settings && res.settings.abcConfig) {
const changed = applyAbcCloudConfig(setting, res.settings.abcConfig);
if (changed) persistSettingState(setting);
return changed;
}
return false;
};
let abcCloudSaveTimer = null;
let abcCloudApplying = false;
const scheduleSaveAbcCloudConfig = (setting, token) => {
if (!token || abcCloudApplying) return;
if (abcCloudSaveTimer) clearTimeout(abcCloudSaveTimer);
abcCloudSaveTimer = setTimeout(async () => {
const current = await localBackendJson("https://xs.openget.cn/api/user/settings", null, token);
const settings = current.code === 200 && current.settings && typeof current.settings === "object" ? current.settings : {};
settings.abcConfig = serializeAbcCloudConfig(setting);
await localBackendJson("https://xs.openget.cn/api/user/settings", { settings }, token);
}, 800);
};
let scriptLogQueue = [];
let scriptLogFlushTimer = null;
const plainLogText = (text) => {
const div = document.createElement("div");
div.innerHTML = String(text || "");
return (div.textContent || div.innerText || String(text || "")).slice(0, 4000);
};
const getScriptLogToken = () => {
try {
const setting = useSettingStore();
const scriptKey = setting.config && setting.config.basicConfig && setting.config.basicConfig.token && setting.config.basicConfig.token.value || "";
if (scriptKey) return scriptKey;
const remembered = getRememberedLogin();
if (remembered && remembered.token) return remembered.token;
return "";
} catch (_) {
try {
const remembered = getRememberedLogin();
if (remembered && remembered.token) return remembered.token;
} catch (e2) {}
return "";
}
};
const flushScriptLogs = async () => {
const token = getScriptLogToken();
if (!token || !scriptLogQueue.length) return;
const logs = scriptLogQueue.splice(0, 80);
const res = await localBackendJson("https://xs.openget.cn/api/script-logs", { logs }, token);
if (!res || res.code !== 200) {
scriptLogQueue = logs.concat(scriptLogQueue).slice(-200);
}
};
const enqueueScriptLog = (logItem) => {
scriptLogQueue.push({
event_type: logItem.type === "warning" ? "warning" : "log",
level: logItem.type === "warning" ? "warning" : "info",
message: plainLogText(logItem.text),
page_url: location.href,
extra: {
tabIndex: logItem.tabIndex,
time: logItem.time,
script_version: getScriptInfo().version,
raw_html: String(logItem.text || "").slice(0, 1000)
},
timestamp: Date.now()
});
if (scriptLogQueue.length > 200) scriptLogQueue = scriptLogQueue.slice(-200);
if (scriptLogFlushTimer) clearTimeout(scriptLogFlushTimer);
scriptLogFlushTimer = setTimeout(flushScriptLogs, 1200);
};
// 一场答题一条小结。不进 scriptLogQueue:那条队列有 200 条上限、还等 1200ms debounce,
// 交卷后页面马上跳走,小结会被普通日志挤掉。这里单独直发。
const enqueueScriptEvent = (extra, opts = {}) => {
const token = getScriptLogToken();
if (!token) return Promise.resolve(false);
const item = {
event_type: opts.eventType || "answer_summary",
level: "info",
message: String(opts.message || "").slice(0, 4000),
page_url: location.href,
extra: Object.assign({
time: getDateTime(),
script_version: getScriptInfo().version
}, extra || {}),
timestamp: Date.now()
};
return localBackendJson("https://xs.openget.cn/api/script-logs", { logs: [item] }, token).then(
(res) => Boolean(res && res.code === 200)
);
};
// 「一卷一报」:本场共几题、几题真填上了、答案分别来自题库/缓存/模型、哪几题没填上。
// 只统计「点上了没有」——平台交卷后不回吐分数,score 恒为 null,别当成判对率。
const ANSWER_SUMMARY_LIMIT = 40;
// 余额不足不是答题能力问题,进统计只会把填充率/卡点榜刷脏,直接丢掉。
const isQuotaFailReason = (msg) => /余额不足|题数不足|点数不足|insufficient.?quota|请到用户中心购买|请联系您的服务方/i.test(String(msg || ""));
const buildAnswerSummary = async (platform, questions) => {
const list = Array.isArray(questions) ? questions : [];
let filled = 0;
let skippedQuota = 0;
const src = { bank: 0, cache: 0, ai: 0, unknown: 0 };
const unanswered = [];
for (const question of list) {
const ans = question && question.answer || {};
// code 1=题库直答、2=模型答,两者都代表填充后回读确认过;填不上时上面已改成 0
if (ans.code === 1 || ans.code === 2) {
filled += 1;
if (ans.src === "bank" || ans.src === "cache" || ans.src === "ai") src[ans.src] += 1;
else src.unknown += 1;
continue;
}
// 余额不足:整题不进 total / unanswered
if (isQuotaFailReason(ans.msg) || Number(ans.code) === 402) {
skippedQuota += 1;
continue;
}
// code -1 = 该题型脚本还不支持:压根没去点,但它确实没填上。
// 留在卡点清单里,后台才拼得平「没填上」的数;否则填充率掉了一块却查不到原因。
// 指纹要逐题 await 一次 sha256:一场 200 题全卡住的整卷,不设上限就是答完再排 200 次哈希,
// 而后台只吃前 40 条,到量就停——total/filled 另有算法,不受这里影响。
if (unanswered.length >= ANSWER_SUMMARY_LIMIT) continue;
unanswered.push({
type: String(question && question.type || "").slice(0, 20),
// code 是排查用的原始状态:0=拿到了但点不上,-1004=题库没答案,其它=后端报错码
code: Number(ans.code) || 0,
// -1004 既可能是「题库没收录」也可能是「额度用尽/超时」,只有 msg 分得开
reason: String(ans.msg || (Number(ans.code) === 0 ? "答案拿到了但没点上去" : "")).slice(0, 80),
// 题干前 60 字:后台看到「这道题卡了 37 次」得能认出是哪道题,纯 hash 没人查得动
title: String(question && question.title || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 60),
// 客户端指纹,仅用于「同一道题反复点不上」的归类;与后端 bank_call_logs.question_hash
// 不是同一算法,不能拿来 join
hash: await sha256Text(createAnswerRecordCacheKey(question)).catch(() => "")
});
}
const total = list.length - skippedQuota;
let threshold = 0;
try {
threshold = Number(useSettingStore().config.basicConfig.accuracy.value) || 0;
} catch (_) {
}
return {
platform: String(platform || "unknown").slice(0, 20),
total,
filled,
unfilled: Math.max(0, total - filled),
src,
unanswered,
accuracy: Number((filled / Math.max(total, 1) * 100).toFixed(1)),
threshold,
score: null
};
};
// 不 await:答题刚结束就可能自动交卷,为一行日志把交卷卡住不值当。
const reportAnswerSummary = (platform, questions) => {
return buildAnswerSummary(platform, questions).then((summary) => {
// 整场都是余额不足时 total=0,不写库
if (!summary.total) return false;
return enqueueScriptEvent(summary, {
message: `[答题小结] ${summary.platform} 共${summary.total}题 填上${summary.filled}题`
});
}).catch((error) => {
console.warn("answer summary report failed", error);
return false;
});
};
const formatDateTime = (dt) => {
dt.getFullYear();
dt.getMonth() + 1;
dt.getDate();
let hours = dt.getHours();
let minutes = dt.getMinutes();
let seconds = dt.getSeconds();
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
};
const pad = (n) => {
return n < 10 ? "0" + n : n.toString();
};
const getDateTime = () => {
let now = /* @__PURE__ */ new Date();
return formatDateTime(now);
};
const useLogStore = pinia.defineStore("log", {
state: () => ({
logList: []
}),
actions: {
insertLog(text, type = null, tabIndex = null) {
const logItem = {
text,
time: getDateTime(),
type,
tabIndex
};
this.logList.push(logItem);
enqueueScriptLog(logItem);
}
}
});
const _hoisted_1$8 = { class: "log-wrap" };
const _hoisted_2$7 = ["innerHTML"];
const _hoisted_3$6 = { key: 0 };
const _sfc_main$9 = {
__name: "UserLog",
setup(__props) {
const log = useLogStore();
const logList = vue.ref(log.logList);
const logDom = vue.ref(null);
vue.watch(
() => log.logList,
() => {
vue.nextTick(() => {
logDom.value.scrollTop = logDom.value.scrollHeight;
});
},
{ deep: true }
);
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$8, [
vue.createElementVNode("div", {
class: "console",
ref_key: "logDom",
ref: logDom
}, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(logList.value, (item, index) => {
return vue.openBlock(), vue.createElementBlock("div", { key: index }, [
vue.createElementVNode("span", null, vue.toDisplayString(item.time), 1),
vue.createElementVNode("span", {
class: vue.normalizeClass(item.type == null ? "log" : "warning")
}, "[" + vue.toDisplayString(item.type == null ? "日志" : "警告") + "]", 3),
vue.createElementVNode("span", {
innerHTML: item.text
}, null, 8, _hoisted_2$7)
]);
}), 128)),
logList.value.length == 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_3$6, _cache[0] || (_cache[0] = [
vue.createElementVNode("span", { class: "log" }, "[日志]", -1),
vue.createElementVNode("span", null, "等待用户进入正确页面..", -1)
]))) : vue.createCommentVNode("", true)
], 512)
]);
};
}
};
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(target)) {
if (!(key in source)) continue;
const tVal = target[key];
const sVal = source[key];
if (tVal !== null && typeof tVal === "object" && !Array.isArray(tVal) && sVal !== null && typeof sVal === "object" && !Array.isArray(sVal)) {
result[key] = deepMerge(tVal, sVal);
} else {
result[key] = sVal;
}
}
return result;
}
// 哪些平台有哪些能力:设置页按当前平台过滤,没这个功能的平台不显示对应设置
const ANSWER_PLATFORM_KEYS = ["cx", "zhs", "qs", "ct"];
const WATCH_PLATFORM_KEYS = ["ykt", "mskt", "icve", "ouchn", "mooc", "uschool", "xtzx", "ytccr", "sdzj", "xczx"];
// 自动切换页签时,别抢用户手动点开的页签(设置、搜题、记录、队列)
const AUTO_TAB_KEYS = ["0", "1"];
const autoSetTab = (settingStore, key) => {
try {
if (settingStore && AUTO_TAB_KEYS.includes(String(settingStore.tabIndex))) settingStore.tabIndex = key;
} catch (_) {}
};
const useSettingStore = pinia.defineStore("setting", {
state: () => {
const scriptInfo = getScriptInfo();
const defaultTop = typeof window !== "undefined" ? `${Math.round(window.innerHeight * 0.2)}px` : "120px";
const defaultSetting = {
tabIndex: "0",
version: scriptInfo.version,
position: {
right: "0px",
top: defaultTop
},
shopLink: "",
latestVersion: "",
chatWebUrl: "",
config: {
cxConfig: {
showCustomEditor: {
text: "编辑时显示自定义编辑框",
value: true,
type: "switch",
tips: "开启后,编辑时会显示自定义编辑框"
},
faceRecognitionNotify: {
text: "出现人脸识别时通知我",
value: true,
type: "switch",
tips: "开启后,出现人脸识别时会发送通知"
}
},
basicConfig: {
token: {
text: "本地后端密钥",
value: "",
type: "input",
tips: "点击上方“点我获取密钥”打开用户中心,登录后复制脚本密钥并填入这里;填写后刷新页面生效。",
placeholder: "请粘贴用户中心生成的脚本密钥"
},
answerMode: {
text: "答题模式",
platforms: ANSWER_PLATFORM_KEYS,
value: "questionBankAndAI",
type: "select",
tips: "纯题库:仅题库搜索;纯AI:仅AI答题;题库+AI:题库优先,无结果时AI答题",
options: [
{ value: "questionBank", label: "纯题库" },
{ value: "ai", label: "纯AI" },
{ value: "questionBankAndAI", label: "题库+AI" }
]
},
matchMode: {
text: "题库匹配方式",
platforms: ANSWER_PLATFORM_KEYS,
value: "auto",
type: "select",
tips: "自动:题库里有相似题时,按题面/选项相似度自动选最匹配的一条作答;手动:遇到相似题不自动作答,留空由你自行选择(作业入口会列出候选题)",
options: [
{ value: "auto", label: "自动选最优" },
{ value: "manual", label: "手动选择" }
]
},
manualWaitMode: {
text: "手动选择的处理",
platforms: ANSWER_PLATFORM_KEYS,
value: "wait",
type: "select",
tips: "仅「手动选择」时生效。等待:遇到相似题就停下来,等你选完再继续答后面的题、再提交;跳过:不等待,该题留空、本轮不自动提交,等你事后自己处理",
options: [
{ value: "wait", label: "等待我选择后继续" },
{ value: "skip", label: "跳过此题(不自动提交)" }
]
},
videoPlayrate: {
text: "视频倍速",
value: 2,
type: "input-number",
min: 1,
max: 16,
step: 0.5,
precision: 1,
tips: "❗❗❗视频观看倍速(支持自定义,0.5 递增),高倍速可能面临进度清空风险,请慎重选择"
},
reqIntervalTime: {
text: "搜题间隔(秒)",
platforms: ANSWER_PLATFORM_KEYS,
value: 2,
type: "input-number",
min: 1,
max: 3,
precision: 0,
tips: "每题搜题间隔,不建议太低,避免服务器压力过大"
},
accuracy: {
text: "正确率",
platforms: ANSWER_PLATFORM_KEYS,
value: 80,
type: "slider",
tips: "作业、测验、考试满足设定的正确率才会提交,不满足自动保存,建议80%"
},
autoSubmit: {
text: "章节、作业自动提交",
platforms: ANSWER_PLATFORM_KEYS,
value: true,
type: "switch",
tips: "(建议开启)开启后,满足正确率则自动提交作业、章节,手动答题时建议关闭此功能"
},
autoChangeChapter: {
text: "自动切换章节",
value: true,
type: "switch",
tips: "(建议开启)开启后,自动切换章节,手动答题时,建议关闭"
},
skipFinishedChapter: {
text: "跳过已完成章节",
platforms: ANSWER_PLATFORM_KEYS,
value: true,
type: "switch",
tips: "(建议开启)自动切章时优先跳到目录里未完成的章节,减少空跳"
},
autoAnswer: {
text: "自动答题",
platforms: ANSWER_PLATFORM_KEYS,
value: true,
type: "switch",
tips: "手动答题时,建议关闭,学习时,建议开启"
},
autoRefresh: {
text: "定时刷新(防卡死)",
platforms: ANSWER_PLATFORM_KEYS,
value: true,
type: "switch",
tips: "开启后每30分钟检查:前台空闲或视频持续30分钟无进展时刷新;正常播放、答题、后台时不刷新;关闭立即停止定时器"
},
assistVideo: {
text: "视频助手",
value: true,
type: "switch",
platforms: ["zhs"],
tips: "关闭后进入学习页不会自动播放视频、不会自动切章节;答题助手打开后才会开始答题"
},
assistAnswer: {
text: "答题助手",
value: true,
type: "switch",
platforms: ["zhs"],
tips: "关闭后进入作业/考试页不会自动作答,可手动点开"
},
masteryFull: {
text: "掌握度满分",
value: true,
type: "switch",
platforms: ["zhs"],
tips: "开启后「答题」页会多一个「掌握度满分」切换,点开始就按知识点刷到 100%、记住答案并可导出题库;关闭则这些页面走原来的答题流程"
},
watchAssist: {
text: "视频助手",
value: true,
type: "switch",
platforms: WATCH_PLATFORM_KEYS,
tips: "开启后进入课程页自动播放视频;倍速用上面的「视频倍速」,播完自动翻页用「自动切换章节」"
}
},
examConfig: {
autoSubmit: {
value: false,
text: "自动交卷",
type: "switch",
tips: "手动考试时建议关闭;自动考试答题完成后,将自动点击交卷。"
}
},
searchConfig: {
searchBySelectedText: {
value: false,
text: "划词搜题"
}
}
},
userInfo: {
remainCount: 0,
activeMember: false,
memberUntil: "",
pointsBalance: 0
}
};
try {
const localStore = JSON.parse(_GM_getValue("setting"));
if (localStore) {
const merged = deepMerge(defaultSetting, localStore);
const vpConfig = merged.config && merged.config.basicConfig && merged.config.basicConfig.videoPlayrate;
if (vpConfig) {
vpConfig.type = "input-number";
delete vpConfig.options;
if (vpConfig.min === undefined) vpConfig.min = 1;
if (vpConfig.max === undefined) vpConfig.max = 16;
if (vpConfig.step === undefined) vpConfig.step = 0.5;
if (vpConfig.precision === undefined) vpConfig.precision = 1;
}
for (const [section, entries] of Object.entries(defaultSetting.config)) {
for (const [key, entry] of Object.entries(entries)) {
const savedValue = merged.config[section][key].value;
merged.config[section][key] = { ...entry, value: savedValue };
}
}
merged.version = scriptInfo.version;
return merged;
}
} catch (error) {
console.log("localStore error:", error);
}
return defaultSetting;
}
});
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const _hoisted_1$7 = { class: "home-wrap" };
const _hoisted_2$6 = { class: "tips" };
const _hoisted_3$5 = { key: 0 };
const _hoisted_4$5 = ["innerHTML"];
const _sfc_main$8 = {
__name: "ScriptHome",
setup(__props) {
const scriptInfo = getScriptInfo();
const setting = useSettingStore();
const { latestVersion, chatWebUrl, shopLink } = pinia.storeToRefs(setting);
const showTips = vue.ref(false);
const showTutorial = vue.ref(false);
const announce = vue.ref(null);
vue.onMounted(async () => {
const res = await localBackendJson("https://xs.openget.cn/api/v1/announce");
if (res && res.code === 200 && res.data && res.data.enabled && res.data.text) {
announce.value = res.data;
}
});
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$7, [
announce.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: "announce",
style: { "margin": "8px", "padding": "10px 12px", "background": "var(--t-surface)", "border": "1px solid var(--t-border)", "border-left": "3px solid var(--t-warning)", "border-radius": "8px", "color": "var(--t-text2)", "font-size": "12px", "line-height": "1.65", "white-space": "pre-line" }
}, [
vue.createElementVNode("div", { style: { "font-weight": "700", "font-size": "13px", "margin-bottom": "4px", "display": "flex", "align-items": "center", "gap": "6px", "color": "var(--t-warning)" } }, [
vue.createTextVNode("公告 · "),
vue.createTextVNode(vue.toDisplayString(announce.value.title), 1)
]),
vue.createTextVNode(vue.toDisplayString(announce.value.text), 1)
])) : vue.createCommentVNode("", true),
// >>> FX-STRIP:promo
vue.createElementVNode("div", { class: "home-section", style: {"margin-top":"8px"} }, [
vue.createElementVNode("div", { class: "section-title" }, "推广返利"),
vue.createElementVNode("a", {
class: "home-card-btn",
href: "https://xs.openget.cn/user?section=promotion",
target: "_blank"
}, "去推广赚钱")
]),
// <<< FX-STRIP:promo
// >>> FX-STRIP:qq-channel
vue.createElementVNode("div", { class: "home-section", style: {"margin-top":"8px"} }, [
vue.createElementVNode("div", { class: "section-title" }, "QQ频道"),
vue.createElementVNode("a", {
class: "home-card-btn",
href: "https://pd.qq.com/s/ga2bgmmf8",
target: "_blank",
style: { "background": "linear-gradient(135deg,#12b7f5,#0a6bff)", "border": "1px solid rgba(255,255,255,.35)" }
}, [
vue.createTextVNode("加入腾讯频道【学神助手】"),
vue.createElementVNode("span", { style: { "display": "block", "font-size": "11px", "font-weight": "400", "opacity": ".9", "margin-top": "2px" } }, "频道号 pd22434767 · 点击立即加入")
])
]),
// <<< FX-STRIP:qq-channel
vue.createElementVNode("div", {
class: "home-section collapsible-section",
style: {"margin-top":"8px"}
}, [
vue.createElementVNode("div", {
class: "section-title",
title: "点击展开/收起",
onClick: () => showTips.value = !showTips.value
}, [
vue.createTextVNode("使用提示"),
vue.createElementVNode("span", {
class: "collapse-icon"
}, vue.toDisplayString(showTips.value ? "−" : "+"), 1)
])
]),
showTips.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: "tips-content",
style: { "margin": "0 8px 10px", "padding": "10px 12px", "background": "var(--t-surface)", "border": "1px solid var(--t-border)", "border-radius": "6px" }
}, [
vue.createElementVNode("ul", null, [
// >>> FX-STRIP:scriptcat-update
vue.unref(scriptInfo).version !== vue.unref(latestVersion) ? (vue.openBlock(), vue.createElementBlock("li", _hoisted_3$5, _cache[0] || (_cache[0] = [
vue.createTextVNode("检测到脚本非最新版本,"),
vue.createElementVNode("a", {
href: "https://scriptcat.org/zh-CN/script-show-page/2827",
target: "_blank"
}, "点我更新", -1)
]))) : vue.createCommentVNode("", true),
// <<< FX-STRIP:scriptcat-update
_cache[1] || (_cache[1] = vue.createElementVNode("li", null, "请手动进入视频、作业、考试页面,脚本会自动运行。", -1)),
_cache[2] || (_cache[2] = vue.createElementVNode("li", null, [
vue.createTextVNode("如脚本无法正常运行,请使用 "),
vue.createElementVNode("a", {
target: "_blank",
href: "https://www.google.com/intl/zh-CN/chrome/"
}, "Chrome"),
vue.createTextVNode(" 、 "),
vue.createElementVNode("a", {
target: "_blank",
href: "https://www.firefox.com/"
}, "火狐浏览器")
], -1)),
vue.unref(chatWebUrl) ? (vue.openBlock(), vue.createElementBlock("li", {
key: 1,
innerHTML: vue.unref(chatWebUrl)
}, null, 8, _hoisted_4$5)) : vue.createCommentVNode("", true)
])
])) : vue.createCommentVNode("", true),
vue.createElementVNode("div", {
class: "home-section collapsible-section",
style: {"margin-top":"8px"}
}, [
vue.createElementVNode("div", {
class: "section-title",
title: "点击展开/收起",
onClick: () => showTutorial.value = !showTutorial.value
}, [
vue.createTextVNode("使用教程"),
vue.createElementVNode("span", {
class: "collapse-icon"
}, vue.toDisplayString(showTutorial.value ? "−" : "+"), 1)
])
]),
showTutorial.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: "tutorial-content",
style: { "margin": "0 8px 10px", "padding": "10px 12px", "background": "var(--t-surface)", "border": "1px solid var(--t-border)", "border-radius": "6px" }
}, [
vue.createElementVNode("ol", null, [
vue.createElementVNode("li", null, "打开任意网课平台,等待脚本加载。"),
vue.createElementVNode("li", null, "如果自动答题搜索不到,可尝试在线搜索,答案基本都有。"),
vue.createElementVNode("li", null, "如果不支持当前网课,则不会有相应的提示以及设置面板。"),
vue.createElementVNode("li", null, [
vue.createElementVNode("span", null, "最后温馨提示: "),
vue.createElementVNode("div", null, "- 禁止与其他脚本一起使用,否则出现答案选不上或者页面卡死,无限刷新,等问题一律后果自负。"),
vue.createElementVNode("div", null, "- 请将浏览器页面保持最大化,或者缩小窗口,不能最小化,否则可能导致脚本卡死!")
])
])
])) : vue.createCommentVNode("", true),
]);
};
}
};
const ScriptHome = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-137ea562"]]);
// 「模糊题手动选择」:最近一次带填题逻辑的处理器把自己登记在这里,
// 这样「题目结果」里点选候选题时能用页面原本的填充逻辑(含超星/智慧树各自的点击方式)。
const _questionFillHook = { handler: null };
const registerQuestionFillHook = (handler) => {
_questionFillHook.handler = handler || null;
};
const getQuestionFillHook = () => _questionFillHook.handler;
// 手动选择完成后通知答题流程:等待中的 loop 会继续,搁置的提交/切章会补上
const _manualPickListeners = /* @__PURE__ */ new Set();
const onManualPickResolved = (fn) => {
_manualPickListeners.add(fn);
return () => _manualPickListeners.delete(fn);
};
const notifyManualPickResolved = (questionNo) => {
for (const fn of _manualPickListeners) {
try { fn(questionNo); } catch (_) { }
}
};
const applyPickedCandidate = (index, candidate) => {
const questionStore = useQuestionStore();
const question = questionStore.questionList[index];
const hook = getQuestionFillHook();
if (!question || !hook || !hook.questions || !hook.questions[index]) return false;
const real = hook.questions[index];
const answer = normalizeLocalBackendAnswers(candidate && candidate.answer);
if (!answer.length) return false;
real.answer = { code: 1, answer, src: "bank" };
question.answer = { code: 1, answer, src: "bank" };
const filled = hook.fillQuestion(real, index);
questionStore.questionList = [...questionStore.questionList];
return !!filled;
};
const useQuestionStore = pinia.defineStore("questionStore", {
state: () => ({
questionList: [],
currentQuestionIndex: 0,
accuracy: -1,
searchText: "",
fuzzySearchResult: []
}),
actions: {
clearQuestion() {
this.questionList = [];
},
clearFuzzySearchResult() {
this.fuzzySearchResult = [];
}
}
});
const _hoisted_1$6 = { class: "answer-wrap" };
const _hoisted_2$5 = { class: "tips" };
const _hoisted_3$4 = { class: "answer-tips" };
const _hoisted_4$4 = { style: { "margin-bottom": "10px" } };
const _hoisted_5$4 = { key: 0 };
const _hoisted_6$4 = { key: 1 };
const _hoisted_7$4 = { key: 2 };
const _hoisted_8$4 = {
key: 0,
style: { "text-align": "center" }
};
const _hoisted_9$4 = { key: 1 };
const _hoisted_10$2 = { class: "answer-list" };
const _hoisted_11$2 = ["onClick"];
const _hoisted_12$2 = { key: 0 };
const _hoisted_13$1 = ["innerHTML"];
const _hoisted_14$1 = { class: "answer-content" };
const _hoisted_15 = ["innerHTML"];
const _hoisted_16 = {
key: 0,
style: { "color": "#f94f17" }
};
const _sfc_main$7 = {
__name: "AutoAnswer",
emits: ["changeTab"],
setup(__props, { emit: __emit }) {
const codeMap = {
1: "success",
"-1": "default",
2: "ai-success"
};
const questionStore = useQuestionStore();
const selectedIndex = vue.ref(0);
const answerDom = vue.ref(null);
const setting = useSettingStore();
const {
questionList,
currentQuestionIndex: currentIndex,
accuracy
} = pinia.storeToRefs(questionStore);
const {
userInfo
} = pinia.storeToRefs(setting);
const emit = __emit;
const goSetting = () => {
window.open(setting.shopLink || "https://xs.openget.cn/user?section=account", "_blank");
};
const tokenReady = vue.computed(() => {
const remain = String(setting.userInfo.remainCount || "");
return !!setting.config.basicConfig.token.value && remain && !["密钥未配置", "密钥无效", "null"].includes(remain);
});
const checkAnswer = (index) => {
if (index > currentIndex.value) return;
const dom = answerDom.value.getElementsByClassName("answer-item")[index];
answerDom.value.scrollTo({ top: dom.offsetTop });
selectedIndex.value = index;
};
const pickCandidate = (qIndex, candidate) => {
const filled = applyPickedCandidate(qIndex, candidate);
const q = questionList.value[qIndex];
if (q && q.answer) {
q.answer.picked = filled;
if (filled) q.answer.code = 1;
}
questionList.value = [...questionList.value];
useLogStore().insertLog(filled ? `第${qIndex + 1}题已按你选中的候选题填入答案` : `第${qIndex + 1}题候选答案没点上去,请手动勾选`, filled ? undefined : "warning");
if (filled) notifyManualPickResolved(qIndex + 1);
};
return (_ctx, _cache) => {
const _component_a_button = vue.resolveComponent("a-button");
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$6, [
vue.createElementVNode("div", _hoisted_2$5, [
vue.createElementVNode("ul", null, [
vue.createElementVNode("li", null, [
_cache[1] || (_cache[1] = vue.createTextVNode(" 使用自动答题功能,请先 ")),
vue.createVNode(_component_a_button, {
onClick: goSetting,
size: "small",
type: "primary",
danger: tokenReady.value
}, {
icon: vue.withCtx(() => [
vue.createVNode(vue.unref(SettingOutlined))
]),
default: vue.withCtx(() => [
vue.createTextVNode(vue.toDisplayString(tokenReady.value ? "已配置密钥" : "登录用户中心获取密钥"), 1)
]),
_: 1
})
]),
_cache[2] || (_cache[2] = vue.createElementVNode("li", null, "如需手动答题,建议关闭自动切换功能", -1)),
_cache[3] || (_cache[3] = vue.createElementVNode("li", null, "每次自动答题开始前,都会清空上一次的搜索结果", -1))
])
]),
_cache[10] || (_cache[10] = vue.createElementVNode("div", {
class: "separator",
style: { "margin": "5px 0" }
}, null, -1)),
vue.createElementVNode("div", _hoisted_3$4, [
vue.createElementVNode("div", _hoisted_4$4, [
vue.createElementVNode("span", null, "答题进度:" + vue.toDisplayString(vue.unref(currentIndex)) + " / " + vue.toDisplayString(vue.unref(questionList).length), 1),
_cache[5] || (_cache[5] = vue.createElementVNode("span", null, "|", -1)),
vue.createElementVNode("span", null, [
_cache[4] || (_cache[4] = vue.createTextVNode(" 准确率: ")),
vue.unref(questionList).length == 0 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_5$4, "未答题")) : vue.unref(accuracy) != -1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_6$4, vue.toDisplayString(vue.unref(accuracy)) + "%", 1)) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_7$4, "作业中.."))
]),
_cache[6] || (_cache[6] = vue.createElementVNode("span", null, "|", -1)),
vue.createElementVNode("span", null, "剩余次数:" + vue.toDisplayString(vue.unref(userInfo).remainCount), 1)
]),
_cache[7] || (_cache[7] = vue.createStaticVNode('
', 1))
]),
_cache[11] || (_cache[11] = vue.createElementVNode("div", {
class: "separator",
style: { "margin": "5px 0" }
}, null, -1)),
vue.unref(questionList).length == 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_8$4, " ❗️ 未检测到试题 ")) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$4, [
vue.createElementVNode("div", _hoisted_10$2, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(questionList), (question, index) => {
var _a;
return vue.openBlock(), vue.createElementBlock("div", {
onClick: ($event) => checkAnswer(index),
class: vue.normalizeClass(`search-info ${index === vue.unref(currentIndex) ? "searching" : ""} ${codeMap[(_a = question.answer) == null ? void 0 : _a.code] || "warning"}`),
key: index
}, vue.toDisplayString(index + 1), 11, _hoisted_11$2);
}), 128))
]),
_cache[9] || (_cache[9] = vue.createElementVNode("div", {
class: "separator",
style: { "margin": "5px 0" }
}, null, -1)),
vue.unref(questionList).length != 0 ? (vue.openBlock(), vue.createElementBlock("div", {
key: 0,
ref_key: "answerDom",
ref: answerDom,
class: "answer-detail"
}, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(currentIndex), (index) => {
var _a, _b, _c, _d;
return vue.openBlock(), vue.createElementBlock("div", {
class: "answer-item",
key: index
}, [
index <= vue.unref(questionList).length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_12$2, [
vue.createElementVNode("div", null, [
vue.createElementVNode("span", null, vue.toDisplayString(index) + ".", 1),
vue.createElementVNode("div", {
style: { "display": "inline-block" },
innerHTML: vue.unref(questionList)[index - 1].title
}, null, 8, _hoisted_13$1)
]),
vue.createElementVNode("div", _hoisted_14$1, [
_cache[8] || (_cache[8] = vue.createElementVNode("span", null, "答案:", -1)),
vue.createElementVNode("div", {
innerHTML: (_b = (_a = vue.unref(questionList)[index - 1].answer) == null ? void 0 : _a.answer) == null ? void 0 : _b.join()
}, null, 8, _hoisted_15),
((_c = vue.unref(questionList)[index - 1].answer) == null ? void 0 : _c.code) === 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_16, "有答案(不完全匹配),请手动选择")) : vue.createCommentVNode("", true),
((_d = vue.unref(questionList)[index - 1].answer) == null ? void 0 : _d.msg) ? (vue.openBlock(), vue.createElementBlock("div", {
key: 1,
style: { "color": "var(--t-text3)", "font-size": "12px", "margin-top": "2px", "word-break": "break-all" }
}, vue.toDisplayString(_d.msg), 1)) : vue.createCommentVNode("", true),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList((_a = vue.unref(questionList)[index - 1].answer) === null || _a === void 0 ? void 0 : _a.candidates, (cand, candIdx) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: "cand-" + candIdx,
onClick: ($event) => pickCandidate(index - 1, cand),
style: { "margin-top": "6px", "padding": "6px 8px", "border": "1px solid var(--t-border)", "border-radius": "6px", "cursor": "pointer", "background": "var(--t-surface)" }
}, [
vue.createElementVNode("div", { style: { "font-weight": "600", "margin-bottom": "2px" } }, "候选" + (candIdx + 1) + "(相似度 " + vue.toDisplayString(cand.score) + "%)", 1),
vue.createElementVNode("div", { innerHTML: cand.question }, null, 8, ["innerHTML"]),
vue.createElementVNode("div", { innerHTML: "答案:" + vue.toDisplayString(cand.answer) }, null, 8, ["innerHTML"]),
vue.createElementVNode("div", { style: { "color": "var(--t-primary)", "margin-top": "2px" } }, "点击填入本题", 1)
], 8, ["onClick"]);
}), 128))
])
])) : vue.createCommentVNode("", true)
]);
}), 128))
], 512)) : vue.createCommentVNode("", true)
]))
]);
};
}
};
const _hoisted_1$5 = { class: "config-wrap" };
const _hoisted_2$4 = { class: "tips" };
const _hoisted_3$3 = { class: "buy-btn" };
const _hoisted_4$3 = ["href"];
const _hoisted_5$3 = { class: "separator" };
const _hoisted_6$3 = {
key: 0,
class: "config"
};
const _hoisted_7$3 = { class: "config-name" };
const _hoisted_8$3 = { class: "config-input" };
const _hoisted_9$3 = { key: 5 };
const _sfc_main$6 = {
__name: "UserConfig",
emits: ["autoRefreshPage"],
setup(__props, { emit: __emit }) {
const setting = useSettingStore();
const { shopLink } = pinia.storeToRefs(setting);
const emit = __emit;
// 只显示当前平台真正有的功能:答题类只在超星/智慧树/青书/出头出现,
// 视频类只在对应平台出现,名称里不再挂「(智慧树)」「(新增平台)」后缀
const configSections = vue.computed(() => {
const platformKey = resolvePlatformKey();
const pick = (group) => {
const result = {};
Object.entries(group || {}).forEach(([name, field]) => {
if (!field || typeof field !== "object") return;
if (Array.isArray(field.platforms) && !field.platforms.includes(platformKey)) return;
result[name] = field;
});
return result;
};
const isWatchSite = WATCH_PLATFORM_KEYS.includes(platformKey);
const sections = [
{ label: "基本设置", configs: pick(setting.config.basicConfig), showRemainCount: true }
];
if (!isWatchSite) sections.push({ label: "考试设置", configs: pick(setting.config.examConfig) });
if (platformKey === "cx") sections.push({ label: "超星设置", configs: pick(setting.config.cxConfig) });
return sections;
});
const remainDisplay = vue.computed(() => {
const ui = setting.userInfo;
if (ui.activeMember && ui.memberUntil) {
return { label: "包月到期:", value: ui.memberUntil.split(" ")[0], isMember: true };
}
const balance = Number(ui.pointsBalance || ui.remainCount || 0);
if (balance >= 999999 || String(ui.remainCount) === "无限") {
return { label: "剩余次数:", value: "无限", isMember: false };
}
return { label: "剩余次数:", value: balance + " 次", isMember: false };
});
vue.watch(
() => setting.config.basicConfig.token,
(newVal) => {
try {
_GM_setValue("setting", JSON.stringify(setting.$state || setting));
} catch (_) {
}
setTimeout(() => window.location.reload(), 350);
},
{
deep: true
}
);
vue.watch(
() => setting.config.basicConfig.videoPlayrate,
(newVal) => {
const rate = newVal && newVal.value;
if (rate) {
applyVideoRateToPage(rate);
useLogStore().insertLog(`已更新视频倍速为 ${rate}x(无需刷新页面)`);
}
},
{
deep: true
}
);
const formatter = (value) => {
return `${value}%`;
};
const closeAllAuto = () => {
const hasAnyEnabled = setting.config.basicConfig.autoSubmit.value || setting.config.basicConfig.autoChangeChapter.value || setting.config.basicConfig.skipFinishedChapter.value || setting.config.basicConfig.autoAnswer.value || setting.config.examConfig.autoSubmit.value || setting.config.basicConfig.autoRefresh.value || setting.config.basicConfig.watchAssist.value;
const newState = !hasAnyEnabled;
setting.config.basicConfig.autoSubmit.value = newState;
setting.config.basicConfig.autoChangeChapter.value = newState;
setting.config.basicConfig.skipFinishedChapter.value = newState;
setting.config.basicConfig.autoAnswer.value = newState;
setting.config.examConfig.autoSubmit.value = newState;
setting.config.basicConfig.autoRefresh.value = newState;
setting.config.basicConfig.watchAssist.value = newState;
};
return (_ctx, _cache) => {
const _component_a_slider = vue.resolveComponent("a-slider");
const _component_a_input_number = vue.resolveComponent("a-input-number");
const _component_a_switch = vue.resolveComponent("a-switch");
const _component_a_input = vue.resolveComponent("a-input");
const _component_a_select = vue.resolveComponent("a-select");
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$5, [
vue.createElementVNode("div", _hoisted_2$4, [
vue.createElementVNode("ul", null, [
vue.createElementVNode("li", null, [
_cache[0] || (_cache[0] = vue.createTextVNode("自动答题功能需填写密钥, ")),
vue.createElementVNode("span", _hoisted_3$3, [
vue.createVNode(vue.unref(BulbOutlined), { style: { "color": "#fff", "margin-right": "2px" } }),
vue.createElementVNode("a", {
href: vue.unref(shopLink),
target: "_blank"
}, "点我登录用户中心获取密钥 ", 8, _hoisted_4$3)
])
]),
_cache[6] || (_cache[6] = vue.createElementVNode("li", null, "设置后进入章节测试,作业,考试页面即可自动答题", -1)),
vue.createElementVNode("li", null, [
_cache[1] || (_cache[1] = vue.createTextVNode("使用")),
_cache[2] || (_cache[2] = vue.createElementVNode("span", { style: { "color": "#ef4444" } }, "AI、在线搜题", -1)),
_cache[3] || (_cache[3] = vue.createTextVNode(",")),
_cache[4] || (_cache[4] = vue.createElementVNode("span", { style: { "color": "#ef4444" } }, "关闭所有自动操作", -1)),
_cache[5] || (_cache[5] = vue.createTextVNode(",防止冲突。 ")),
vue.createElementVNode("span", {
onClick: closeAllAuto,
class: "buy-btn"
}, "一键关闭/开启")
]),
_cache[7] || (_cache[7] = vue.createElementVNode("li", null, [
vue.createTextVNode("⚠️视频中出现题目,可以忽略,"),
vue.createElementVNode("span", { style: { "color": "#ef4444" } }, "不影响学习进度")
], -1))
])
]),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(configSections.value, (section) => {
return vue.openBlock(), vue.createElementBlock(vue.Fragment, {
key: section.label
}, [
vue.createElementVNode("div", _hoisted_5$3, vue.toDisplayString(section.label), 1),
vue.createElementVNode("div", null, [
section.showRemainCount ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_6$3, [
vue.createElementVNode("div", { style: { "color": "var(--t-text2)" } }, vue.toDisplayString(remainDisplay.value.label), 1),
vue.createElementVNode("a", {
href: __billHref || void 0, // FX-CH:remain-billing
title: __billHref ? void 0 : __noShopTip,
target: "_blank",
style: { "color": remainDisplay.value.isMember ? "var(--t-primary)" : "var(--t-text)", "text-decoration": "none" }
}, vue.toDisplayString(remainDisplay.value.value + (__billHref ? "" : "(需卡密激活)")), 1)
])) : vue.createCommentVNode("", true),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(section.configs, (config2) => {
return vue.openBlock(), vue.createElementBlock("div", { key: config2 }, [
vue.createElementVNode("div", {
class: vue.normalizeClass(["config", config2.type === "switch" ? "switch-item" : ""])
}, [
vue.createElementVNode("div", _hoisted_7$3, vue.toDisplayString(config2.text), 1),
vue.createElementVNode("div", _hoisted_8$3, [
config2.type === "slider" ? (vue.openBlock(), vue.createBlock(_component_a_slider, {
key: 0,
value: config2.value,
"onUpdate:value": ($event) => config2.value = $event,
"tip-formatter": formatter
}, null, 8, ["value", "onUpdate:value"])) : config2.type === "input-number" ? (vue.openBlock(), vue.createBlock(_component_a_input_number, {
key: 1,
precision: config2.precision,
step: config2.step,
value: config2.value,
onChange: ($event) => {
if ($event !== null && $event !== undefined) config2.value = $event;
},
min: config2.min,
max: config2.max
}, null, 8, ["value", "onChange", "min", "max", "precision", "step"])) : config2.type === "switch" ? (vue.openBlock(), vue.createBlock(_component_a_switch, {
key: 2,
size: "small",
checked: config2.value,
"onUpdate:checked": ($event) => config2.value = $event
}, null, 8, ["checked", "onUpdate:checked"])) : config2.type === "input" ? (vue.openBlock(), vue.createBlock(_component_a_input, {
key: 3,
value: config2.value,
"onUpdate:value": ($event) => config2.value = $event,
valueModifiers: { lazy: true, trim: true },
placeholder: config2.placeholder
}, null, 8, ["value", "onUpdate:value", "placeholder"])) : config2.type === "select" ? (vue.openBlock(), vue.createBlock(_component_a_select, {
key: 4,
style: { "width": "100px" },
ref_for: true,
ref: "select",
value: config2.value,
"onUpdate:value": ($event) => config2.value = $event,
options: config2.options
}, null, 8, ["value", "onUpdate:value", "options"])) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_9$3, vue.toDisplayString(config2.value), 1))
])
], 2)
]);
}), 128))
])
], 64);
}), 128))
]);
};
}
};
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms * 1e3));
};
const STORAGE_KEY = "answerRecords";
const MAX_RECORDS = 200;
const normalizeText = (value = "") => String(value).replace(/\s+/g, " ").trim();
const normalizeOptions = (options = []) => [...Array.isArray(options) ? options : []].map((item) => normalizeText(item)).filter(Boolean).sort();
const formatRequestTime = (timestamp) => {
const date = new Date(timestamp);
const pad3 = (value) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad3(date.getMonth() + 1)}-${pad3(date.getDate())} ${pad3(date.getHours())}:${pad3(date.getMinutes())}:${pad3(date.getSeconds())}`;
};
const createAnswerRecordCacheKey = (question) => JSON.stringify({
title: normalizeText(question == null ? void 0 : question.title),
type: normalizeText(question == null ? void 0 : question.type),
optionsText: normalizeOptions(question == null ? void 0 : question.optionsText)
});
const loadRecords = () => {
try {
const records = JSON.parse(_GM_getValue(STORAGE_KEY) || "[]");
return Array.isArray(records) ? records : [];
} catch (error) {
console.log("load answer records error:", error);
return [];
}
};
const useAnswerRecordStore = pinia.defineStore("answerRecord", {
state: () => ({
records: loadRecords()
}),
getters: {
displayRecords: (state) => state.records
},
actions: {
persistRecords() {
_GM_setValue(STORAGE_KEY, JSON.stringify(this.records));
},
insertRecord({ question, answer, token, source }) {
const timestamp = Date.now();
const record = {
cacheKey: createAnswerRecordCacheKey(question),
token: token || "",
title: (question == null ? void 0 : question.title) || "",
requestTime: formatRequestTime(timestamp),
timestamp,
answer: Array.isArray(answer) ? answer : [],
type: (question == null ? void 0 : question.type) || "",
optionsText: Array.isArray(question == null ? void 0 : question.optionsText) ? question.optionsText : [],
workType: (question == null ? void 0 : question.workType) || "",
refer: (question == null ? void 0 : question.refer) || "",
source: source || "accurate"
};
this.records.unshift(record);
this.records = this.records.slice(0, MAX_RECORDS);
this.persistRecords();
return record;
},
clearRecords() {
this.records = [];
this.persistRecords();
}
}
});
const getApiBaseUrl = () => {
return "https://xs.openget.cn";
};
const LOCAL_BACKEND_ANSWER_URL = "https://xs.openget.cn/api/v1/cx";
const LOCAL_BACKEND_SEARCH_URL = "https://xs.openget.cn/api/web/search";
const TIMEOUT_ERROR_MSG$1 = "请求超时,请前往设置尝试切换节点..";
const SERVER_MAINTENANCE_ERROR_MSG$1 = "服务器维护中,请前往设置尝试切换节点,自动学习功能正常使用..";
const handleError$1 = (msg) => ({
code: 10003,
data: {
answer: [],
num: "",
usenum: ""
},
msg
});
const getCookieValue = (name) => {
const pageWindow = _unsafeWindow || window;
if (typeof pageWindow.getCookie === "function") {
return pageWindow.getCookie(name) || "";
}
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : "";
};
const getLocalBackendToken = () => {
const keys = [
"GPTJsSetting.token",
"chaoxing.com:GPTJsSetting.token",
"mooc1.chaoxing.com:GPTJsSetting.token"
];
for (const key of keys) {
try {
const v = localStorage.getItem(key);
if (v) return v;
} catch (_) {
}
try {
const v = _GM_getValue ? _GM_getValue(key) : "";
if (v) return v;
} catch (_) {
}
}
try {
const raw = localStorage.getItem("GPTJsSetting.userInfo") || localStorage.getItem("chaoxing.com:GPTJsSetting.userInfo");
const obj = raw ? JSON.parse(raw) : null;
if (obj && obj.token) return obj.token;
} catch (_) {
}
return "";
};
// 题型归一化:平台给的题型常带修饰(如「多项选择题」「多选题(2分)」),
// 统一成后端认识的规范名称,避免多选被当成单选、只答一项
const XS_TYPE_RULES = [
{ keywords: ["多选", "多项选择", "不定项", "X型"], name: "多选题" },
{ keywords: ["判断", "是非"], name: "判断题" },
{ keywords: ["填空", "完型", "完形"], name: "填空题" },
{ keywords: ["名词解释"], name: "名词解释" },
{ keywords: ["连线", "匹配"], name: "连线题" },
{ keywords: ["阅读理解"], name: "阅读理解" },
{ keywords: ["简答"], name: "简答题" },
{ keywords: ["论述"], name: "论述题" },
{ keywords: ["计算"], name: "计算题" },
{ keywords: ["单选", "单项选择", "A1型", "A2型", "A3型"], name: "单选题" }
];
const normalizeQuestionTypeName = (type) => {
const raw = String(type == null ? "" : type).trim();
if (!raw) return "";
if (/^\d{1,3}$/.test(raw)) return raw;
if (/^[a-zA-Z]{3,}$/.test(raw)) return raw;
const hit = XS_TYPE_RULES.find((rule) => rule.keywords.some((keyword) => raw.includes(keyword)));
return hit ? hit.name : raw;
};
// 题型文字缺失或不准时的兜底:题目区域内出现复选框样式就按多选题处理,
// 否则多选会被当成单选,只填一个选项
const detectMultiChoiceDom = (root) => {
if (!root || !root.querySelector) return false;
if (root.nodeType === 9) return false; // document 级容器会扫到别题的控件,不能作为依据
try {
if (root.querySelector("input[type='radio'], [role='radio']")) return false;
if (root.querySelector("input[type='checkbox'], [role='checkbox']")) return true;
const nodes = root.querySelectorAll("[class*='option'], [class*='Option'], label, li, [class*='checkbox']");
for (const node of nodes) {
const cls = String((node && node.className) || "");
if (/el-checkbox-group|checkbox-views|checkbox-list|is-checkbox/i.test(cls)) return true;
}
} catch (_) {
}
return false;
};
const normalizeLocalBackendAnswers = (answer) => {
if (Array.isArray(answer)) return answer.map((x) => String(x).trim()).filter(Boolean);
const text = String(answer || "").replace(/。$/, "").trim();
if (!text) return [];
// 题库/模型偶尔给出 Python 字面量 "['北京', '上海']",JSON.parse 不认单引号,先按字面量拆
if (/^\[\s*'/.test(text) && /'\s*\]$/.test(text)) {
const items = [];
const re = /'((?:[^'\\]|\\.)*)'/g;
let quoted;
while ((quoted = re.exec(text))) {
const item = quoted[1].replace(/\\(.)/g, "$1").trim();
if (item) items.push(item);
}
if (items.length) return items;
}
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) return parsed.map((x) => String(x).trim()).filter(Boolean);
} catch (_) {
}
// 处理无分隔符的连续字母序列,如 "ABCD"/"ABCDE" → ["A","B","C","D","E"]
// 只拆「全大写 + 严格递增」:小写词(apple)与乱序大写词(HTTP、DNA)都是填空/简答内容,
// 拆成字母会点错选项;多选答案按字母序给,乱序说明它是一个词。
if (/^[A-Z]{2,10}$/.test(text) && [...text].every((c, i) => i === 0 || c > text[i - 1])) {
return text.split("").filter(Boolean);
}
// "A 正确" / "A、C 正确":开头是选项字母、后面紧跟中文说明时按字母取,别整句丢给匹配。
// 限定「短 + 中文收尾」:H2O(跟数字)与 SWOT 式简答长句都不是选项字母。
const headLetters = (text.match(/^[A-Z](?:[\s、,,]?\s*[A-Z])*(?=[^A-Za-z]|$)/) || [""])[0];
const headSeq = headLetters.replace(/[\s、,,]+/g, "").split("").filter(Boolean);
const headTail = text.slice(headLetters.length);
if (headSeq.length && text.length <= 12 && /^[\s、,,((::.。-]*[\u4e00-\u9fff]/.test(headTail)) {
return headSeq;
}
if (/^[A-Z](?:[\s,,、;;|]+[A-Z])+$/.test(text.toUpperCase())) {
return text.toUpperCase().split(/[\s,,、;;|]+/).filter(Boolean);
}
// 文本答案不再按分隔符拆分(选项内容可能包含任何标点),保留完整字符串由 fillChoice 子串匹配
return [text];
};
const getImgUrlKey = (url) => {
const clean = String(url || "").split(/[?#]/)[0].replace(/\/+$/, "");
const seg = (clean.split("/").pop() || clean).replace(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i, "");
return seg.toLowerCase();
};
const collectImgSrcs = (htmlOrEl) => {
const out = [];
const push = (u) => {
const s = String(u || "").trim();
if (s && !out.includes(s)) out.push(s);
};
if (htmlOrEl && htmlOrEl.querySelectorAll) {
htmlOrEl.querySelectorAll("img").forEach((img) => push(img.getAttribute("data-src") || img.currentSrc || img.src || ""));
}
const s = typeof htmlOrEl === "string" ? htmlOrEl : "";
const re = /
]+src=["']([^"']+)["']/gi;
let m;
while ((m = re.exec(s))) push(m[1]);
if (s && /https?:\/\//i.test(s) && !/
]+/gi) || []).forEach((u) => {
if (/ananas\.chaoxing\.com|\/origin\/|\.(?:jpg|jpeg|png|gif|webp)(?:$|[?#])/i.test(u)) push(u);
});
}
return out;
};
const imgSrcsMatch = (aSrcs, bSrcs) => {
for (const a of aSrcs || []) {
const ak = getImgUrlKey(a);
if (!ak) continue;
for (const b of bSrcs || []) {
const bk = getImgUrlKey(b);
if (ak === bk || (ak.length >= 8 && bk.length >= 8 && (ak.includes(bk) || bk.includes(ak)))) return true;
}
}
return false;
};
const buildLocalBackendQuestionPayload = (question) => {
return JSON.stringify({
question: question.title || "",
options: question.optionsText || [],
type: question.type || "",
platform: "abc-helper",
refer: question.refer || location.href
});
};
const getProfileRemainingCount = (profile) => {
if (!profile) return 0;
if (profile.active_member) return "无限";
return Number(profile.points_balance || profile.remaining || 0);
};
const updateUserInfoFromProfile = (profile) => {
try {
const setting = useSettingStore();
if (profile) {
setting.userInfo.activeMember = !!profile.active_member;
setting.userInfo.memberUntil = profile.member_until || "";
setting.userInfo.pointsBalance = Number(profile.points_balance || 0);
}
} catch (_) {}
};
const formatLocalRemainCount = (value) => {
if (value === "无限") return "无限";
const n = Number(value || 0);
return n >= 999999 ? "无限" : n;
};
const fetchLocalBackendProfile = async (token) => {
token = token || "";
if (!token) return null;
return new Promise((resolve) => {
GM_xmlhttpRequest({
url: "https://xs.openget.cn/api/user/profile?v=xs-5.0",
method: "GET",
headers: {
"Authorization": "Bearer " + token
},
timeout: 15e3,
onload: (response) => {
try {
const obj = JSON.parse(response.responseText || "{}");
resolve(obj.code === 200 ? obj.profile : null);
} catch (_) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null)
});
});
};
const _downloadImg = (url) => new Promise((resolve) => {
if (/^data:image\/[^;]+;base64,/i.test(String(url))) { resolve(url); return; }
if (!url || String(url).startsWith("blob:")) {
resolve(null);
return;
}
GM_xmlhttpRequest({
url, method: 'GET', responseType: 'arraybuffer', timeout: 15000,
onload: (res) => {
try {
if (res.status < 200 || res.status >= 300) { resolve(null); return; }
const bytes = new Uint8Array(res.response);
if (!bytes.length) { resolve(null); return; }
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk)
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
const type = (res.responseHeaders.match(/content-type:\s*([^\s;\r\n]+)/i) || [])[1] || 'image/jpeg';
if (!/^image\//i.test(type)) { resolve(null); return; }
resolve('data:' + type + ';base64,' + btoa(binary));
} catch (e) { resolve(null); }
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
// 「手动选择」时的处理方式:wait=停下来等我选完再继续;skip=跳过本题、本轮不自动提交
const getManualWaitMode = () => {
try {
const setting = useSettingStore();
const cfg = setting && setting.config && setting.config.basicConfig && setting.config.basicConfig.manualWaitMode;
return cfg && cfg.value === "skip" ? "skip" : "wait";
} catch (_) {
return "wait";
}
};
// 等待用户在第 index 题上点选候选答案;返回选中的答案数组,超时返回 null
const waitForManualPick = (index) => new Promise((resolve) => {
const qs = useQuestionStore();
const started = Date.now();
const LIMIT = 10 * 60 * 1000; // 最多等 10 分钟,避免挂死整个任务
const timer = setInterval(() => {
const q = (qs.questionList || [])[index];
const a = q && q.answer;
if (a && a.picked && a.answer && a.answer.length) {
clearInterval(timer);
resolve(a.answer);
return;
}
if (Date.now() - started > LIMIT) {
clearInterval(timer);
resolve(null);
}
}, 500);
});
// 等待手动选择期间,自动提交/自动切章必须先停下:否则空题会被当成未作答提交,或被刷走
const hasPendingManualPick = () => {
try {
const qs = useQuestionStore();
const list = (qs && qs.questionList) || [];
return list.some((q) => q && q.answer && q.answer.src === "pick" && !q.answer.picked);
} catch (_) {
return false;
}
};
const getQuestionMatchMode = () => {
try {
const setting = useSettingStore();
const cfg = setting && setting.config && setting.config.basicConfig && setting.config.basicConfig.matchMode;
return cfg && cfg.value === "manual" ? "manual" : "auto";
} catch (_) {
return "auto";
}
};
const callLocalBackendAnswer = async (question, extra = {}) => {
const setting = useSettingStore();
const token = setting.config.basicConfig.token.value || getLocalBackendToken() || "";
// 提取题目中的图片并下载为 base64(DOM + 题干/选项 HTML,避免纯图题漏传)
const images = [];
const imageSources = [];
const seenImg = new Set();
const pushImgSrc = (src) => {
const u = String(src || "").trim();
if (!u || seenImg.has(u)) return;
seenImg.add(u);
};
const el = question.element;
if (el && el.querySelectorAll) {
el.querySelectorAll("img").forEach((img) => pushImgSrc(img.getAttribute("data-src") || img.currentSrc || img.src || ""));
}
const htmlBlobs = [question.title, ...(question.optionsText || [])];
if (question.options && typeof question.options === "object") {
Object.values(question.options).forEach((v) => {
if (typeof v === "string") htmlBlobs.push(v);
});
}
htmlBlobs.forEach((html) => {
const re = /
]+src=["']([^"']+)["']/gi;
let m;
while ((m = re.exec(String(html || "")))) pushImgSrc(m[1]);
});
for (const src of seenImg) {
const dataUrl = await _downloadImg(src);
if (!dataUrl) return { code: -1005, data: { answer: [] }, msg: "图片下载失败,请刷新题目后重试" };
images.push(dataUrl);
imageSources.push(src);
}
const payloadObj = {
question: question.title || "",
options: question.optionsText || [],
type: question.type || "",
platform: "abc-helper",
refer: question.refer || location.href
};
if (images.length) { payloadObj.images = images; payloadObj.image_sources = imageSources; }
const payload = JSON.stringify(payloadObj);
const data = "question=" + encodeURIComponent(payload)
+ "&u=" + encodeURIComponent(getCookieValue("_uid") || getCookieValue("UID") || "")
+ "&model_mode=auto"
+ "&match_mode=" + getQuestionMatchMode()
+ (extra && extra.skipBank ? "&skip_bank=1" : "");
const _interval = Number(extra && extra.interval);
await sleep(Number.isFinite(_interval) && _interval >= 0 ? _interval : setting.config.basicConfig.reqIntervalTime.value);
const result = await new Promise((resolve) => {
GM_xmlhttpRequest({
url: LOCAL_BACKEND_ANSWER_URL + "?v=xs-5.0",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Authorization": token ? "Bearer " + token : ""
},
data,
timeout: 24e4,
onload: (response) => {
try {
const obj = JSON.parse(response.responseText || "{}");
if (obj.code === 200 && obj.data && (obj.data.answer || (obj.data.multimatch && obj.data.candidates && obj.data.candidates.length))) {
const candidates = Array.isArray(obj.data.candidates) ? obj.data.candidates : [];
resolve({
code: 1,
data: {
answer: normalizeLocalBackendAnswers(obj.data.answer),
src: obj.data.bank ? "bank" : obj.data.cache ? "cache" : obj.data.multimatch ? "pick" : "ai",
multimatch: !!obj.data.multimatch && candidates.length > 0,
candidates,
remainCount: obj.data.remainCount !== void 0 ? formatLocalRemainCount(obj.data.remainCount) : getProfileRemainingCount(obj.data.profile)
},
msg: obj.data.multimatch ? (obj.msg || "题库中有多道相似题,请手动选择") : (obj.msg || (obj.data.bank ? "题库命中" : obj.data.cache ? "缓存命中" : "本地后端返回"))
});
if (obj.data.profile) updateUserInfoFromProfile(obj.data.profile);
} else if (response.status === 401 || obj.code === 401) {
resolve(handleError$1("本地后端未登录或登录态失效,请先登录用户中心"));
} else {
if (response.status === 429 || obj.code === 429 || /请求过于频繁/.test(obj.msg || "")) {
const seconds = Number(((obj.msg || "").match(/(\d+)\s*秒/) || [])[1] || 6);
resolve({ code: -1006, msg: obj.msg || "请求限流", retryAfter: Math.min(60, Math.max(1, seconds)) });
return;
}
const is200NoAnswer = obj.code === 200 && !(obj.data && obj.data.answer);
resolve({ code: -1004, data: { answer: [], remainCount: 0 }, msg: is200NoAnswer ? "未找到答案" : (obj.msg || "本地题库未命中") });
}
} catch (e) {
resolve(handleError$1("本地后端响应解析失败"));
}
},
onerror: () => resolve(handleError$1("无法连接本地后端 https://xs.openget.cn")),
ontimeout: () => resolve(handleError$1("本地后端请求超时,请检查后端模型配置或稍后重试"))
});
});
if (result.code === -1006 && (extra.retryCount || 0) < 2) {
await sleep(result.retryAfter + 1);
return callLocalBackendAnswer(question, { ...extra, retryCount: (extra.retryCount || 0) + 1 });
}
return result;
};
const saveAnswerRecord = ({ question, answer, source = "accurate" }) => {
const setting = useSettingStore();
const answerRecordStore = useAnswerRecordStore();
return answerRecordStore.insertRecord({
question,
answer,
token: setting.config.basicConfig.token.value,
source
});
};
const getAccurateAnswer = async (question) => {
return callLocalBackendAnswer(question);
};
const fetchFuzzySearch = async (question) => {
const setting = useSettingStore();
const token = setting.config.basicConfig.token.value || getLocalBackendToken() || "";
const data = "question=" + encodeURIComponent(question);
return new Promise((resolve) => {
GM_xmlhttpRequest({
url: LOCAL_BACKEND_SEARCH_URL,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Authorization": token ? "Bearer " + token : ""
},
data,
timeout: 24e4,
onload: (response) => {
try {
const obj = JSON.parse(response.responseText || "{}");
if (obj.code === 200 && obj.data && obj.data.found) {
const matches = obj.data.matches || [];
const items = matches.map((m) => ({
title: m.question || question,
options: JSON.stringify(m.options || []),
answer: JSON.stringify([m.answer || ""]),
type: String(m.type || "8")
}));
resolve({ code: 1, data: { items }, msg: obj.msg || "success" });
} else if (response.status === 401 || obj.code === 401) {
resolve(handleError$1("本地后端未登录或登录态失效,请先登录用户中心"));
} else {
const miss = obj.data && obj.data.found === false;
resolve({ code: obj.code || -1004, data: { items: [] }, msg: miss ? "题库暂未收录这道题" : (obj.msg || "未查询到答案") });
}
} catch (e) {
resolve({ code: -1004, data: { items: [] }, msg: "搜索响应解析失败" });
}
},
onerror: () => resolve({ code: -1004, data: { items: [] }, msg: "无法连接本地后端" }),
ontimeout: () => resolve({ code: -1004, data: { items: [] }, msg: "搜索请求超时,请稍后重试" })
});
});
};
const getAIAnswer = async (question) => {
return callLocalBackendAnswer(question, { skipBank: true });
};
const _hoisted_1$4 = { class: "online-search-wrap" };
const _hoisted_2$3 = { class: "tips" };
const _hoisted_3$2 = { class: "config" };
const _hoisted_4$2 = { class: "config-input" };
const _hoisted_5$2 = { class: "searth-input-wrap" };
const _hoisted_6$2 = { class: "searth-input" };
const _hoisted_7$2 = { class: "search-btn" };
const _hoisted_8$2 = { class: "result-list" };
const _hoisted_9$2 = ["innerHTML"];
const _hoisted_10$1 = ["innerHTML"];
const _hoisted_11$1 = ["innerHTML"];
const _hoisted_12$1 = {
key: 0,
style: { "text-align": "center", "font-size": "15px" }
};
const _sfc_main$5 = {
__name: "OnlineSearch",
emits: ["changeTab"],
setup(__props, { emit: __emit }) {
const [messageApi, contextHolder] = Antd.message.useMessage();
const setting = useSettingStore();
const questionStore = useQuestionStore();
const { searchText } = pinia.storeToRefs(questionStore);
const errInfo = vue.ref("");
const isLoading = vue.ref(false);
const isComposing = vue.ref(false);
const emit = __emit;
const goSetting = () => {
window.open(setting.shopLink || "https://xs.openget.cn/user?section=account", "_blank");
};
const tokenReady = vue.computed(() => {
const remain = String(setting.userInfo.remainCount || "");
return !!setting.config.basicConfig.token.value && remain && !["密钥未配置", "密钥无效", "null"].includes(remain);
});
const typeMap = {
单选题: "0",
多选题: "1",
填空题: "2",
判断题: "3",
简答题: "4",
问答题: "4",
名词解释: "5",
论述题: "6",
计算题: "7",
分录题: "9",
资料题: "10",
连线题: "11",
匹配题: "11",
排序题: "13",
完型填空: "14",
完形填空题: "14",
阅读理解: "15",
程序题: "17",
口语题: "18",
听力题: "19",
共用选项题: "20",
测评题: "21",
钟表题: "23",
选词填空: "24",
选做题: "25",
其它: "8"
};
const getTypeByCode = vue.computed(() => {
return function(type) {
let key = Object.keys(typeMap).find((key2) => typeMap[key2] === String(type));
return key || "";
};
});
const answerJoin = vue.computed(() => {
return function(answerList) {
try {
return JSON.parse(answerList).join(";");
} catch {
return answerList;
}
};
});
const { fuzzySearchResult } = pinia.storeToRefs(questionStore);
const fuzzySearch = async (e) => {
e.preventDefault();
if (!setting.config.basicConfig.token.value) {
messageApi.info("未配置密钥,请先前往“设置”模块填写密钥..");
return;
}
if (!searchText.value || isComposing.value) return;
if (searchText.value) {
errInfo.value = "";
questionStore.clearFuzzySearchResult();
isLoading.value = true;
const res = await fetchFuzzySearch(searchText.value);
isLoading.value = false;
if (res.code === 1) {
const {
data: { items }
} = res;
questionStore.fuzzySearchResult = items;
} else {
errInfo.value = res.msg;
}
} else {
messageApi.info("请输入题目..");
}
};
return (_ctx, _cache) => {
const _component_a_button = vue.resolveComponent("a-button");
const _component_a_switch = vue.resolveComponent("a-switch");
const _component_a_textarea = vue.resolveComponent("a-textarea");
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$4, [
vue.createVNode(vue.unref(contextHolder)),
vue.createElementVNode("div", _hoisted_2$3, [
vue.createElementVNode("ul", null, [
vue.createElementVNode("li", null, [
_cache[5] || (_cache[5] = vue.createTextVNode(" 使用在线搜题前,请先前往 ")),
vue.createVNode(_component_a_button, {
onClick: goSetting,
size: "small",
type: "primary",
danger: tokenReady.value
}, {
icon: vue.withCtx(() => [
vue.createVNode(vue.unref(SettingOutlined))
]),
default: vue.withCtx(() => [
vue.createTextVNode(vue.toDisplayString(tokenReady.value ? "已配置密钥" : "登录用户中心获取密钥"), 1)
]),
_: 1
})
]),
_cache[6] || (_cache[6] = vue.createElementVNode("li", null, "如需手动答题,建议关闭自动切换功能,防止操作冲突", -1))
])
]),
vue.createElementVNode("div", _hoisted_3$2, [
_cache[7] || (_cache[7] = vue.createElementVNode("div", { class: "config-name" }, "划词搜题", -1)),
vue.createElementVNode("div", _hoisted_4$2, [
vue.createVNode(_component_a_switch, {
size: "small",
checked: vue.unref(setting).config.searchConfig.searchBySelectedText.value,
"onUpdate:checked": _cache[0] || (_cache[0] = ($event) => vue.unref(setting).config.searchConfig.searchBySelectedText.value = $event)
}, null, 8, ["checked"])
])
]),
vue.createElementVNode("div", _hoisted_5$2, [
_cache[8] || (_cache[8] = vue.createElementVNode("div", { class: "title" }, "搜索题目", -1)),
vue.createElementVNode("div", _hoisted_6$2, [
vue.createVNode(_component_a_textarea, {
onKeydown: vue.withKeys(vue.withModifiers(fuzzySearch, ["exact"]), ["enter"]),
onCompositionstart: _cache[1] || (_cache[1] = ($event) => isComposing.value = true),
onCompositionend: _cache[2] || (_cache[2] = ($event) => isComposing.value = false),
value: vue.unref(searchText),
"onUpdate:value": _cache[3] || (_cache[3] = ($event) => vue.isRef(searchText) ? searchText.value = $event : null),
placeholder: "请输入题目",
rows: 4
}, null, 8, ["onKeydown", "value"])
])
]),
vue.createElementVNode("div", _hoisted_7$2, [
vue.createVNode(_component_a_button, {
onClick: fuzzySearch,
style: { "width": "120px" },
type: "primary",
size: "small"
}, {
default: vue.withCtx(() => _cache[9] || (_cache[9] = [
vue.createTextVNode("搜索")
])),
_: 1
})
]),
vue.createElementVNode("div", _hoisted_8$2, [
vue.createElementVNode("div", { innerHTML: errInfo.value }, null, 8, _hoisted_9$2),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(fuzzySearchResult), (result, index) => {
return vue.openBlock(), vue.createElementBlock("div", {
class: "result-item",
key: index
}, [
vue.createElementVNode("div", null, [
vue.createElementVNode("span", null, vue.toDisplayString(`${index + 1}.【${getTypeByCode.value(result.type)}】`), 1),
vue.createElementVNode("span", {
innerHTML: result.title
}, null, 8, _hoisted_10$1)
]),
vue.createElementVNode("div", null, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(JSON.parse(result.options), (option, index2) => {
return vue.openBlock(), vue.createElementBlock("div", { key: index2 }, [
vue.createElementVNode("span", null, vue.toDisplayString(String.fromCharCode(65 + index2)) + ".", 1),
vue.createElementVNode("span", null, vue.toDisplayString(option), 1)
]);
}), 128))
]),
vue.createElementVNode("div", {
class: "result-answer",
innerHTML: answerJoin.value(result.answer)
}, null, 8, _hoisted_11$1)
]);
}), 128)),
isLoading.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_12$1, [
_cache[10] || (_cache[10] = vue.createElementVNode("span", { style: { "margin-right": "10px" } }, "搜索中", -1)),
vue.createVNode(vue.unref(LoadingOutlined))
])) : vue.createCommentVNode("", true)
])
]);
};
}
};
const _hoisted_1$2 = { class: "record-wrap" };
const _hoisted_2$1 = { class: "record-tip" };
const _hoisted_3 = { class: "record-filter" };
const _hoisted_4 = ["onClick"];
const _hoisted_5 = {
key: 0,
class: "record-list"
};
const _hoisted_6 = { class: "record-row" };
const _hoisted_7 = ["title"];
const _hoisted_8 = { class: "record-row" };
const _hoisted_9 = ["title"];
const _hoisted_10 = { class: "record-row" };
const _hoisted_11 = ["title"];
const _hoisted_12 = { class: "record-row" };
const _hoisted_13 = ["title"];
const _hoisted_14 = {
key: 1,
class: "tips"
};
const _sfc_main$3 = {
__name: "AnswerRecord",
setup(__props) {
const answerRecordStore = useAnswerRecordStore();
const { displayRecords } = pinia.storeToRefs(answerRecordStore);
const activeFilter = vue.ref("all");
const keyword = vue.ref("");
const filterOptions = [
{ label: "所有", value: "all" },
{ label: "今天", value: "today" },
{ label: "昨天", value: "yesterday" },
{ label: "近一周", value: "week" },
{ label: "近一月", value: "month" }
];
const getDayStart = (date = /* @__PURE__ */ new Date()) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
const filteredRecords = vue.computed(() => {
const now = Date.now();
const todayStart = getDayStart();
const yesterdayStart = todayStart - 24 * 60 * 60 * 1e3;
const kw = keyword.value.trim().toLowerCase();
return displayRecords.value.filter((record) => {
if (kw && !String(record.title || "").toLowerCase().includes(kw)) {
return false;
}
const timestamp = record.timestamp || 0;
if (activeFilter.value === "all") {
return true;
}
if (activeFilter.value === "today") {
return timestamp >= todayStart;
}
if (activeFilter.value === "yesterday") {
return timestamp >= yesterdayStart && timestamp < todayStart;
}
if (activeFilter.value === "week") {
return timestamp >= now - 7 * 24 * 60 * 60 * 1e3;
}
if (activeFilter.value === "month") {
return timestamp >= now - 30 * 24 * 60 * 60 * 1e3;
}
return true;
});
});
const clearRecords = () => {
answerRecordStore.clearRecords();
};
const formatAnswer = (answer) => {
if (!Array.isArray(answer) || answer.length === 0) return "-";
return answer.join(";");
};
return (_ctx, _cache) => {
const _component_a_button = vue.resolveComponent("a-button");
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$2, [
_cache[5] || (_cache[5] = vue.createElementVNode("div", { class: "separator" }, "▤ 记录", -1)),
vue.createElementVNode("div", _hoisted_2$1, [
vue.createElementVNode("span", null, vue.toDisplayString(keyword.value.trim() ? "找到 " + filteredRecords.value.length + " 条记录" : "最多保留 200 条答题记录,当前 " + filteredRecords.value.length + " 条"), 1),
vue.createVNode(_component_a_button, {
class: "clear-btn",
size: "small",
onClick: clearRecords
}, {
default: vue.withCtx(() => _cache[0] || (_cache[0] = [
vue.createTextVNode("清空记录")
])),
_: 1
})
]),
vue.createElementVNode("div", { class: "record-search-wrap" }, [
vue.createElementVNode("input", {
class: "record-search",
value: keyword.value,
placeholder: "按题目关键词搜索",
onInput: ($event) => keyword.value = $event.target.value
}, null, 8, ["value"])
]),
vue.createElementVNode("div", _hoisted_3, [
(vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(filterOptions, (item) => {
return vue.createElementVNode("button", {
key: item.value,
class: vue.normalizeClass(["filter-btn", { active: activeFilter.value === item.value }]),
onClick: ($event) => activeFilter.value = item.value
}, vue.toDisplayString(item.label), 11, _hoisted_4);
}), 64))
]),
filteredRecords.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_5, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(filteredRecords.value, (record) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: `${record.cacheKey}-${record.timestamp}`,
class: "record-item"
}, [
vue.createElementVNode("div", _hoisted_6, [
_cache[1] || (_cache[1] = vue.createElementVNode("span", { class: "record-label" }, "密钥", -1)),
vue.createElementVNode("span", {
class: "record-value",
title: record.token || "未配置"
}, vue.toDisplayString(record.token || "未配置"), 9, _hoisted_7)
]),
vue.createElementVNode("div", _hoisted_8, [
_cache[2] || (_cache[2] = vue.createElementVNode("span", { class: "record-label" }, "题目", -1)),
vue.createElementVNode("span", {
class: "record-value",
title: record.title
}, vue.toDisplayString(record.title), 9, _hoisted_9)
]),
vue.createElementVNode("div", _hoisted_10, [
_cache[3] || (_cache[3] = vue.createElementVNode("span", { class: "record-label" }, "答案", -1)),
vue.createElementVNode("span", {
class: "record-value",
title: formatAnswer(record.answer)
}, vue.toDisplayString(formatAnswer(record.answer)), 9, _hoisted_11)
]),
vue.createElementVNode("div", _hoisted_12, [
_cache[4] || (_cache[4] = vue.createElementVNode("span", { class: "record-label" }, "请求时间", -1)),
vue.createElementVNode("span", {
class: "record-value",
title: record.requestTime
}, vue.toDisplayString(record.requestTime), 9, _hoisted_13)
])
]);
}), 128))
])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_14, vue.toDisplayString(keyword.value.trim() ? "没有找到包含「" + keyword.value.trim() + "」的记录。" : " 当前筛选条件下暂无答题记录。成功搜到答案并扣次后,会自动写入本地缓存。 "), 1))
]);
};
}
};
const useMessageStore = pinia.defineStore("messageStore", {
state: () => ({
messageList: []
}),
actions: {
insertMessage(text, type = "success") {
const message2 = {
text,
type
};
this.messageList.push(message2);
},
clearMessage() {
this.messageList = [];
}
}
});
const TIMEOUT_ERROR_MSG = "请求超时,请前往设置尝试切换节点..";
const SERVER_MAINTENANCE_ERROR_MSG = "服务器维护中,请前往设置尝试切换节点,自动学习功能正常使用..";
const handleError = (msg) => ({
code: 10003,
data: {
answer: [],
num: "",
usenum: ""
},
msg
});
const fetchBasicInfo = async () => {
const scriptInfo = getScriptInfo();
return {
code: 1,
data: {
shopLink: __DIST.shop || "https://xs.openget.cn/user?section=account", // FX-CH:basic-info-shop
version: scriptInfo.version,
chatWebUrl: "已接入 AI 智能答题云服务"
}
};
};
const getTokenInfo = async () => {
let token = "";
try {
const setting = useSettingStore();
token = setting.config.basicConfig.token.value || getLocalBackendToken() || "";
} catch (_) {
token = getLocalBackendToken() || "";
}
const profile = await fetchLocalBackendProfile(token);
const remainCount = getProfileRemainingCount(profile);
return {
code: profile ? 1 : 10003,
data: {
count: remainCount,
usedCount: 0,
remainCount
},
msg: profile ? "本地后端已同步题数" : "密钥无效或已过期,请到用户中心重新生成脚本密钥"
};
};
const cxEditorDocuments = new WeakSet();
let activeCxEditor = null, cxEditorWatcherInstalled = false;
const installCxCustomEditor = (doc, setting) => {
if (!doc || cxEditorDocuments.has(doc)) return;
cxEditorDocuments.add(doc);
if (!cxEditorWatcherInstalled) {
cxEditorWatcherInstalled = true;
vue.watch(() => setting.config.cxConfig.showCustomEditor.value, enabled => {
if (!enabled && activeCxEditor) { activeCxEditor.remove(); activeCxEditor = null; }
});
}
doc.addEventListener("focusin", event => {
if (!setting.config.cxConfig.showCustomEditor.value) return;
const target = event.target;
if (!target || !target.matches || !target.matches('textarea, [contenteditable="true"]') || target.disabled || target.readOnly) return;
if (target.closest('[data-xs-custom-editor], [data-xs-abc-app]')) return;
const hostDoc = getTopDocument() || doc;
if (activeCxEditor) activeCxEditor.remove();
const panel = hostDoc.createElement("div");
panel.setAttribute("data-xs-custom-editor", "true");
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-label", "自定义答案编辑框");
panel.style.cssText = "position:fixed;right:24px;bottom:24px;width:360px;max-width:85vw;padding:16px;background:#fff;color:#111;border:1px solid #aaa;border-radius:8px;z-index:2147483647;box-shadow:0 4px 24px #0003";
const label = hostDoc.createElement("div"); label.textContent = "编辑答案(应用后写回原输入框,不提交)";
const editor = hostDoc.createElement("textarea");
editor.value = target.tagName === "TEXTAREA" ? target.value : (target.innerText || target.textContent || "");
editor.style.cssText = "box-sizing:border-box;width:100%;height:140px;margin:12px 0;color:#111;background:#fff";
const apply = hostDoc.createElement("button"); apply.textContent = "应用";
const close = hostDoc.createElement("button"); close.textContent = "取消";
apply.type = close.type = "button";
const dismiss = () => { panel.remove(); if (activeCxEditor === panel) activeCxEditor = null; };
apply.onclick = () => {
if (!setting.config.cxConfig.showCustomEditor.value || !target.isConnected) { dismiss(); return; }
if (target.tagName === "TEXTAREA") target.value = editor.value;
else target.textContent = editor.value;
const EventType = target.ownerDocument.defaultView.Event;
target.dispatchEvent(new EventType("input", {bubbles:true}));
target.dispatchEvent(new EventType("change", {bubbles:true}));
dismiss();
};
close.onclick = dismiss;
panel.append(label, editor, apply, close); hostDoc.body.appendChild(panel);
activeCxEditor = panel; editor.focus();
});
const installFrame = frame => { try { if (frame.contentDocument) installCxCustomEditor(frame.contentDocument, setting); } catch (_) {} };
doc.querySelectorAll("iframe").forEach(installFrame);
doc.addEventListener("load", event => { if (event.target && event.target.tagName === "IFRAME") installFrame(event.target); }, true);
};
const selectTextSearchLogic = async (iframeDocument, iframeWindow) => {
const questionStore = useQuestionStore();
const setting = useSettingStore();
if (/chaoxing\.com$/.test(location.hostname)) installCxCustomEditor(iframeDocument, setting);
_unsafeWindow.document.onselectstart = true;
_unsafeWindow.document.oncontextmenu = true;
_unsafeWindow.document.oncut = true;
_unsafeWindow.document.oncopy = true;
_unsafeWindow.document.onpaste = true;
const getSelectedText = () => {
if (!setting.config.searchConfig.searchBySelectedText.value) return;
let selectedText = "";
if (iframeWindow.getSelection) {
selectedText = iframeWindow.getSelection().toString();
} else if (iframeDocument.selection) {
selectedText = iframeDocument.selection.createRange().text;
}
if (selectedText != "") {
questionStore.searchText = selectedText;
setting.tabIndex = "3";
}
};
iframeDocument.addEventListener("mouseup", getSelectedText);
};
var Typr$1 = {};
var Typr = {};
Typr.parse = function(buff) {
var bin = Typr._bin;
var data = new Uint8Array(buff);
var tag = bin.readASCII(data, 0, 4);
if (tag == "ttcf") {
var offset = 4;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var numF = bin.readUint(data, offset);
offset += 4;
var fnts = [];
for (var i = 0; i < numF; i++) {
var foff = bin.readUint(data, offset);
offset += 4;
fnts.push(Typr._readFont(data, foff));
}
return fnts;
} else
return [Typr._readFont(data, 0)];
};
Typr._readFont = function(data, offset) {
var bin = Typr._bin;
var ooff = offset;
bin.readFixed(data, offset);
offset += 4;
var numTables = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var tags = [
"cmap",
"head",
"hhea",
"maxp",
"hmtx",
"name",
"OS/2",
"post",
//"cvt",
//"fpgm",
"loca",
"glyf",
"kern",
//"prep"
//"gasp"
"CFF ",
"GPOS",
"GSUB",
"SVG "
//"VORG",
];
var obj = { _data: data, _offset: ooff };
var tabs = {};
for (var i = 0; i < numTables; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
bin.readUint(data, offset);
offset += 4;
var toffset = bin.readUint(data, offset);
offset += 4;
var length = bin.readUint(data, offset);
offset += 4;
tabs[tag] = { offset: toffset, length };
}
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
if (tabs[t])
obj[t.trim()] = Typr[t.trim()].parse(data, tabs[t].offset, tabs[t].length, obj);
}
return obj;
};
Typr._tabOffset = function(data, tab, foff) {
var bin = Typr._bin;
var numTables = bin.readUshort(data, foff + 4);
var offset = foff + 12;
for (var i = 0; i < numTables; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
bin.readUint(data, offset);
offset += 4;
var toffset = bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
if (tag == tab)
return toffset;
}
return 0;
};
Typr._bin = {
readFixed: function(data, o) {
return (data[o] << 8 | data[o + 1]) + (data[o + 2] << 8 | data[o + 3]) / (256 * 256 + 4);
},
readF2dot14: function(data, o) {
var num = Typr._bin.readShort(data, o);
return num / 16384;
},
readInt: function(buff, p) {
return Typr._bin._view(buff).getInt32(p);
},
readInt8: function(buff, p) {
return Typr._bin._view(buff).getInt8(p);
},
readShort: function(buff, p) {
return Typr._bin._view(buff).getInt16(p);
},
readUshort: function(buff, p) {
return Typr._bin._view(buff).getUint16(p);
},
readUshorts: function(buff, p, len) {
var arr = [];
for (var i = 0; i < len; i++)
arr.push(Typr._bin.readUshort(buff, p + i * 2));
return arr;
},
readUint: function(buff, p) {
return Typr._bin._view(buff).getUint32(p);
},
readUint64: function(buff, p) {
return Typr._bin.readUint(buff, p) * (4294967295 + 1) + Typr._bin.readUint(buff, p + 4);
},
readASCII: function(buff, p, l) {
var s = "";
for (var i = 0; i < l; i++)
s += String.fromCharCode(buff[p + i]);
return s;
},
readUnicode: function(buff, p, l) {
var s = "";
for (var i = 0; i < l; i++) {
var c = buff[p++] << 8 | buff[p++];
s += String.fromCharCode(c);
}
return s;
},
_tdec: typeof window !== "undefined" && window["TextDecoder"] ? new window["TextDecoder"]() : null,
readUTF8: function(buff, p, l) {
var tdec = Typr._bin._tdec;
if (tdec && p == 0 && l == buff.length)
return tdec["decode"](buff);
return Typr._bin.readASCII(buff, p, l);
},
readBytes: function(buff, p, l) {
var arr = [];
for (var i = 0; i < l; i++)
arr.push(buff[p + i]);
return arr;
},
readASCIIArray: function(buff, p, l) {
var s = [];
for (var i = 0; i < l; i++)
s.push(String.fromCharCode(buff[p + i]));
return s;
},
_view: function(buff) {
return buff._dataView || (buff._dataView = buff.buffer ? new DataView(buff.buffer, buff.byteOffset, buff.byteLength) : new DataView(new Uint8Array(buff).buffer));
}
};
Typr._lctf = {};
Typr._lctf.parse = function(data, offset, length, font, subt) {
var bin = Typr._bin;
var obj = {};
var offset0 = offset;
bin.readFixed(data, offset);
offset += 4;
var offScriptList = bin.readUshort(data, offset);
offset += 2;
var offFeatureList = bin.readUshort(data, offset);
offset += 2;
var offLookupList = bin.readUshort(data, offset);
offset += 2;
obj.scriptList = Typr._lctf.readScriptList(data, offset0 + offScriptList);
obj.featureList = Typr._lctf.readFeatureList(data, offset0 + offFeatureList);
obj.lookupList = Typr._lctf.readLookupList(data, offset0 + offLookupList, subt);
return obj;
};
Typr._lctf.readLookupList = function(data, offset, subt) {
var bin = Typr._bin;
var offset0 = offset;
var obj = [];
var count = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count; i++) {
var noff = bin.readUshort(data, offset);
offset += 2;
var lut = Typr._lctf.readLookupTable(data, offset0 + noff, subt);
obj.push(lut);
}
return obj;
};
Typr._lctf.readLookupTable = function(data, offset, subt) {
var bin = Typr._bin;
var offset0 = offset;
var obj = { tabs: [] };
obj.ltype = bin.readUshort(data, offset);
offset += 2;
obj.flag = bin.readUshort(data, offset);
offset += 2;
var cnt = bin.readUshort(data, offset);
offset += 2;
var ltype = obj.ltype;
for (var i = 0; i < cnt; i++) {
var noff = bin.readUshort(data, offset);
offset += 2;
var tab = subt(data, ltype, offset0 + noff, obj);
obj.tabs.push(tab);
}
return obj;
};
Typr._lctf.numOfOnes = function(n) {
var num = 0;
for (var i = 0; i < 32; i++)
if ((n >>> i & 1) != 0)
num++;
return num;
};
Typr._lctf.readClassDef = function(data, offset) {
var bin = Typr._bin;
var obj = [];
var format = bin.readUshort(data, offset);
offset += 2;
if (format == 1) {
var startGlyph = bin.readUshort(data, offset);
offset += 2;
var glyphCount = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < glyphCount; i++) {
obj.push(startGlyph + i);
obj.push(startGlyph + i);
obj.push(bin.readUshort(data, offset));
offset += 2;
}
}
if (format == 2) {
var count = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count; i++) {
obj.push(bin.readUshort(data, offset));
offset += 2;
obj.push(bin.readUshort(data, offset));
offset += 2;
obj.push(bin.readUshort(data, offset));
offset += 2;
}
}
return obj;
};
Typr._lctf.getInterval = function(tab, val) {
for (var i = 0; i < tab.length; i += 3) {
var start = tab[i], end = tab[i + 1];
tab[i + 2];
if (start <= val && val <= end)
return i;
}
return -1;
};
Typr._lctf.readCoverage = function(data, offset) {
var bin = Typr._bin;
var cvg = {};
cvg.fmt = bin.readUshort(data, offset);
offset += 2;
var count = bin.readUshort(data, offset);
offset += 2;
if (cvg.fmt == 1)
cvg.tab = bin.readUshorts(data, offset, count);
if (cvg.fmt == 2)
cvg.tab = bin.readUshorts(data, offset, count * 3);
return cvg;
};
Typr._lctf.coverageIndex = function(cvg, val) {
var tab = cvg.tab;
if (cvg.fmt == 1)
return tab.indexOf(val);
if (cvg.fmt == 2) {
var ind = Typr._lctf.getInterval(tab, val);
if (ind != -1)
return tab[ind + 2] + (val - tab[ind]);
}
return -1;
};
Typr._lctf.readFeatureList = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = [];
var count = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
var noff = bin.readUshort(data, offset);
offset += 2;
var feat = Typr._lctf.readFeatureTable(data, offset0 + noff);
feat.tag = tag.trim();
obj.push(feat);
}
return obj;
};
Typr._lctf.readFeatureTable = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var feat = {};
var featureParams = bin.readUshort(data, offset);
offset += 2;
if (featureParams > 0) {
feat.featureParams = offset0 + featureParams;
}
var lookupCount = bin.readUshort(data, offset);
offset += 2;
feat.tab = [];
for (var i = 0; i < lookupCount; i++)
feat.tab.push(bin.readUshort(data, offset + 2 * i));
return feat;
};
Typr._lctf.readScriptList = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = {};
var count = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < count; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
var noff = bin.readUshort(data, offset);
offset += 2;
obj[tag.trim()] = Typr._lctf.readScriptTable(data, offset0 + noff);
}
return obj;
};
Typr._lctf.readScriptTable = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = {};
var defLangSysOff = bin.readUshort(data, offset);
offset += 2;
if (defLangSysOff > 0) {
obj["default"] = Typr._lctf.readLangSysTable(data, offset0 + defLangSysOff);
}
var langSysCount = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < langSysCount; i++) {
var tag = bin.readASCII(data, offset, 4);
offset += 4;
var langSysOff = bin.readUshort(data, offset);
offset += 2;
obj[tag.trim()] = Typr._lctf.readLangSysTable(data, offset0 + langSysOff);
}
return obj;
};
Typr._lctf.readLangSysTable = function(data, offset) {
var bin = Typr._bin;
var obj = {};
bin.readUshort(data, offset);
offset += 2;
obj.reqFeature = bin.readUshort(data, offset);
offset += 2;
var featureCount = bin.readUshort(data, offset);
offset += 2;
obj.features = bin.readUshorts(data, offset, featureCount);
return obj;
};
Typr.CFF = {};
Typr.CFF.parse = function(data, offset, length) {
var bin = Typr._bin;
data = new Uint8Array(data.buffer, offset, length);
offset = 0;
data[offset];
offset++;
data[offset];
offset++;
data[offset];
offset++;
data[offset];
offset++;
var ninds = [];
offset = Typr.CFF.readIndex(data, offset, ninds);
var names2 = [];
for (var i = 0; i < ninds.length - 1; i++)
names2.push(bin.readASCII(data, offset + ninds[i], ninds[i + 1] - ninds[i]));
offset += ninds[ninds.length - 1];
var tdinds = [];
offset = Typr.CFF.readIndex(data, offset, tdinds);
var topDicts = [];
for (var i = 0; i < tdinds.length - 1; i++)
topDicts.push(Typr.CFF.readDict(data, offset + tdinds[i], offset + tdinds[i + 1]));
offset += tdinds[tdinds.length - 1];
var topdict = topDicts[0];
var sinds = [];
offset = Typr.CFF.readIndex(data, offset, sinds);
var strings = [];
for (var i = 0; i < sinds.length - 1; i++)
strings.push(bin.readASCII(data, offset + sinds[i], sinds[i + 1] - sinds[i]));
offset += sinds[sinds.length - 1];
Typr.CFF.readSubrs(data, offset, topdict);
if (topdict.CharStrings) {
offset = topdict.CharStrings;
var sinds = [];
offset = Typr.CFF.readIndex(data, offset, sinds);
var cstr = [];
for (var i = 0; i < sinds.length - 1; i++)
cstr.push(bin.readBytes(data, offset + sinds[i], sinds[i + 1] - sinds[i]));
topdict.CharStrings = cstr;
}
if (topdict.ROS) {
offset = topdict.FDArray;
var fdind = [];
offset = Typr.CFF.readIndex(data, offset, fdind);
topdict.FDArray = [];
for (var i = 0; i < fdind.length - 1; i++) {
var dict = Typr.CFF.readDict(data, offset + fdind[i], offset + fdind[i + 1]);
Typr.CFF._readFDict(data, dict, strings);
topdict.FDArray.push(dict);
}
offset += fdind[fdind.length - 1];
offset = topdict.FDSelect;
topdict.FDSelect = [];
var fmt = data[offset];
offset++;
if (fmt == 3) {
var rns = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < rns + 1; i++) {
topdict.FDSelect.push(bin.readUshort(data, offset), data[offset + 2]);
offset += 3;
}
} else
throw fmt;
}
if (topdict.Encoding)
topdict.Encoding = Typr.CFF.readEncoding(data, topdict.Encoding, topdict.CharStrings.length);
if (topdict.charset)
topdict.charset = Typr.CFF.readCharset(data, topdict.charset, topdict.CharStrings.length);
Typr.CFF._readFDict(data, topdict, strings);
return topdict;
};
Typr.CFF._readFDict = function(data, dict, ss) {
var offset;
if (dict.Private) {
offset = dict.Private[1];
dict.Private = Typr.CFF.readDict(data, offset, offset + dict.Private[0]);
if (dict.Private.Subrs)
Typr.CFF.readSubrs(data, offset + dict.Private.Subrs, dict.Private);
}
for (var p in dict)
if (["FamilyName", "FontName", "FullName", "Notice", "version", "Copyright"].indexOf(p) != -1)
dict[p] = ss[dict[p] - 426 + 35];
};
Typr.CFF.readSubrs = function(data, offset, obj) {
var bin = Typr._bin;
var gsubinds = [];
offset = Typr.CFF.readIndex(data, offset, gsubinds);
var bias, nSubrs = gsubinds.length;
if (nSubrs < 1240)
bias = 107;
else if (nSubrs < 33900)
bias = 1131;
else
bias = 32768;
obj.Bias = bias;
obj.Subrs = [];
for (var i = 0; i < gsubinds.length - 1; i++)
obj.Subrs.push(bin.readBytes(data, offset + gsubinds[i], gsubinds[i + 1] - gsubinds[i]));
};
Typr.CFF.tableSE = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
0,
111,
112,
113,
114,
0,
115,
116,
117,
118,
119,
120,
121,
122,
0,
123,
0,
124,
125,
126,
127,
128,
129,
130,
131,
0,
132,
133,
0,
134,
135,
136,
137,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
138,
0,
139,
0,
0,
0,
0,
140,
141,
142,
143,
0,
0,
0,
0,
0,
144,
0,
0,
0,
145,
0,
0,
146,
147,
148,
149,
0,
0,
0,
0
];
Typr.CFF.glyphByUnicode = function(cff, code) {
for (var i = 0; i < cff.charset.length; i++)
if (cff.charset[i] == code)
return i;
return -1;
};
Typr.CFF.glyphBySE = function(cff, charcode) {
if (charcode < 0 || charcode > 255)
return -1;
return Typr.CFF.glyphByUnicode(cff, Typr.CFF.tableSE[charcode]);
};
Typr.CFF.readEncoding = function(data, offset, num) {
Typr._bin;
var array = [".notdef"];
var format = data[offset];
offset++;
if (format == 0) {
var nCodes = data[offset];
offset++;
for (var i = 0; i < nCodes; i++)
array.push(data[offset + i]);
} else
throw "error: unknown encoding format: " + format;
return array;
};
Typr.CFF.readCharset = function(data, offset, num) {
var bin = Typr._bin;
var charset = [".notdef"];
var format = data[offset];
offset++;
if (format == 0) {
for (var i = 0; i < num; i++) {
var first = bin.readUshort(data, offset);
offset += 2;
charset.push(first);
}
} else if (format == 1 || format == 2) {
while (charset.length < num) {
var first = bin.readUshort(data, offset);
offset += 2;
var nLeft = 0;
if (format == 1) {
nLeft = data[offset];
offset++;
} else {
nLeft = bin.readUshort(data, offset);
offset += 2;
}
for (var i = 0; i <= nLeft; i++) {
charset.push(first);
first++;
}
}
} else
throw "error: format: " + format;
return charset;
};
Typr.CFF.readIndex = function(data, offset, inds) {
var bin = Typr._bin;
var count = bin.readUshort(data, offset) + 1;
offset += 2;
var offsize = data[offset];
offset++;
if (offsize == 1)
for (var i = 0; i < count; i++)
inds.push(data[offset + i]);
else if (offsize == 2)
for (var i = 0; i < count; i++)
inds.push(bin.readUshort(data, offset + i * 2));
else if (offsize == 3)
for (var i = 0; i < count; i++)
inds.push(bin.readUint(data, offset + i * 3 - 1) & 16777215);
else if (count != 1)
throw "unsupported offset size: " + offsize + ", count: " + count;
offset += count * offsize;
return offset - 1;
};
Typr.CFF.getCharString = function(data, offset, o) {
var bin = Typr._bin;
var b0 = data[offset], b1 = data[offset + 1];
data[offset + 2];
data[offset + 3];
data[offset + 4];
var vs = 1;
var op = null, val = null;
if (b0 <= 20) {
op = b0;
vs = 1;
}
if (b0 == 12) {
op = b0 * 100 + b1;
vs = 2;
}
if (21 <= b0 && b0 <= 27) {
op = b0;
vs = 1;
}
if (b0 == 28) {
val = bin.readShort(data, offset + 1);
vs = 3;
}
if (29 <= b0 && b0 <= 31) {
op = b0;
vs = 1;
}
if (32 <= b0 && b0 <= 246) {
val = b0 - 139;
vs = 1;
}
if (247 <= b0 && b0 <= 250) {
val = (b0 - 247) * 256 + b1 + 108;
vs = 2;
}
if (251 <= b0 && b0 <= 254) {
val = -(b0 - 251) * 256 - b1 - 108;
vs = 2;
}
if (b0 == 255) {
val = bin.readInt(data, offset + 1) / 65535;
vs = 5;
}
o.val = val != null ? val : "o" + op;
o.size = vs;
};
Typr.CFF.readCharString = function(data, offset, length) {
var end = offset + length;
var bin = Typr._bin;
var arr = [];
while (offset < end) {
var b0 = data[offset], b1 = data[offset + 1];
data[offset + 2];
data[offset + 3];
data[offset + 4];
var vs = 1;
var op = null, val = null;
if (b0 <= 20) {
op = b0;
vs = 1;
}
if (b0 == 12) {
op = b0 * 100 + b1;
vs = 2;
}
if (b0 == 19 || b0 == 20) {
op = b0;
vs = 2;
}
if (21 <= b0 && b0 <= 27) {
op = b0;
vs = 1;
}
if (b0 == 28) {
val = bin.readShort(data, offset + 1);
vs = 3;
}
if (29 <= b0 && b0 <= 31) {
op = b0;
vs = 1;
}
if (32 <= b0 && b0 <= 246) {
val = b0 - 139;
vs = 1;
}
if (247 <= b0 && b0 <= 250) {
val = (b0 - 247) * 256 + b1 + 108;
vs = 2;
}
if (251 <= b0 && b0 <= 254) {
val = -(b0 - 251) * 256 - b1 - 108;
vs = 2;
}
if (b0 == 255) {
val = bin.readInt(data, offset + 1) / 65535;
vs = 5;
}
arr.push(val != null ? val : "o" + op);
offset += vs;
}
return arr;
};
Typr.CFF.readDict = function(data, offset, end) {
var bin = Typr._bin;
var dict = {};
var carr = [];
while (offset < end) {
var b0 = data[offset], b1 = data[offset + 1];
data[offset + 2];
data[offset + 3];
data[offset + 4];
var vs = 1;
var key = null, val = null;
if (b0 == 28) {
val = bin.readShort(data, offset + 1);
vs = 3;
}
if (b0 == 29) {
val = bin.readInt(data, offset + 1);
vs = 5;
}
if (32 <= b0 && b0 <= 246) {
val = b0 - 139;
vs = 1;
}
if (247 <= b0 && b0 <= 250) {
val = (b0 - 247) * 256 + b1 + 108;
vs = 2;
}
if (251 <= b0 && b0 <= 254) {
val = -(b0 - 251) * 256 - b1 - 108;
vs = 2;
}
if (b0 == 255) {
val = bin.readInt(data, offset + 1) / 65535;
vs = 5;
throw "unknown number";
}
if (b0 == 30) {
var nibs = [];
vs = 1;
while (true) {
var b = data[offset + vs];
vs++;
var nib0 = b >> 4, nib1 = b & 15;
if (nib0 != 15)
nibs.push(nib0);
if (nib1 != 15)
nibs.push(nib1);
if (nib1 == 15)
break;
}
var s = "";
var chars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ".", "e", "e-", "reserved", "-", "endOfNumber"];
for (var i = 0; i < nibs.length; i++)
s += chars[nibs[i]];
val = parseFloat(s);
}
if (b0 <= 21) {
var keys = [
"version",
"Notice",
"FullName",
"FamilyName",
"Weight",
"FontBBox",
"BlueValues",
"OtherBlues",
"FamilyBlues",
"FamilyOtherBlues",
"StdHW",
"StdVW",
"escape",
"UniqueID",
"XUID",
"charset",
"Encoding",
"CharStrings",
"Private",
"Subrs",
"defaultWidthX",
"nominalWidthX"
];
key = keys[b0];
vs = 1;
if (b0 == 12) {
var keys = [
"Copyright",
"isFixedPitch",
"ItalicAngle",
"UnderlinePosition",
"UnderlineThickness",
"PaintType",
"CharstringType",
"FontMatrix",
"StrokeWidth",
"BlueScale",
"BlueShift",
"BlueFuzz",
"StemSnapH",
"StemSnapV",
"ForceBold",
0,
0,
"LanguageGroup",
"ExpansionFactor",
"initialRandomSeed",
"SyntheticBase",
"PostScript",
"BaseFontName",
"BaseFontBlend",
0,
0,
0,
0,
0,
0,
"ROS",
"CIDFontVersion",
"CIDFontRevision",
"CIDFontType",
"CIDCount",
"UIDBase",
"FDArray",
"FDSelect",
"FontName"
];
key = keys[b1];
vs = 2;
}
}
if (key != null) {
dict[key] = carr.length == 1 ? carr[0] : carr;
carr = [];
} else
carr.push(val);
offset += vs;
}
return dict;
};
Typr.cmap = {};
Typr.cmap.parse = function(data, offset, length) {
data = new Uint8Array(data.buffer, offset, length);
offset = 0;
var bin = Typr._bin;
var obj = {};
bin.readUshort(data, offset);
offset += 2;
var numTables = bin.readUshort(data, offset);
offset += 2;
var offs = [];
obj.tables = [];
for (var i = 0; i < numTables; i++) {
var platformID = bin.readUshort(data, offset);
offset += 2;
var encodingID = bin.readUshort(data, offset);
offset += 2;
var noffset = bin.readUint(data, offset);
offset += 4;
var id = "p" + platformID + "e" + encodingID;
var tind = offs.indexOf(noffset);
if (tind == -1) {
tind = obj.tables.length;
var subt;
offs.push(noffset);
var format = bin.readUshort(data, noffset);
if (format == 0)
subt = Typr.cmap.parse0(data, noffset);
else if (format == 4)
subt = Typr.cmap.parse4(data, noffset);
else if (format == 6)
subt = Typr.cmap.parse6(data, noffset);
else if (format == 12)
subt = Typr.cmap.parse12(data, noffset);
else
console.warn("unknown format: " + format, platformID, encodingID, noffset);
obj.tables.push(subt);
}
if (obj[id] != null)
throw "multiple tables for one platform+encoding";
obj[id] = tind;
}
return obj;
};
Typr.cmap.parse0 = function(data, offset) {
var bin = Typr._bin;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
var len = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
obj.map = [];
for (var i = 0; i < len - 6; i++)
obj.map.push(data[offset + i]);
return obj;
};
Typr.cmap.parse4 = function(data, offset) {
var bin = Typr._bin;
var offset0 = offset;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
var length = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var segCountX2 = bin.readUshort(data, offset);
offset += 2;
var segCount = segCountX2 / 2;
obj.searchRange = bin.readUshort(data, offset);
offset += 2;
obj.entrySelector = bin.readUshort(data, offset);
offset += 2;
obj.rangeShift = bin.readUshort(data, offset);
offset += 2;
obj.endCount = bin.readUshorts(data, offset, segCount);
offset += segCount * 2;
offset += 2;
obj.startCount = bin.readUshorts(data, offset, segCount);
offset += segCount * 2;
obj.idDelta = [];
for (var i = 0; i < segCount; i++) {
obj.idDelta.push(bin.readShort(data, offset));
offset += 2;
}
obj.idRangeOffset = bin.readUshorts(data, offset, segCount);
offset += segCount * 2;
obj.glyphIdArray = [];
while (offset < offset0 + length) {
obj.glyphIdArray.push(bin.readUshort(data, offset));
offset += 2;
}
return obj;
};
Typr.cmap.parse6 = function(data, offset) {
var bin = Typr._bin;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
obj.firstCode = bin.readUshort(data, offset);
offset += 2;
var entryCount = bin.readUshort(data, offset);
offset += 2;
obj.glyphIdArray = [];
for (var i = 0; i < entryCount; i++) {
obj.glyphIdArray.push(bin.readUshort(data, offset));
offset += 2;
}
return obj;
};
Typr.cmap.parse12 = function(data, offset) {
var bin = Typr._bin;
var obj = {};
obj.format = bin.readUshort(data, offset);
offset += 2;
offset += 2;
bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
var nGroups = bin.readUint(data, offset);
offset += 4;
obj.groups = [];
for (var i = 0; i < nGroups; i++) {
var off = offset + i * 12;
var startCharCode = bin.readUint(data, off + 0);
var endCharCode = bin.readUint(data, off + 4);
var startGlyphID = bin.readUint(data, off + 8);
obj.groups.push([startCharCode, endCharCode, startGlyphID]);
}
return obj;
};
Typr.glyf = {};
Typr.glyf.parse = function(data, offset, length, font) {
var obj = [];
for (var g = 0; g < font.maxp.numGlyphs; g++)
obj.push(null);
return obj;
};
Typr.glyf._parseGlyf = function(font, g) {
var bin = Typr._bin;
var data = font._data;
var offset = Typr._tabOffset(data, "glyf", font._offset) + font.loca[g];
if (font.loca[g] == font.loca[g + 1])
return null;
var gl = {};
gl.noc = bin.readShort(data, offset);
offset += 2;
gl.xMin = bin.readShort(data, offset);
offset += 2;
gl.yMin = bin.readShort(data, offset);
offset += 2;
gl.xMax = bin.readShort(data, offset);
offset += 2;
gl.yMax = bin.readShort(data, offset);
offset += 2;
if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax)
return null;
if (gl.noc > 0) {
gl.endPts = [];
for (var i = 0; i < gl.noc; i++) {
gl.endPts.push(bin.readUshort(data, offset));
offset += 2;
}
var instructionLength = bin.readUshort(data, offset);
offset += 2;
if (data.length - offset < instructionLength)
return null;
gl.instructions = bin.readBytes(data, offset, instructionLength);
offset += instructionLength;
var crdnum = gl.endPts[gl.noc - 1] + 1;
gl.flags = [];
for (var i = 0; i < crdnum; i++) {
var flag = data[offset];
offset++;
gl.flags.push(flag);
if ((flag & 8) != 0) {
var rep = data[offset];
offset++;
for (var j = 0; j < rep; j++) {
gl.flags.push(flag);
i++;
}
}
}
gl.xs = [];
for (var i = 0; i < crdnum; i++) {
var i8 = (gl.flags[i] & 2) != 0, same = (gl.flags[i] & 16) != 0;
if (i8) {
gl.xs.push(same ? data[offset] : -data[offset]);
offset++;
} else {
if (same)
gl.xs.push(0);
else {
gl.xs.push(bin.readShort(data, offset));
offset += 2;
}
}
}
gl.ys = [];
for (var i = 0; i < crdnum; i++) {
var i8 = (gl.flags[i] & 4) != 0, same = (gl.flags[i] & 32) != 0;
if (i8) {
gl.ys.push(same ? data[offset] : -data[offset]);
offset++;
} else {
if (same)
gl.ys.push(0);
else {
gl.ys.push(bin.readShort(data, offset));
offset += 2;
}
}
}
var x = 0, y = 0;
for (var i = 0; i < crdnum; i++) {
x += gl.xs[i];
y += gl.ys[i];
gl.xs[i] = x;
gl.ys[i] = y;
}
} else {
var ARG_1_AND_2_ARE_WORDS = 1 << 0;
var ARGS_ARE_XY_VALUES = 1 << 1;
var WE_HAVE_A_SCALE = 1 << 3;
var MORE_COMPONENTS = 1 << 5;
var WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6;
var WE_HAVE_A_TWO_BY_TWO = 1 << 7;
var WE_HAVE_INSTRUCTIONS = 1 << 8;
gl.parts = [];
var flags;
do {
flags = bin.readUshort(data, offset);
offset += 2;
var part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 };
gl.parts.push(part);
part.glyphIndex = bin.readUshort(data, offset);
offset += 2;
if (flags & ARG_1_AND_2_ARE_WORDS) {
var arg1 = bin.readShort(data, offset);
offset += 2;
var arg2 = bin.readShort(data, offset);
offset += 2;
} else {
var arg1 = bin.readInt8(data, offset);
offset++;
var arg2 = bin.readInt8(data, offset);
offset++;
}
if (flags & ARGS_ARE_XY_VALUES) {
part.m.tx = arg1;
part.m.ty = arg2;
} else {
part.p1 = arg1;
part.p2 = arg2;
}
if (flags & WE_HAVE_A_SCALE) {
part.m.a = part.m.d = bin.readF2dot14(data, offset);
offset += 2;
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
part.m.a = bin.readF2dot14(data, offset);
offset += 2;
part.m.d = bin.readF2dot14(data, offset);
offset += 2;
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
part.m.a = bin.readF2dot14(data, offset);
offset += 2;
part.m.b = bin.readF2dot14(data, offset);
offset += 2;
part.m.c = bin.readF2dot14(data, offset);
offset += 2;
part.m.d = bin.readF2dot14(data, offset);
offset += 2;
}
} while (flags & MORE_COMPONENTS);
if (flags & WE_HAVE_INSTRUCTIONS) {
var numInstr = bin.readUshort(data, offset);
offset += 2;
gl.instr = [];
for (var i = 0; i < numInstr; i++) {
gl.instr.push(data[offset]);
offset++;
}
}
}
return gl;
};
Typr.GPOS = {};
Typr.GPOS.parse = function(data, offset, length, font) {
return Typr._lctf.parse(data, offset, length, font, Typr.GPOS.subt);
};
Typr.GPOS.subt = function(data, ltype, offset, ltable) {
var bin = Typr._bin, offset0 = offset, tab = {};
tab.fmt = bin.readUshort(data, offset);
offset += 2;
if (ltype == 1 || ltype == 2 || ltype == 3 || ltype == 7 || ltype == 8 && tab.fmt <= 2) {
var covOff = bin.readUshort(data, offset);
offset += 2;
tab.coverage = Typr._lctf.readCoverage(data, covOff + offset0);
}
if (ltype == 1 && tab.fmt == 1) {
var valFmt1 = bin.readUshort(data, offset);
offset += 2;
var ones1 = Typr._lctf.numOfOnes(valFmt1);
if (valFmt1 != 0)
tab.pos = Typr.GPOS.readValueRecord(data, offset, valFmt1);
} else if (ltype == 2 && tab.fmt >= 1 && tab.fmt <= 2) {
var valFmt1 = bin.readUshort(data, offset);
offset += 2;
var valFmt2 = bin.readUshort(data, offset);
offset += 2;
var ones1 = Typr._lctf.numOfOnes(valFmt1);
var ones2 = Typr._lctf.numOfOnes(valFmt2);
if (tab.fmt == 1) {
tab.pairsets = [];
var psc = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < psc; i++) {
var psoff = offset0 + bin.readUshort(data, offset);
offset += 2;
var pvc = bin.readUshort(data, psoff);
psoff += 2;
var arr = [];
for (var j = 0; j < pvc; j++) {
var gid2 = bin.readUshort(data, psoff);
psoff += 2;
var value1, value2;
if (valFmt1 != 0) {
value1 = Typr.GPOS.readValueRecord(data, psoff, valFmt1);
psoff += ones1 * 2;
}
if (valFmt2 != 0) {
value2 = Typr.GPOS.readValueRecord(data, psoff, valFmt2);
psoff += ones2 * 2;
}
arr.push({ gid2, val1: value1, val2: value2 });
}
tab.pairsets.push(arr);
}
}
if (tab.fmt == 2) {
var classDef1 = bin.readUshort(data, offset);
offset += 2;
var classDef2 = bin.readUshort(data, offset);
offset += 2;
var class1Count = bin.readUshort(data, offset);
offset += 2;
var class2Count = bin.readUshort(data, offset);
offset += 2;
tab.classDef1 = Typr._lctf.readClassDef(data, offset0 + classDef1);
tab.classDef2 = Typr._lctf.readClassDef(data, offset0 + classDef2);
tab.matrix = [];
for (var i = 0; i < class1Count; i++) {
var row = [];
for (var j = 0; j < class2Count; j++) {
var value1 = null, value2 = null;
if (valFmt1 != 0) {
value1 = Typr.GPOS.readValueRecord(data, offset, valFmt1);
offset += ones1 * 2;
}
if (valFmt2 != 0) {
value2 = Typr.GPOS.readValueRecord(data, offset, valFmt2);
offset += ones2 * 2;
}
row.push({ val1: value1, val2: value2 });
}
tab.matrix.push(row);
}
}
} else if (ltype == 9 && tab.fmt == 1) {
var extType = bin.readUshort(data, offset);
offset += 2;
var extOffset = bin.readUint(data, offset);
offset += 4;
if (ltable.ltype == 9) {
ltable.ltype = extType;
} else if (ltable.ltype != extType) {
throw "invalid extension substitution";
}
return Typr.GPOS.subt(data, ltable.ltype, offset0 + extOffset);
} else
console.warn("unsupported GPOS table LookupType", ltype, "format", tab.fmt);
return tab;
};
Typr.GPOS.readValueRecord = function(data, offset, valFmt) {
var bin = Typr._bin;
var arr = [];
arr.push(valFmt & 1 ? bin.readShort(data, offset) : 0);
offset += valFmt & 1 ? 2 : 0;
arr.push(valFmt & 2 ? bin.readShort(data, offset) : 0);
offset += valFmt & 2 ? 2 : 0;
arr.push(valFmt & 4 ? bin.readShort(data, offset) : 0);
offset += valFmt & 4 ? 2 : 0;
arr.push(valFmt & 8 ? bin.readShort(data, offset) : 0);
offset += valFmt & 8 ? 2 : 0;
return arr;
};
Typr.GSUB = {};
Typr.GSUB.parse = function(data, offset, length, font) {
return Typr._lctf.parse(data, offset, length, font, Typr.GSUB.subt);
};
Typr.GSUB.subt = function(data, ltype, offset, ltable) {
var bin = Typr._bin, offset0 = offset, tab = {};
tab.fmt = bin.readUshort(data, offset);
offset += 2;
if (ltype != 1 && ltype != 4 && ltype != 5 && ltype != 6)
return null;
if (ltype == 1 || ltype == 4 || ltype == 5 && tab.fmt <= 2 || ltype == 6 && tab.fmt <= 2) {
var covOff = bin.readUshort(data, offset);
offset += 2;
tab.coverage = Typr._lctf.readCoverage(data, offset0 + covOff);
}
if (ltype == 1 && tab.fmt >= 1 && tab.fmt <= 2) {
if (tab.fmt == 1) {
tab.delta = bin.readShort(data, offset);
offset += 2;
} else if (tab.fmt == 2) {
var cnt = bin.readUshort(data, offset);
offset += 2;
tab.newg = bin.readUshorts(data, offset, cnt);
offset += tab.newg.length * 2;
}
} else if (ltype == 4) {
tab.vals = [];
var cnt = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var loff = bin.readUshort(data, offset);
offset += 2;
tab.vals.push(Typr.GSUB.readLigatureSet(data, offset0 + loff));
}
} else if (ltype == 5 && tab.fmt == 2) {
if (tab.fmt == 2) {
var cDefOffset = bin.readUshort(data, offset);
offset += 2;
tab.cDef = Typr._lctf.readClassDef(data, offset0 + cDefOffset);
tab.scset = [];
var subClassSetCount = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < subClassSetCount; i++) {
var scsOff = bin.readUshort(data, offset);
offset += 2;
tab.scset.push(scsOff == 0 ? null : Typr.GSUB.readSubClassSet(data, offset0 + scsOff));
}
}
} else if (ltype == 6 && tab.fmt == 3) {
if (tab.fmt == 3) {
for (var i = 0; i < 3; i++) {
var cnt = bin.readUshort(data, offset);
offset += 2;
var cvgs = [];
for (var j = 0; j < cnt; j++)
cvgs.push(Typr._lctf.readCoverage(data, offset0 + bin.readUshort(data, offset + j * 2)));
offset += cnt * 2;
if (i == 0)
tab.backCvg = cvgs;
if (i == 1)
tab.inptCvg = cvgs;
if (i == 2)
tab.ahedCvg = cvgs;
}
var cnt = bin.readUshort(data, offset);
offset += 2;
tab.lookupRec = Typr.GSUB.readSubstLookupRecords(data, offset, cnt);
}
} else if (ltype == 7 && tab.fmt == 1) {
var extType = bin.readUshort(data, offset);
offset += 2;
var extOffset = bin.readUint(data, offset);
offset += 4;
if (ltable.ltype == 9) {
ltable.ltype = extType;
} else if (ltable.ltype != extType) {
throw "invalid extension substitution";
}
return Typr.GSUB.subt(data, ltable.ltype, offset0 + extOffset);
} else
console.warn("unsupported GSUB table LookupType", ltype, "format", tab.fmt);
return tab;
};
Typr.GSUB.readSubClassSet = function(data, offset) {
var rUs = Typr._bin.readUshort, offset0 = offset, lset = [];
var cnt = rUs(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var loff = rUs(data, offset);
offset += 2;
lset.push(Typr.GSUB.readSubClassRule(data, offset0 + loff));
}
return lset;
};
Typr.GSUB.readSubClassRule = function(data, offset) {
var rUs = Typr._bin.readUshort, rule = {};
var gcount = rUs(data, offset);
offset += 2;
var scount = rUs(data, offset);
offset += 2;
rule.input = [];
for (var i = 0; i < gcount - 1; i++) {
rule.input.push(rUs(data, offset));
offset += 2;
}
rule.substLookupRecords = Typr.GSUB.readSubstLookupRecords(data, offset, scount);
return rule;
};
Typr.GSUB.readSubstLookupRecords = function(data, offset, cnt) {
var rUs = Typr._bin.readUshort;
var out = [];
for (var i = 0; i < cnt; i++) {
out.push(rUs(data, offset), rUs(data, offset + 2));
offset += 4;
}
return out;
};
Typr.GSUB.readChainSubClassSet = function(data, offset) {
var bin = Typr._bin, offset0 = offset, lset = [];
var cnt = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < cnt; i++) {
var loff = bin.readUshort(data, offset);
offset += 2;
lset.push(Typr.GSUB.readChainSubClassRule(data, offset0 + loff));
}
return lset;
};
Typr.GSUB.readChainSubClassRule = function(data, offset) {
var bin = Typr._bin, rule = {};
var pps = ["backtrack", "input", "lookahead"];
for (var pi = 0; pi < pps.length; pi++) {
var cnt = bin.readUshort(data, offset);
offset += 2;
if (pi == 1)
cnt--;
rule[pps[pi]] = bin.readUshorts(data, offset, cnt);
offset += rule[pps[pi]].length * 2;
}
var cnt = bin.readUshort(data, offset);
offset += 2;
rule.subst = bin.readUshorts(data, offset, cnt * 2);
offset += rule.subst.length * 2;
return rule;
};
Typr.GSUB.readLigatureSet = function(data, offset) {
var bin = Typr._bin, offset0 = offset, lset = [];
var lcnt = bin.readUshort(data, offset);
offset += 2;
for (var j = 0; j < lcnt; j++) {
var loff = bin.readUshort(data, offset);
offset += 2;
lset.push(Typr.GSUB.readLigature(data, offset0 + loff));
}
return lset;
};
Typr.GSUB.readLigature = function(data, offset) {
var bin = Typr._bin, lig = { chain: [] };
lig.nglyph = bin.readUshort(data, offset);
offset += 2;
var ccnt = bin.readUshort(data, offset);
offset += 2;
for (var k = 0; k < ccnt - 1; k++) {
lig.chain.push(bin.readUshort(data, offset));
offset += 2;
}
return lig;
};
Typr.head = {};
Typr.head.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
bin.readFixed(data, offset);
offset += 4;
obj.fontRevision = bin.readFixed(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
obj.flags = bin.readUshort(data, offset);
offset += 2;
obj.unitsPerEm = bin.readUshort(data, offset);
offset += 2;
obj.created = bin.readUint64(data, offset);
offset += 8;
obj.modified = bin.readUint64(data, offset);
offset += 8;
obj.xMin = bin.readShort(data, offset);
offset += 2;
obj.yMin = bin.readShort(data, offset);
offset += 2;
obj.xMax = bin.readShort(data, offset);
offset += 2;
obj.yMax = bin.readShort(data, offset);
offset += 2;
obj.macStyle = bin.readUshort(data, offset);
offset += 2;
obj.lowestRecPPEM = bin.readUshort(data, offset);
offset += 2;
obj.fontDirectionHint = bin.readShort(data, offset);
offset += 2;
obj.indexToLocFormat = bin.readShort(data, offset);
offset += 2;
obj.glyphDataFormat = bin.readShort(data, offset);
offset += 2;
return obj;
};
Typr.hhea = {};
Typr.hhea.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
bin.readFixed(data, offset);
offset += 4;
obj.ascender = bin.readShort(data, offset);
offset += 2;
obj.descender = bin.readShort(data, offset);
offset += 2;
obj.lineGap = bin.readShort(data, offset);
offset += 2;
obj.advanceWidthMax = bin.readUshort(data, offset);
offset += 2;
obj.minLeftSideBearing = bin.readShort(data, offset);
offset += 2;
obj.minRightSideBearing = bin.readShort(data, offset);
offset += 2;
obj.xMaxExtent = bin.readShort(data, offset);
offset += 2;
obj.caretSlopeRise = bin.readShort(data, offset);
offset += 2;
obj.caretSlopeRun = bin.readShort(data, offset);
offset += 2;
obj.caretOffset = bin.readShort(data, offset);
offset += 2;
offset += 4 * 2;
obj.metricDataFormat = bin.readShort(data, offset);
offset += 2;
obj.numberOfHMetrics = bin.readUshort(data, offset);
offset += 2;
return obj;
};
Typr.hmtx = {};
Typr.hmtx.parse = function(data, offset, length, font) {
var bin = Typr._bin;
var obj = {};
obj.aWidth = [];
obj.lsBearing = [];
var aw = 0, lsb = 0;
for (var i = 0; i < font.maxp.numGlyphs; i++) {
if (i < font.hhea.numberOfHMetrics) {
aw = bin.readUshort(data, offset);
offset += 2;
lsb = bin.readShort(data, offset);
offset += 2;
}
obj.aWidth.push(aw);
obj.lsBearing.push(lsb);
}
return obj;
};
Typr.kern = {};
Typr.kern.parse = function(data, offset, length, font) {
var bin = Typr._bin;
var version = bin.readUshort(data, offset);
offset += 2;
if (version == 1)
return Typr.kern.parseV1(data, offset - 2, length, font);
var nTables = bin.readUshort(data, offset);
offset += 2;
var map2 = { glyph1: [], rval: [] };
for (var i = 0; i < nTables; i++) {
offset += 2;
var length = bin.readUshort(data, offset);
offset += 2;
var coverage = bin.readUshort(data, offset);
offset += 2;
var format = coverage >>> 8;
format &= 15;
if (format == 0)
offset = Typr.kern.readFormat0(data, offset, map2);
else
throw "unknown kern table format: " + format;
}
return map2;
};
Typr.kern.parseV1 = function(data, offset, length, font) {
var bin = Typr._bin;
bin.readFixed(data, offset);
offset += 4;
var nTables = bin.readUint(data, offset);
offset += 4;
var map2 = { glyph1: [], rval: [] };
for (var i = 0; i < nTables; i++) {
bin.readUint(data, offset);
offset += 4;
var coverage = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var format = coverage >>> 8;
format &= 15;
if (format == 0)
offset = Typr.kern.readFormat0(data, offset, map2);
else
throw "unknown kern table format: " + format;
}
return map2;
};
Typr.kern.readFormat0 = function(data, offset, map2) {
var bin = Typr._bin;
var pleft = -1;
var nPairs = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
for (var j = 0; j < nPairs; j++) {
var left = bin.readUshort(data, offset);
offset += 2;
var right = bin.readUshort(data, offset);
offset += 2;
var value = bin.readShort(data, offset);
offset += 2;
if (left != pleft) {
map2.glyph1.push(left);
map2.rval.push({ glyph2: [], vals: [] });
}
var rval = map2.rval[map2.rval.length - 1];
rval.glyph2.push(right);
rval.vals.push(value);
pleft = left;
}
return offset;
};
Typr.loca = {};
Typr.loca.parse = function(data, offset, length, font) {
var bin = Typr._bin;
var obj = [];
var ver = font.head.indexToLocFormat;
var len = font.maxp.numGlyphs + 1;
if (ver == 0)
for (var i = 0; i < len; i++)
obj.push(bin.readUshort(data, offset + (i << 1)) << 1);
if (ver == 1)
for (var i = 0; i < len; i++)
obj.push(bin.readUint(data, offset + (i << 2)));
return obj;
};
Typr.maxp = {};
Typr.maxp.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
var ver = bin.readUint(data, offset);
offset += 4;
obj.numGlyphs = bin.readUshort(data, offset);
offset += 2;
if (ver == 65536) {
obj.maxPoints = bin.readUshort(data, offset);
offset += 2;
obj.maxContours = bin.readUshort(data, offset);
offset += 2;
obj.maxCompositePoints = bin.readUshort(data, offset);
offset += 2;
obj.maxCompositeContours = bin.readUshort(data, offset);
offset += 2;
obj.maxZones = bin.readUshort(data, offset);
offset += 2;
obj.maxTwilightPoints = bin.readUshort(data, offset);
offset += 2;
obj.maxStorage = bin.readUshort(data, offset);
offset += 2;
obj.maxFunctionDefs = bin.readUshort(data, offset);
offset += 2;
obj.maxInstructionDefs = bin.readUshort(data, offset);
offset += 2;
obj.maxStackElements = bin.readUshort(data, offset);
offset += 2;
obj.maxSizeOfInstructions = bin.readUshort(data, offset);
offset += 2;
obj.maxComponentElements = bin.readUshort(data, offset);
offset += 2;
obj.maxComponentDepth = bin.readUshort(data, offset);
offset += 2;
}
return obj;
};
Typr.name = {};
Typr.name.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
bin.readUshort(data, offset);
offset += 2;
var count = bin.readUshort(data, offset);
offset += 2;
bin.readUshort(data, offset);
offset += 2;
var names2 = [
"copyright",
"fontFamily",
"fontSubfamily",
"ID",
"fullName",
"version",
"postScriptName",
"trademark",
"manufacturer",
"designer",
"description",
"urlVendor",
"urlDesigner",
"licence",
"licenceURL",
"---",
"typoFamilyName",
"typoSubfamilyName",
"compatibleFull",
"sampleText",
"postScriptCID",
"wwsFamilyName",
"wwsSubfamilyName",
"lightPalette",
"darkPalette"
];
var offset0 = offset;
for (var i = 0; i < count; i++) {
var platformID = bin.readUshort(data, offset);
offset += 2;
var encodingID = bin.readUshort(data, offset);
offset += 2;
var languageID = bin.readUshort(data, offset);
offset += 2;
var nameID = bin.readUshort(data, offset);
offset += 2;
var slen = bin.readUshort(data, offset);
offset += 2;
var noffset = bin.readUshort(data, offset);
offset += 2;
var cname = names2[nameID];
var soff = offset0 + count * 12 + noffset;
var str;
if (platformID == 0)
str = bin.readUnicode(data, soff, slen / 2);
else if (platformID == 3 && encodingID == 0)
str = bin.readUnicode(data, soff, slen / 2);
else if (encodingID == 0)
str = bin.readASCII(data, soff, slen);
else if (encodingID == 1)
str = bin.readUnicode(data, soff, slen / 2);
else if (encodingID == 3)
str = bin.readUnicode(data, soff, slen / 2);
else if (platformID == 1) {
str = bin.readASCII(data, soff, slen);
console.warn("reading unknown MAC encoding " + encodingID + " as ASCII");
} else
throw "unknown encoding " + encodingID + ", platformID: " + platformID;
var tid = "p" + platformID + "," + languageID.toString(16);
if (obj[tid] == null)
obj[tid] = {};
obj[tid][cname !== void 0 ? cname : nameID] = str;
obj[tid]._lang = languageID;
}
for (var p in obj)
if (obj[p].postScriptName != null && obj[p]._lang == 1033)
return obj[p];
for (var p in obj)
if (obj[p].postScriptName != null && obj[p]._lang == 0)
return obj[p];
for (var p in obj)
if (obj[p].postScriptName != null && obj[p]._lang == 3084)
return obj[p];
for (var p in obj)
if (obj[p].postScriptName != null)
return obj[p];
var tname;
for (var p in obj) {
tname = p;
break;
}
console.warn("returning name table with languageID " + obj[tname]._lang);
return obj[tname];
};
Typr["OS/2"] = {};
Typr["OS/2"].parse = function(data, offset, length) {
var bin = Typr._bin;
var ver = bin.readUshort(data, offset);
offset += 2;
var obj = {};
if (ver == 0)
Typr["OS/2"].version0(data, offset, obj);
else if (ver == 1)
Typr["OS/2"].version1(data, offset, obj);
else if (ver == 2 || ver == 3 || ver == 4)
Typr["OS/2"].version2(data, offset, obj);
else if (ver == 5)
Typr["OS/2"].version5(data, offset, obj);
else
throw "unknown OS/2 table version: " + ver;
return obj;
};
Typr["OS/2"].version0 = function(data, offset, obj) {
var bin = Typr._bin;
obj.xAvgCharWidth = bin.readShort(data, offset);
offset += 2;
obj.usWeightClass = bin.readUshort(data, offset);
offset += 2;
obj.usWidthClass = bin.readUshort(data, offset);
offset += 2;
obj.fsType = bin.readUshort(data, offset);
offset += 2;
obj.ySubscriptXSize = bin.readShort(data, offset);
offset += 2;
obj.ySubscriptYSize = bin.readShort(data, offset);
offset += 2;
obj.ySubscriptXOffset = bin.readShort(data, offset);
offset += 2;
obj.ySubscriptYOffset = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptXSize = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptYSize = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptXOffset = bin.readShort(data, offset);
offset += 2;
obj.ySuperscriptYOffset = bin.readShort(data, offset);
offset += 2;
obj.yStrikeoutSize = bin.readShort(data, offset);
offset += 2;
obj.yStrikeoutPosition = bin.readShort(data, offset);
offset += 2;
obj.sFamilyClass = bin.readShort(data, offset);
offset += 2;
obj.panose = bin.readBytes(data, offset, 10);
offset += 10;
obj.ulUnicodeRange1 = bin.readUint(data, offset);
offset += 4;
obj.ulUnicodeRange2 = bin.readUint(data, offset);
offset += 4;
obj.ulUnicodeRange3 = bin.readUint(data, offset);
offset += 4;
obj.ulUnicodeRange4 = bin.readUint(data, offset);
offset += 4;
obj.achVendID = [bin.readInt8(data, offset), bin.readInt8(data, offset + 1), bin.readInt8(data, offset + 2), bin.readInt8(data, offset + 3)];
offset += 4;
obj.fsSelection = bin.readUshort(data, offset);
offset += 2;
obj.usFirstCharIndex = bin.readUshort(data, offset);
offset += 2;
obj.usLastCharIndex = bin.readUshort(data, offset);
offset += 2;
obj.sTypoAscender = bin.readShort(data, offset);
offset += 2;
obj.sTypoDescender = bin.readShort(data, offset);
offset += 2;
obj.sTypoLineGap = bin.readShort(data, offset);
offset += 2;
obj.usWinAscent = bin.readUshort(data, offset);
offset += 2;
obj.usWinDescent = bin.readUshort(data, offset);
offset += 2;
return offset;
};
Typr["OS/2"].version1 = function(data, offset, obj) {
var bin = Typr._bin;
offset = Typr["OS/2"].version0(data, offset, obj);
obj.ulCodePageRange1 = bin.readUint(data, offset);
offset += 4;
obj.ulCodePageRange2 = bin.readUint(data, offset);
offset += 4;
return offset;
};
Typr["OS/2"].version2 = function(data, offset, obj) {
var bin = Typr._bin;
offset = Typr["OS/2"].version1(data, offset, obj);
obj.sxHeight = bin.readShort(data, offset);
offset += 2;
obj.sCapHeight = bin.readShort(data, offset);
offset += 2;
obj.usDefault = bin.readUshort(data, offset);
offset += 2;
obj.usBreak = bin.readUshort(data, offset);
offset += 2;
obj.usMaxContext = bin.readUshort(data, offset);
offset += 2;
return offset;
};
Typr["OS/2"].version5 = function(data, offset, obj) {
var bin = Typr._bin;
offset = Typr["OS/2"].version2(data, offset, obj);
obj.usLowerOpticalPointSize = bin.readUshort(data, offset);
offset += 2;
obj.usUpperOpticalPointSize = bin.readUshort(data, offset);
offset += 2;
return offset;
};
Typr.post = {};
Typr.post.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = {};
obj.version = bin.readFixed(data, offset);
offset += 4;
obj.italicAngle = bin.readFixed(data, offset);
offset += 4;
obj.underlinePosition = bin.readShort(data, offset);
offset += 2;
obj.underlineThickness = bin.readShort(data, offset);
offset += 2;
return obj;
};
Typr.SVG = {};
Typr.SVG.parse = function(data, offset, length) {
var bin = Typr._bin;
var obj = { entries: [] };
var offset0 = offset;
bin.readUshort(data, offset);
offset += 2;
var svgDocIndexOffset = bin.readUint(data, offset);
offset += 4;
bin.readUint(data, offset);
offset += 4;
offset = svgDocIndexOffset + offset0;
var numEntries = bin.readUshort(data, offset);
offset += 2;
for (var i = 0; i < numEntries; i++) {
var startGlyphID = bin.readUshort(data, offset);
offset += 2;
var endGlyphID = bin.readUshort(data, offset);
offset += 2;
var svgDocOffset = bin.readUint(data, offset);
offset += 4;
var svgDocLength = bin.readUint(data, offset);
offset += 4;
var sbuf = new Uint8Array(data.buffer, offset0 + svgDocOffset + svgDocIndexOffset, svgDocLength);
var svg = bin.readUTF8(sbuf, 0, sbuf.length);
for (var f = startGlyphID; f <= endGlyphID; f++) {
obj.entries[f] = svg;
}
}
return obj;
};
Typr.SVG.toPath = function(str) {
var pth = { cmds: [], crds: [] };
if (str == null)
return pth;
var prsr = new DOMParser();
var doc = prsr["parseFromString"](str, "image/svg+xml");
var svg = doc.firstChild;
while (svg.tagName != "svg")
svg = svg.nextSibling;
var vb = svg.getAttribute("viewBox");
if (vb)
vb = vb.trim().split(" ").map(parseFloat);
else
vb = [0, 0, 1e3, 1e3];
Typr.SVG._toPath(svg.children, pth);
for (var i = 0; i < pth.crds.length; i += 2) {
var x = pth.crds[i], y = pth.crds[i + 1];
x -= vb[0];
y -= vb[1];
y = -y;
pth.crds[i] = x;
pth.crds[i + 1] = y;
}
return pth;
};
Typr.SVG._toPath = function(nds, pth, fill) {
for (var ni = 0; ni < nds.length; ni++) {
var nd = nds[ni], tn = nd.tagName;
var cfl = nd.getAttribute("fill");
if (cfl == null)
cfl = fill;
if (tn == "g")
Typr.SVG._toPath(nd.children, pth, cfl);
else if (tn == "path") {
pth.cmds.push(cfl ? cfl : "#000000");
var d = nd.getAttribute("d");
var toks = Typr.SVG._tokens(d);
Typr.SVG._toksToPath(toks, pth);
pth.cmds.push("X");
} else if (tn == "defs") ;
else
console.warn(tn, nd);
}
};
Typr.SVG._tokens = function(d) {
var ts = [], off = 0, rn = false, cn = "";
while (off < d.length) {
var cc = d.charCodeAt(off), ch = d.charAt(off);
off++;
var isNum = 48 <= cc && cc <= 57 || ch == "." || ch == "-";
if (rn) {
if (ch == "-") {
ts.push(parseFloat(cn));
cn = ch;
} else if (isNum)
cn += ch;
else {
ts.push(parseFloat(cn));
if (ch != "," && ch != " ")
ts.push(ch);
rn = false;
}
} else {
if (isNum) {
cn = ch;
rn = true;
} else if (ch != "," && ch != " ")
ts.push(ch);
}
}
if (rn)
ts.push(parseFloat(cn));
return ts;
};
Typr.SVG._toksToPath = function(ts, pth) {
var i = 0, x = 0, y = 0, ox = 0, oy = 0;
var pc = { "M": 2, "L": 2, "H": 1, "V": 1, "S": 4, "C": 6 };
var cmds = pth.cmds, crds = pth.crds;
while (i < ts.length) {
var cmd = ts[i];
i++;
if (cmd == "z") {
cmds.push("Z");
x = ox;
y = oy;
} else {
var cmu = cmd.toUpperCase();
var ps = pc[cmu], reps = Typr.SVG._reps(ts, i, ps);
for (var j = 0; j < reps; j++) {
var xi = 0, yi = 0;
if (cmd != cmu) {
xi = x;
yi = y;
}
if (cmu == "M") {
x = xi + ts[i++];
y = yi + ts[i++];
cmds.push("M");
crds.push(x, y);
ox = x;
oy = y;
} else if (cmu == "L") {
x = xi + ts[i++];
y = yi + ts[i++];
cmds.push("L");
crds.push(x, y);
} else if (cmu == "H") {
x = xi + ts[i++];
cmds.push("L");
crds.push(x, y);
} else if (cmu == "V") {
y = yi + ts[i++];
cmds.push("L");
crds.push(x, y);
} else if (cmu == "C") {
var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
cmds.push("C");
crds.push(x1, y1, x2, y2, x3, y3);
x = x3;
y = y3;
} else if (cmu == "S") {
var co = Math.max(crds.length - 4, 0);
var x1 = x + x - crds[co], y1 = y + y - crds[co + 1];
var x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
cmds.push("C");
crds.push(x1, y1, x2, y2, x3, y3);
x = x3;
y = y3;
} else
console.warn("Unknown SVG command " + cmd);
}
}
}
};
Typr.SVG._reps = function(ts, off, ps) {
var i = off;
while (i < ts.length) {
if (typeof ts[i] == "string")
break;
i += ps;
}
return (i - off) / ps;
};
if (Typr == null)
Typr = {};
if (Typr.U == null)
Typr.U = {};
Typr.U.codeToGlyph = function(font, code) {
var cmap = font.cmap;
for (var _i = 0, _a = [cmap.p0e4, cmap.p3e1, cmap.p3e10, cmap.p0e3, cmap.p1e0]; _i < _a.length; _i++) {
var tind = _a[_i];
if (tind == null)
continue;
var tab = cmap.tables[tind];
if (tab.format == 0) {
if (code >= tab.map.length)
continue;
return tab.map[code];
} else if (tab.format == 4) {
var sind = -1;
for (var i = 0; i < tab.endCount.length; i++) {
if (code <= tab.endCount[i]) {
sind = i;
break;
}
}
if (sind == -1)
continue;
if (tab.startCount[sind] > code)
continue;
var gli = 0;
if (tab.idRangeOffset[sind] != 0) {
gli = tab.glyphIdArray[code - tab.startCount[sind] + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)];
} else {
gli = code + tab.idDelta[sind];
}
return gli & 65535;
} else if (tab.format == 12) {
if (code > tab.groups[tab.groups.length - 1][1])
continue;
for (var i = 0; i < tab.groups.length; i++) {
var grp = tab.groups[i];
if (grp[0] <= code && code <= grp[1])
return grp[2] + (code - grp[0]);
}
continue;
} else {
throw "unknown cmap table format " + tab.format;
}
}
return 0;
};
Typr.U.glyphToPath = function(font, gid) {
var path = { cmds: [], crds: [] };
if (font.SVG && font.SVG.entries[gid]) {
var p = font.SVG.entries[gid];
if (p == null)
return path;
if (typeof p == "string") {
p = Typr.SVG.toPath(p);
font.SVG.entries[gid] = p;
}
return p;
} else if (font.CFF) {
var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: font.CFF.Private ? font.CFF.Private.defaultWidthX : 0, open: false };
var cff = font.CFF, pdct = font.CFF.Private;
if (cff.ROS) {
var gi = 0;
while (cff.FDSelect[gi + 2] <= gid)
gi += 2;
pdct = cff.FDArray[cff.FDSelect[gi + 1]].Private;
}
Typr.U._drawCFF(font.CFF.CharStrings[gid], state, cff, pdct, path);
} else if (font.glyf) {
Typr.U._drawGlyf(gid, font, path);
}
return path;
};
Typr.U._drawGlyf = function(gid, font, path) {
var gl = font.glyf[gid];
if (gl == null)
gl = font.glyf[gid] = Typr.glyf._parseGlyf(font, gid);
if (gl != null) {
if (gl.noc > -1) {
Typr.U._simpleGlyph(gl, path);
} else {
Typr.U._compoGlyph(gl, font, path);
}
}
};
Typr.U._simpleGlyph = function(gl, p) {
for (var c = 0; c < gl.noc; c++) {
var i0 = c == 0 ? 0 : gl.endPts[c - 1] + 1;
var il = gl.endPts[c];
for (var i = i0; i <= il; i++) {
var pr = i == i0 ? il : i - 1;
var nx = i == il ? i0 : i + 1;
var onCurve = gl.flags[i] & 1;
var prOnCurve = gl.flags[pr] & 1;
var nxOnCurve = gl.flags[nx] & 1;
var x = gl.xs[i], y = gl.ys[i];
if (i == i0) {
if (onCurve) {
if (prOnCurve) {
Typr.U.P.moveTo(p, gl.xs[pr], gl.ys[pr]);
} else {
Typr.U.P.moveTo(p, x, y);
continue;
}
} else {
if (prOnCurve) {
Typr.U.P.moveTo(p, gl.xs[pr], gl.ys[pr]);
} else {
Typr.U.P.moveTo(p, (gl.xs[pr] + x) / 2, (gl.ys[pr] + y) / 2);
}
}
}
if (onCurve) {
if (prOnCurve)
Typr.U.P.lineTo(p, x, y);
} else {
if (nxOnCurve) {
Typr.U.P.qcurveTo(p, x, y, gl.xs[nx], gl.ys[nx]);
} else {
Typr.U.P.qcurveTo(p, x, y, (x + gl.xs[nx]) / 2, (y + gl.ys[nx]) / 2);
}
}
}
Typr.U.P.closePath(p);
}
};
Typr.U._compoGlyph = function(gl, font, p) {
for (var j = 0; j < gl.parts.length; j++) {
var path = { cmds: [], crds: [] };
var prt = gl.parts[j];
Typr.U._drawGlyf(prt.glyphIndex, font, path);
var m = prt.m;
for (var i = 0; i < path.crds.length; i += 2) {
var x = path.crds[i], y = path.crds[i + 1];
p.crds.push(x * m.a + y * m.b + m.tx);
p.crds.push(x * m.c + y * m.d + m.ty);
}
for (var i = 0; i < path.cmds.length; i++) {
p.cmds.push(path.cmds[i]);
}
}
};
Typr.U._getGlyphClass = function(g, cd) {
var intr = Typr._lctf.getInterval(cd, g);
return intr == -1 ? 0 : cd[intr + 2];
};
Typr.U.getPairAdjustment = function(font, g1, g2) {
var hasGPOSkern = false;
if (font.GPOS) {
var gpos = font["GPOS"];
var llist = gpos.lookupList, flist = gpos.featureList;
var tused = [];
for (var i = 0; i < flist.length; i++) {
var fl = flist[i];
if (fl.tag != "kern")
continue;
hasGPOSkern = true;
for (var ti = 0; ti < fl.tab.length; ti++) {
if (tused[fl.tab[ti]])
continue;
tused[fl.tab[ti]] = true;
var tab = llist[fl.tab[ti]];
for (var j = 0; j < tab.tabs.length; j++) {
if (tab.tabs[j] == null)
continue;
var ltab = tab.tabs[j], ind;
if (ltab.coverage) {
ind = Typr._lctf.coverageIndex(ltab.coverage, g1);
if (ind == -1)
continue;
}
if (tab.ltype == 1) ;
else if (tab.ltype == 2) {
var adj = null;
if (ltab.fmt == 1) {
var right = ltab.pairsets[ind];
for (var i = 0; i < right.length; i++) {
if (right[i].gid2 == g2)
adj = right[i];
}
} else if (ltab.fmt == 2) {
var c1 = Typr.U._getGlyphClass(g1, ltab.classDef1);
var c2 = Typr.U._getGlyphClass(g2, ltab.classDef2);
adj = ltab.matrix[c1][c2];
}
if (adj) {
var offset = 0;
if (adj.val1 && adj.val1[2])
offset += adj.val1[2];
if (adj.val2 && adj.val2[0])
offset += adj.val2[0];
return offset;
}
}
}
}
}
}
if (font.kern && !hasGPOSkern) {
var ind1 = font.kern.glyph1.indexOf(g1);
if (ind1 != -1) {
var ind2 = font.kern.rval[ind1].glyph2.indexOf(g2);
if (ind2 != -1)
return font.kern.rval[ind1].vals[ind2];
}
}
return 0;
};
Typr.U.stringToGlyphs = function(font, str) {
var gls = [];
for (var i = 0; i < str.length; i++) {
var cc = str.codePointAt(i);
if (cc > 65535)
i++;
gls.push(Typr.U.codeToGlyph(font, cc));
}
for (var i = 0; i < str.length; i++) {
var cc = str.codePointAt(i);
if (cc == 2367) {
var t = gls[i - 1];
gls[i - 1] = gls[i];
gls[i] = t;
}
if (cc > 65535)
i++;
}
var gsub = font["GSUB"];
if (gsub == null)
return gls;
var llist = gsub.lookupList, flist = gsub.featureList;
var cligs = [
"rlig",
"liga",
"mset",
"isol",
"init",
"fina",
"medi",
"half",
"pres",
"blws"
/* Tibetan fonts like Himalaya.ttf */
];
var tused = [];
for (var fi = 0; fi < flist.length; fi++) {
var fl = flist[fi];
if (cligs.indexOf(fl.tag) == -1)
continue;
for (var ti = 0; ti < fl.tab.length; ti++) {
if (tused[fl.tab[ti]])
continue;
tused[fl.tab[ti]] = true;
var tab = llist[fl.tab[ti]];
for (var ci = 0; ci < gls.length; ci++) {
var feat = Typr.U._getWPfeature(str, ci);
if ("isol,init,fina,medi".indexOf(fl.tag) != -1 && fl.tag != feat)
continue;
Typr.U._applySubs(gls, ci, tab, llist);
}
}
}
return gls;
};
Typr.U._getWPfeature = function(str, ci) {
var wsep = '\n " ,.:;!?() ،';
var R = "آأؤإاةدذرزوٱٲٳٵٶٷڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙۀۃۄۅۆۇۈۉۊۋۍۏےۓەۮۯܐܕܖܗܘܙܞܨܪܬܯݍݙݚݛݫݬݱݳݴݸݹࡀࡆࡇࡉࡔࡧࡩࡪࢪࢫࢬࢮࢱࢲࢹૅેૉૐૡ૯ஃஅஉஎஏனப";
var L = "ꡲ્";
var slft = ci == 0 || wsep.indexOf(str[ci - 1]) != -1;
var srgt = ci == str.length - 1 || wsep.indexOf(str[ci + 1]) != -1;
if (!slft && R.indexOf(str[ci - 1]) != -1)
slft = true;
if (!srgt && R.indexOf(str[ci]) != -1)
srgt = true;
if (!srgt && L.indexOf(str[ci + 1]) != -1)
srgt = true;
if (!slft && L.indexOf(str[ci]) != -1)
slft = true;
var feat = null;
if (slft) {
feat = srgt ? "isol" : "init";
} else {
feat = srgt ? "fina" : "medi";
}
return feat;
};
Typr.U._applySubs = function(gls, ci, tab, llist) {
var rlim = gls.length - ci - 1;
for (var j = 0; j < tab.tabs.length; j++) {
if (tab.tabs[j] == null)
continue;
var ltab = tab.tabs[j], ind;
if (ltab.coverage) {
ind = Typr._lctf.coverageIndex(ltab.coverage, gls[ci]);
if (ind == -1)
continue;
}
if (tab.ltype == 1) {
gls[ci];
if (ltab.fmt == 1)
gls[ci] = gls[ci] + ltab.delta;
else
gls[ci] = ltab.newg[ind];
} else if (tab.ltype == 4) {
var vals = ltab.vals[ind];
for (var k = 0; k < vals.length; k++) {
var lig = vals[k], rl = lig.chain.length;
if (rl > rlim)
continue;
var good = true, em1 = 0;
for (var l = 0; l < rl; l++) {
while (gls[ci + em1 + (1 + l)] == -1)
em1++;
if (lig.chain[l] != gls[ci + em1 + (1 + l)])
good = false;
}
if (!good)
continue;
gls[ci] = lig.nglyph;
for (var l = 0; l < rl + em1; l++)
gls[ci + l + 1] = -1;
break;
}
} else if (tab.ltype == 5 && ltab.fmt == 2) {
var cind = Typr._lctf.getInterval(ltab.cDef, gls[ci]);
var cls = ltab.cDef[cind + 2], scs = ltab.scset[cls];
for (var i = 0; i < scs.length; i++) {
var sc = scs[i], inp = sc.input;
if (inp.length > rlim)
continue;
var good = true;
for (var l = 0; l < inp.length; l++) {
var cind2 = Typr._lctf.getInterval(ltab.cDef, gls[ci + 1 + l]);
if (cind == -1 && ltab.cDef[cind2 + 2] != inp[l]) {
good = false;
break;
}
}
if (!good)
continue;
var lrs = sc.substLookupRecords;
for (var k = 0; k < lrs.length; k += 2) {
lrs[k];
lrs[k + 1];
}
}
} else if (tab.ltype == 6 && ltab.fmt == 3) {
if (!Typr.U._glsCovered(gls, ltab.backCvg, ci - ltab.backCvg.length))
continue;
if (!Typr.U._glsCovered(gls, ltab.inptCvg, ci))
continue;
if (!Typr.U._glsCovered(gls, ltab.ahedCvg, ci + ltab.inptCvg.length))
continue;
var lr = ltab.lookupRec;
for (var i = 0; i < lr.length; i += 2) {
var cind = lr[i], tab2 = llist[lr[i + 1]];
Typr.U._applySubs(gls, ci + cind, tab2, llist);
}
}
}
};
Typr.U._glsCovered = function(gls, cvgs, ci) {
for (var i = 0; i < cvgs.length; i++) {
var ind = Typr._lctf.coverageIndex(cvgs[i], gls[ci + i]);
if (ind == -1)
return false;
}
return true;
};
Typr.U.glyphsToPath = function(font, gls, clr) {
var tpath = { cmds: [], crds: [] };
var x = 0;
for (var i = 0; i < gls.length; i++) {
var gid = gls[i];
if (gid == -1)
continue;
var gid2 = i < gls.length - 1 && gls[i + 1] != -1 ? gls[i + 1] : 0;
var path = Typr.U.glyphToPath(font, gid);
for (var j = 0; j < path.crds.length; j += 2) {
tpath.crds.push(path.crds[j] + x);
tpath.crds.push(path.crds[j + 1]);
}
if (clr)
tpath.cmds.push(clr);
for (var j = 0; j < path.cmds.length; j++)
tpath.cmds.push(path.cmds[j]);
if (clr)
tpath.cmds.push("X");
x += font.hmtx.aWidth[gid];
if (i < gls.length - 1)
x += Typr.U.getPairAdjustment(font, gid, gid2);
}
return tpath;
};
Typr.U.pathToSVG = function(path, prec) {
if (prec == null)
prec = 5;
var out = [], co = 0, lmap = { "M": 2, "L": 2, "Q": 4, "C": 6 };
for (var i = 0; i < path.cmds.length; i++) {
var cmd = path.cmds[i], cn = co + (lmap[cmd] ? lmap[cmd] : 0);
out.push(cmd);
while (co < cn) {
var c = path.crds[co++];
out.push(parseFloat(c.toFixed(prec)) + (co == cn ? "" : " "));
}
}
return out.join("");
};
Typr.U.pathToContext = function(path, ctx) {
var c = 0, crds = path.crds;
for (var j = 0; j < path.cmds.length; j++) {
var cmd = path.cmds[j];
if (cmd == "M") {
ctx.moveTo(crds[c], crds[c + 1]);
c += 2;
} else if (cmd == "L") {
ctx.lineTo(crds[c], crds[c + 1]);
c += 2;
} else if (cmd == "C") {
ctx.bezierCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3], crds[c + 4], crds[c + 5]);
c += 6;
} else if (cmd == "Q") {
ctx.quadraticCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3]);
c += 4;
} else if (cmd.charAt(0) == "#") {
ctx.beginPath();
ctx.fillStyle = cmd;
} else if (cmd == "Z") {
ctx.closePath();
} else if (cmd == "X") {
ctx.fill();
}
}
};
Typr.U.P = {};
Typr.U.P.moveTo = function(p, x, y) {
p.cmds.push("M");
p.crds.push(x, y);
};
Typr.U.P.lineTo = function(p, x, y) {
p.cmds.push("L");
p.crds.push(x, y);
};
Typr.U.P.curveTo = function(p, a, b, c, d, e, f) {
p.cmds.push("C");
p.crds.push(a, b, c, d, e, f);
};
Typr.U.P.qcurveTo = function(p, a, b, c, d) {
p.cmds.push("Q");
p.crds.push(a, b, c, d);
};
Typr.U.P.closePath = function(p) {
p.cmds.push("Z");
};
Typr.U._drawCFF = function(cmds, state, font, pdct, p) {
var stack = state.stack;
var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open;
var i = 0;
var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0;
var o = { val: 0, size: 0 };
while (i < cmds.length) {
Typr.CFF.getCharString(cmds, i, o);
var v = o.val;
i += o.size;
if (v == "o1" || v == "o18") {
var hasWidthArg;
hasWidthArg = stack.length % 2 !== 0;
if (hasWidthArg && !haveWidth) {
width = stack.shift() + pdct.nominalWidthX;
}
nStems += stack.length >> 1;
stack.length = 0;
haveWidth = true;
} else if (v == "o3" || v == "o23") {
var hasWidthArg;
hasWidthArg = stack.length % 2 !== 0;
if (hasWidthArg && !haveWidth) {
width = stack.shift() + pdct.nominalWidthX;
}
nStems += stack.length >> 1;
stack.length = 0;
haveWidth = true;
} else if (v == "o4") {
if (stack.length > 1 && !haveWidth) {
width = stack.shift() + pdct.nominalWidthX;
haveWidth = true;
}
if (open)
Typr.U.P.closePath(p);
y += stack.pop();
Typr.U.P.moveTo(p, x, y);
open = true;
} else if (v == "o5") {
while (stack.length > 0) {
x += stack.shift();
y += stack.shift();
Typr.U.P.lineTo(p, x, y);
}
} else if (v == "o6" || v == "o7") {
var count = stack.length;
var isX = v == "o6";
for (var j = 0; j < count; j++) {
var sval = stack.shift();
if (isX) {
x += sval;
} else {
y += sval;
}
isX = !isX;
Typr.U.P.lineTo(p, x, y);
}
} else if (v == "o8" || v == "o24") {
var count = stack.length;
var index = 0;
while (index + 6 <= count) {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
index += 6;
}
if (v == "o24") {
x += stack.shift();
y += stack.shift();
Typr.U.P.lineTo(p, x, y);
}
} else if (v == "o11") {
break;
} else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237") {
if (v == "o1234") {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y;
c3x = jpx + stack.shift();
c3y = c2y;
c4x = c3x + stack.shift();
c4y = y;
x = c4x + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
if (v == "o1235") {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y + stack.shift();
c3x = jpx + stack.shift();
c3y = jpy + stack.shift();
c4x = c3x + stack.shift();
c4y = c3y + stack.shift();
x = c4x + stack.shift();
y = c4y + stack.shift();
stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
if (v == "o1236") {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y;
c3x = jpx + stack.shift();
c3y = c2y;
c4x = c3x + stack.shift();
c4y = c3y + stack.shift();
x = c4x + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
if (v == "o1237") {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
jpx = c2x + stack.shift();
jpy = c2y + stack.shift();
c3x = jpx + stack.shift();
c3y = jpy + stack.shift();
c4x = c3x + stack.shift();
c4y = c3y + stack.shift();
if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
x = c4x + stack.shift();
} else {
y = c4y + stack.shift();
}
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y);
}
} else if (v == "o14") {
if (stack.length > 0 && !haveWidth) {
width = stack.shift() + font.nominalWidthX;
haveWidth = true;
}
if (stack.length == 4) {
var adx = stack.shift();
var ady = stack.shift();
var bchar = stack.shift();
var achar = stack.shift();
var bind2 = Typr.CFF.glyphBySE(font, bchar);
var aind = Typr.CFF.glyphBySE(font, achar);
Typr.U._drawCFF(font.CharStrings[bind2], state, font, pdct, p);
state.x = adx;
state.y = ady;
Typr.U._drawCFF(font.CharStrings[aind], state, font, pdct, p);
}
if (open) {
Typr.U.P.closePath(p);
open = false;
}
} else if (v == "o19" || v == "o20") {
var hasWidthArg;
hasWidthArg = stack.length % 2 !== 0;
if (hasWidthArg && !haveWidth) {
width = stack.shift() + pdct.nominalWidthX;
}
nStems += stack.length >> 1;
stack.length = 0;
haveWidth = true;
i += nStems + 7 >> 3;
} else if (v == "o21") {
if (stack.length > 2 && !haveWidth) {
width = stack.shift() + pdct.nominalWidthX;
haveWidth = true;
}
y += stack.pop();
x += stack.pop();
if (open)
Typr.U.P.closePath(p);
Typr.U.P.moveTo(p, x, y);
open = true;
} else if (v == "o22") {
if (stack.length > 1 && !haveWidth) {
width = stack.shift() + pdct.nominalWidthX;
haveWidth = true;
}
x += stack.pop();
if (open)
Typr.U.P.closePath(p);
Typr.U.P.moveTo(p, x, y);
open = true;
} else if (v == "o25") {
while (stack.length > 6) {
x += stack.shift();
y += stack.shift();
Typr.U.P.lineTo(p, x, y);
}
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
} else if (v == "o26") {
if (stack.length % 2) {
x += stack.shift();
}
while (stack.length > 0) {
c1x = x;
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x;
y = c2y + stack.shift();
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
}
} else if (v == "o27") {
if (stack.length % 2) {
y += stack.shift();
}
while (stack.length > 0) {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y;
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
}
} else if (v == "o10" || v == "o29") {
var obj = v == "o10" ? pdct : font;
if (stack.length == 0) {
console.warn("error: empty stack");
} else {
var ind = stack.pop();
var subr = obj.Subrs[ind + obj.Bias];
state.x = x;
state.y = y;
state.nStems = nStems;
state.haveWidth = haveWidth;
state.width = width;
state.open = open;
Typr.U._drawCFF(subr, state, font, pdct, p);
x = state.x;
y = state.y;
nStems = state.nStems;
haveWidth = state.haveWidth;
width = state.width;
open = state.open;
}
} else if (v == "o30" || v == "o31") {
var count, count1 = stack.length;
var index = 0;
var alternate = v == "o31";
count = count1 & ~2;
index += count1 - count;
while (index < count) {
if (alternate) {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
y = c2y + stack.shift();
if (count - index == 5) {
x = c2x + stack.shift();
index++;
} else {
x = c2x;
}
alternate = false;
} else {
c1x = x;
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
if (count - index == 5) {
y = c2y + stack.shift();
index++;
} else {
y = c2y;
}
alternate = true;
}
Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y);
index += 4;
}
} else if ((v + "").charAt(0) == "o") {
console.warn("Unknown operation: " + v, cmds);
throw v;
} else
stack.push(v);
}
state.x = x;
state.y = y;
state.nStems = nStems;
state.haveWidth = haveWidth;
state.width = width;
state.open = open;
};
Typr$1.Typr = Typr;
var Typr_js_1 = Typr$1;
var friendlyTags = { "aalt": "Access All Alternates", "abvf": "Above-base Forms", "abvm": "Above - base Mark Positioning", "abvs": "Above - base Substitutions", "afrc": "Alternative Fractions", "akhn": "Akhands", "blwf": "Below - base Forms", "blwm": "Below - base Mark Positioning", "blws": "Below - base Substitutions", "calt": "Contextual Alternates", "case": "Case - Sensitive Forms", "ccmp": "Glyph Composition / Decomposition", "cfar": "Conjunct Form After Ro", "cjct": "Conjunct Forms", "clig": "Contextual Ligatures", "cpct": "Centered CJK Punctuation", "cpsp": "Capital Spacing", "cswh": "Contextual Swash", "curs": "Cursive Positioning", "c2pc": "Petite Capitals From Capitals", "c2sc": "Small Capitals From Capitals", "dist": "Distances", "dlig": "Discretionary Ligatures", "dnom": "Denominators", "dtls": "Dotless Forms", "expt": "Expert Forms", "falt": "Final Glyph on Line Alternates", "fin2": "Terminal Forms #2", "fin3": "Terminal Forms #3", "fina": "Terminal Forms", "flac": "Flattened accent forms", "frac": "Fractions", "fwid": "Full Widths", "half": "Half Forms", "haln": "Halant Forms", "halt": "Alternate Half Widths", "hist": "Historical Forms", "hkna": "Horizontal Kana Alternates", "hlig": "Historical Ligatures", "hngl": "Hangul", "hojo": "Hojo Kanji Forms(JIS X 0212 - 1990 Kanji Forms)", "hwid": "Half Widths", "init": "Initial Forms", "isol": "Isolated Forms", "ital": "Italics", "jalt": "Justification Alternates", "jp78": "JIS78 Forms", "jp83": "JIS83 Forms", "jp90": "JIS90 Forms", "jp04": "JIS2004 Forms", "kern": "Kerning", "lfbd": "Left Bounds", "liga": "Standard Ligatures", "ljmo": "Leading Jamo Forms", "lnum": "Lining Figures", "locl": "Localized Forms", "ltra": "Left - to - right alternates", "ltrm": "Left - to - right mirrored forms", "mark": "Mark Positioning", "med2": "Medial Forms #2", "medi": "Medial Forms", "mgrk": "Mathematical Greek", "mkmk": "Mark to Mark Positioning", "mset": "Mark Positioning via Substitution", "nalt": "Alternate Annotation Forms", "nlck": "NLC Kanji Forms", "nukt": "Nukta Forms", "numr": "Numerators", "onum": "Oldstyle Figures", "opbd": "Optical Bounds", "ordn": "Ordinals", "ornm": "Ornaments", "palt": "Proportional Alternate Widths", "pcap": "Petite Capitals", "pkna": "Proportional Kana", "pnum": "Proportional Figures", "pref": "Pre - Base Forms", "pres": "Pre - base Substitutions", "pstf": "Post - base Forms", "psts": "Post - base Substitutions", "pwid": "Proportional Widths", "qwid": "Quarter Widths", "rand": "Randomize", "rclt": "Required Contextual Alternates", "rkrf": "Rakar Forms", "rlig": "Required Ligatures", "rphf": "Reph Forms", "rtbd": "Right Bounds", "rtla": "Right - to - left alternates", "rtlm": "Right - to - left mirrored forms", "ruby": "Ruby Notation Forms", "rvrn": "Required Variation Alternates", "salt": "Stylistic Alternates", "sinf": "Scientific Inferiors", "size": "Optical size", "smcp": "Small Capitals", "smpl": "Simplified Forms", "ssty": "Math script style alternates", "stch": "Stretching Glyph Decomposition", "subs": "Subscript", "sups": "Superscript", "swsh": "Swash", "titl": "Titling", "tjmo": "Trailing Jamo Forms", "tnam": "Traditional Name Forms", "tnum": "Tabular Figures", "trad": "Traditional Forms", "twid": "Third Widths", "unic": "Unicase", "valt": "Alternate Vertical Metrics", "vatu": "Vattu Variants", "vert": "Vertical Writing", "vhal": "Alternate Vertical Half Metrics", "vjmo": "Vowel Jamo Forms", "vkna": "Vertical Kana Alternates", "vkrn": "Vertical Kerning", "vpal": "Proportional Alternate Vertical Metrics", "vrt2": "Vertical Alternates and Rotation", "vrtr": "Vertical Alternates for Rotation", "zero": "Slashed Zero" };
var Font = (
/** @class */
function() {
function Font2(data) {
var obj = Typr_js_1.Typr.parse(data);
if (!obj.length || typeof obj[0] !== "object" || typeof obj[0].hasOwnProperty !== "function") {
throw "unable to parse font";
}
for (var n in obj[0]) {
this[n] = obj[0][n];
}
this.enabledGSUB = {};
}
Font2.prototype.getFamilyName = function() {
return this.name && (this.name.typoFamilyName || this.name.fontFamily) || "";
};
Font2.prototype.getSubFamilyName = function() {
return this.name && (this.name.typoSubfamilyName || this.name.fontSubfamily) || "";
};
Font2.prototype.glyphToPath = function(gid) {
return Typr_js_1.Typr.U.glyphToPath(this, gid);
};
Font2.prototype.getPairAdjustment = function(gid1, gid2) {
return Typr_js_1.Typr.U.getPairAdjustment(this, gid1, gid2);
};
Font2.prototype.stringToGlyphs = function(str) {
return Typr_js_1.Typr.U.stringToGlyphs(this, str);
};
Font2.prototype.glyphsToPath = function(gls) {
return Typr_js_1.Typr.U.glyphsToPath(this, gls);
};
Font2.prototype.pathToSVG = function(path, prec) {
return Typr_js_1.Typr.U.pathToSVG(path, prec);
};
Font2.prototype.pathToContext = function(path, ctx) {
return Typr_js_1.Typr.U.pathToContext(path, ctx);
};
Font2.prototype.lookupFriendlyName = function(table, feature) {
if (this[table] !== void 0) {
var tbl = this[table];
var feat = tbl.featureList[feature];
return this.featureFriendlyName(feat);
}
return "";
};
Font2.prototype.featureFriendlyName = function(feature) {
if (friendlyTags[feature.tag]) {
return friendlyTags[feature.tag];
}
if (feature.tag.match(/ss[0-2][0-9]/)) {
var name_1 = "Stylistic Set " + Number(feature.tag.substr(2, 2)).toString();
if (feature.featureParams) {
var version = Typr_js_1.Typr._bin.readUshort(this._data, feature.featureParams);
if (version === 0) {
var nameID = Typr_js_1.Typr._bin.readUshort(this._data, feature.featureParams + 2);
if (this.name && this.name[nameID] !== void 0) {
return name_1 + " - " + this.name[nameID];
}
}
}
return name_1;
}
if (feature.tag.match(/cv[0-9][0-9]/)) {
return "Character Variant " + Number(feature.tag.substr(2, 2)).toString();
}
return "";
};
Font2.prototype.enableGSUB = function(featureNumber) {
if (this.GSUB) {
var feature = this.GSUB.featureList[featureNumber];
if (feature) {
for (var i = 0; i < feature.tab.length; ++i) {
this.enabledGSUB[feature.tab[i]] = (this.enabledGSUB[feature.tab[i]] || 0) + 1;
}
}
}
};
Font2.prototype.disableGSUB = function(featureNumber) {
if (this.GSUB) {
var feature = this.GSUB.featureList[featureNumber];
if (feature) {
for (var i = 0; i < feature.tab.length; ++i) {
if (this.enabledGSUB[feature.tab[i]] > 1) {
--this.enabledGSUB[feature.tab[i]];
} else {
delete this.enabledGSUB[feature.tab[i]];
}
}
}
}
};
Font2.prototype.codeToGlyph = function(code) {
var g = Typr_js_1.Typr.U.codeToGlyph(this, code);
if (this.GSUB) {
var gls = [g];
for (var n in this.enabledGSUB) {
var l = this.GSUB.lookupList[n];
Typr_js_1.Typr.U._applySubs(gls, 0, l, this.GSUB.lookupList);
}
if (gls.length === 1)
return gls[0];
}
return g;
};
return Font2;
}()
);
var Font_1 = Font;
const base64ToUint8Array = (base64) => {
const data = window.atob(base64);
const buffer = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
buffer[i] = data.charCodeAt(i);
}
return buffer;
};
const parseResourceText = (resourceName) => {
try {
return JSON.parse(GM_getResourceText(resourceName));
} catch (e) {
console.log("parseResourceText error:", e);
return null;
}
};
const findEncryptedFontStyle = (document2) => {
const styles = Array.from(document2.querySelectorAll("style"));
return styles.find((style) => {
var _a;
return (_a = style.textContent) == null ? void 0 : _a.includes("font-cxsecret");
});
};
const buildDecryptionTable = (font, lookupTable) => {
const match = {};
const CHINESE_CHAR_START = 19968;
const CHINESE_CHAR_END = 40870;
for (let charCode = CHINESE_CHAR_START; charCode < CHINESE_CHAR_END; charCode++) {
const glyph = font.codeToGlyph(charCode);
if (!glyph) continue;
const path = font.glyphToPath(glyph);
const hash = md5(JSON.stringify(path)).slice(24);
match[charCode] = lookupTable[hash];
}
return match;
};
const replaceEncryptedText = (elements, decryptionTable) => {
elements.forEach((el) => {
let html = el.innerHTML;
Object.entries(decryptionTable).forEach(([key, value]) => {
const encryptedChar = String.fromCharCode(Number(key));
const decryptedChar = String.fromCharCode(value);
html = html.replace(new RegExp(encryptedChar, "g"), decryptedChar);
});
el.classList.remove("font-cxsecret");
el.innerHTML = html;
});
};
function decrypt(iframeDocument) {
var _a, _b;
const fontStyle = findEncryptedFontStyle(iframeDocument);
if (!fontStyle) return;
const fontData = (_b = (_a = fontStyle.textContent) == null ? void 0 : _a.match(/base64,([\w\W]+?)'/)) == null ? void 0 : _b[1];
if (!fontData) return;
const fontArray = base64ToUint8Array(fontData);
const font = new Font_1(fontArray);
const lookupTable = parseResourceText("ttf") || parseResourceText("ttf2");
if (!lookupTable) return;
const decryptionTable = buildDecryptionTable(font, lookupTable);
const elements = iframeDocument.querySelectorAll(".font-cxsecret");
replaceEncryptedText(elements, decryptionTable);
}
function isFunction(value) {
return typeof value === "function";
}
function hasLift(source) {
return isFunction(source === null || source === void 0 ? void 0 : source.lift);
}
function operate(init) {
return function(source) {
if (hasLift(source)) {
return source.lift(function(liftedSource) {
try {
return init(liftedSource, this);
} catch (err) {
this.error(err);
}
});
}
throw new TypeError("Unable to lift unknown Observable type");
};
}
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) {
d2.__proto__ = b2;
} || function(d2, b2) {
for (var p in b2) if (Object.prototype.hasOwnProperty.call(b2, p)) d2[p] = b2[p];
};
return extendStatics(d, b);
};
function __extends(d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() {
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() {
if (t[0] & 1) throw t[1];
return t[1];
}, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() {
return this;
}), g;
function verb(n) {
return function(v) {
return step([n, v]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return { value: op[1], done: false };
case 5:
_.label++;
y = op[1];
op = [0];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5) throw op[1];
return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __values(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function() {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
}
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
} catch (error) {
e = { error };
} finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
} finally {
if (e) throw e.error;
}
}
return ar;
}
function __spreadArray(to, from2, pack) {
if (arguments.length === 2) for (var i = 0, l = from2.length, ar; i < l; i++) {
if (ar || !(i in from2)) {
if (!ar) ar = Array.prototype.slice.call(from2, 0, i);
ar[i] = from2[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from2));
}
function __await(v) {
return this instanceof __await ? (this.v = v, this) : new __await(v);
}
function __asyncGenerator(thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function() {
return this;
}, i;
function awaitReturn(f) {
return function(v) {
return Promise.resolve(v).then(f, reject);
};
}
function verb(n, f) {
if (g[n]) {
i[n] = function(v) {
return new Promise(function(a, b) {
q.push([n, v, a, b]) > 1 || resume(n, v);
});
};
if (f) i[n] = f(i[n]);
}
}
function resume(n, v) {
try {
step(g[n](v));
} catch (e) {
settle(q[0][3], e);
}
}
function step(r) {
r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);
}
function fulfill(value) {
resume("next", value);
}
function reject(value) {
resume("throw", value);
}
function settle(f, v) {
if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]);
}
}
function __asyncValues(o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function() {
return this;
}, i);
function verb(n) {
i[n] = o[n] && function(v) {
return new Promise(function(resolve, reject) {
v = o[n](v), settle(resolve, reject, v.done, v.value);
});
};
}
function settle(resolve, reject, d, v) {
Promise.resolve(v).then(function(v2) {
resolve({ value: v2, done: d });
}, reject);
}
}
typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message2) {
var e = new Error(message2);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var isArrayLike = function(x) {
return x && typeof x.length === "number" && typeof x !== "function";
};
function isPromise(value) {
return isFunction(value === null || value === void 0 ? void 0 : value.then);
}
function createErrorClass(createImpl) {
var _super = function(instance) {
Error.call(instance);
instance.stack = new Error().stack;
};
var ctorFunc = createImpl(_super);
ctorFunc.prototype = Object.create(Error.prototype);
ctorFunc.prototype.constructor = ctorFunc;
return ctorFunc;
}
var UnsubscriptionError = createErrorClass(function(_super) {
return function UnsubscriptionErrorImpl(errors) {
_super(this);
this.message = errors ? errors.length + " errors occurred during unsubscription:\n" + errors.map(function(err, i) {
return i + 1 + ") " + err.toString();
}).join("\n ") : "";
this.name = "UnsubscriptionError";
this.errors = errors;
};
});
function arrRemove(arr, item) {
if (arr) {
var index = arr.indexOf(item);
0 <= index && arr.splice(index, 1);
}
}
var Subscription = function() {
function Subscription2(initialTeardown) {
this.initialTeardown = initialTeardown;
this.closed = false;
this._parentage = null;
this._finalizers = null;
}
Subscription2.prototype.unsubscribe = function() {
var e_1, _a, e_2, _b;
var errors;
if (!this.closed) {
this.closed = true;
var _parentage = this._parentage;
if (_parentage) {
this._parentage = null;
if (Array.isArray(_parentage)) {
try {
for (var _parentage_1 = __values(_parentage), _parentage_1_1 = _parentage_1.next(); !_parentage_1_1.done; _parentage_1_1 = _parentage_1.next()) {
var parent_1 = _parentage_1_1.value;
parent_1.remove(this);
}
} catch (e_1_1) {
e_1 = { error: e_1_1 };
} finally {
try {
if (_parentage_1_1 && !_parentage_1_1.done && (_a = _parentage_1.return)) _a.call(_parentage_1);
} finally {
if (e_1) throw e_1.error;
}
}
} else {
_parentage.remove(this);
}
}
var initialFinalizer = this.initialTeardown;
if (isFunction(initialFinalizer)) {
try {
initialFinalizer();
} catch (e) {
errors = e instanceof UnsubscriptionError ? e.errors : [e];
}
}
var _finalizers = this._finalizers;
if (_finalizers) {
this._finalizers = null;
try {
for (var _finalizers_1 = __values(_finalizers), _finalizers_1_1 = _finalizers_1.next(); !_finalizers_1_1.done; _finalizers_1_1 = _finalizers_1.next()) {
var finalizer = _finalizers_1_1.value;
try {
execFinalizer(finalizer);
} catch (err) {
errors = errors !== null && errors !== void 0 ? errors : [];
if (err instanceof UnsubscriptionError) {
errors = __spreadArray(__spreadArray([], __read(errors)), __read(err.errors));
} else {
errors.push(err);
}
}
}
} catch (e_2_1) {
e_2 = { error: e_2_1 };
} finally {
try {
if (_finalizers_1_1 && !_finalizers_1_1.done && (_b = _finalizers_1.return)) _b.call(_finalizers_1);
} finally {
if (e_2) throw e_2.error;
}
}
}
if (errors) {
throw new UnsubscriptionError(errors);
}
}
};
Subscription2.prototype.add = function(teardown) {
var _a;
if (teardown && teardown !== this) {
if (this.closed) {
execFinalizer(teardown);
} else {
if (teardown instanceof Subscription2) {
if (teardown.closed || teardown._hasParent(this)) {
return;
}
teardown._addParent(this);
}
(this._finalizers = (_a = this._finalizers) !== null && _a !== void 0 ? _a : []).push(teardown);
}
}
};
Subscription2.prototype._hasParent = function(parent) {
var _parentage = this._parentage;
return _parentage === parent || Array.isArray(_parentage) && _parentage.includes(parent);
};
Subscription2.prototype._addParent = function(parent) {
var _parentage = this._parentage;
this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;
};
Subscription2.prototype._removeParent = function(parent) {
var _parentage = this._parentage;
if (_parentage === parent) {
this._parentage = null;
} else if (Array.isArray(_parentage)) {
arrRemove(_parentage, parent);
}
};
Subscription2.prototype.remove = function(teardown) {
var _finalizers = this._finalizers;
_finalizers && arrRemove(_finalizers, teardown);
if (teardown instanceof Subscription2) {
teardown._removeParent(this);
}
};
Subscription2.EMPTY = function() {
var empty = new Subscription2();
empty.closed = true;
return empty;
}();
return Subscription2;
}();
Subscription.EMPTY;
function isSubscription(value) {
return value instanceof Subscription || value && "closed" in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe);
}
function execFinalizer(finalizer) {
if (isFunction(finalizer)) {
finalizer();
} else {
finalizer.unsubscribe();
}
}
var config = {
Promise: void 0,
useDeprecatedNextContext: false
};
var timeoutProvider = {
setTimeout: function(handler, timeout) {
var args = [];
for (var _i = 2; _i < arguments.length; _i++) {
args[_i - 2] = arguments[_i];
}
return setTimeout.apply(void 0, __spreadArray([handler, timeout], __read(args)));
},
clearTimeout: function(handle) {
return clearTimeout(handle);
},
delegate: void 0
};
function reportUnhandledError(err) {
timeoutProvider.setTimeout(function() {
{
throw err;
}
});
}
function noop() {
}
function errorContext(cb) {
{
cb();
}
}
var Subscriber = function(_super) {
__extends(Subscriber2, _super);
function Subscriber2(destination) {
var _this = _super.call(this) || this;
_this.isStopped = false;
if (destination) {
_this.destination = destination;
if (isSubscription(destination)) {
destination.add(_this);
}
} else {
_this.destination = EMPTY_OBSERVER;
}
return _this;
}
Subscriber2.create = function(next, error, complete) {
return new SafeSubscriber(next, error, complete);
};
Subscriber2.prototype.next = function(value) {
if (this.isStopped) ;
else {
this._next(value);
}
};
Subscriber2.prototype.error = function(err) {
if (this.isStopped) ;
else {
this.isStopped = true;
this._error(err);
}
};
Subscriber2.prototype.complete = function() {
if (this.isStopped) ;
else {
this.isStopped = true;
this._complete();
}
};
Subscriber2.prototype.unsubscribe = function() {
if (!this.closed) {
this.isStopped = true;
_super.prototype.unsubscribe.call(this);
this.destination = null;
}
};
Subscriber2.prototype._next = function(value) {
this.destination.next(value);
};
Subscriber2.prototype._error = function(err) {
try {
this.destination.error(err);
} finally {
this.unsubscribe();
}
};
Subscriber2.prototype._complete = function() {
try {
this.destination.complete();
} finally {
this.unsubscribe();
}
};
return Subscriber2;
}(Subscription);
var _bind = Function.prototype.bind;
function bind(fn, thisArg) {
return _bind.call(fn, thisArg);
}
var ConsumerObserver = function() {
function ConsumerObserver2(partialObserver) {
this.partialObserver = partialObserver;
}
ConsumerObserver2.prototype.next = function(value) {
var partialObserver = this.partialObserver;
if (partialObserver.next) {
try {
partialObserver.next(value);
} catch (error) {
handleUnhandledError(error);
}
}
};
ConsumerObserver2.prototype.error = function(err) {
var partialObserver = this.partialObserver;
if (partialObserver.error) {
try {
partialObserver.error(err);
} catch (error) {
handleUnhandledError(error);
}
} else {
handleUnhandledError(err);
}
};
ConsumerObserver2.prototype.complete = function() {
var partialObserver = this.partialObserver;
if (partialObserver.complete) {
try {
partialObserver.complete();
} catch (error) {
handleUnhandledError(error);
}
}
};
return ConsumerObserver2;
}();
var SafeSubscriber = function(_super) {
__extends(SafeSubscriber2, _super);
function SafeSubscriber2(observerOrNext, error, complete) {
var _this = _super.call(this) || this;
var partialObserver;
if (isFunction(observerOrNext) || !observerOrNext) {
partialObserver = {
next: observerOrNext !== null && observerOrNext !== void 0 ? observerOrNext : void 0,
error: error !== null && error !== void 0 ? error : void 0,
complete: complete !== null && complete !== void 0 ? complete : void 0
};
} else {
var context_1;
if (_this && config.useDeprecatedNextContext) {
context_1 = Object.create(observerOrNext);
context_1.unsubscribe = function() {
return _this.unsubscribe();
};
partialObserver = {
next: observerOrNext.next && bind(observerOrNext.next, context_1),
error: observerOrNext.error && bind(observerOrNext.error, context_1),
complete: observerOrNext.complete && bind(observerOrNext.complete, context_1)
};
} else {
partialObserver = observerOrNext;
}
}
_this.destination = new ConsumerObserver(partialObserver);
return _this;
}
return SafeSubscriber2;
}(Subscriber);
function handleUnhandledError(error) {
{
reportUnhandledError(error);
}
}
function defaultErrorHandler(err) {
throw err;
}
var EMPTY_OBSERVER = {
closed: true,
next: noop,
error: defaultErrorHandler,
complete: noop
};
var observable = function() {
return typeof Symbol === "function" && Symbol.observable || "@@observable";
}();
function identity(x) {
return x;
}
function pipeFromArray(fns) {
if (fns.length === 0) {
return identity;
}
if (fns.length === 1) {
return fns[0];
}
return function piped(input) {
return fns.reduce(function(prev, fn) {
return fn(prev);
}, input);
};
}
var Observable = function() {
function Observable2(subscribe) {
if (subscribe) {
this._subscribe = subscribe;
}
}
Observable2.prototype.lift = function(operator) {
var observable2 = new Observable2();
observable2.source = this;
observable2.operator = operator;
return observable2;
};
Observable2.prototype.subscribe = function(observerOrNext, error, complete) {
var _this = this;
var subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);
errorContext(function() {
var _a = _this, operator = _a.operator, source = _a.source;
subscriber.add(operator ? operator.call(subscriber, source) : source ? _this._subscribe(subscriber) : _this._trySubscribe(subscriber));
});
return subscriber;
};
Observable2.prototype._trySubscribe = function(sink) {
try {
return this._subscribe(sink);
} catch (err) {
sink.error(err);
}
};
Observable2.prototype.forEach = function(next, promiseCtor) {
var _this = this;
promiseCtor = getPromiseCtor(promiseCtor);
return new promiseCtor(function(resolve, reject) {
var subscriber = new SafeSubscriber({
next: function(value) {
try {
next(value);
} catch (err) {
reject(err);
subscriber.unsubscribe();
}
},
error: reject,
complete: resolve
});
_this.subscribe(subscriber);
});
};
Observable2.prototype._subscribe = function(subscriber) {
var _a;
return (_a = this.source) === null || _a === void 0 ? void 0 : _a.subscribe(subscriber);
};
Observable2.prototype[observable] = function() {
return this;
};
Observable2.prototype.pipe = function() {
var operations = [];
for (var _i = 0; _i < arguments.length; _i++) {
operations[_i] = arguments[_i];
}
return pipeFromArray(operations)(this);
};
Observable2.prototype.toPromise = function(promiseCtor) {
var _this = this;
promiseCtor = getPromiseCtor(promiseCtor);
return new promiseCtor(function(resolve, reject) {
var value;
_this.subscribe(function(x) {
return value = x;
}, function(err) {
return reject(err);
}, function() {
return resolve(value);
});
});
};
Observable2.create = function(subscribe) {
return new Observable2(subscribe);
};
return Observable2;
}();
function getPromiseCtor(promiseCtor) {
var _a;
return (_a = promiseCtor !== null && promiseCtor !== void 0 ? promiseCtor : config.Promise) !== null && _a !== void 0 ? _a : Promise;
}
function isObserver(value) {
return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);
}
function isSubscriber(value) {
return value && value instanceof Subscriber || isObserver(value) && isSubscription(value);
}
function isInteropObservable(input) {
return isFunction(input[observable]);
}
function isAsyncIterable(obj) {
return Symbol.asyncIterator && isFunction(obj === null || obj === void 0 ? void 0 : obj[Symbol.asyncIterator]);
}
function createInvalidObservableTypeError(input) {
return new TypeError("You provided " + (input !== null && typeof input === "object" ? "an invalid object" : "'" + input + "'") + " where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.");
}
function getSymbolIterator() {
if (typeof Symbol !== "function" || !Symbol.iterator) {
return "@@iterator";
}
return Symbol.iterator;
}
var iterator = getSymbolIterator();
function isIterable(input) {
return isFunction(input === null || input === void 0 ? void 0 : input[iterator]);
}
function readableStreamLikeToAsyncGenerator(readableStream) {
return __asyncGenerator(this, arguments, function readableStreamLikeToAsyncGenerator_1() {
var reader, _a, value, done;
return __generator(this, function(_b) {
switch (_b.label) {
case 0:
reader = readableStream.getReader();
_b.label = 1;
case 1:
_b.trys.push([1, , 9, 10]);
_b.label = 2;
case 2:
return [4, __await(reader.read())];
case 3:
_a = _b.sent(), value = _a.value, done = _a.done;
if (!done) return [3, 5];
return [4, __await(void 0)];
case 4:
return [2, _b.sent()];
case 5:
return [4, __await(value)];
case 6:
return [4, _b.sent()];
case 7:
_b.sent();
return [3, 2];
case 8:
return [3, 10];
case 9:
reader.releaseLock();
return [7];
case 10:
return [2];
}
});
});
}
function isReadableStreamLike(obj) {
return isFunction(obj === null || obj === void 0 ? void 0 : obj.getReader);
}
function innerFrom(input) {
if (input instanceof Observable) {
return input;
}
if (input != null) {
if (isInteropObservable(input)) {
return fromInteropObservable(input);
}
if (isArrayLike(input)) {
return fromArrayLike(input);
}
if (isPromise(input)) {
return fromPromise(input);
}
if (isAsyncIterable(input)) {
return fromAsyncIterable(input);
}
if (isIterable(input)) {
return fromIterable(input);
}
if (isReadableStreamLike(input)) {
return fromReadableStreamLike(input);
}
}
throw createInvalidObservableTypeError(input);
}
function fromInteropObservable(obj) {
return new Observable(function(subscriber) {
var obs = obj[observable]();
if (isFunction(obs.subscribe)) {
return obs.subscribe(subscriber);
}
throw new TypeError("Provided object does not correctly implement Symbol.observable");
});
}
function fromArrayLike(array) {
return new Observable(function(subscriber) {
for (var i = 0; i < array.length && !subscriber.closed; i++) {
subscriber.next(array[i]);
}
subscriber.complete();
});
}
function fromPromise(promise) {
return new Observable(function(subscriber) {
promise.then(function(value) {
if (!subscriber.closed) {
subscriber.next(value);
subscriber.complete();
}
}, function(err) {
return subscriber.error(err);
}).then(null, reportUnhandledError);
});
}
function fromIterable(iterable) {
return new Observable(function(subscriber) {
var e_1, _a;
try {
for (var iterable_1 = __values(iterable), iterable_1_1 = iterable_1.next(); !iterable_1_1.done; iterable_1_1 = iterable_1.next()) {
var value = iterable_1_1.value;
subscriber.next(value);
if (subscriber.closed) {
return;
}
}
} catch (e_1_1) {
e_1 = { error: e_1_1 };
} finally {
try {
if (iterable_1_1 && !iterable_1_1.done && (_a = iterable_1.return)) _a.call(iterable_1);
} finally {
if (e_1) throw e_1.error;
}
}
subscriber.complete();
});
}
function fromAsyncIterable(asyncIterable) {
return new Observable(function(subscriber) {
process(asyncIterable, subscriber).catch(function(err) {
return subscriber.error(err);
});
});
}
function fromReadableStreamLike(readableStream) {
return fromAsyncIterable(readableStreamLikeToAsyncGenerator(readableStream));
}
function process(asyncIterable, subscriber) {
var asyncIterable_1, asyncIterable_1_1;
var e_2, _a;
return __awaiter(this, void 0, void 0, function() {
var value, e_2_1;
return __generator(this, function(_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 5, 6, 11]);
asyncIterable_1 = __asyncValues(asyncIterable);
_b.label = 1;
case 1:
return [4, asyncIterable_1.next()];
case 2:
if (!(asyncIterable_1_1 = _b.sent(), !asyncIterable_1_1.done)) return [3, 4];
value = asyncIterable_1_1.value;
subscriber.next(value);
if (subscriber.closed) {
return [2];
}
_b.label = 3;
case 3:
return [3, 1];
case 4:
return [3, 11];
case 5:
e_2_1 = _b.sent();
e_2 = { error: e_2_1 };
return [3, 11];
case 6:
_b.trys.push([6, , 9, 10]);
if (!(asyncIterable_1_1 && !asyncIterable_1_1.done && (_a = asyncIterable_1.return))) return [3, 8];
return [4, _a.call(asyncIterable_1)];
case 7:
_b.sent();
_b.label = 8;
case 8:
return [3, 10];
case 9:
if (e_2) throw e_2.error;
return [7];
case 10:
return [7];
case 11:
subscriber.complete();
return [2];
}
});
});
}
function createOperatorSubscriber(destination, onNext, onComplete, onError, onFinalize) {
return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);
}
var OperatorSubscriber = function(_super) {
__extends(OperatorSubscriber2, _super);
function OperatorSubscriber2(destination, onNext, onComplete, onError, onFinalize, shouldUnsubscribe) {
var _this = _super.call(this, destination) || this;
_this.onFinalize = onFinalize;
_this.shouldUnsubscribe = shouldUnsubscribe;
_this._next = onNext ? function(value) {
try {
onNext(value);
} catch (err) {
destination.error(err);
}
} : _super.prototype._next;
_this._error = onError ? function(err) {
try {
onError(err);
} catch (err2) {
destination.error(err2);
} finally {
this.unsubscribe();
}
} : _super.prototype._error;
_this._complete = onComplete ? function() {
try {
onComplete();
} catch (err) {
destination.error(err);
} finally {
this.unsubscribe();
}
} : _super.prototype._complete;
return _this;
}
OperatorSubscriber2.prototype.unsubscribe = function() {
var _a;
if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {
var closed_1 = this.closed;
_super.prototype.unsubscribe.call(this);
!closed_1 && ((_a = this.onFinalize) === null || _a === void 0 ? void 0 : _a.call(this));
}
};
return OperatorSubscriber2;
}(Subscriber);
function map(project, thisArg) {
return operate(function(source, subscriber) {
var index = 0;
source.subscribe(createOperatorSubscriber(subscriber, function(value) {
subscriber.next(project.call(thisArg, value, index++));
}));
});
}
function mergeInternals(source, subscriber, project, concurrent, onBeforeNext, expand, innerSubScheduler, additionalFinalizer) {
var buffer = [];
var active = 0;
var index = 0;
var isComplete = false;
var checkComplete = function() {
if (isComplete && !buffer.length && !active) {
subscriber.complete();
}
};
var outerNext = function(value) {
return active < concurrent ? doInnerSub(value) : buffer.push(value);
};
var doInnerSub = function(value) {
active++;
var innerComplete = false;
innerFrom(project(value, index++)).subscribe(createOperatorSubscriber(subscriber, function(innerValue) {
{
subscriber.next(innerValue);
}
}, function() {
innerComplete = true;
}, void 0, function() {
if (innerComplete) {
try {
active--;
var _loop_1 = function() {
var bufferedValue = buffer.shift();
if (innerSubScheduler) ;
else {
doInnerSub(bufferedValue);
}
};
while (buffer.length && active < concurrent) {
_loop_1();
}
checkComplete();
} catch (err) {
subscriber.error(err);
}
}
}));
};
source.subscribe(createOperatorSubscriber(subscriber, outerNext, function() {
isComplete = true;
checkComplete();
}));
return function() {
};
}
function mergeMap(project, resultSelector, concurrent) {
if (concurrent === void 0) {
concurrent = Infinity;
}
if (isFunction(resultSelector)) {
return mergeMap(function(a, i) {
return map(function(b, ii) {
return resultSelector(a, b, i, ii);
})(innerFrom(project(a, i)));
}, concurrent);
} else if (typeof resultSelector === "number") {
concurrent = resultSelector;
}
return operate(function(source, subscriber) {
return mergeInternals(source, subscriber, project, concurrent);
});
}
function mergeAll(concurrent) {
return mergeMap(identity, concurrent);
}
function concatAll() {
return mergeAll(1);
}
function concatMap(project, resultSelector) {
return isFunction(resultSelector) ? mergeMap(project, resultSelector, 1) : mergeMap(project, 1);
}
class BasicQuestionHandler {
constructor() {
this._document = document;
this._window = _unsafeWindow;
this.questions = [];
this.correctNum = 0;
this.setting = useSettingStore();
this.log = useLogStore();
this.questionStore = useQuestionStore();
this.parseHtml = () => {
throw new Error("请使用继承类的重写方法");
};
this.fillQuestion = (question) => {
throw new Error("请使用继承类的重写方法");
};
this.questionType = {
"A1型选择题": "0",
"X型选择题": "1",
"单选题": "0",
"单项选择题": "0",
"单项选择": "0",
"A1型题": "0",
"多选题": "1",
"多项选择题": "1",
"多项选择": "1",
"X型题": "1",
"不定项选择题": "1",
"不定项": "1",
"填空题": "2",
"判断题": "3",
"是非题": "3",
"简答题": "4",
"名词解释": "5",
"论述题": "6",
"计算题": "7",
"连线题": "11",
"匹配题": "11",
"完型填空": "14",
"完形填空题": "14",
"阅读理解": "15",
"单选": "0",
"A1型": "0",
"多选": "1",
"填空": "2",
"判断": "3",
"简答": "4",
"论述": "6",
"计算": "7",
"思政元素": "2"
};
this.questionTypeKeywordRules = [
{
keywords: ["单项选择", "单选"],
type: "0"
},
{
keywords: ["多项选择", "多选", "不定项", "X型"],
type: "1"
},
{
keywords: ["填空", "思政元素"],
type: "2"
},
{
keywords: ["判断", "是非"],
type: "3"
},
{
keywords: ["简答"],
type: "4"
},
{
keywords: ["名词解释"],
type: "5"
},
{
keywords: ["论述"],
type: "6"
},
{
keywords: ["计算"],
type: "7"
},
{
keywords: ["连线", "匹配"],
type: "11"
},
{
keywords: ["完型填空", "完形填空"],
type: "14"
},
{
keywords: ["阅读理解"],
type: "15"
}
];
this.removeHtml = (htmlStr, replacen = true) => {
const textArea = document.createElement("textarea");
textArea.innerHTML = htmlStr;
htmlStr = textArea.value;
htmlStr = htmlStr.replace(/[\t\r\xa0]/g, " ");
htmlStr = htmlStr.replace(/[\u2000-\u200a]/g, " ");
htmlStr = htmlStr.replace(/
/g, "\n");
if (replacen) {
htmlStr = htmlStr.replace(/<(\/)?(p|div).*?>/g, "\n");
}
htmlStr = htmlStr.replace(/ {2,}/g, " ");
htmlStr = htmlStr.replace(/\n{2,}/g, "\n");
htmlStr = htmlStr.replace(//g, "");
htmlStr = htmlStr.replace(/<\/xmp>/g, "");
htmlStr = DOMPurify.sanitize(htmlStr, { ALLOWED_TAGS: ["img", "br", "sub", "sup"], ALLOWED_ATTR: ["src", "href"], ALLOW_DATA_ATTR: false, KEEP_CONTENT: true });
htmlStr = htmlStr.replace(/<((?!img|sub|sup|br)[^>]+)>/g, "").replace(/
/g, "\n");
let imgReg = //g;
let imgArr = htmlStr.match(imgReg);
if (imgArr) {
htmlStr = htmlStr.replace(new RegExp("
]*)(?", "g"), "
");
imgArr.forEach((item) => {
let src = item.match(/src="(.*?)"/);
if (src) {
if (src[1].indexOf("http") == -1 && !src[1].includes("data:image")) {
if (src[1].startsWith("/")) {
htmlStr = htmlStr.replace(src[1], location.origin + src[1]);
} else {
const fullURL = new URL(src[1], document.baseURI).href;
htmlStr = htmlStr.replace(src[1], fullURL);
}
}
}
});
}
return htmlStr.trim();
};
this.removeAllTags = (htmlStr) => {
htmlStr = DOMPurify.sanitize(htmlStr, { ALLOWED_TAGS: ["img"], ALLOWED_ATTR: ["src", "href"], ALLOW_DATA_ATTR: false, KEEP_CONTENT: true });
let imgReg = //g;
let imgArr = htmlStr.match(imgReg);
if (imgArr) {
htmlStr = htmlStr.replace(new RegExp("
]*)(?", "g"), "
");
imgArr.forEach((item) => {
let src = item.match(/src="(.*?)"/);
if (src) {
if (src[1].indexOf("http") == -1 && !src[1].includes("data:image")) {
if (src[1].startsWith("/")) {
htmlStr = htmlStr.replace(src[1], location.origin + src[1]);
} else {
const fullURL = new URL(src[1], document.baseURI).href;
htmlStr = htmlStr.replace(src[1], fullURL);
}
}
}
});
} else {
htmlStr = htmlStr.replace(/[\p{P}\s]/gu, "").toLowerCase();
}
htmlStr = htmlStr.replace(/ |nbsp/g, "");
return htmlStr.trim();
};
this.clean = (str) => {
return str.replace(/^【.*?】\s*/, "").replace(/\s*(\d+\.\d+分)$/, "");
};
this.normalizeQuestionTypeText = (text = "") => {
let normalizedText = String(text).normalize("NFKC").trim();
normalizedText = normalizedText.replace(/<[^>]+>/g, "").replace(/[【】\[\]()()]/g, "").replace(/[::]/g, "").replace(/\s+/g, "").replace(/第\s*\d+\s*题/g, "").replace(/共\s*\d+(?:\.\d+)?\s*分/g, "").replace(/本题/g, "").replace(/题型/g, "").trim();
const canonicalQuestionTypeMap = {
"单项选择题": "单项选择",
"单项选择": "单项选择",
"单选题": "单项选择",
"单选": "单项选择",
"多项选择题": "多项选择",
"多项选择": "多项选择",
"多选题": "多项选择",
"多选": "多项选择",
"判断题": "判断",
"判断": "判断",
"是非题": "判断",
"是非": "判断",
"填空题": "填空",
"填空": "填空",
"简答题": "简答",
"简答": "简答",
"论述题": "论述",
"论述": "论述",
"计算题": "计算",
"计算": "计算",
"连线题": "连线",
"连线": "连线",
"匹配题": "匹配",
"匹配": "匹配",
"完型填空": "完形填空",
"完形填空题": "完形填空",
"完形填空": "完形填空",
"阅读理解": "阅读理解"
};
return canonicalQuestionTypeMap[normalizedText] || normalizedText;
};
this.resolveQuestionType = (text = "") => {
const normalizedText = this.normalizeQuestionTypeText(text);
const exactQuestionTypeMap = Object.entries(this.questionType).reduce((result, [key, value]) => {
result[this.normalizeQuestionTypeText(key)] = value;
return result;
}, {});
if (exactQuestionTypeMap[normalizedText]) {
return exactQuestionTypeMap[normalizedText];
}
const matchedRule = this.questionTypeKeywordRules.find(
(rule) => rule.keywords.some((keyword) => normalizedText.includes(keyword))
);
return (matchedRule == null ? void 0 : matchedRule.type) || "999";
};
this.similarCharMap = {
"ɑ": "α",
// 国际音标 ɑ (U+0251) → 希腊字母 α (U+03B1)
"Α": "A",
// 希腊大写 Α → 拉丁大写 A
"Β": "B",
// 希腊大写 Β → 拉丁大写 B
"Ε": "E",
// 希腊大写 Ε → 拉丁大写 E
"Ζ": "Z",
// 希腊大写 Ζ → 拉丁大写 Z
"Η": "H",
// 希腊大写 Η → 拉丁大写 H
"Ι": "I",
// 希腊大写 Ι → 拉丁大写 I
"Κ": "K",
// 希腊大写 Κ → 拉丁大写 K
"Μ": "M",
// 希腊大写 Μ → 拉丁大写 M
"Ν": "N",
// 希腊大写 Ν → 拉丁大写 N
"Ο": "O",
// 希腊大写 Ο → 拉丁大写 O
"Ρ": "P",
// 希腊大写 Ρ → 拉丁大写 P
"Τ": "T",
// 希腊大写 Τ → 拉丁大写 T
"Υ": "Y",
// 希腊大写 Υ → 拉丁大写 Y
"Χ": "X",
// 希腊大写 Χ → 拉丁大写 X
"ο": "o",
// 希腊小写 ο → 拉丁小写 o
"ν": "v"
// 希腊小写 ν → 拉丁小写 v (有时相似)
};
this.clearMark = (str) => {
let result = str;
for (const [similar, standard] of Object.entries(this.similarCharMap)) {
result = result.replace(new RegExp(similar, "g"), standard);
}
return result.replace(/[\p{P}\p{S}]/gu, "");
};
}
}
let QuestionHandler$1 = class QuestionHandler extends BasicQuestionHandler {
constructor(type, iframe) {
super();
__publicField(this, "type");
__publicField(this, "AI_ALLOW_QUESTION_TYPE", ["0", "1", "2", "3", "4", "5"]);
__publicField(this, "answerSeparators", ["===", "#", "---", "###", "|", ";", ";"]);
__publicField(this, "init", async () => {
var _that = this;
this.parseHtml();
if (this.questions.length) {
this.log.insertLog(
`成功解析到${this.questions.length}个题目,开始答题..`
);
setTimeout(() => {
autoSetTab(_that.setting, "2");
}, 1e3);
const { answerMode } = this.setting.config.basicConfig;
const mode = answerMode.value;
registerQuestionFillHook(this);
for (const [index, question] of this.questions.entries()) {
if (!this.setting.config.basicConfig.autoAnswer.value) break;
this.questionStore.currentQuestionIndex = index;
if (mode !== "ai") {
const answerData = await getAccurateAnswer(question);
if (!this.setting.config.basicConfig.autoAnswer.value) break;
if (answerData.code === 1) {
question.answer = {
code: answerData.data.multimatch ? 0 : 1,
answer: answerData.data.answer,
src: answerData.data.src
};
this.setting.userInfo.remainCount = formatLocalRemainCount(answerData.data.remainCount);
if (answerData.data.multimatch) {
// 手动选择模式:题库有多道相似题,先不填,等用户自己挑
question.answer.msg = "存在多道相似题,请先手动选择后再填写";
this.log.insertLog(`第${index + 1}题有${answerData.data.candidates.length}道相似题,已暂停自动作答,请去「题目结果」里手动选择`);
continue;
}
saveAnswerRecord({
question,
answer: answerData.data.answer,
source: "accurate"
});
let fillSuccess = this.fillQuestion(question);
if (["0", "1"].includes(question.type)) {
for (let attempt = 0; attempt < 3; attempt++) {
await sleep(0.2);
if (!this.setting.config.basicConfig.autoAnswer.value) break;
fillSuccess = this.fillQuestion(question);
if (fillSuccess) break;
}
}
if (fillSuccess) {
this.correctNum += 1;
continue;
}
question.answer.code = 0;
question.answer.msg = "答案拿到了但没点上去";
// 题库答案填不上(常见于图片选项),改走模型
} else if (mode === "questionBank" || answerData.code !== -1004) {
question.answer = {
code: answerData.code,
answer: [],
msg: answerData.msg
};
continue;
}
}
if (this.AI_ALLOW_QUESTION_TYPE.includes(question.type)) {
const answerData = await getAIAnswer(question);
if (!this.setting.config.basicConfig.autoAnswer.value) break;
if (answerData.code === 1) {
question.answer = {
code: 2,
answer: answerData.data.answer,
src: answerData.data.src || "ai"
};
this.setting.userInfo.remainCount = formatLocalRemainCount(answerData.data.remainCount);
saveAnswerRecord({
question,
answer: answerData.data.answer,
source: "ai"
});
let fillSuccess = this.fillQuestion(question);
if (["0", "1"].includes(question.type)) {
for (let attempt = 0; attempt < 3; attempt++) {
await sleep(0.2);
if (!this.setting.config.basicConfig.autoAnswer.value) break;
fillSuccess = this.fillQuestion(question);
if (fillSuccess) break;
}
}
if (!fillSuccess) {
question.answer.code = 0;
question.answer.msg = "答案拿到了但没点上去";
continue;
}
this.correctNum += 1;
} else if (!(question.answer && question.answer.code === 0 && Array.isArray(question.answer.answer) && question.answer.answer.length)) {
// 前面已经「拿到答案但点不上」时,别被后续余额不足盖掉,否则统计会把填充问题算成欠费
question.answer = {
code: answerData.code,
answer: [],
msg: answerData.msg
};
} else if (!question.answer.msg) {
question.answer.msg = "答案拿到了但没点上去";
}
} else if (!(question.answer && question.answer.code === 0 && Array.isArray(question.answer.answer) && question.answer.answer.length)) {
question.answer = {
code: -1,
answer: ["该题型不支持AI答题"]
};
}
}
this.questionStore.currentQuestionIndex += 1;
} else
this.log.insertLog(
"没有解析到题目,请检查是否进入正确页面...",
"warning"
);
setTimeout(() => {
autoSetTab(_that.setting, "1");
}, 2e3);
reportAnswerSummary("cx-" + this.type, this.questions);
if (this.questions.length === 0) return Promise.resolve("0");
return Promise.resolve(
(this.correctNum / this.questions.length * 100).toFixed(1)
);
});
__publicField(this, "parseHtml", () => {
this.questionStore.currentQuestionIndex = 0;
if (!this._document) return [];
if (["zj"].includes(this.type)) {
const questionElements = this._document.querySelectorAll(".TiMu");
this.addQuestions(questionElements);
} else if (["zy", "ks"].includes(this.type)) {
const questionElements = this._document.querySelectorAll(".questionLi");
this.addQuestions(questionElements);
}
this.questionStore.questionList = this.questions;
});
__publicField(this, "getChoiceOptionContainer", (optionElement) => {
if (!optionElement) return null;
if (["ks"].includes(this.type)) {
return optionElement.closest(".answerBg") || optionElement;
}
return optionElement;
});
__publicField(this, "getCxQuestionTypeFromCode", (code) => {
if (Number.isNaN(code)) return "";
if (code === 0) return "0";
if (code === 1) return "1";
if (code === 3) return "3";
if ([2, 4, 5, 6, 7, 8, 9, 10].includes(code)) return "2";
if (code === 11) return "11";
if (code === 14) return "14";
if (code === 15) return "15";
return "";
});
__publicField(this, "getCxQuestionTypeInput", (element) => {
if (!element) return null;
if (["ks"].includes(this.type)) {
return element.querySelector('input[name^="type"]');
}
return element.querySelector('input[id^="answertype"]');
});
__publicField(this, "resolveCxQuestionType", (element, questionTypeText) => {
const typeInput = this.getCxQuestionTypeInput(element);
const typeFromCode = this.getCxQuestionTypeFromCode(
Number.parseInt((typeInput == null ? void 0 : typeInput.value) || "", 10)
);
if ((!typeFromCode || ["0", "1"].includes(typeFromCode)) &&
(element.querySelector('input[type="checkbox"], [role="checkbox"]') || /多选|多项选择|不定项|X型/i.test(questionTypeText))) return "1";
if (typeFromCode) return typeFromCode;
return this.resolveQuestionType(questionTypeText);
});
__publicField(this, "getChoiceOptionInputs", (optionContainer) => {
var _a;
const inputs = (_a = optionContainer.querySelectorAll) == null ? void 0 : _a.call(optionContainer, 'input[type="radio"], input[type="checkbox"]');
return inputs ? [...inputs] : [];
});
// 超星自己的脚本用 jQuery attr('checked','checked') 写选中态,回读也只认属性;
// 浏览器在点击后不保证属性与 property 双向同步,所以两者任一为真即视为已选(与 ocsjs 口径一致)。
__publicField(this, "isChoiceInputChecked", (input) => {
var _a;
if (input.checked === true) return true;
return Boolean(((_a = input.getAttribute) == null ? void 0 : _a.call(input, "checked")) != null);
});
__publicField(this, "getChoiceOptionSig", (optionContainer) => {
var _a;
const inputs = this.getChoiceOptionInputs(optionContainer);
return [
String(optionContainer.className || ""),
((_a = optionContainer.getAttribute) == null ? void 0 : _a.call(optionContainer, "aria-checked")) || "",
inputs.map((input) => this.isChoiceInputChecked(input) ? 1 : 0).join("")
].join("|");
});
__publicField(this, "isChoiceOptionSelected", (optionElement) => {
var _a;
const optionContainer = this.getChoiceOptionContainer(optionElement);
if (!optionContainer) return false;
// 原生 radio/checkbox 的 checked(property 或属性)与 aria-checked 是通用信号,先读它们:
// 认不出已选会导致对已选项目再点一次,反而把它取消掉
const inputs = this.getChoiceOptionInputs(optionContainer);
if (inputs.some((input) => this.isChoiceInputChecked(input))) return true;
if (optionContainer.getAttribute("aria-checked") === "true") return true;
const tokens = String(optionContainer.className || "").toLowerCase().split(/\s+/);
if (tokens.some((t) => t === "is-checked" || t === "ischecked" || t === "checked")) return true;
if (["ks"].includes(this.type)) {
return Boolean(
optionContainer.querySelector(".check_answer, .check_answer_dx")
);
}
return false;
});
__publicField(this, "selectChoiceOption", (optionElement) => {
var _a;
const optionContainer = this.getChoiceOptionContainer(optionElement);
if (!optionContainer) return false;
if (this.isChoiceOptionSelected(optionContainer)) return true;
const tryClick = (el) => {
if (!el) return;
try {
el.click();
} catch (_) {
}
};
// 超星章节/作业常把事件绑在字母链或 input 上,只点外层 li 可能没反应
tryClick(optionContainer);
if (this.isChoiceOptionSelected(optionContainer)) return true;
const inputs = this.getChoiceOptionInputs(optionContainer);
for (const input of inputs) {
tryClick(input);
if (this.isChoiceOptionSelected(optionContainer)) return true;
}
tryClick(optionContainer.querySelector && optionContainer.querySelector(".fl.before, a.before, label, .answer_p, span"));
if (this.isChoiceOptionSelected(optionContainer)) return true;
try {
(_a = optionContainer.dispatchEvent) == null ? void 0 : _a.call(optionContainer, new this._window.MouseEvent("click", { bubbles: true }));
} catch (_) {
}
return this.isChoiceOptionSelected(optionContainer);
});
__publicField(this, "optionTextMatches", (optionKey, answerText) => {
const rawOpt = String(optionKey || "").trim();
const rawAns = String(answerText || "").trim();
if (!rawOpt || !rawAns) return false;
if (rawOpt === rawAns) return true;
const stripLead = (s) => String(s || "").replace(/^[A-Za-z][.、.)):\s]*/, "").trim();
const a = this.clearMark(rawAns);
const b = this.clearMark(rawOpt);
const a2 = this.clearMark(stripLead(rawAns));
const b2 = this.clearMark(stripLead(rawOpt));
if (a && b && a === b) return true;
if (a2 && b2 && a2 === b2) return true;
// 选项/答案一侧带了多余说明时,用包含关系兜底(要求足够长,防短串误撞)
if (a2 && b2 && a2.length >= 4 && b2.length >= 4 && (a2.includes(b2) || b2.includes(a2))) return true;
return false;
});
__publicField(this, "splitAnswer", (answer = "") => {
const normalizedAnswer = String(answer || "").trim();
if (!normalizedAnswer) return [];
for (const separator of this.answerSeparators) {
if (normalizedAnswer.includes(separator)) {
return normalizedAnswer.split(separator).map((item) => item.trim()).filter(Boolean);
}
}
return [normalizedAnswer];
});
__publicField(this, "buildAnswerGroups", (answers = []) => {
if (!Array.isArray(answers)) return [this.splitAnswer(answers)];
if (answers.length === 1) return [this.splitAnswer(answers[0])];
const normalizedAnswers = answers.map((answer) => String(answer || "").trim()).filter(Boolean);
const groups = [normalizedAnswers];
normalizedAnswers.forEach((answer) => {
const splitAnswers = this.splitAnswer(answer);
if (splitAnswers.length > 1) groups.push(splitAnswers);
});
return groups;
});
__publicField(this, "findElementByAttr", (root, selector, attrName, attrValue) => {
return [...root.querySelectorAll(selector)].find(
(element) => element.getAttribute(attrName) === attrValue
);
});
__publicField(this, "resolveBlankAnswers", (answers = [], blankCount = 1) => {
const list = (Array.isArray(answers) ? answers : [answers]).map((a) => String(a == null ? "" : a).trim()).filter(Boolean);
if (list.length >= blankCount) return list.slice(0, blankCount);
const flat = [];
list.forEach((a) => {
a.split(/[,,、;;|]+/).forEach((piece) => {
const trimmed = piece.trim();
if (trimmed) flat.push(trimmed);
});
});
return flat.slice(0, blankCount);
});
__publicField(this, "resolveJudgeAnswer", (rawAnswer) => {
const text = String(rawAnswer == null ? "" : rawAnswer).replace(/\s+/g, "");
if (!text) return "";
const neg = /(错误|不对|非|否|误|错|×|✗|(^|[,,、;;((【])(wrong|false|no|n|f)([,,、;;))】]|$))/i;
const pos = /(正确|对|是|√|✓|(^|[,,、;;((【])(true|right|yes|t|y|ri)([,,、;;))】]|$))/i;
if (neg.test(text)) return "false";
if (pos.test(text)) return "true";
return "";
});
__publicField(this, "fillLineQuestion", (question) => {
const answerGroups = this.buildAnswerGroups(question.answer.answer);
const selectBoxes = question.element.querySelectorAll(
".line_answer_ct .selectBox, .thirdUlList .dept_select"
);
const answerInputs = question.element.querySelectorAll(
".line_answer input[name^='answer']"
);
if (!selectBoxes.length) return false;
for (const answers of answerGroups) {
if (!answers.length) continue;
if (answerInputs.length && answers.length !== selectBoxes.length) continue;
if (!answerInputs.length && answers.length !== selectBoxes.length) continue;
let filledCount = 0;
selectBoxes.forEach((box, index) => {
var _a, _b;
const answer = answers[index];
if (!answer) return;
const optionItem = this.findElementByAttr(box, "li[data]", "data", answer);
const optionLink = optionItem == null ? void 0 : optionItem.querySelector("a");
if (optionLink) {
optionLink.click();
filledCount += 1;
return;
}
const selectOption = this.findElementByAttr(
box,
"option[value]",
"value",
answer
);
if (!selectOption) return;
(_a = box.querySelector("option[selected]")) == null ? void 0 : _a.removeAttribute("selected");
selectOption.setAttribute("selected", "");
box.value = answer;
const selectedText = (_b = box.parentElement) == null ? void 0 : _b.querySelector(".chosen-single span");
if (selectedText) selectedText.textContent = answer;
filledCount += 1;
});
if (filledCount === selectBoxes.length) return true;
}
return false;
});
__publicField(this, "fillCompositeChoiceQuestion", (question, containerSelector) => {
const answerGroups = this.buildAnswerGroups(question.answer.answer);
const containers = question.element.querySelectorAll(containerSelector);
if (!containers.length) return false;
for (const answers of answerGroups) {
if (!answers.length) continue;
let filledCount = 0;
answers.forEach((answer, index) => {
const container = containers[index];
if (!container || !answer) return;
const option = this.findElementByAttr(
container,
"span.saveSingleSelect[data]",
"data",
answer
);
if (!option) return;
option.click();
filledCount += 1;
});
if (filledCount > 0) return true;
}
return false;
});
__publicField(this, "fillQuestion", (question) => {
var _a;
if (!this._window) return false;
let filled = false;
if (question.type === "0" || question.type === "1") {
const optionKeys = Object.keys(question.options);
const targets = new Set();
const answers = question.answer.answer || [];
if (!answers.length) return false;
for (const answer of answers) {
const clearAnswer = this.removeHtml(String(answer));
const text = String(clearAnswer).normalize("NFKC").trim();
let matches = optionKeys.filter(k => k === clearAnswer);
if (!matches.length) {
const letters = text.replace(/[\s,,、;;|]+/g, "").toUpperCase();
const textMatches = optionKeys.filter(k => this.optionTextMatches(k, clearAnswer) || this.optionTextMatches(k, answer));
if (textMatches.length === 1 && letters.length > 1) matches = textMatches;
else if (/^[A-Z]+$/.test(letters)) {
if ([...letters].some(c => c.charCodeAt(0) - 65 >= optionKeys.length)) return false;
matches = [...new Set([...letters].map(c => optionKeys[c.charCodeAt(0) - 65]))];
} else {
matches = textMatches;
if (matches.length > 1) return false;
}
}
if (!matches.length) {
const imgs = collectImgSrcs(answer);
matches = optionKeys.filter(k => imgSrcsMatch(imgs, collectImgSrcs(k).concat(collectImgSrcs(question.options[k]))));
if (matches.length !== 1) return false;
}
matches.forEach(k => targets.add(k));
}
if (!targets.size || (question.type === "0" && targets.size !== 1)) return false;
// 先清理旧答案,再逐项选择;只有最终选中集合完全一致才算填充成功。
for (const key of optionKeys) {
if (!targets.has(key) && this.isChoiceOptionSelected(question.options[key])) {
const container = this.getChoiceOptionContainer(question.options[key]);
try { container.click(); } catch (_) {}
if (this.isChoiceOptionSelected(question.options[key])) {
const input = this.getChoiceOptionInputs(container).find(i => this.isChoiceInputChecked(i));
try { if (input) input.click(); } catch (_) {}
}
}
}
for (const key of targets) this.selectChoiceOption(question.options[key]);
filled = optionKeys.every(key => this.isChoiceOptionSelected(question.options[key]) === targets.has(key));
} else if (question.type === "2") {
const textareaElements = question.element.querySelectorAll("textarea");
if (textareaElements.length === 0) return false;
const blankAnswers = this.resolveBlankAnswers(
question.answer.answer,
textareaElements.length
);
let blankFilled = 0;
textareaElements.forEach((textareaElement, index) => {
const blankAnswer = blankAnswers[index];
if (!blankAnswer) return;
try {
const ueditor = this._window.UE.getEditor(textareaElement.name);
ueditor.setContent(blankAnswer);
blankFilled += 1;
} catch (e) {
try {
textareaElement.value = blankAnswer;
blankFilled += 1;
} catch (_) {
}
}
});
filled = blankFilled > 0 && blankFilled === textareaElements.length;
} else if (question.type === "3") {
const answer = this.resolveJudgeAnswer(question.answer.answer[0]);
if (!answer) {
this.log.insertLog(
`判断题答案无法判定对错,本题不填:${question.answer.answer[0] || "空"}`,
"warning"
);
return false;
}
const trueOrFalse = {
true: "对",
false: "错"
};
for (const key in question.options) {
if (["zj", "zy"].includes(this.type)) {
if ((_a = question.options[key].getAttribute("aria-label")) == null ? void 0 : _a.includes(`${trueOrFalse[answer]}选择`)) {
filled = this.selectChoiceOption(question.options[key]);
break;
}
} else if (["ks"].includes(this.type)) {
const optionElement = question.options[key].querySelector(
`span[data='${answer}']`
);
if (!optionElement) continue;
filled = this.selectChoiceOption(optionElement);
break;
}
}
} else if (question.type === "4" || question.type === "5" || question.type === "6") {
const textareaElement = question.element.querySelector("textarea");
if (!textareaElement) return false;
const longAnswer = this.resolveBlankAnswers(question.answer.answer, 1)[0] || "";
if (!longAnswer) return false;
try {
const ueditor = this._window.UE.getEditor(textareaElement.name);
ueditor.setContent(longAnswer);
filled = true;
} catch (e) {
try {
textareaElement.value = longAnswer;
filled = true;
} catch (_) {
}
}
} else if (question.type === "11") {
filled = this.fillLineQuestion(question);
} else if (question.type === "14") {
filled = this.fillCompositeChoiceQuestion(question, ".filling_answer");
} else if (question.type === "15") {
filled = this.fillCompositeChoiceQuestion(question, ".reading_answer");
} else {
this.log.insertLog(
`异常题型:type:${question.type},请联系客服..`,
"warning"
);
autoSetTab(this.setting, "1");
return false;
}
return filled;
});
__publicField(this, "addQuestions", (questionElements) => {
questionElements.forEach((element) => {
var _a, _b, _c, _d;
let questionTitle = "";
let questionTypeText = "";
let optionElements;
let optionsObject = {};
let optionTexts = [];
if (["zj"].includes(this.type)) {
questionTitle = this.removeHtml(
((_a = element.querySelector(".fontLabel")) == null ? void 0 : _a.innerHTML) || ""
);
questionTypeText = this.removeHtml(
((_b = element.querySelector(".newZy_TItle")) == null ? void 0 : _b.innerHTML) || ""
);
optionElements = element.querySelectorAll('[class*="before-after"]');
[optionsObject, optionTexts] = this.extractOptions(
optionElements,
".fl.after"
);
} else if (["zy", "ks"].includes(this.type)) {
const titleElement = ((_c = element == null ? void 0 : element.querySelector("h3")) == null ? void 0 : _c.innerHTML) || "";
const colorShallowElement = ((_d = element.querySelector(".colorShallow")) == null ? void 0 : _d.outerHTML) || "";
if (["zy"].includes(this.type)) {
questionTypeText = (element == null ? void 0 : element.getAttribute("typename")) || "";
} else if (["ks"].includes(this.type)) {
questionTypeText = this.removeHtml(colorShallowElement).slice(1, 4) || "";
}
questionTitle = this.removeHtml(
colorShallowElement ? titleElement.replace(colorShallowElement, "") : titleElement
);
optionElements = element.querySelectorAll(".answerBg");
[optionsObject, optionTexts] = this.extractOptions(
optionElements,
".answer_p"
);
}
const resolvedQuestionType = this.resolveCxQuestionType(
element,
questionTypeText
);
if (["14", "15"].includes(resolvedQuestionType)) {
[optionsObject, optionTexts] = this.extractCompositeChoiceOptions(
element,
resolvedQuestionType
);
}
if (resolvedQuestionType === "999") {
this.log.insertLog(
`未识别题型:${questionTypeText || "空题型"},请反馈给开发者补充规则..`,
"warning"
);
}
this.questions.push({
element,
type: resolvedQuestionType,
title: this.clean(questionTitle),
optionsText: optionTexts,
options: optionsObject,
answer: { code: -1, answer: [] },
workType: this.type,
refer: this._window.location.href
});
});
});
this.type = type;
if (iframe) {
this._document = iframe.contentDocument;
this._window = iframe.contentWindow;
}
}
extractOptions(optionElements, optionSelector) {
const optionsObject = {};
const optionTexts = [];
optionElements.forEach((optionElement, index) => {
var _a;
let optionTextContent = this.removeHtml(
((_a = optionElement.querySelector(optionSelector)) == null ? void 0 : _a.innerHTML) || ""
);
if (!optionTextContent || !String(optionTextContent).replace(/
]*>/gi, "").replace(/\s+/g, "")) {
const img = optionElement.querySelector && optionElement.querySelector("img");
const src = img ? (img.getAttribute("data-src") || img.currentSrc || img.src || "") : "";
if (src) optionTextContent = `
`;
else if (!optionTextContent) optionTextContent = `选项${String.fromCharCode(65 + index)}`;
}
optionsObject[`${String.fromCharCode(65 + index)}. ${optionTextContent}`] = optionElement;
optionTexts.push(optionTextContent);
});
return [optionsObject, optionTexts];
}
extractCompositeChoiceOptions(element, questionType) {
const optionsObject = {};
const optionTexts = [];
const containerSelector = questionType === "14" ? ".filling_answer" : ".reading_answer";
const optionElements = element.querySelectorAll(
`${containerSelector} span.saveSingleSelect`
);
optionElements.forEach((optionElement) => {
const optionTextContent = this.removeHtml(optionElement.innerHTML || "");
if (!optionTextContent) return;
optionsObject[optionTextContent] = optionElement;
optionTexts.push(optionTextContent);
});
return [optionsObject, optionTexts];
}
};
class IframeUtils {
static getIframes(element) {
return Array.from(element.querySelectorAll("iframe"));
}
static getAllNestedIframes(element) {
const iframes = IframeUtils.getIframes(element);
if (iframes.length === 0) {
return rxjs.of([]);
} else {
return rxjs.from(iframes).pipe(
mergeMap(
(iframe) => new rxjs.Observable((subscriber) => {
if (iframe.contentDocument) {
const iframeDocument = iframe.contentDocument;
const nestedIframes = IframeUtils.getAllNestedIframes(
iframeDocument.documentElement
);
nestedIframes.subscribe((nestedIframes2) => {
subscriber.next([iframe, ...nestedIframes2]);
subscriber.complete();
});
} else {
subscriber.next([]);
subscriber.complete();
}
})
),
concatAll(),
rxjs.toArray()
);
}
}
}
const videoRateStates = new WeakMap();
const hookVedio = (video, playbackRate = 2) => {
const rate = Number(playbackRate);
if (!video || !Number.isFinite(rate) || rate < 0.5 || rate > 16) return false;
let state = videoRateStates.get(video);
if (!state) {
// 从媒体元素自己的原型链读取原生访问器,兼容 iframe 的独立 window。
let proto = Object.getPrototypeOf(video), descriptor;
while (proto && !descriptor) {
descriptor = Object.getOwnPropertyDescriptor(proto, "playbackRate");
proto = Object.getPrototypeOf(proto);
}
if (!descriptor || !descriptor.get || !descriptor.set) return false;
state = { rate, descriptor };
state.apply = () => {
try {
if (descriptor.get.call(video) !== state.rate) descriptor.set.call(video, state.rate);
video.defaultPlaybackRate = state.rate;
} catch (e) { useLogStore().insertLog("设置视频倍速失败:" + e.message); }
};
videoRateStates.set(video, state);
["loadedmetadata", "playing", "ratechange"].forEach(event => video.addEventListener(event, state.apply));
}
state.rate = (video.__xsRateRestricted || video.__xsFinishingAtNormalRate) ? 1 : rate;
try {
Object.defineProperty(video, "playbackRate", {
configurable: true,
get: () => state.descriptor.get.call(video),
set: () => state.apply()
});
} catch (_) {}
state.apply();
return state.descriptor.get.call(video) === state.rate;
};
const applyVideoRateToPage = (rate) => {
if (!Number.isFinite(Number(rate)) || Number(rate) < 0.5 || Number(rate) > 16) return;
const applyOnDoc = (doc) => {
if (!doc) return;
doc.querySelectorAll("video, audio").forEach(el => hookVedio(el, rate));
};
const collectFrames = (doc, depth) => {
if (depth > 3 || !doc) return;
doc.querySelectorAll("iframe").forEach((frame) => {
try {
if (frame.contentDocument) {
applyOnDoc(frame.contentDocument);
collectFrames(frame.contentDocument, depth + 1);
}
} catch (e) {
}
});
};
applyOnDoc(document);
collectFrames(document, 0);
};
const refreshMediaSamples = new WeakMap();
const canRefreshStudyPage = (doc, seen = new Set()) => {
if (!doc || doc.hidden || doc.visibilityState === "hidden" || seen.has(doc)) return false;
seen.add(doc);
if (doc.querySelector(".TiMu, .questionLi, textarea, [contenteditable='true']")) return false;
let allowed = true;
const media = [...doc.querySelectorAll("video, audio")];
for (const el of media) {
const now = Date.now(), position = Number(el.currentTime), source = el.currentSrc || el.src;
const previous = refreshMediaSamples.get(el);
if (!Number.isFinite(position) || !previous || previous.position !== position || previous.source !== source) {
refreshMediaSamples.set(el, {position, source, since: now});
allowed = false;
} else if (now - previous.since < 1800000) allowed = false;
}
const frames = [...doc.querySelectorAll("iframe")];
if (!media.length && !frames.length && doc.querySelector(".ans-job:not(.ans-job-finished)")) return false;
for (const frame of frames) {
try { if (!frame.contentDocument || !canRefreshStudyPage(frame.contentDocument, seen)) allowed = false; }
catch (_) { allowed = false; }
}
return allowed;
};
const watchMediaRecovery = (media, { recover, checkComplete, onError, documents = [] }) => {
let stopped = false, running = false;
const listeners = [];
const check = async () => {
if (stopped || running) return;
running = true;
try {
if (checkComplete()) return;
if (!media.ended && media.paused) await recover();
} catch (e) { if (onError) onError(e); }
finally { running = false; }
};
const listen = (target, event) => {
if (!target || !target.addEventListener) return;
target.addEventListener(event, check);
listeners.push([target, event]);
};
for (const doc of new Set(documents)) {
["visibilitychange", "resume"].forEach(event => listen(doc, event));
["pageshow", "online", "focus"].forEach(event => listen(doc.defaultView, event));
}
["pause", "canplay", "ended"].forEach(event => listen(media, event));
// 定时器是兜底;页面恢复时直接复核,不能依赖后台定时器准点执行。
const timer = setInterval(check, 15000);
return () => {
stopped = true;
clearInterval(timer);
listeners.forEach(([target, event]) => target.removeEventListener(event, check));
};
};
const FACE_CHECK_INTERVAL_SECONDS = 3;
const FACE_RECOGNITION_TEXTS = [
"人脸信息采集",
"采集人脸信息",
"请使用手机APP采集人脸信息后继续学习",
"请扫描下方二维码进入"
];
const GENERIC_FACE_RECOGNITION_TEXTS = [
"人脸识别",
"继续学习"
];
let activeWaitPromise = null;
const getTopDocument = () => {
var _a, _b;
try {
return ((_a = window.top) == null ? void 0 : _a.document) || document;
} catch {
try {
if (typeof unsafeWindow !== "undefined") {
return ((_b = unsafeWindow.top) == null ? void 0 : _b.document) || document;
}
} catch {
return document;
}
}
return document;
};
const isVisible$1 = (element) => {
var _a;
if (!element) return false;
const style = (_a = element.ownerDocument.defaultView) == null ? void 0 : _a.getComputedStyle(element);
if (style && (style.display === "none" || style.visibility === "hidden")) {
return false;
}
return element.offsetParent !== null || element.getClientRects().length > 0;
};
const getSearchDocuments = (extraDocuments = []) => {
const docs = [];
const queue = [getTopDocument(), document, ...extraDocuments].filter(Boolean);
const seen = /* @__PURE__ */ new Set();
while (queue.length) {
const doc = queue.shift();
if (!doc || seen.has(doc)) continue;
seen.add(doc);
docs.push(doc);
try {
Array.from(doc.querySelectorAll("iframe")).forEach((iframe) => {
if (iframe.contentDocument) queue.push(iframe.contentDocument);
});
} catch {
}
}
return docs;
};
const hasLegacyFaceRecognition = (doc) => {
return Array.from(doc.querySelectorAll("#fcqrimg")).some((element) => {
return Boolean(element.getAttribute("src")) && isVisible$1(element);
});
};
const hasNewFaceRecognition = (doc) => {
return Array.from(doc.querySelectorAll(".chapterVideoFaceMaskDiv")).some(isVisible$1);
};
const hasFaceRecognitionText = (doc) => {
var _a, _b;
const bodyText = ((_a = doc.body) == null ? void 0 : _a.innerText) || ((_b = doc.body) == null ? void 0 : _b.textContent) || "";
if (FACE_RECOGNITION_TEXTS.some((keyword) => bodyText.includes(keyword))) {
return true;
}
const visibleElements = Array.from(
doc.querySelectorAll("div, section, article, body")
).filter(isVisible$1);
return visibleElements.some((element) => {
const text = element.innerText || element.textContent || "";
const hasFaceText = GENERIC_FACE_RECOGNITION_TEXTS.some(
(keyword) => text.includes(keyword)
);
const hasQRCode = Boolean(element.querySelector("canvas, img, svg"));
return hasFaceText && (hasQRCode || text.includes("人脸"));
});
};
const hasCxFaceRecognition = (extraDocuments = []) => {
return getSearchDocuments(extraDocuments).some((doc) => {
return hasLegacyFaceRecognition(doc) || hasNewFaceRecognition(doc) || hasFaceRecognitionText(doc);
});
};
const waitForCxFaceRecognition = async ({
log,
setting,
messageStore,
extraDocuments = []
} = {}) => {
if (!hasCxFaceRecognition(extraDocuments)) return;
if (activeWaitPromise) return activeWaitPromise;
activeWaitPromise = (async () => {
var _a, _b, _c;
const message2 = "检测到人脸识别,请手动完成后脚本会继续运行。";
log == null ? void 0 : log.insertLog(message2, "warning", 4);
if ((_c = (_b = (_a = setting == null ? void 0 : setting.config) == null ? void 0 : _a.cxConfig) == null ? void 0 : _b.faceRecognitionNotify) == null ? void 0 : _c.value) {
messageStore == null ? void 0 : messageStore.insertMessage(message2, "warning");
}
while (hasCxFaceRecognition(extraDocuments)) {
await sleep(FACE_CHECK_INTERVAL_SECONDS);
}
log == null ? void 0 : log.insertLog("人脸识别已完成,继续处理当前任务。");
})();
try {
return await activeWaitPromise;
} finally {
activeWaitPromise = null;
}
};
const TASK_RESULT = {
finished: "finished",
unconfirmed: "unconfirmed",
skipped: "skipped"
};
const BOOK_READ_EXTRA_SECONDS = 8;
const BOOK_READ_MAX_ROUNDS = 4;
const jobContainerOf = (target) => {
if (!target) return null;
if (target.tagName === "IFRAME") return target.closest(".ans-job") || target.parentElement;
return target;
};
const JOB_CONFIRM_TIMEOUT = 8;
const PLAYBACK_WATCHDOG_INTERVAL = 5;
const PLAY_FAIL_HINT_THRESHOLD = 3;
const parseJobReport = (responseText) => {
try {
const data = JSON.parse(responseText);
return (data == null ? void 0 : data.status) === true || (data == null ? void 0 : data.status) === "true";
} catch (error) {
return String(responseText || "").includes("success");
}
};
const watchJobReport = (iframeWindow) => {
var _a;
const prototype = (_a = iframeWindow == null ? void 0 : iframeWindow.XMLHttpRequest) == null ? void 0 : _a.prototype;
if (!prototype) {
return { result: Promise.resolve(false), restore: () => {} };
}
const originalOpen = prototype.open;
const originalSend = prototype.send;
let reportSettled = null;
const result = new Promise((resolve) => {
reportSettled = resolve;
});
prototype.open = function(method, url2, ...rest) {
this.__xsJobRequestUrl = typeof url2 === "string" ? url2 : "";
return originalOpen.call(this, method, url2, ...rest);
};
prototype.send = function(...args) {
if (String(this.__xsJobRequestUrl || "").includes("/ananas/job")) {
this.addEventListener("load", () => {
try { reportSettled(parseJobReport(this.responseText)); } catch (_) { reportSettled(false); }
});
this.addEventListener("error", () => { try { reportSettled(false); } catch (_) {} });
}
return originalSend.apply(this, args);
};
return {
result,
restore: () => {
prototype.open = originalOpen;
prototype.send = originalSend;
}
};
};
const waitJobIconFinished = async (target, timeoutSec) => {
const taskContainer = jobContainerOf(target);
if (!taskContainer) return false;
const deadline = Date.now() + timeoutSec * 1e3;
while (Date.now() < deadline) {
if (taskContainer.classList.contains("ans-job-finished")) return true;
await sleep(0.5);
}
return taskContainer.classList.contains("ans-job-finished");
};
const confirmJobFinished = (target, reportResult, timeoutSec = JOB_CONFIRM_TIMEOUT) => {
return new Promise((resolve) => {
const taskContainer = jobContainerOf(target);
const signals = [reportResult, waitJobIconFinished(taskContainer, timeoutSec)];
let pending = signals.length;
let settled = false;
const finish = (value) => {
if (settled) return;
settled = true;
resolve(value);
};
signals.forEach((signal) => {
Promise.resolve(signal).then((confirmed) => {
if (confirmed) finish(true);
else if (--pending === 0) finish(false);
}).catch(() => {
if (--pending === 0) finish(false);
});
});
sleep(timeoutSec).then(() => finish(false));
});
};
const cxChapterLogic = () => {
const log = useLogStore();
const init = () => {
const currentUrl = window.location.href;
if (!currentUrl.includes("&mooc2=1")) {
window.location.href = currentUrl + "&mooc2=1";
}
};
log.insertLog("进入学习页面,脚本开始工作中..");
if (document.wasDiscarded) log.insertLog("检测到浏览器回收了后台页面;请将学习网站设为始终保持活动/不休眠,进度以平台记录为准", "warning");
const setting = useSettingStore();
const questionStore = useQuestionStore();
const messageStore = useMessageStore();
const setupFaceRecognitionWatcher = () => {
setInterval(async () => {
await waitForCxFaceRecognition({
log,
setting,
messageStore
});
}, 3e3);
};
const processIframeTask = async () => {
var _a;
const documentElement = document.documentElement;
const iframe = documentElement.querySelector("iframe");
if (!iframe) {
console.warn("No iframe found.");
return;
}
if (setting.config.basicConfig.autoChangeChapter.value && ((_a = setting.config.basicConfig.skipFinishedChapter) == null ? void 0 : _a.value) && isCurrentChapterFinishedByCatalog() && await changeToNextUnfinishedChapter()) {
return;
}
watchIframe(documentElement);
iframe.addEventListener("load", function() {
watchIframe(documentElement);
});
};
const waitIframeLoad = async (iframe) => {
return new Promise((resolve) => {
const intervalId = setInterval(async () => {
var _a;
if (iframe.contentDocument && ((_a = iframe.contentDocument) == null ? void 0 : _a.readyState) == "complete") {
resolve();
clearInterval(intervalId);
}
}, 500);
});
};
const hideCxWorkConfirmPopup = () => {
var _a, _b, _c;
try {
const topWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow.top : window.top;
const topDocument = (topWindow == null ? void 0 : topWindow.document) || document;
(_c = (_b = (_a = topWindow == null ? void 0 : topWindow.$) == null ? void 0 : _a.call(topWindow, "#workpop")) == null ? void 0 : _b.hide) == null ? void 0 : _c.call(_b);
const workPopup = topDocument.querySelector("#workpop");
if (workPopup) {
workPopup.style.display = "none";
}
} catch (error) {
console.warn("hide work popup failed.", error);
}
};
const processWork = async (iframe, iframeDocument, iframeWindow) => {
var _a;
const workName = ((_a = iframeDocument.documentElement.querySelector(
"#RightCon > div.radiusBG > div > div.ceyan_name>h3"
)) == null ? void 0 : _a.innerText) || "未知测验";
log.insertLog(`发现测验:${workName},正在解析..`);
{
if (!iframeDocument) return;
if (iframeDocument.documentElement.innerText.includes("已完成") || iframeDocument.documentElement.innerText.includes("待批阅")) {
log.insertLog("测验已经完成,跳转中..");
return;
}
decrypt(iframeDocument);
if (!setting.config.basicConfig.autoAnswer.value) {
log.insertLog(
`自动答题已关闭,请前往设置模块中更改`
);
return;
}
questionStore.accuracy = -1;
const accuracy = await new QuestionHandler$1("zj", iframe).init();
questionStore.accuracy = accuracy;
iframeWindow.alert = () => {
};
if (setting.config.basicConfig.autoSubmit.value && setting.config.basicConfig.autoAnswer.value && hasPendingManualPick()) {
log.insertLog("还有题目等待手动选择,已暂停自动提交,请先去「题目结果」选择", "warning", 8);
await iframeWindow.noSubmit();
} else if (setting.config.basicConfig.autoSubmit.value && setting.config.basicConfig.autoAnswer.value) {
log.insertLog("自动提交已开启,提交中...");
if (accuracy < Number(setting.config.basicConfig.accuracy.value)) {
log.insertLog(
`正确率小于${setting.config.basicConfig.accuracy.value}%,暂存,如需调整正确率请前往设置模块中更改`,
"warning",
4
);
await iframeWindow.noSubmit();
} else {
log.insertLog(
`正确率大于${setting.config.basicConfig.accuracy.value}%,提交中...`
);
await iframeWindow.btnBlueSubmit();
await sleep(setting.config.basicConfig.reqIntervalTime.value / 2);
if (!setting.config.basicConfig.autoSubmit.value || !setting.config.basicConfig.autoAnswer.value) return;
await iframeWindow.submitCheckTimes();
hideCxWorkConfirmPopup();
log.insertLog("提交成功");
}
} else {
log.insertLog(
"自动提交未开启,已暂存,可前往设置模块中更改",
"warning",
4
);
await iframeWindow.noSubmit();
}
return;
}
};
const setupInterceptor = () => {
let currentUrl = window.location.href;
setInterval(() => {
if (currentUrl !== window.location.href) {
currentUrl = window.location.href;
processIframeTask();
}
}, 2e3);
};
const getTopLearningContext = () => {
let topWindow = window, topDocument = document;
try {
topWindow = (typeof unsafeWindow !== "undefined" ? unsafeWindow.top : window.top) || window;
topDocument = topWindow.document || document;
} catch (_) {
topWindow = window;
topDocument = document;
}
return { topWindow, topDocument };
};
const getChapterIdFromTeacherAjax = (element) => {
var _a, _b;
return ((_b = (_a = element == null ? void 0 : element.getAttribute("onclick")) == null ? void 0 : _a.match(/\('(.*)','(.*)','(.*)'\)/)) == null ? void 0 : _b[3]) || "";
};
const getChapterName = (chapterInfo) => {
var _a, _b, _c, _d, _e, _f, _g;
return ((_d = (_c = (_b = (_a = chapterInfo == null ? void 0 : chapterInfo.element) == null ? void 0 : _a.parentElement) == null ? void 0 : _b.querySelector(".posCatalog_name")) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) || ((_g = (_f = (_e = chapterInfo == null ? void 0 : chapterInfo.element) == null ? void 0 : _e.parentElement) == null ? void 0 : _f.textContent) == null ? void 0 : _g.trim()) || "未知章节";
};
const parseUnfinishCount = (container) => {
const element = container == null ? void 0 : container.querySelector(".jobUnfinishCount");
if (!element) return null;
const count = Number.parseInt(String(element.value ?? "").trim(), 10);
return Number.isFinite(count) ? count : null;
};
const resolveChapterFinished = (container) => {
if (!container) return null;
if (container.querySelector(".icon_Completed")) return true;
const count = parseUnfinishCount(container);
return count === null ? null : count === 0;
};
const isChapterFinished = (chapter) => chapter.finished === true;
const isChapterPending = (chapter) => chapter.finished === false;
const isCurrentChapterFinishedByCatalog = () => {
const { topDocument } = getTopLearningContext();
const activeChapter = topDocument.querySelector(".posCatalog_active");
if (!activeChapter) return false;
return resolveChapterFinished(activeChapter) === true;
};
const isInFinalTab = () => {
const { topDocument } = getTopLearningContext();
const tabs = Array.from(topDocument.querySelectorAll("#prev_tab .prev_ul li"));
if (tabs.length === 0) return true;
return tabs[tabs.length - 1].classList.contains("active");
};
const isInFinalChapter = () => {
const { topDocument } = getTopLearningContext();
const chapterNodes = Array.from(topDocument.querySelectorAll(".posCatalog_select:not(.firstLayer)"));
if (chapterNodes.length === 0) return false;
return chapterNodes[chapterNodes.length - 1].classList.contains("posCatalog_active");
};
const hasLockedChapters = () => {
const { topDocument } = getTopLearningContext();
return topDocument.querySelectorAll(".catalog_points_sa, .catalog_points_er").length > 0;
};
const chapterEnterCounter = /* @__PURE__ */ new Map();
const REPEAT_ENTER_MAX = 3;
const isStuckOnSameChapter = () => {
var _a;
if (!hasLockedChapters()) return false;
const { topDocument } = getTopLearningContext();
const chapterId = (_a = topDocument.querySelector(".posCatalog_active")) == null ? void 0 : _a.getAttribute("id");
if (!chapterId) return false;
const count = (chapterEnterCounter.get(chapterId) || 0) + 1;
if (count >= REPEAT_ENTER_MAX) {
chapterEnterCounter.set(chapterId, 0);
return true;
}
chapterEnterCounter.set(chapterId, count);
return false;
};
const getChapterInfos = (topDocument) => {
return Array.from(topDocument.querySelectorAll('[onclick^="getTeacherAjax"]')).map((element) => ({
element,
chapterId: getChapterIdFromTeacherAjax(element),
finished: resolveChapterFinished(element.parentElement)
})).filter((chapter) => chapter.chapterId);
};
const waitForChapterInfos = async (timeout = 10) => {
const { topDocument } = getTopLearningContext();
const deadline = Date.now() + timeout * 1e3;
let lastCount = -1;
while (Date.now() < deadline) {
const chapters = getChapterInfos(topDocument);
if (chapters.length > 0 && chapters.length === lastCount) {
return { chapters, stable: true };
}
lastCount = chapters.length;
await sleep(1);
}
return { chapters: getChapterInfos(topDocument), stable: false };
};
const findNextUnfinishedChapter = async () => {
var _a;
const { topDocument } = getTopLearningContext();
const { chapters, stable } = await waitForChapterInfos();
if (!chapters.length) return { hasCatalog: false, chapter: null, allFinished: false };
const currentChapterId = ((_a = topDocument.querySelector("#curChapterId")) == null ? void 0 : _a.value) || "";
const currentIndex = chapters.findIndex((chapter) => chapter.chapterId === currentChapterId);
const orderedChapters = currentIndex >= 0 ? [...chapters.slice(currentIndex + 1), ...chapters.slice(0, currentIndex)] : chapters;
return {
hasCatalog: true,
chapter: orderedChapters.find(isChapterPending) || null,
allFinished: stable && !hasLockedChapters() && chapters.every(isChapterFinished)
};
};
const jumpToChapter = async (chapterInfo) => {
var _a, _b, _c;
if (!chapterInfo) return false;
const { topWindow, topDocument } = getTopLearningContext();
const chapterId = chapterInfo.chapterId;
const courseId = (_a = topDocument.querySelector("#curCourseId")) == null ? void 0 : _a.value;
const classId = (_b = topDocument.querySelector("#curClazzId")) == null ? void 0 : _b.value;
log.insertLog(`跳转到未完成章节:${getChapterName(chapterInfo)}`);
if ((topWindow == null ? void 0 : topWindow.getTeacherAjax) && courseId && classId && chapterId) {
topWindow.getTeacherAjax(courseId, classId, chapterId);
} else {
chapterInfo.element.click();
}
await sleep(1);
(_c = topDocument.querySelector(".posCatalog_active")) == null ? void 0 : _c.scrollIntoView({ behavior: "smooth", block: "center" });
return true;
};
const changeToNextUnfinishedChapter = async () => {
var _a;
if (!((_a = setting.config.basicConfig.skipFinishedChapter) == null ? void 0 : _a.value)) return false;
const { hasCatalog, chapter, allFinished } = await findNextUnfinishedChapter();
if (!hasCatalog) {
log.insertLog("未读取到章节任务点数据,已改用普通章节切换", "warning");
return false;
}
if (allFinished) {
log.insertLog("全部章节任务点已完成");
return true;
}
if (!chapter) {
if (hasLockedChapters()) {
log.insertLog("后续章节尚未解锁,按顺序推进中");
} else {
log.insertLog("部分章节完成状态读取不到,已改用普通章节切换", "warning");
}
return false;
}
return jumpToChapter(chapter);
};
const changeToNextChapter = async (documentElement, isCurrent = () => true) => {
const enabled = () => setting.config.basicConfig.autoChangeChapter.value && isCurrent();
if (!enabled()) return false;
if (hasPendingManualPick()) {
log.insertLog("还有题目等待手动选择,已暂停自动切换章节;选完后继续", "warning", 8);
return false;
}
if (isInFinalTab() && isStuckOnSameChapter()) {
log.insertLog("已多次重复进入当前章节,可能存在需要手动完成的章节测试,请手动完成后再继续。", "warning", 4);
return false;
}
if (isInFinalTab() && isCurrentChapterFinishedByCatalog() && await changeToNextUnfinishedChapter()) {
return true;
}
if (isInFinalTab() && isInFinalChapter()) {
const { hasCatalog, allFinished } = await findNextUnfinishedChapter();
if (hasCatalog) {
if (allFinished) {
log.insertLog("全部章节任务点已完成");
} else if (hasLockedChapters()) {
log.insertLog("仍有未解锁的章节,请完成当前章节的任务点后继续", "warning", 4);
} else {
log.insertLog("已到最后一章,但仍有章节未完成,请手动切换到未完成的章节", "warning", 4);
}
return true;
}
}
await sleep(setting.config.basicConfig.reqIntervalTime.value);
if (!enabled()) return false;
const { topWindow, topDocument } = getTopLearningContext();
const course = topDocument.querySelector("#curCourseId"), chapter = topDocument.querySelector("#curChapterId"), clazz = topDocument.querySelector("#curClazzId");
if (topWindow.PCount && typeof topWindow.PCount.next === "function" && course && chapter && clazz) {
try {
if (!enabled()) return false;
topWindow._preChapterId = chapter.value;
try {
try {
const active = topDocument.querySelector(".posCatalog_active");
if (active) active.scrollIntoView({ behavior: "smooth", block: "center" });
} catch (_) {}
} catch (_) {}
await sleep(0.2);
topWindow.PCount.next(String(topDocument.querySelectorAll("#prev_tab .prev_ul li").length), chapter.value, course.value, clazz.value, "");
return true;
} catch (error) { console.warn("PCount.next failed", error); }
}
for (const root of new Set([topDocument, documentElement, document])) {
for (const button of root.querySelectorAll(".nextChapter, #prevNextFocusNext")) {
if (button.disabled || button.getAttribute("aria-disabled") === "true" || button.style.display === "none") continue;
if (!enabled()) return false;
button.click(); return true;
}
}
// 普通下一章找不到时,再尝试目录跳未完成章
if (await changeToNextUnfinishedChapter()) return true;
log.insertLog("未找到可用下一章入口,请检查是否已到最后一章或章节尚未解锁", "warning", 4);
return false;
};
let currentWatchIframeTaskId = 0;
let pendingChapterAdvance = null, advancingChapter = false;
const advancePendingChapter = async () => {
if (advancingChapter || !pendingChapterAdvance || !setting.config.basicConfig.autoChangeChapter.value) return;
const pending = pendingChapterAdvance;
advancingChapter = true;
try {
if (await changeToNextChapter(pending.root, () => pending.id === currentWatchIframeTaskId)) {
if (pendingChapterAdvance === pending) pendingChapterAdvance = null;
}
} finally { advancingChapter = false; }
};
vue.watch(() => setting.config.basicConfig.autoChangeChapter.value, enabled => { if (enabled) advancePendingChapter(); });
// 手动选择完成后,若之前因等待选择而搁置了切章,这里自动补上
let manualPickWatchTimer = null;
const watchPendingManualPick = () => {
if (manualPickWatchTimer) return;
manualPickWatchTimer = setInterval(() => {
if (advancingChapter) return;
if (hasPendingManualPick()) return;
if (!pendingChapterAdvance) return;
if (!setting.config.basicConfig.autoChangeChapter.value) return;
advancePendingChapter();
}, 2000);
};
watchPendingManualPick();
let _afterPickBusy = false;
const afterManualPickResolved = async (questionNo) => {
if (_afterPickBusy || hasPendingManualPick()) return;
_afterPickBusy = true;
try {
if (pendingChapterAdvance && setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(`第${questionNo}题已手动选择,继续切换章节`);
await advancePendingChapter();
}
} finally { _afterPickBusy = false; }
};
onManualPickResolved(afterManualPickResolved);
const watchIframe = (documentElement) => {
const thisTaskId = ++currentWatchIframeTaskId;
pendingChapterAdvance = null;
IframeUtils.getAllNestedIframes(documentElement).subscribe((allIframes) => {
const taskResults = [];
rxjs.from(allIframes).pipe(
concatMap((iframe) => processIframe(iframe).catch((error) => {
console.warn("[cx] 任务点处理异常。", error);
log.insertLog("当前任务点处理异常", "warning");
return TASK_RESULT.unconfirmed;
}))
).subscribe({
next: (result) => taskResults.push(result || TASK_RESULT.skipped),
error: (error) => {
console.warn("[cx] 任务链异常终止。", error);
log.insertLog("任务链处理异常,请尝试刷新页面", "warning", 4);
},
complete: async () => {
var _a, _b;
const chapterName = ((_a = documentElement.querySelector(
".posCatalog_select.posCatalog_active>.posCatalog_name"
)) == null ? void 0 : _a.innerText) || "未知章节";
const currentTaskName = ((_b = document.querySelector(".prev_ul > li.active .spanText")) == null ? void 0 : _b.textContent.trim()) || "";
if (thisTaskId !== currentWatchIframeTaskId) return;
if (taskResults.includes(TASK_RESULT.unconfirmed)) {
log.insertLog(
`任务点 ${chapterName}-${currentTaskName} 未确认完成,已停止自动跳转,请手动确认后继续`,
"warning",
4
);
return;
}
log.insertLog(`任务点 ${chapterName}-${currentTaskName} 已处理完毕`);
pendingChapterAdvance = { root: documentElement, id: thisTaskId };
if (setting.config.basicConfig.autoChangeChapter.value) {
await advancePendingChapter();
} else {
log.insertLog(
`自动切换章节未开启,前往设置模块中更改`,
"warning",
4
);
}
}
});
});
};
const logDocumentDiagnostics = (iframeWindow) => {
var _a;
try {
const moduleDocument = iframeWindow.document;
console.info("[cx] 文档模块结构未识别", {
url: (_a = iframeWindow.location) == null ? void 0 : _a.href,
innerIframes: Array.from(moduleDocument.querySelectorAll("iframe")).map((element) => element.src),
scrollable: Array.from(moduleDocument.querySelectorAll("*")).filter((element) => element.scrollHeight > element.clientHeight + 5).slice(0, 10).map((element) => `${element.tagName}#${element.id}.${element.className}`)
});
} catch (error) {
console.warn("[cx] 收集文档模块结构失败。", error);
}
};
const readSlides = async (iframeWindow) => {
const moduleDocument = iframeWindow.document;
moduleDocument.querySelectorAll("audio").forEach((audio) => {
audio.muted = true;
audio.addEventListener("play", () => { audio.muted = true; });
});
if (typeof iframeWindow.swiperNext !== "function") return false;
const slideCount = moduleDocument.querySelectorAll(".swiper-container .swiper-slide").length;
for (let index = 0; index < slideCount; index += 1) {
iframeWindow.swiperNext();
await sleep(1);
}
return true;
};
const runDocumentTask = async (iframeWindow) => {
const moduleDocument = iframeWindow.document;
const hasFinishEntry = typeof iframeWindow.finishJob === "function";
const isImageViewer = Boolean(moduleDocument.querySelector("#img.imglook"));
const hasSlides = Boolean(moduleDocument.querySelector(".swiper-container"));
const panView = moduleDocument.querySelector("#panView");
if (hasFinishEntry && (isImageViewer || !hasSlides)) {
iframeWindow.finishJob();
return true;
}
if (hasSlides) return readSlides(iframeWindow);
// 兼容旧版 PPT:滚动到底
if (panView && panView.contentWindow) {
try {
const pptWindow = panView.contentWindow;
const waitReady = () => new Promise((resolve) => {
const tick = () => {
if (pptWindow.document && pptWindow.document.readyState === "complete") resolve();
else setTimeout(tick, 100);
};
tick();
});
await waitReady();
pptWindow.scrollTo({ top: pptWindow.document.body.scrollHeight, behavior: "smooth" });
await sleep(1);
return true;
} catch (_) {}
}
return false;
};
const processDocument = async (iframeWindow, iframe) => {
log.insertLog("发现一个文档任务点,正在处理...");
const reporter = watchJobReport(iframeWindow);
try {
const started = await runDocumentTask(iframeWindow);
if (!started) {
logDocumentDiagnostics(iframeWindow);
log.insertLog("该文档任务点缺少可用的完成入口,请手动完成后继续", "warning", 4);
return TASK_RESULT.unconfirmed;
}
const confirmed = await confirmJobFinished(iframe, reporter.result);
if (!confirmed) {
log.insertLog("文档任务点未确认完成,请手动确认", "warning", 4);
return TASK_RESULT.unconfirmed;
}
log.insertLog("文档任务点已完成");
return TASK_RESULT.finished;
} finally {
reporter.restore();
}
};
const getBookRequiredSeconds = (iframeWindow) => {
var _a;
const readerSrc = ((_a = iframeWindow.document.querySelector('iframe[name="bookifame"]')) == null ? void 0 : _a.getAttribute("src")) || "";
if (!readerSrc.includes("timing")) return 0;
try {
const timing = new URL(readerSrc, iframeWindow.location.href).searchParams.get("timing");
const seconds = Number.parseInt(timing || "0", 10);
return Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
} catch (error) {
console.warn("[cx] 解析阅读时长失败。", error);
return 0;
}
};
const keepReading = async (requiredSeconds, iframe, reportResult) => {
for (let round = 1; round <= BOOK_READ_MAX_ROUNDS; round += 1) {
const confirmed = await confirmJobFinished(iframe, reportResult, requiredSeconds + BOOK_READ_EXTRA_SECONDS);
if (confirmed) return true;
if (round < BOOK_READ_MAX_ROUNDS) {
log.insertLog(`阅读时长尚未达标,继续保持第 ${round + 1} 轮阅读`);
}
}
return false;
};
const setupVideoQuizHandler = (iframeDocument) => {
const intervalId = setInterval(() => {
const quizContainer = iframeDocument.querySelector(".ans-timelineobjects");
if (!quizContainer || quizContainer.offsetParent === null) return;
const submitBtn = iframeDocument.querySelector("#videoquiz-submit");
if (!submitBtn || submitBtn.style.display === "none" || submitBtn.offsetParent === null) return;
quizContainer.style.display = "none";
const quizBg = iframeDocument.querySelector(".ans-timelineobjectsbg");
if (quizBg) quizBg.style.display = "none";
log.insertLog("检测到视频弹题,已跳过。");
const video = iframeDocument.querySelector("video");
if (video) video.play();
}, 2e3);
return intervalId;
};
const isVideoPlaybackRateDisabled = (iframeDocument) => {
const control = iframeDocument.querySelector(".vjs-playback-rate");
// 菜单可能尚未渲染,缺菜单不等于平台明确禁止倍速。
return !!(control && (control.getAttribute("aria-disabled") === "true" || control.classList.contains("vjs-disabled")));
};
const processMedia = async (mediaType, iframeDocument, iframe) => {
return new Promise((resolve) => {
const taskContainer = iframe && (iframe.closest(".ans-job") || iframe.parentElement);
const iframeWindow = iframe && iframe.contentWindow;
const userRate = setting.config.basicConfig.videoPlayrate.value;
const isRateDisabled = mediaType === "video" && isVideoPlaybackRateDisabled(iframeDocument);
const finalRate = isRateDisabled ? 1 : userRate;
const MEDIA_CONFIRM_RELOAD_KEY = "xs-cx-media-confirm-reload";
const mediaJobKey = (() => {
try {
const chapter = (document.querySelector("#curChapterId") || {}).value || "";
const jobId = (taskContainer && (taskContainer.id || taskContainer.getAttribute("data") || "")) || "";
const src = (iframe && iframe.src) || "";
return `${location.pathname}|${chapter}|${jobId}|${src}`.slice(0, 240);
} catch (_) {
return `${mediaType}|${Date.now()}`;
}
})();
const isJobFinished = () => !!(taskContainer && taskContainer.classList.contains("ans-job-finished"));
log.insertLog(`发现一个${mediaType},开始播放,倍速 ${finalRate}x`);
let isExecuted = false;
let isSpeedRestored = false;
let hasPlaybackEnded = false;
let isResolved = false;
let observer = null;
let quizHandlerId = null;
let watchdogId = null;
let stopRecovery = null;
let jobWatcher = null;
let isResuming = false;
let pausedChecks = 0;
if (isRateDisabled && userRate > 1) {
log.insertLog(
"视频禁止倍速,已临时调整为1倍速,强制倍速回导致任务点无法完成。"
);
}
const intervalId = setInterval(async () => {
const mediaElement = iframeDocument.documentElement.querySelector(mediaType);
if (mediaElement && !isExecuted) {
isExecuted = true;
clearInterval(intervalId);
// 播完刷新回来:复查任务点是否已画上完成态
let afterConfirmReload = false;
try {
if (sessionStorage.getItem(MEDIA_CONFIRM_RELOAD_KEY) === mediaJobKey) {
afterConfirmReload = true;
sessionStorage.removeItem(MEDIA_CONFIRM_RELOAD_KEY);
}
} catch (_) {}
if (afterConfirmReload && taskContainer) {
log.insertLog(`${mediaType}刷新后复查任务点完成状态…`);
if (await waitJobIconFinished(taskContainer, 5)) {
log.insertLog(`${mediaType}刷新后任务点已确认完成`);
resolve();
return;
}
log.insertLog(`${mediaType}刷新后仍未标记完成,将重新播放`, "warning");
}
await waitForCxFaceRecognition({
log,
setting,
messageStore,
extraDocuments: [iframeDocument]
});
jobWatcher = watchJobReport(iframeWindow);
mediaElement.muted = true;
mediaElement.__xsRateRestricted = isRateDisabled;
mediaElement.__xsFinishingAtNormalRate = false;
if (mediaType === "video") {
quizHandlerId = setupVideoQuizHandler(iframeDocument);
}
const applyPlaybackRate = (rate) => {
delete mediaElement.playbackRate;
hookVedio(mediaElement, rate);
};
const tryPlay = () => {
var _a, _b;
mediaElement.muted = true;
(_b = (_a = mediaElement.play()) == null ? void 0 : _a.catch) == null ? void 0 : _b.call(_a, (error) => {
console.warn("[cx] 起播失败,稍后重试。", error);
});
};
const resumePlayback = async () => {
if (hasPlaybackEnded || isResolved || isResuming || mediaElement.ended) return;
isResuming = true;
try {
await waitForCxFaceRecognition({
log,
setting,
messageStore,
extraDocuments: [iframeDocument]
});
await sleep(1);
if (hasPlaybackEnded || isResolved || mediaElement.ended) return;
tryPlay();
} finally {
isResuming = false;
}
};
const playingHandler = () => {
pausedChecks = 0;
applyPlaybackRate(isSpeedRestored ? 1 : finalRate);
};
mediaElement.addEventListener("playing", playingHandler);
mediaElement.addEventListener("pause", resumePlayback);
watchdogId = setInterval(() => {
if (!mediaElement.paused || hasPlaybackEnded || isResolved) return;
pausedChecks += 1;
if (pausedChecks === PLAY_FAIL_HINT_THRESHOLD) {
log.insertLog(
`${mediaType}尚未开始播放,可能是资源加载缓慢,仍在重试中`,
"warning",
4
);
}
resumePlayback();
}, PLAYBACK_WATCHDOG_INTERVAL * 1e3);
const timeUpdateHandler = () => {
if (!isSpeedRestored && mediaElement.duration - mediaElement.currentTime < 10) {
isSpeedRestored = true;
mediaElement.__xsFinishingAtNormalRate = true;
applyPlaybackRate(1);
}
};
if (finalRate > 1) {
mediaElement.addEventListener("timeupdate", timeUpdateHandler);
}
const cleanup = () => {
if (stopRecovery) { stopRecovery(); stopRecovery = null; }
mediaElement.removeEventListener("playing", playingHandler);
mediaElement.removeEventListener("pause", resumePlayback);
mediaElement.removeEventListener("timeupdate", timeUpdateHandler);
if (quizHandlerId) {
clearInterval(quizHandlerId);
quizHandlerId = null;
}
if (watchdogId) {
clearInterval(watchdogId);
watchdogId = null;
}
if (observer) {
observer.disconnect();
observer = null;
}
if (jobWatcher) {
try { jobWatcher.restore(); } catch (_) {}
jobWatcher = null;
}
};
const finishTask = (result = TASK_RESULT.finished) => {
if (isResolved) return;
isResolved = true;
cleanup();
resolve(result);
};
const reloadToConfirmProgress = () => {
try {
if (sessionStorage.getItem(MEDIA_CONFIRM_RELOAD_KEY) === mediaJobKey) return false;
sessionStorage.setItem(MEDIA_CONFIRM_RELOAD_KEY, mediaJobKey);
} catch (_) {}
log.insertLog(`${mediaType}进度未刷出完成态,立即刷新确认任务点`, "warning");
cleanup();
isResolved = true;
setTimeout(() => {
try { location.reload(); } catch (_) { window.location.reload(); }
}, 300);
return true;
};
mediaElement.addEventListener("ended", async () => {
if (isResolved || hasPlaybackEnded) return;
hasPlaybackEnded = true;
if (!taskContainer) {
log.insertLog(`${mediaType}已播放完成`);
finishTask();
return;
}
if (isJobFinished()) {
log.insertLog(`${mediaType}任务点已完成,停止播放`);
try { mediaElement.pause(); } catch (_) {}
finishTask();
return;
}
log.insertLog(`${mediaType}已播放完成,等待平台确认学习进度…`);
const reportPromise = (jobWatcher && jobWatcher.result) || Promise.resolve(false);
const confirmed = await confirmJobFinished(taskContainer, reportPromise, JOB_CONFIRM_TIMEOUT);
if (isResolved) return;
if (confirmed || isJobFinished()) {
log.insertLog(`${mediaType}任务点已完成,停止播放`);
try { mediaElement.pause(); } catch (_) {}
finishTask();
return;
}
// 平台常需刷新才画完成态:确认超时后立刻刷新(只刷一次)
if (!reloadToConfirmProgress()) {
log.insertLog(`${mediaType}已刷新过仍未确认,停止自动切章以免空跳`, "warning");
try { mediaElement.pause(); } catch (_) {}
finishTask(TASK_RESULT.unconfirmed);
}
});
if (taskContainer) {
observer = new MutationObserver(() => {
if (taskContainer.classList.contains("ans-job-finished")) {
log.insertLog(`${mediaType}任务点已完成,停止播放`);
try { mediaElement.pause(); } catch (_) {}
finishTask();
}
});
observer.observe(taskContainer, {
attributes: true,
attributeFilter: ["class"],
subtree: true
});
if (isJobFinished()) {
log.insertLog(`${mediaType}任务点已完成,停止播放`);
try { mediaElement.pause(); } catch (_) {}
finishTask();
}
}
if (!isResolved) {
stopRecovery = watchMediaRecovery(mediaElement, {
documents: [document, iframeDocument],
recover: resumePlayback,
checkComplete: () => {
if (isResolved) return true;
if (isJobFinished()) {
try { mediaElement.pause(); } catch (_) {}
finishTask();
return true;
}
return false;
},
onError: e => log.insertLog("后台恢复播放失败,将继续重试:" + e.message, "warning")
});
applyPlaybackRate(finalRate);
tryPlay();
log.insertLog(`播放成功,当前实际播放倍速为${mediaElement.playbackRate || finalRate}x.`);
}
}
}, 2500);
});
};
const processRead = async (iframe, iframeWindow) => {
try {
const reporter = watchJobReport(iframeWindow);
try {
const triggerFinish = () => {
try {
if (iframeWindow && typeof iframeWindow.finishJob === "function" && iframeWindow.jobid) {
iframeWindow.finishJob(iframeWindow.jobid);
return true;
}
} catch (e) {}
return false;
};
if (!triggerFinish()) {
const subFrame = (iframe.contentDocument == null ? void 0 : iframe.contentDocument.querySelector('iframe[src*="readjob"]')) || null;
const subWin = (subFrame == null ? void 0 : subFrame.contentWindow) || null;
try {
if (subWin && subWin.parent && typeof subWin.parent.finishJob === "function" && subWin.data && subWin.data.jobid) {
subWin.parent.finishJob(subWin.data.jobid);
}
} catch (e) {}
}
if (await confirmJobFinished(iframe, reporter.result, 15)) {
log.insertLog("阅读任务点已完成");
return TASK_RESULT.finished;
}
log.insertLog("阅读任务完成超时,请手动检查", "warning", 4);
return TASK_RESULT.unconfirmed;
} finally {
reporter.restore();
}
} catch (e) {
return TASK_RESULT.unconfirmed;
}
};
const processBook = async (iframeWindow, iframe) => {
const requiredSeconds = getBookRequiredSeconds(iframeWindow);
const reporter = watchJobReport(iframeWindow);
try {
if (requiredSeconds > 0) {
log.insertLog(`发现一个计时阅读任务点,需保持约 ${requiredSeconds} 秒,请勿切换页面`);
if (await keepReading(requiredSeconds, iframe, reporter.result)) {
log.insertLog("计时阅读任务点已完成");
return TASK_RESULT.finished;
}
log.insertLog("计时阅读任务点未确认完成,请手动确认", "warning", 4);
return TASK_RESULT.unconfirmed;
}
log.insertLog("发现一个电子书任务点,正在处理...");
const topWindow = (typeof unsafeWindow !== "undefined" ? unsafeWindow.top : window.top) || window.top;
if (typeof (topWindow == null ? void 0 : topWindow.onchangepage) !== "function" || typeof iframeWindow.getFrameAttr !== "function") {
log.insertLog("该电子书任务点缺少可用的翻页入口,请手动完成后继续", "warning", 4);
return TASK_RESULT.unconfirmed;
}
topWindow.onchangepage(iframeWindow.getFrameAttr("end"));
if (await confirmJobFinished(iframe, reporter.result)) {
log.insertLog("电子书任务点已完成");
return TASK_RESULT.finished;
}
log.insertLog("电子书任务点未确认完成,请手动确认", "warning", 4);
return TASK_RESULT.unconfirmed;
} finally {
reporter.restore();
}
};
const processLive = async (iframe, iframeWindow) => {
try {
if (iframeWindow) {
const bodyTxt = (iframeWindow.document.body == null ? void 0 : iframeWindow.document.body.innerText) || "";
if (bodyTxt.includes("已完成") && /观看|任务/.test(bodyTxt)) {
log.insertLog("直播任务已完成,自动继续", "success", 4);
return TASK_RESULT.finished;
}
}
if (iframeWindow && !iframeWindow.__xsLiveOpened) {
try {
const card = (iframeWindow == null ? void 0 : iframeWindow.document) == null ? void 0 : iframeWindow.document.querySelector(".liveHref");
if (card) {
card.click();
iframeWindow.__xsLiveOpened = true;
log.insertLog("已自动打开直播回放,观看时长达到90%后将自动完成并跳转", "info", 4);
} else {
log.insertLog("当前直播未开播或不可回放", "warning", 4);
return TASK_RESULT.finished;
}
} catch (e) {
}
}
const readLiveProgress = () => {
try {
const info = iframeWindow.document.querySelector(".LiveInfo2");
const txt = (info == null ? void 0 : info.textContent) || "";
const m = txt.match(/观看进度为\s*([\d.]+)\s*%/);
return m ? parseFloat(m[1]) : null;
} catch (e) {
return null;
}
};
let waited = 0;
while (waited < 3 * 60 * 60 * 1000) {
const parentEl = iframe.parentElement;
if (parentEl && parentEl.className.includes("ans-job-finished")) {
log.insertLog("直播任务已完成,自动继续", "success", 4);
return TASK_RESULT.finished;
}
const pct = readLiveProgress();
if (pct !== null && pct >= 90) {
log.insertLog("直播观看进度已达90%,刷新页面确认任务完成", "success", 4);
try {
iframeWindow.top.location.reload();
} catch (e) {
location.reload();
}
return TASK_RESULT.finished;
}
await sleep(30);
waited += 30;
if (waited % 600 === 0) {
log.insertLog(`直播挂机中,已等待 ${Math.round(waited / 60000)} 分钟(进度${pct === null ? "未知" : pct + "%"}),请保持回放页在前台播放`, "warning", 4);
}
}
log.insertLog("直播任务超时未完成,请手动检查", "warning", 4);
return TASK_RESULT.unconfirmed;
} catch (e) {
return TASK_RESULT.unconfirmed;
}
};
const inFlightTasks = /* @__PURE__ */ new WeakMap();
const processIframe = (iframe) => {
const cached = inFlightTasks.get(iframe);
if (cached && cached.src === iframe.src && iframe.isConnected) {
return cached.task;
}
const task = runIframeTask(iframe).finally(() => {
var _a;
if (((_a = inFlightTasks.get(iframe)) == null ? void 0 : _a.task) === task) {
inFlightTasks.delete(iframe);
}
});
inFlightTasks.set(iframe, { src: iframe.src, task });
return task;
};
const runIframeTask = async (iframe) => {
var _a, _b;
const iframeSrc = iframe.src;
const iframeDocument = iframe.contentDocument;
const iframeWindow = iframe.contentWindow;
if (!iframeDocument || !iframeWindow) return TASK_RESULT.skipped;
if (iframeSrc.includes("javascript:")) return TASK_RESULT.skipped;
await waitIframeLoad(iframe);
const parentClass = ((_a = iframe.parentElement) == null ? void 0 : _a.className) || "";
if (parentClass.includes("ans-job-finished")) return TASK_RESULT.skipped;
if (iframeSrc.includes("modules/read")) {
return processRead(iframe, iframeWindow);
}
if (iframeSrc.includes("modules/live")) {
return processLive(iframe, iframeWindow);
}
if (iframeSrc.includes("api/work")) {
selectTextSearchLogic(iframeDocument, iframeWindow);
await processWork(iframe, iframeDocument, iframeWindow);
return TASK_RESULT.finished;
}
const ansJobIcon = (_b = iframe.parentElement) == null ? void 0 : _b.querySelector(".ans-job-icon");
if (ansJobIcon) {
if (iframeSrc.includes("video")) {
return await processMedia("video", iframeDocument, iframe) || TASK_RESULT.finished;
}
if (iframeSrc.includes("audio")) {
return await processMedia("audio", iframeDocument, iframe) || TASK_RESULT.finished;
}
if (["ppt", "doc", "pptx", "docx", "pdf"].some((type) => iframeSrc.includes("modules/" + type))) {
return processDocument(iframeWindow, iframe);
}
if (["innerbook"].some((type) => iframeSrc.includes("modules/" + type))) {
return processBook(iframeWindow, iframe);
}
}
return TASK_RESULT.skipped;
};
init();
setupFaceRecognitionWatcher();
processIframeTask();
setupInterceptor();
};
const cxLiveReplayLogic = () => {
const log = useLogStore();
log.insertLog("进入直播回放页,脚本正在控制视频播放..");
let notified = false;
const getVideo = () => document.querySelector("video");
const ensurePlaying = async () => {
const v = getVideo();
if (!v) return;
if (v.paused || v.ended) {
v.muted = true;
try {
await v.play();
} catch (e) {
}
}
};
const isWatchedEnough = () => {
const v = getVideo();
if (!v || typeof watchTime === "undefined") return false;
return watchTime >= v.duration * 0.9;
};
const checkTask = async () => {
await ensurePlaying();
if (notified || !isWatchedEnough()) return;
notified = true;
log.insertLog("直播观看进度已达90%,正在返回课程页确认完成", "success", 4);
try {
const opener = window.opener;
if (opener && !opener.closed) {
try {
opener.location.reload();
} catch (e) {
}
}
} catch (e) {
}
setTimeout(() => {
try {
window.close();
} catch (e) {
}
}, 1500);
};
setInterval(checkTask, 5000);
setTimeout(checkTask, 1500);
};
const submitCxHomework = async (log, setting, accuracy, pageWindow) => {
const enabled = () => setting.config.basicConfig.autoSubmit.value && setting.config.basicConfig.autoAnswer.value;
if (!enabled()) return false;
if (hasPendingManualPick()) {
log.insertLog("还有题目等待手动选择,已暂停自动提交;选完后再提交", "warning", 8);
return false;
}
const rate = Number(accuracy);
if (!Number.isFinite(rate) || rate < Number(setting.config.basicConfig.accuracy.value)) {
log.insertLog("未达到自动提交阈值,保留作业答案", "warning"); return false;
}
if (typeof pageWindow.btnBlueSubmit !== "function" || typeof pageWindow.submitCheckTimes !== "function") {
log.insertLog("自动提交已开启,此作业页的提交入口未识别,请手动确认", "warning"); return false;
}
await pageWindow.btnBlueSubmit();
await sleep(setting.config.basicConfig.reqIntervalTime.value / 2);
if (!enabled()) return false;
await pageWindow.submitCheckTimes();
log.insertLog("已执行提交,请以平台显示的结果为准"); return true;
};
const cxHomeworkLogic = async () => {
const log = useLogStore();
const questionStore = useQuestionStore();
const setting = useSettingStore();
log.insertLog(`进入新版作业页面,开始准备答题..`);
if (!setting.config.basicConfig.autoAnswer.value) {
log.insertLog(
`自动答题已关闭,请前往设置模块中更改`
);
return;
}
questionStore.accuracy = -1;
const accuracy = await new QuestionHandler$1("zy").init();
questionStore.accuracy = accuracy;
log.insertLog(
`答题完毕,正确率为${accuracy}%,详情请前往答题模块查看..`
);
await submitCxHomework(log, setting, accuracy, typeof unsafeWindow !== "undefined" ? unsafeWindow : window);
};
const isPreviewDisabled = () => {
var _a, _b;
return (_b = (_a = document.querySelector(".mark_info")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.includes("不允许整卷预览");
};
const isVisible = (element) => {
if (!element) return false;
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
};
const getVisibleElement = (selector) => [...document.querySelectorAll(selector)].find(isVisible);
const getNextQuestionBtn = () => getVisibleElement('[onclick="getTheNextQuestion(1)"]');
const getSubmitConfirmBtn = () => {
const selectors = [
".popBottom a[onclick*='answerDirectSubmit']",
"a[onclick*='answerDirectSubmit']",
"a.confirm[onclick*='finalSubmit']"
];
for (const selector of selectors) {
const confirmBtn = getVisibleElement(selector);
if (confirmBtn) return confirmBtn;
}
return null;
};
const waitForSubmitConfirmBtn = async () => {
for (let i = 0; i < 10; i++) {
const confirmBtn = getSubmitConfirmBtn();
if (confirmBtn) return confirmBtn;
await sleep(0.5);
}
return null;
};
const submitCxExam = async (log, setting) => {
if (!setting.config.examConfig.autoSubmit.value) {
log.insertLog(
"自动交卷未开启,请前往设置模块中更改",
"warning",
4
);
return;
}
log.insertLog("自动交卷已开启,正在提交试卷");
await sleep(setting.config.basicConfig.reqIntervalTime.value);
if (!setting.config.examConfig.autoSubmit.value || !setting.config.basicConfig.autoAnswer.value) return;
const submitBtn = getVisibleElement("div.sub-button.fr a.completeBtn");
if (!submitBtn) {
log.insertLog("未找到交卷按钮,请手动交卷", "warning", 4);
return;
}
submitBtn.click();
const confirmBtn = await waitForSubmitConfirmBtn();
if (confirmBtn && setting.config.examConfig.autoSubmit.value && setting.config.basicConfig.autoAnswer.value) {
confirmBtn.click();
log.insertLog("已确认提交试卷");
return;
}
log.insertLog("未找到提交确认按钮,请手动确认交卷", "warning", 4);
};
const runSingleQuestionExamLogic = async (log, setting) => {
log.insertLog(
"当前考试不允许整卷预览,将按单题页面顺序处理,请勿手动切换题目",
"warning",
4
);
while (setting.config.basicConfig.autoAnswer.value) {
await new QuestionHandler$1("ks").init();
if (!setting.config.basicConfig.autoAnswer.value) return;
const nextBtn = getNextQuestionBtn();
if (!nextBtn) break;
nextBtn.click();
log.insertLog("已切换到下一题,继续处理..");
await sleep(1);
}
if (setting.config.basicConfig.autoAnswer.value) await submitCxExam(log, setting);
};
const enterCxExamPreviewLogic = async () => {
const log = useLogStore();
const setting = useSettingStore();
if (!setting.config.basicConfig.autoAnswer.value) {
log.insertLog(
`自动答题已关闭,请前往设置模块中更改`
);
return;
}
if (isPreviewDisabled()) {
await runSingleQuestionExamLogic(log, setting);
return;
}
log.insertLog("进入考试开始页,正在切换到整卷预览..");
for (let i = 0; i < 10; i++) {
if (!setting.config.basicConfig.autoAnswer.value) return;
const previewBtn = document.querySelector("div.sub-button a");
if (previewBtn) {
previewBtn.click();
log.insertLog("已点击整卷预览,正在进入答题页面..");
return;
}
await sleep(0.5);
}
log.insertLog("未找到整卷预览按钮,请手动点击", "warning", 4);
};
const cxExamLogic = async () => {
const log = useLogStore();
const setting = useSettingStore();
log.insertLog(`进入新版考试页面,开始准备答题..`);
if (!setting.config.basicConfig.autoAnswer.value) {
log.insertLog(
`自动答题已关闭,请前往设置模块中更改`
);
return;
}
await new QuestionHandler$1("ks").init();
await submitCxExam(log, setting);
};
const cxRoutes = {
match: [
{ keyword: "zhibo.chaoxing.com", tab: "0", logic: cxLiveReplayLogic },
{ keyword: "/base", tab: "0" },
{ keyword: "/space/index", tab: "0" },
// 积分课程
{ keyword: "/plaza/?courseId", tab: "0" },
{ keyword: "mycourse/stu?courseid", tab: "0" },
{ keyword: "/mycourse/studentstudy", tab: "1", logic: cxChapterLogic },
{ keyword: "/mooc2/work/dowork", tab: "1", logic: cxHomeworkLogic },
{ keyword: "/examcode/examnotes", tab: "0", log: "等待用户进入答题页面中.." },
{ keyword: "exam-ans/exam/test/reVersionTestStartNew", tab: "1", logic: enterCxExamPreviewLogic },
{ keyword: "mooc-ans/exam/test/reVersionTestStartNew", tab: "1", logic: enterCxExamPreviewLogic },
{ keyword: "/exam/test/reVersionTestStartNew", tab: "1", logic: enterCxExamPreviewLogic },
{ keyword: "/mooc2/exam/preview", tab: "1", logic: cxExamLogic },
{ keyword: "exam/preview", tab: "1", logic: cxExamLogic }
],
defaultRoute: {
show: true,
tab: "0"
}
};
class QuestionHandler2 extends BasicQuestionHandler {
constructor(type, iframe) {
super();
__publicField(this, "type");
__publicField(this, "AI_ALLOW_QUESTION_TYPE", ["单选题", "多选题", "判断题", "多项选择题", "X型选择题", "X型题", "不定项选择题", "不定项"]);
__publicField(this, "init", async () => {
var _that = this;
this.questions = [];
this.parseHtml();
if (this.questions.length) {
this.log.insertLog(
`成功解析到${this.questions.length}个题目,开始答题..`
);
setTimeout(() => {
autoSetTab(_that.setting, "2");
}, 1e3);
const { answerMode } = this.setting.config.basicConfig;
const mode = answerMode.value;
// 一屏一题:页面渲染的题目节点比接口题目少,切题后必须等新题渲染出来
const nodeCount = this._document ? this._document.querySelectorAll(".subject_node").length : 0;
const pagedMode = nodeCount > 0 && nodeCount < this.questions.length;
for (const [index, question] of this.questions.entries()) {
this.questionStore.currentQuestionIndex = index;
let fillFailed = false;
let handled = false;
if (mode !== "ai") {
const answerData = await getAccurateAnswer(question);
if (answerData.code === 1) {
if (answerData.data.multimatch) {
// 手动选择模式:题库有多道相似题,先不填,等用户自己挑
question.answer = { code: 0, answer: [], src: "pick", msg: "存在多道相似题,请先手动选择后再填写", candidates: answerData.data.candidates };
if (getManualWaitMode() === "skip") {
this.log.insertLog(`第${index + 1}题有${answerData.data.candidates.length}道相似题,已按设置跳过(本轮不自动提交)`, "warning", 6);
continue;
}
this.log.insertLog(`第${index + 1}题有${answerData.data.candidates.length}道相似题,已暂停,等待你选择后继续…`, "warning", 8);
const picked = await waitForManualPick(index);
if (!picked) {
this.log.insertLog(`第${index + 1}题等待选择超时,已跳过(本轮不自动提交)`, "warning", 6);
continue;
}
question.answer = Object.assign({}, question.answer, { code: 1, answer: picked });
this.log.insertLog(`第${index + 1}题已按你的选择填入,继续答题`, void 0, 4);
continue;
}
question.answer = {
code: 1,
answer: answerData.data.answer,
src: answerData.data.src
};
this.setting.userInfo.remainCount = formatLocalRemainCount(answerData.data.remainCount);
saveAnswerRecord({
question,
answer: answerData.data.answer,
source: "accurate"
});
const errorStatus = !this.fillQuestion(question, index);
if (errorStatus) {
question.answer.code = 0;
question.answer.msg = "答案拿到了但没点上去";
fillFailed = true;
} else {
this.correctNum += 1;
handled = true;
}
} else if (mode === "questionBank" || answerData.code !== -1004) {
question.answer = {
code: answerData.code,
answer: [],
msg: answerData.msg
};
handled = true;
}
}
if (!handled) {
if (this.AI_ALLOW_QUESTION_TYPE.includes(question.type)) {
const aiQuestion = {
...question,
type: this.questionType[question.type] || question.type
};
const aiAnswerData = await getAIAnswer(aiQuestion);
if (aiAnswerData.code === 1) {
question.answer = {
code: 2,
answer: aiAnswerData.data.answer,
src: aiAnswerData.data.src || "ai"
};
this.setting.userInfo.remainCount = formatLocalRemainCount(aiAnswerData.data.remainCount);
saveAnswerRecord({
question,
answer: aiAnswerData.data.answer,
source: "ai"
});
const errorStatus = !this.fillQuestion(question, index);
if (errorStatus) {
question.answer.code = 0;
question.answer.msg = "答案拿到了但没点上去";
fillFailed = true;
} else {
this.correctNum += 1;
}
} else if (!(question.answer && question.answer.code === 0 && Array.isArray(question.answer.answer) && question.answer.answer.length)) {
question.answer = {
code: aiAnswerData.code,
answer: [],
msg: aiAnswerData.msg
};
} else if (!question.answer.msg) {
question.answer.msg = "答案拿到了但没点上去";
}
} else if (!(question.answer && question.answer.code === 0 && Array.isArray(question.answer.answer) && question.answer.answer.length)) {
question.answer = {
code: -1,
answer: ["该题型不支持AI答题"]
};
}
}
if (!fillFailed && this.type == "zy") {
// 新版页面可能没有 switch-btn-box,原来直接取 [0].innerText 会抛错并中断整个答题流程,
// 表现就是「答完第一题就不动了」。这里改成:先按原结构找,找不到再按按钮文字找「下一题」。
let nextBtn = this._document.querySelector(
"div.switch-btn-box > button:nth-child(2) > span"
);
if (!nextBtn || (nextBtn.innerText || nextBtn.textContent || "").trim() !== "下一题") {
nextBtn = Array.from(this._document.querySelectorAll("button, span, a")).find(
(el) => el.children.length === 0 && (el.textContent || "").trim() === "下一题"
);
}
if (nextBtn && (nextBtn.innerText || nextBtn.textContent || "").trim() === "下一题") {
const visibleNode = () => Array.from(this._document.querySelectorAll(".subject_node")).find((n) => {
const rect = n.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
const prevText = pagedMode ? ((visibleNode() || {}).innerText || "") : "";
nextBtn.click();
// 一屏一题:等新题真的渲染出来再处理下一道,否则还会点到上一题的选项上
if (pagedMode) {
for (let wait = 0; wait < 20; wait++) {
await sleep(0.3);
const node = visibleNode();
if (node && node.innerText && node.innerText !== prevText) break;
}
}
}
}
}
this.questionStore.currentQuestionIndex += 1;
} else
this.log.insertLog(
"没有解析到题目,请检查是否进入正确页面...",
"warning"
);
setTimeout(() => {
autoSetTab(_that.setting, "1");
}, 2e3);
reportAnswerSummary("zhs-web", this.questions);
return Promise.resolve(
(this.correctNum / this.questions.length * 100).toFixed(1)
);
});
__publicField(this, "parseHtml", () => {
this.questionStore.currentQuestionIndex = 0;
if (!this._document) return [];
this.addQuestions(this.questionStore.questionList);
this.questionStore.questionList = this.questions;
});
__publicField(this, "fillQuestion", (question, index) => {
let isSelected = false;
if (!this._window) return;
// 智慧树新版作业页一屏只渲染一道题,按题目序号取节点会取空,导致第二题起答不上、
// 只能刷新页面才恢复。节点总数少于题目总数时,改为取「当前屏幕上那道题」
// (和 OCS 每轮重新抓当前题的做法一致),并按文本记住答过的题,避免重复点。
const allNodes = Array.from(document.querySelectorAll(".subject_node"));
const total = (this.questions && this.questions.length) || 0;
const paged = total > 0 && allNodes.length > 0 && allNodes.length < total;
let subjectNode = allNodes[index];
if (paged) {
const normNode = (n) => this.removeHtml(n.innerHTML).replace(/\s+/g, "").slice(0, 40);
const done = this.__answeredNodes || (this.__answeredNodes = new Set());
const visible = allNodes.filter((n) => {
const rect = n.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
const pool = visible.length ? visible : allNodes;
subjectNode = pool.find((n) => !done.has(normNode(n))) || pool[pool.length - 1];
done.add(normNode(subjectNode));
}
if (!subjectNode) return false;
const optionsDOM = subjectNode.querySelectorAll(".node_detail");
const normalizeOptText = (str) => String(str ?? "").replace(/^[A-Z][.、.]\s*/i, "").replace(/\s+/g, "").replace(/[^\p{L}\p{N}√×]/gu, "").toLowerCase();
if (question.type === "单选题" || question.type === "多选题" || /多选|X型|不定项/.test(question.type || "")) {
// 先把命中的选项序号收齐再去点,避免同一选项被点两次(多选题再点一次会取消勾选,
// 表现就是「多选题只答一个」)
const pickedIdx = new Set();
const answerList = normalizeChoiceAnswerList(question.answer.answer);
answerList.forEach((answer) => {
// 字母答案映射:A / ABD / A,B,D → 按选项序号勾选
const letterText = String(answer || "").trim();
let letters = [];
const prefixed = letterText.match(/^([A-Za-z])\s*[.、.))::]\s*(\S[\s\S]*)$/);
if (prefixed) {
const pIdx = prefixed[1].toUpperCase().charCodeAt(0) - 65;
const pOpt = optionsDOM[pIdx];
const pOptText = pOpt ? normalizeOptText(this.removeHtml(pOpt.innerHTML)) : "";
const pBody = normalizeOptText(prefixed[2]);
// 字母与选项正文对得上才按字母点,防止选项顺序被打乱时点错
if (!pOptText || !pBody || pOptText.includes(pBody) || pBody.includes(pOptText)) letters = [prefixed[1].toUpperCase()];
} else if (/^[A-Za-z]$/.test(letterText)) letters = [letterText.toUpperCase()];
else if (/^[A-Za-z]{2,12}$/.test(letterText)) letters = letterText.toUpperCase().split("");
else if (/^[A-Z](?:[\s,,、;;|]+[A-Z])+$/i.test(letterText)) letters = letterText.toUpperCase().split(/[\s,,、;;|]+/).filter(Boolean);
optionsDOM.forEach((optionDOM, optIdx) => {
if (letters.length) {
if (letters.includes(String.fromCharCode(65 + optIdx))) pickedIdx.add(optIdx);
return;
}
const optionText = this.removeHtml(optionDOM.innerHTML);
const nAnswer = normalizeOptText(answer);
const nOption = normalizeOptText(optionText);
if (answer == optionText || this.removeAllTags(answer) == this.removeAllTags(optionText) || (nAnswer.length >= 2 && nOption && nOption.includes(nAnswer))) {
pickedIdx.add(optIdx);
} else if (imgSrcsMatch(collectImgSrcs(answer), collectImgSrcs(optionDOM).concat(collectImgSrcs(optionText)))) {
pickedIdx.add(optIdx);
}
});
});
Array.from(pickedIdx).sort((a, b) => a - b).forEach((optIdx) => {
const optionDOM = optionsDOM[optIdx];
if (!optionDOM) return;
isSelected = true;
optionDOM.click();
});
} else if (question.type === "判断题") {
let answer = "对";
if (question.answer.answer[0].match(
/(^|,)(正确|是|对|√|T|ri|right|true)(,|$)/
)) {
answer = "对";
} else if (question.answer.answer[0].match(
/(^|,)(错误|否|错|×|F|wr|wrong|false)(,|$)/
)) {
answer = "错";
}
optionsDOM.forEach((optionDOM) => {
if (answer == this.removeHtml(optionDOM.innerHTML)) {
isSelected = true;
optionDOM.click();
}
});
} else ;
return isSelected;
});
__publicField(this, "extractOptions", (questionOptions, questionType) => {
const optionsObject = {};
const optionTexts = [];
if (!questionOptions) return [optionsObject, optionTexts];
questionOptions.forEach((questionOption) => {
const optionTextContent = this.removeHtml(questionOption.content);
optionsObject[optionTextContent] = questionOption.content;
if (questionType !== "判断题") optionTexts.push(optionTextContent);
});
return [optionsObject, optionTexts];
});
__publicField(this, "addQuestions", (questions) => {
questions.forEach((question) => {
const questionTitle = this.removeHtml(question.name);
const rawType = question && question.questionType;
const apiTypeText = rawType && typeof rawType === "object" ? (rawType.name || rawType.code || "") : rawType;
let questionType = normalizeQuestionTypeName(apiTypeText);
const [optionsObject, optionTexts] = this.extractOptions(
question.questionOptions,
questionType
);
const subjectNodes = (this._document && this._document.querySelectorAll)
? this._document.querySelectorAll(".subject_node")
: [];
const subjectNode = subjectNodes[this.questions.length] || this._document || null;
// 接口给的题型为空或写成单选、但页面选项是复选框时按多选题处理,避免只填一个选项
if ((!questionType || questionType === "单选题") && detectMultiChoiceDom(subjectNode)) questionType = "多选题";
this.questions.push({
element: subjectNode,
type: questionType,
title: questionTitle,
optionsText: optionTexts,
options: optionsObject,
answer: { code: -1, answer: [] },
workType: "zhs",
refer: this._window.location.href,
id: "zhs"
});
});
});
this.type = type;
if (iframe) {
this._document = iframe.contentDocument;
this._window = iframe.contentWindow;
}
}
}
const ANSWER_SHEET_EVENT = "ABC_ZHS_ANSWER_SHEET";
const ANSWER_SHEET_URL = "getAnswerSheetInformation";
const ANSWER_SHEET_KEY = Symbol.for("__xsASKey");
const saveAnswerSheetPayload = (payload) => {
const pageWindow = _unsafeWindow || window;
pageWindow[ANSWER_SHEET_KEY] = payload;
pageWindow.dispatchEvent(new CustomEvent(ANSWER_SHEET_EVENT, { detail: payload }));
};
const parseResponseText = (text) => {
try {
return JSON.parse(text);
} catch {
return text;
}
};
const getRequestUrl = (input) => {
if (typeof input === "string") return input;
if (input == null ? void 0 : input.url) return input.url;
return input ? String(input) : "";
};
const installAnswerSheetHook = () => {
const pageWindow = _unsafeWindow || window;
const hookFlag = Symbol.for("__xsAsHooked");
if (pageWindow[hookFlag]) return;
pageWindow[hookFlag] = true;
const originalFetch = pageWindow.fetch;
if (typeof originalFetch === "function") {
pageWindow.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
const requestUrl = getRequestUrl(args[0]);
if (requestUrl.includes(ANSWER_SHEET_URL)) {
response.clone().text().then((text) => {
saveAnswerSheetPayload(parseResponseText(text));
}).catch(() => {
});
}
return response;
};
try {
const fetchToString = originalFetch.toString.bind(originalFetch);
Object.defineProperty(pageWindow.fetch, "toString", {
value: fetchToString,
writable: true, configurable: true
});
} catch (_) {}
}
const OriginalXHR = pageWindow.XMLHttpRequest;
if (typeof OriginalXHR === "function" && !OriginalXHR[hookFlag]) {
const originalOpen = OriginalXHR.prototype.open;
const originalSend = OriginalXHR.prototype.send;
const reqUrlKey = Symbol.for("__xsReqUrl");
OriginalXHR[hookFlag] = true;
OriginalXHR.prototype.open = function(method, url2, ...rest) {
this[reqUrlKey] = url2 || "";
return originalOpen.call(this, method, url2, ...rest);
};
try {
const nativeOpenStr = originalOpen.toString.bind(originalOpen);
Object.defineProperty(OriginalXHR.prototype.open, "toString", {
value: nativeOpenStr, writable: true, configurable: true
});
} catch (_) {}
OriginalXHR.prototype.send = function(...args) {
this.addEventListener("load", function() {
const requestUrl = getRequestUrl(this.responseURL || this[reqUrlKey]);
if (requestUrl.includes(ANSWER_SHEET_URL)) {
saveAnswerSheetPayload(parseResponseText(this.responseText));
}
});
return originalSend.apply(this, args);
};
try {
const sendToString = originalSend.toString.bind(originalSend);
Object.defineProperty(OriginalXHR.prototype.send, "toString", {
value: sendToString,
writable: true, configurable: true
});
} catch (_) {}
}
};
const fetchHook = (url2, callback) => {
const questionStore = useQuestionStore();
function addXMLRequestCallback(callback2) {
var oldSend, i;
if (XMLHttpRequest.callbacks) {
XMLHttpRequest.callbacks.push(callback2);
} else {
XMLHttpRequest.callbacks = [callback2];
oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
for (i = 0; i < XMLHttpRequest.callbacks.length; i++) {
XMLHttpRequest.callbacks[i](this);
}
oldSend.apply(this, arguments);
};
}
}
addXMLRequestCallback(function(xhr) {
xhr.addEventListener("load", function() {
if (xhr.readyState == 4 && xhr.status == 200) {
if (xhr.responseURL.includes(url2)) {
const keywords = ["doExam", "doHomework"];
if (keywords.some((keyword) => xhr.responseURL.includes(keyword))) {
let questions = [];
const questionsParts = JSON.parse(xhr.response).rt.examBase.workExamParts;
questionsParts.forEach((part) => {
console.log("part", part.questionDtos);
questions = questions.concat(part.questionDtos);
});
questionStore.questionList = questions;
}
callback();
}
}
});
});
};
const autoAnswerLogic = async () => {
const log = useLogStore();
const questionStore = useQuestionStore();
log.insertLog("进入作业页面,脚本开始工作中..");
questionStore.accuracy = -1;
const handleFetchHook = async () => {
const accuracy = await new QuestionHandler2("zy").init();
questionStore.accuracy = accuracy;
log.insertLog(`答题完毕,正确率为${accuracy}%,详情请前往答题模块查看..`);
};
["doHomework", "doExam"].forEach((type) => {
fetchHook(type, () => handleFetchHook());
});
};
// 智慧树课中弹题:三种页面(旧版视频页、AI 智慧课、新共享课)共用同一套弹窗结构和 Vue 数据
const zhsPageDocument = () => (_unsafeWindow && _unsafeWindow.document) || document;
const zhsWaitForYidun = async (log) => {
if (!document.querySelector(".yidun_popup")) return true;
log.insertLog("检测到验证码,请完成验证后继续..", "warning");
const startTime = Date.now();
while (document.querySelector(".yidun_popup")) {
if (Date.now() - startTime > 5 * 60 * 1e3) {
log.insertLog("等待验证码完成超时,请手动刷新页面后继续..", "warning");
return false;
}
await sleep(1);
}
log.insertLog("验证码已完成,继续学习..");
return true;
};
const zhsGetTopicVm = () => {
var _a, _b;
const dialog = zhsPageDocument().querySelector("#playTopic-dialog");
return ((_b = (_a = dialog == null ? void 0 : dialog.__vue__) == null ? void 0 : _a.$parent) == null ? void 0 : _b.$parent) || null;
};
const zhsHasTopicDialog = () => {
const dialog = document.querySelector("#playTopic-dialog");
if (!dialog) return false;
const vm = zhsGetTopicVm();
if (vm && vm.testDialog) return true;
// 拿不到 Vue 实例时看弹窗是否真的显示着
const rect = dialog.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
// 直接从页面 Vue 实例里读答案(答案本来就下发在前端数据里,不用猜)
const zhsAnswerTopicByVm = async (log) => {
var _a, _b, _c;
const vm = zhsGetTopicVm();
if (!(vm == null ? void 0 : vm.testDialog)) return false;
const question = (_b = (_a = vm.topicInfo) == null ? void 0 : _a.lessonTestQuestionUseInterfaceDtos) == null ? void 0 : _b[0];
const options = (_c = question == null ? void 0 : question.testQuestion) == null ? void 0 : _c.questionOptions;
const answers = question == null ? void 0 : question.answerUs;
log.insertLog("检测到弹题测验,尝试自动作答..");
const captchaPassed = await zhsWaitForYidun(log);
if (!captchaPassed) return false;
let count = 0;
if (options && answers && typeof vm.topicClickQot === "function") {
for (const option of options) {
if (answers.indexOf(option.sortUs) === -1) continue;
await sleep(0.8);
vm.topicClickQot(option);
count++;
}
}
await sleep(Math.max(2, 5 - count * 0.8));
vm.testDialog = false;
if (count) {
log.insertLog(`弹题已自动作答完成(${count}题),继续播放视频`);
} else {
log.insertLog("未读取到弹题答案,已关闭弹窗", "warning");
}
return true;
};
// 读不到 Vue 数据时的兜底:逐页随机点一个选项再关窗,至少能把视频放下去
const zhsHandleTopicDialogByDom = async (log) => {
var _a, _b;
const pages = Array.from(document.querySelectorAll("#playTopic-dialog .el-pager .number"));
if (!pages.length) return false;
log.insertLog("检测到弹题测验,正在处理..");
try {
for (const page of pages) {
if (!page.isConnected) continue;
if (!page.classList.contains("active")) {
page.click();
await sleep(0.5);
}
const options = Array.from(document.querySelectorAll("#playTopic-dialog ul .topic-item"));
if (!options.length) continue;
const captchaPassed = await zhsWaitForYidun(log);
if (!captchaPassed) return false;
const index = Math.floor(Math.random() * options.length);
await sleep(1);
const target = document.querySelector(
`#playTopic-dialog .topic .radio ul > li:nth-child(${index + 1})`
);
(_a = target || options[index]) == null ? void 0 : _a.click();
await sleep(1);
}
await sleep(1);
(_b = document.querySelector("#playTopic-dialog .close-btn, #playTopic-dialog .btn")) == null ? void 0 : _b.click();
log.insertLog("弹题测验已关闭");
return true;
} catch (error) {
log.insertLog(`弹题测验处理失败: ${error.message || "未知错误"}`, "warning");
return false;
}
};
const videoStudyLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
let videoEl = null;
let pollIntervalId = null;
let isAnsweringQuiz = false;
const injectQuizHelper = () => {
const script = document.createElement("script");
script.textContent = `
(function() {
window.addEventListener("message", function(e) {
if (!e.data || e.data.type !== "ABC_ZHS_QUIZ_ANSWER") return;
try {
var dialog = document.querySelector("#playTopic-dialog");
if (!dialog || !dialog.__vue__) {
window.postMessage({ type: "ABC_ZHS_QUIZ_RESULT", success: false }, "*");
return;
}
var vm = dialog.__vue__.$parent.$parent;
if (!vm || !vm.topicInfo) {
window.postMessage({ type: "ABC_ZHS_QUIZ_RESULT", success: false }, "*");
return;
}
var dto = vm.topicInfo.lessonTestQuestionUseInterfaceDtos;
if (!dto || !dto[0]) {
window.postMessage({ type: "ABC_ZHS_QUIZ_RESULT", success: false }, "*");
return;
}
var options = dto[0].testQuestion.questionOptions;
var answers = dto[0].answerUs;
if (!options || !answers) {
window.postMessage({ type: "ABC_ZHS_QUIZ_RESULT", success: false }, "*");
return;
}
var count = 0;
options.forEach(function(opt, i) {
if (answers.indexOf(opt.sortUs) !== -1) {
setTimeout(function() { vm.topicClickQot(options[i]); }, 800 * count);
count++;
}
});
var closeDelay = Math.max(count * 800 + 2000, 5000);
setTimeout(function() {
vm.testDialog = false;
window.postMessage({ type: "ABC_ZHS_QUIZ_RESULT", success: true, count: count }, "*");
}, closeDelay);
} catch (err) {
window.postMessage({ type: "ABC_ZHS_QUIZ_RESULT", success: false, error: err.message }, "*");
}
});
})();
`;
(document.head || document.documentElement).appendChild(script);
script.remove();
};
const clearOverlays = () => {
var _a, _b;
(_a = document.querySelector(".v-modal")) == null ? void 0 : _a.remove();
(_b = document.querySelector(".brush-warning")) == null ? void 0 : _b.remove();
};
const isChapterCompleted = (el) => !!(el == null ? void 0 : el.querySelector(".time_icofinish"));
const waitForVideo = (timeout = 15e3) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const video = document.querySelector("video");
if (video) {
clearInterval(check);
resolve(video);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error("等待视频元素超时"));
}
}, 500);
});
};
const clickEl = (el) => {
if (el) {
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
};
const getCurrentChapterTitle = () => {
var _a, _b;
return ((_b = (_a = document.querySelector("li.video.current_play .catalogue_title")) == null ? void 0 : _a.innerText) == null ? void 0 : _b.trim()) || "未知小节";
};
const getChapterLabel = (chapterEl) => {
var _a, _b, _c, _d;
const title = ((_b = (_a = chapterEl.querySelector(".catalogue_title")) == null ? void 0 : _a.innerText) == null ? void 0 : _b.trim()) || "";
const num = ((_d = (_c = chapterEl.querySelector(".hour")) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) || "";
return num ? `${num} ${title}` : title;
};
const playVideo = () => {
clearOverlays();
videoEl = document.querySelector("#vjs_container_html5_api");
if (!videoEl) return;
videoEl.muted = true;
videoEl.play().catch(() => {
});
};
const startVideo = () => {
playVideo();
if (!videoEl) return;
videoEl.addEventListener("play", () => {
const title = getCurrentChapterTitle();
log.insertLog(`${title} - 视频开始播放`);
hookVedio(videoEl, 1.5);
log.insertLog(`已设置播放倍速: 1.5x`);
}, { once: true });
videoEl.addEventListener("playing", () => {
clearOverlays();
});
videoEl.addEventListener("pause", () => {
if (videoEl.ended || isAnsweringQuiz) return;
const dialog = document.querySelector("#playTopic-dialog");
if (!dialog) {
log.insertLog("视频被暂停,将在下次轮询中恢复..");
}
});
videoEl.addEventListener("ended", () => {
log.insertLog("当前视频播放完毕");
nextTask();
});
};
const handlePopupQuiz = async () => {
var _a;
if (isAnsweringQuiz) return;
isAnsweringQuiz = true;
await sleep(1);
const dialog = document.querySelector("#playTopic-dialog");
if (!dialog) {
isAnsweringQuiz = false;
return;
}
const headerText = ((_a = dialog.querySelector(".el-dialog__header h4")) == null ? void 0 : _a.innerText) || "";
if (!headerText.includes("给你出题啦") && !headerText.includes("弹题测验")) {
isAnsweringQuiz = false;
return;
}
const radioEl = dialog.querySelector(".radio");
const topicTitle = radioEl == null ? void 0 : radioEl.querySelector(".topic-title");
if (topicTitle && (topicTitle.querySelector(".right") || topicTitle.querySelector(".error"))) {
log.insertLog("弹题已作答过,关闭弹窗..");
await sleep(2);
closeQuizDialog();
isAnsweringQuiz = false;
await sleep(1);
playVideo();
return;
}
log.insertLog("检测到弹题测验,尝试自动作答..");
// 弹题前可能弹验证码,等用户过验证再继续,别硬点
const captchaPassed = await zhsWaitForYidun(log);
if (!captchaPassed) {
isAnsweringQuiz = false;
return;
}
return new Promise((resolve) => {
const timeout = setTimeout(() => {
window.removeEventListener("message", handler);
log.insertLog("弹题自动作答超时,关闭弹窗..", "warning");
closeQuizDialog();
isAnsweringQuiz = false;
setTimeout(playVideo, 1e3);
resolve();
}, 2e4);
const handler = async (e) => {
var _a2;
if (((_a2 = e.data) == null ? void 0 : _a2.type) !== "ABC_ZHS_QUIZ_RESULT") return;
clearTimeout(timeout);
window.removeEventListener("message", handler);
if (e.data.success) {
log.insertLog(`弹题已自动作答完成(${e.data.count}题),继续播放视频`);
} else {
log.insertLog(
`弹题自动作答失败${e.data.error ? ": " + e.data.error : ""},改用页面兜底作答..`,
"warning"
);
// 读不到页面数据时逐页随机点一个,保证视频不被弹窗卡住
const domHandled = await zhsHandleTopicDialogByDom(log);
if (!domHandled) closeQuizDialog();
}
isAnsweringQuiz = false;
setTimeout(playVideo, 1e3);
resolve();
};
window.addEventListener("message", handler);
window.postMessage({ type: "ABC_ZHS_QUIZ_ANSWER" }, "*");
});
};
const closeQuizDialog = () => {
const dialog = document.querySelector("#playTopic-dialog");
const closeBtn = dialog == null ? void 0 : dialog.querySelector(".el-dialog__headerbtn");
clickEl(closeBtn);
};
const handleMidwayDialogs = () => {
const dialogs = document.querySelectorAll(".el-dialog");
const visibleDialog = Array.from(dialogs).find(
(d) => {
var _a;
return ((_a = d == null ? void 0 : d.parentElement) == null ? void 0 : _a.style.display) !== "none";
}
);
if (!visibleDialog) {
clearOverlays();
return;
}
if (visibleDialog.ariaLabel === "温馨提示") {
const btn = visibleDialog.querySelector(".el-button.btn");
if ((btn == null ? void 0 : btn.textContent) === "我知道了") {
clickEl(btn);
log.insertLog("已关闭「温馨提示」弹窗");
setTimeout(playVideo, 500);
return;
}
}
if (visibleDialog.ariaLabel === "弹题测验") {
handlePopupQuiz();
}
};
const findNextChapter = () => {
const allChapters = Array.from(document.querySelectorAll("li.video"));
const currentChapter = document.querySelector("li.video.current_play");
if (!currentChapter || allChapters.length === 0) return null;
const currentIndex = allChapters.indexOf(currentChapter);
const ordered = [
...allChapters.slice(currentIndex + 1),
...allChapters.slice(0, currentIndex)
];
return ordered.find((el) => !isChapterCompleted(el));
};
const nextTask = async () => {
var _a;
if (!setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(
"自动切换章节未开启,前往设置模块中更改",
"warning"
);
return;
}
const currentTitle = ((_a = document.querySelector("li.video.current_play .catalogue_title")) == null ? void 0 : _a.innerText) || "当前章节";
log.insertLog(`「${currentTitle}」已完成`);
const next = findNextChapter();
if (next) {
const nextLabel = getChapterLabel(next);
log.insertLog(`跳转到: ${nextLabel}`);
clickEl(next);
await sleep(5);
initVideoTask();
} else {
reportCourseResult("done", "视频章节全部完成");
log.insertLog("所有章节视频已完成!");
clearPollInterval();
}
};
const startPolling = () => {
let lastHour = null;
pollIntervalId = setInterval(() => {
var _a;
const currentEl = document.querySelector(".video.current_play");
if (!currentEl) return;
const hour = (_a = currentEl.querySelector(".hour")) == null ? void 0 : _a.textContent;
if (hour && lastHour !== null && hour !== lastHour) {
lastHour = hour;
setTimeout(startVideo, 3e3);
}
if (hour) lastHour = hour;
if (isChapterCompleted(currentEl)) {
nextTask();
return;
}
const video = document.querySelector("#vjs_container_html5_api");
if (isAnsweringQuiz) return;
// 弹题只要出现就处理,不再依赖「视频刚好停在暂停状态」这个条件
if (zhsHasTopicDialog()) {
handlePopupQuiz();
return;
}
if ((video == null ? void 0 : video.paused) && !video.ended) {
handleMidwayDialogs();
}
}, 2e3);
};
const clearPollInterval = () => {
if (pollIntervalId) {
clearInterval(pollIntervalId);
pollIntervalId = null;
}
};
const initVideoTask = async () => {
try {
await waitForVideo();
startVideo();
} catch (e) {
log.insertLog(`${e.message},尝试跳转下一节..`, "warning");
nextTask();
}
};
const start = async () => {
var _a;
log.insertLog("进入智慧树视频学习页面,脚本开始工作..");
autoSetTab(setting, "1");
injectQuizHelper();
await sleep(5);
const noteClose = document.querySelector(".dialog-read .iconguanbi");
if (noteClose) {
const parent = (_a = noteClose.closest) == null ? void 0 : _a.call(noteClose, ".dialog-read");
if (parent && getComputedStyle(parent).display !== "none") {
clickEl(noteClose);
log.insertLog("已关闭阅读提示弹窗");
}
}
handleMidwayDialogs();
startPolling();
initVideoTask();
};
start();
};
const learnPageLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
let videoEl = null;
let currentResourceIndex = 0;
let resourceItems = [];
const waitFor = (selector, timeout = 1e4) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(check);
resolve(el);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error(`等待元素 ${selector} 超时`));
}
}, 300);
});
};
const clickEl = (el) => {
if (el) el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
};
const RESOURCE_TYPES = { VIDEO: "video", BOOK: "book", PPT: "other" };
const getResourceType = (resourceEl) => {
const iconBox = resourceEl == null ? void 0 : resourceEl.querySelector(".icon-box");
if (!iconBox) return "unknown";
const classes = iconBox.className;
if (classes.includes("video")) return RESOURCE_TYPES.VIDEO;
if (classes.includes("book")) return RESOURCE_TYPES.BOOK;
if (classes.includes("other") || classes.includes("ppt")) return RESOURCE_TYPES.PPT;
return "unknown";
};
const getAllResourceEls = () => Array.from(document.querySelectorAll(".resources-list .resources-item"));
const getCurrentResourceEl = () => {
const items = getAllResourceEls();
const idx = resourceItems[currentResourceIndex];
return items[idx] ?? null;
};
const detectCurrentResourceType = () => {
const el = getCurrentResourceEl();
el ? getResourceType(el) : "unknown";
};
const onResourceComplete = async () => {
currentResourceIndex++;
detectCurrentResourceType();
await processNextResource();
};
const processVideo = async () => {
log.insertLog("发现视频资源,正在播放..");
try {
await sleep(5);
videoEl = document.querySelector(".preview-section .video-player-wrapper video") || document.querySelector(".preview-section video");
if (!videoEl) {
log.insertLog("未找到视频元素,跳过..", "warning");
onResourceComplete();
return;
}
videoEl.muted = true;
videoEl.play().catch(() => {
});
const userRate = Number(setting.config.basicConfig.videoPlayrate.value) || 1;
const rates = [userRate, 2, 1.75, 1.5, 1.25];
for (const rate of rates) {
try {
videoEl.playbackRate = rate;
if (videoEl.playbackRate === rate) {
log.insertLog(`已设置播放倍速: ${rate}x`);
break;
}
} catch {
}
}
videoEl.addEventListener("pause", () => {
if (videoEl.ended) return;
if (videoEl.duration > 0 && videoEl.duration - videoEl.currentTime <= 1) {
log.insertLog("视频播放完毕");
onResourceComplete();
return;
}
setTimeout(() => {
if (videoEl && !videoEl.ended && videoEl.paused) {
videoEl.play().catch(() => {
});
}
}, 2e3);
});
videoEl.addEventListener("ended", () => {
log.insertLog("视频播放完毕");
onResourceComplete();
}, { once: true });
} catch (e) {
log.insertLog(`视频处理出错: ${e.message}`, "warning");
onResourceComplete();
}
};
const processPpt = async () => {
var _a;
log.insertLog("发现PPT资源,正在自动翻页..");
try {
await sleep(2);
const totalText = (_a = document.querySelector(
".preview-section .ppt-preview-container .page-total"
)) == null ? void 0 : _a.innerText;
const totalPages = parseInt(totalText, 10) || 0;
if (!totalPages) {
log.insertLog("无法获取PPT页数,跳过..", "warning");
onResourceComplete();
return;
}
const containers = document.querySelectorAll(
".catalogue-container .div-img-container"
);
let currentPage = Array.from(containers).findIndex((c) => c.classList.contains("choose")) + 1 || 1;
log.insertLog(`PPT共 ${totalPages} 页,从第 ${currentPage} 页开始`);
const flipPage = async (page) => {
if (page >= totalPages) {
log.insertLog("PPT翻页完成");
onResourceComplete();
return;
}
const nextBtn = document.querySelector(
".preview-section .ppt-preview-container .next-page-btn"
);
clickEl(nextBtn);
await sleep(5);
flipPage(page + 1);
};
flipPage(currentPage);
} catch (e) {
log.insertLog(`PPT处理出错: ${e.message}`, "warning");
onResourceComplete();
}
};
const processBook = async () => {
log.insertLog("发现电子书资源,正在自动阅读..");
try {
await waitFor(".preview-section .book-mark .book-content");
await waitFor(".preview-section .book-mark .canvas-wrapper");
const pages = document.querySelectorAll(
".preview-section .book-mark .canvas-wrapper"
);
if (!pages.length) {
log.insertLog("未找到电子书页面,跳过..", "warning");
onResourceComplete();
return;
}
const firstRect = pages[0].getBoundingClientRect();
const marginTop = parseInt(getComputedStyle(pages[0]).marginTop, 10) || 0;
const pageHeight = Math.max(firstRect.height + marginTop, 100);
const container = document.querySelector(
".preview-section .book-mark .book-container"
);
if (!container) {
log.insertLog("未找到电子书滚动容器,跳过..", "warning");
onResourceComplete();
return;
}
const scrollStep = async () => {
const target = Math.min(
container.scrollTop + pageHeight,
container.scrollHeight - container.clientHeight
);
container.scrollTo({ top: target, behavior: "smooth" });
await sleep(1.5);
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 2) {
log.insertLog("电子书阅读完成");
onResourceComplete();
} else {
scrollStep();
}
};
scrollStep();
} catch (e) {
log.insertLog(`电子书处理出错: ${e.message}`, "warning");
onResourceComplete();
}
};
const processNextResource = async () => {
if (currentResourceIndex >= resourceItems.length) {
log.insertLog("当前小节所有资源已完成");
nextSection();
return;
}
const el = getCurrentResourceEl();
if (!el) {
log.insertLog("未找到资源元素,跳过..", "warning");
onResourceComplete();
return;
}
if (currentResourceIndex > 0) {
const clickTarget = el.querySelector(".basic-info-video-card-container");
clickEl(clickTarget || el);
await sleep(3);
}
const type = getResourceType(el);
log.insertLog(
`处理资源 [${currentResourceIndex + 1}/${resourceItems.length}] 类型: ${type}`
);
switch (type) {
case RESOURCE_TYPES.VIDEO:
processVideo();
break;
case RESOURCE_TYPES.BOOK:
processBook();
break;
case RESOURCE_TYPES.PPT:
processPpt();
break;
default:
log.insertLog(`不支持的资源类型 (${type}),跳过`);
onResourceComplete();
}
};
const nextSection = async () => {
var _a, _b, _c, _d;
if (!setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(
"自动切换章节未开启,前往设置模块中更改",
"warning"
);
return;
}
const sections = document.querySelectorAll(".section-item-collapse-info");
const active = document.querySelector(".section-item-collapse-info.active");
if (!active) {
log.insertLog("未找到当前活跃章节");
return;
}
const currentTitle = ((_b = (_a = active.querySelector(".section-item-collapse-title")) == null ? void 0 : _a.innerText) == null ? void 0 : _b.trim()) || "当前章节";
log.insertLog(`「${currentTitle}」所有资源已完成`);
const arr = Array.from(sections);
const nextIdx = arr.indexOf(active) + 1;
if (nextIdx < arr.length) {
const next = arr[nextIdx];
const nextTitle = ((_d = (_c = next.querySelector(".section-item-collapse-title")) == null ? void 0 : _c.innerText) == null ? void 0 : _d.trim()) || "下一节";
log.insertLog(`跳转到: ${nextTitle}`);
clickEl(next);
await sleep(5);
initResources();
} else {
reportCourseResult("done", "学习资源全部完成");
log.insertLog("所有章节已完成!");
}
};
const initResources = async () => {
currentResourceIndex = 0;
resourceItems = [];
videoEl = null;
try {
await waitFor(".resources-detail-card .resources-list .resources-item", 15e3);
const items = getAllResourceEls();
if (!items.length) {
log.insertLog("未找到学习资源,尝试跳转下一节..", "warning");
nextSection();
return;
}
resourceItems = items.map((_, i) => i);
log.insertLog(`发现 ${resourceItems.length} 个学习资源`);
detectCurrentResourceType();
await processNextResource();
} catch (e) {
log.insertLog(`初始化资源列表失败: ${e.message}`, "warning");
nextSection();
}
};
const watchUrlChange = () => {
let prevUrl = location.href;
setInterval(() => {
if (location.href === prevUrl) return;
const wasLearnPage = prevUrl.includes("/learnPage");
prevUrl = location.href;
if (location.href.includes("/learnPage") && !wasLearnPage) {
log.insertLog("检测到页面切换,重新初始化..");
setTimeout(initResources, 3e3);
}
}, 1500);
};
const start = async () => {
log.insertLog("进入智慧树新版学习页面,脚本开始工作..");
autoSetTab(setting, "1");
await sleep(6);
const noteDialog = document.querySelector(".read-note-dialog");
if (noteDialog && getComputedStyle(noteDialog).display !== "none") {
const btn = noteDialog.querySelector("button");
if (btn) {
clickEl(btn);
log.insertLog("已关闭阅读提示弹窗");
}
}
try {
await waitFor(".preview-content", 15e3);
} catch {
log.insertLog("等待页面内容加载超时..", "warning");
}
watchUrlChange();
initResources();
};
start();
};
const aiSmartLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
let videoEl = null;
let currentResourceIndex = 0;
let resourceItems = [];
let currentResourceType = "";
let isStopped = false;
let quizTimer = null;
let isAnsweringQuiz = false;
let chapterObserver = null;
let resourceObserver = null;
const waitFor = (selector, timeout = 1e4) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(check);
resolve(el);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error(`等待元素 ${selector} 超时`));
}
}, 300);
});
};
const waitForHeight = (selector, timeout = 15e3) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const el = document.querySelector(selector);
if (el && el.offsetHeight > 0) {
resolve(el);
return;
}
if (Date.now() - startTime > timeout) {
reject(new Error("等待元素高度超时"));
return;
}
setTimeout(check, 100);
};
check();
});
};
const clickEl = (el) => {
if (el) el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
};
const RESOURCE_TYPES = { VIDEO: "video", PPT: "ppt" };
const getResourceType = (resourceEl) => {
var _a;
const fileType = resourceEl == null ? void 0 : resourceEl.querySelector(".file-type");
if (!fileType) return "unknown";
const secondClass = fileType.classList[1];
if (!secondClass) return "unknown";
const type = (_a = secondClass.split("-")) == null ? void 0 : _a[1];
if (type === "video") return RESOURCE_TYPES.VIDEO;
if (type === "ppt" || type === "other") return RESOURCE_TYPES.PPT;
return type || "unknown";
};
const getAllResourceEls = () => Array.from(document.querySelectorAll(".resources-list .resources-item"));
const getCurrentResourceEl = () => {
const items = getAllResourceEls();
const idx = resourceItems[currentResourceIndex];
return items[idx] ?? null;
};
const detectCurrentResourceType = () => {
const el = getCurrentResourceEl();
currentResourceType = el ? getResourceType(el) : "unknown";
};
const findVideoElement = async (retries = 3, interval = 500) => {
for (let i = 0; i < retries; i++) {
const video = document.querySelector("#container #vjs_container video");
if (video) return video;
await new Promise((r) => setTimeout(r, interval));
}
return null;
};
const cleanupVideo = () => {
if (videoEl) {
videoEl.removeEventListener("ended", onVideoEnded);
videoEl.removeEventListener("playing", onVideoPlaying);
videoEl.removeEventListener("pause", onVideoPause);
videoEl = null;
}
};
const onVideoPlaying = () => {
log.insertLog("视频正在播放中..");
};
const onVideoEnded = () => {
log.insertLog("视频播放完毕");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
};
const onVideoPause = () => {
if (!videoEl || videoEl.ended) return;
// 弹题弹窗开着时不要抢着播放,答完自然会继续
if (isAnsweringQuiz || zhsHasTopicDialog()) return;
log.insertLog("视频被暂停,尝试恢复..");
setTimeout(() => {
if (videoEl && !videoEl.ended && videoEl.paused) {
videoEl.play().catch(() => {
});
}
}, 2e3);
};
// AI 智慧课的弹题和旧版视频页是同一套弹窗,这里单独轮询,不依赖视频暂停事件
const watchPopupQuiz = () => {
if (quizTimer) return;
quizTimer = setInterval(async () => {
if (isAnsweringQuiz || !zhsHasTopicDialog()) return;
isAnsweringQuiz = true;
try {
const answered = await zhsAnswerTopicByVm(log);
if (!answered) await zhsHandleTopicDialogByDom(log);
} catch (e) {
log.insertLog(`弹题处理异常: ${e.message}`, "warning");
} finally {
isAnsweringQuiz = false;
if (videoEl && videoEl.paused && !videoEl.ended) {
videoEl.play().catch(() => {
});
}
}
}, 3e3);
};
const processVideo = async () => {
var _a, _b;
log.insertLog("发现视频资源,正在播放..");
try {
cleanupVideo();
videoEl = await findVideoElement();
if (!videoEl) {
log.insertLog("未找到视频元素,跳过..", "warning");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
videoEl.muted = true;
const muteHandler = () => {
if (videoEl) videoEl.muted = false;
};
(_a = document.body) == null ? void 0 : _a.addEventListener("click", muteHandler, { once: true });
(_b = document.querySelector("#container")) == null ? void 0 : _b.addEventListener("click", muteHandler, { once: true });
const userRate = Number(setting.config.basicConfig.videoPlayrate.value) || 1;
const rates = [userRate, 2, 1.75, 1.5, 1.25];
for (const rate of rates) {
try {
videoEl.playbackRate = rate;
if (videoEl.playbackRate === rate) {
log.insertLog(`已设置播放倍速: ${rate}x`);
break;
}
} catch {
}
}
videoEl.play().catch(() => {
videoEl.muted = true;
videoEl.play().catch(() => {
});
});
videoEl.addEventListener("playing", onVideoPlaying, { once: true });
videoEl.addEventListener("pause", onVideoPause);
videoEl.addEventListener("ended", onVideoEnded, { once: true });
} catch (e) {
log.insertLog(`视频处理出错: ${e.message}`, "warning");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
}
};
const getPptPageInfo = () => {
var _a, _b;
const container = document.querySelector(".ppt-btn-container");
if (!container) return [];
const current = parseInt(
(_a = container.querySelector(".page-content .page-current")) == null ? void 0 : _a.innerText,
10
) || 0;
const total = parseInt(
(_b = container.querySelector(".page-content .page-total")) == null ? void 0 : _b.innerText,
10
) || 0;
return current && total ? [current, total] : [];
};
const processPpt = async () => {
log.insertLog("发现PPT资源,正在自动翻页..");
try {
const pptContainer = document.querySelector(
".ppt-box .ppt-preview-container"
);
if (!pptContainer) {
log.insertLog("未找到PPT容器,跳过..", "warning");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
let [currentPage, totalPages] = getPptPageInfo();
if (!currentPage || !totalPages) {
log.insertLog("无法获取PPT页数,跳过..", "warning");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
if (currentPage >= totalPages) {
log.insertLog("PPT已翻到最后一页");
await sleep(2);
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
log.insertLog(`PPT共 ${totalPages} 页,从第 ${currentPage} 页开始`);
while (currentPage < totalPages) {
if (isStopped) break;
const nextBtn = pptContainer.querySelector(".next-page-btn");
clickEl(nextBtn);
await sleep(4);
const [newCurrent, newTotal] = getPptPageInfo();
currentPage = newCurrent;
totalPages = newTotal;
if (currentPage >= totalPages) {
log.insertLog("PPT翻页完成");
await sleep(2);
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
}
} catch (e) {
log.insertLog(`PPT处理出错: ${e.message}`, "warning");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
}
};
const processNextResource = async () => {
if (currentResourceIndex >= resourceItems.length) {
log.insertLog("当前小节所有资源已完成");
nextSection();
return;
}
const el = getCurrentResourceEl();
if (!el) {
log.insertLog("未找到资源元素,跳过..", "warning");
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
if (currentResourceIndex > 0) {
const type = getResourceType(el);
if (!["video", "ppt"].includes(type)) {
log.insertLog(`不支持的资源类型 (${type}),跳过`);
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
return;
}
const clickTarget = el.querySelector(".basic-info-video-card-container");
clickEl(clickTarget || el);
await sleep(3);
}
detectCurrentResourceType();
log.insertLog(
`处理资源 [${currentResourceIndex + 1}/${resourceItems.length}] 类型: ${currentResourceType}`
);
switch (currentResourceType) {
case RESOURCE_TYPES.VIDEO:
processVideo();
break;
case RESOURCE_TYPES.PPT:
processPpt();
break;
default:
log.insertLog(`不支持的资源类型 (${currentResourceType}),跳过`);
currentResourceIndex++;
detectCurrentResourceType();
processNextResource();
}
};
const getAllChapters = () => Array.from(document.querySelectorAll(".inner-li li.clearfix")).filter(
(el) => !el.classList.contains("child-chapter") && !el.classList.contains("forbidClick")
);
const nextSection = async () => {
var _a, _b, _c, _d;
if (!setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(
"自动切换章节未开启,前往设置模块中更改",
"warning"
);
return;
}
const chapters = getAllChapters();
const active = document.querySelector(".clearfix.video.activeNode");
if (!active) {
log.insertLog("未找到当前活跃章节");
return;
}
const currentTitle = ((_b = (_a = active.querySelector(".title")) == null ? void 0 : _a.innerText) == null ? void 0 : _b.trim()) || "当前章节";
log.insertLog(`「${currentTitle}」所有资源已完成`);
const currentIdx = chapters.indexOf(active);
const next = chapters[currentIdx + 1];
if (next) {
const nextTitle = ((_d = (_c = next.querySelector(".title")) == null ? void 0 : _c.innerText) == null ? void 0 : _d.trim()) || "下一节";
log.insertLog(`跳转到: ${nextTitle}`);
next.click();
next.scrollIntoView({ behavior: "smooth" });
} else {
reportCourseResult("done", "章节全部完成");
log.insertLog("所有章节已完成!");
}
};
const observeChapterList = () => {
const listEl = document.querySelector(".chapterList-box .list");
if (!listEl) return;
chapterObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== "attributes" || mutation.attributeName !== "class")
continue;
const target = mutation.target;
const isActive = target.classList.contains("activeNode") || target.classList.contains("active");
if (isActive) {
onChapterChange();
}
}
});
chapterObserver.observe(listEl, {
attributes: true,
attributeFilter: ["class"],
subtree: true,
childList: false
});
};
const observeResourceList = () => {
const resourceListEl = document.querySelector(
".resource-warp .resources-list"
);
if (!resourceListEl) return;
resourceObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== "attributes" || mutation.attributeName !== "class")
continue;
const target = mutation.target;
const isActive = target.classList.contains("active");
if (isActive) {
onChapterChange();
}
}
});
resourceObserver.observe(resourceListEl, {
attributes: true,
attributeFilter: ["class"],
subtree: true,
childList: false
});
};
const onChapterChange = async (isActive) => {
{
await sleep(5);
isStopped = false;
await initResourcesFromDom();
}
};
const initResourcesFromDom = async () => {
if (resourceObserver) resourceObserver.disconnect();
try {
await waitFor(".resource-warp .resources-list .resources-item", 15e3);
const items = getAllResourceEls();
if (!items.length) {
log.insertLog("未找到学习资源,尝试跳转下一节..", "warning");
nextSection();
return;
}
const activeIdx = Array.from(items).findIndex(
(el) => el.classList.contains("active")
);
currentResourceIndex = activeIdx >= 0 ? activeIdx : 0;
resourceItems = items.map((_, i) => i);
log.insertLog(`发现 ${resourceItems.length} 个学习资源`);
observeResourceList();
detectCurrentResourceType();
await processNextResource();
} catch (e) {
log.insertLog(`初始化资源列表失败: ${e.message}`, "warning");
nextSection();
}
};
const dismissPopups = () => {
const popup = document.querySelector(".courseRemind.khfaPop");
const closeBtn = popup == null ? void 0 : popup.querySelector(
".header-slot-box .el-icon-error"
);
if (closeBtn) {
closeBtn.click();
log.insertLog("已关闭课程提醒弹窗");
}
};
const watchUrlChange = () => {
let prevUrl = location.href;
setInterval(() => {
if (location.href === prevUrl) return;
prevUrl = location.href;
if (location.href.includes("ai-smart-course-student-pro.zhihuishu.com") || location.href.includes("fusioncourseh5.zhihuishu.com") || location.href.includes("smartcoursestudent.zhihuishu.com")) {
log.insertLog("检测到页面切换,重新初始化..");
setTimeout(() => initAll(), 3e3);
}
}, 1500);
};
const initAll = async () => {
try {
await waitForHeight(".preview-warp");
} catch {
log.insertLog("等待页面内容加载超时..", "warning");
}
dismissPopups();
observeChapterList();
await sleep(5);
await initResourcesFromDom();
};
const start = async () => {
log.insertLog("进入智慧树AI智慧课学习页面,脚本开始工作..");
autoSetTab(setting, "1");
await sleep(6);
watchUrlChange();
watchPopupQuiz();
await initAll();
};
start();
};
const smartStudyLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
const RESOURCE_TYPES = {
VIDEO: "video",
BOOK: "book",
PPT: "ppt",
TEXT: "text",
LINK: "link",
UNKNOWN: "unknown"
};
let currentRunId = 0;
let videoEl = null;
let urlWatcher = null;
const waitFor = (selector, timeout = 1e4) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(check);
resolve(el);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error(`等待元素 ${selector} 超时`));
}
}, 300);
});
};
const waitForMedia = (timeout = 5e3) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const media = document.querySelector("video,audio");
if (media && media.src) {
clearInterval(check);
resolve(media);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error("未找到音视频元素"));
}
}, 200);
});
};
const clickEl = (el) => {
if (!el) return;
if (typeof el.click === "function") {
el.click();
return;
}
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
};
const getVideoRate = () => {
const rate = Number(setting.config.basicConfig.videoPlayrate.value);
return Math.min(rate || 1, 16);
};
const getResourceCards = () => Array.from(document.querySelectorAll('[class*="card-container"]'));
const getActiveCard = () => getResourceCards().find((card) => card.classList.contains("active")) || null;
const getCardTitleEl = (card) => (card == null ? void 0 : card.querySelector(".common-text")) || (card == null ? void 0 : card.querySelector(".video-title")) || (card == null ? void 0 : card.querySelector(".basic-info-video-card-container")) || card;
const getResourceType = (card) => {
const iconBox = card == null ? void 0 : card.querySelector(".icon-box");
const classes = (iconBox == null ? void 0 : iconBox.className) || "";
if (classes.includes("video")) return RESOURCE_TYPES.VIDEO;
if (classes.includes("book")) return RESOURCE_TYPES.BOOK;
if (classes.includes("ppt") || classes.includes("other")) return RESOURCE_TYPES.PPT;
if (classes.includes("text")) return RESOURCE_TYPES.TEXT;
if (classes.includes("link")) return RESOURCE_TYPES.LINK;
return RESOURCE_TYPES.UNKNOWN;
};
const isCardFinished = (card) => {
var _a;
const finishedIcon = card == null ? void 0 : card.querySelector(".finished-icon");
if (!finishedIcon) return false;
const text = ((_a = finishedIcon.textContent) == null ? void 0 : _a.trim()) || "";
return !text || text.includes("已完成");
};
const getCurrentChapterName = () => {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
const sectionTitle = ((_b = (_a = document.querySelector(".point-title-text")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) || ((_d = (_c = document.querySelector(".section-item-collapse-info.active .section-item-collapse-title")) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) || "未知章节";
const cardTitle = ((_g = (_f = (_e = getActiveCard()) == null ? void 0 : _e.querySelector(".video-title")) == null ? void 0 : _f.textContent) == null ? void 0 : _g.trim()) || ((_j = (_i = (_h = getActiveCard()) == null ? void 0 : _h.querySelector(".common-text")) == null ? void 0 : _i.textContent) == null ? void 0 : _j.trim()) || "未知小节";
return `${sectionTitle}-${cardTitle}`;
};
const getNextCard = () => {
const cards = getResourceCards();
if (!cards.length) return null;
const activeIndex = cards.findIndex((card) => card.classList.contains("active"));
const startIndex = activeIndex >= 0 ? activeIndex + 1 : 0;
for (let index = startIndex; index < cards.length; index++) {
const card = cards[index];
if (!isCardFinished(card)) return card;
}
return null;
};
const parseRequiredProgress = (section) => {
var _a;
const text = ((_a = section == null ? void 0 : section.querySelector(".collapse-info-progress .progress-text")) == null ? void 0 : _a.textContent) || "";
const match = text.replace(/\s/g, "").match(/(\d+)\/(\d+)/);
if (!match) return null;
const progress = parseInt(match[1], 10);
const total = parseInt(match[2], 10);
if (Number.isNaN(progress) || Number.isNaN(total)) return null;
return { progress, total };
};
const getNextSection = () => {
var _a;
const sections = Array.from(
document.querySelectorAll(".section-item-collapse-info")
);
const active = sections.find((section) => section.classList.contains("active"));
const unfinishedSections = sections.map((section) => ({ section, progress: parseRequiredProgress(section) })).filter(({ progress }) => progress && progress.progress < progress.total);
if (!unfinishedSections.length) return null;
const activeIndex = unfinishedSections.findIndex(
({ section }) => section === active
);
if (activeIndex >= 0) {
return ((_a = unfinishedSections[activeIndex + 1]) == null ? void 0 : _a.section) || null;
}
return unfinishedSections[0].section;
};
const setCardActive = (card) => {
getResourceCards().forEach((item) => item.classList.remove("active"));
card == null ? void 0 : card.classList.add("active");
};
const clickResourceCard = async (card) => {
const type = getResourceType(card);
const target = getCardTitleEl(card);
if (type !== RESOURCE_TYPES.UNKNOWN) {
clickEl(target);
return;
}
setCardActive(card);
const pageWindow = globalThis.unsafeWindow || window;
const originalOpen = pageWindow.open;
try {
pageWindow.open = () => null;
clickEl(target);
await sleep(3);
} finally {
pageWindow.open = originalOpen;
}
};
const cleanupVideo = () => {
if (!videoEl) return;
videoEl.onpause = null;
videoEl.onended = null;
videoEl = null;
};
const processNext = async (runId) => {
var _a, _b, _c, _d;
if (runId !== currentRunId) return;
const nextCard = getNextCard();
if (nextCard) {
const title = ((_b = (_a = nextCard.querySelector(".common-text")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) || ((_d = (_c = nextCard.querySelector(".video-title")) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) || "下一资源";
log.insertLog(`跳转到资源: ${title}`);
await clickResourceCard(nextCard);
await sleep(3);
await doWork();
return;
}
await nextSection(runId);
};
const processVideo = async (runId) => {
try {
const wrapper = document.querySelector(".video-player-wrapper");
if (!wrapper || wrapper.style.display === "none") {
throw new Error("视频未加载");
}
videoEl = await waitForMedia();
if (runId !== currentRunId) return;
videoEl.currentTime = Math.max(videoEl.currentTime, 1);
videoEl.volume = 0;
videoEl.muted = true;
videoEl.playbackRate = getVideoRate();
log.insertLog(`已设置播放倍速: ${videoEl.playbackRate}x`);
videoEl.play().catch(() => {
videoEl.muted = true;
videoEl.play().catch(() => {
});
});
log.insertLog(`正在学习: ${getCurrentChapterName()}`);
videoEl.onpause = async () => {
if (runId !== currentRunId || !videoEl || videoEl.ended) return;
await sleep(1);
videoEl == null ? void 0 : videoEl.play().catch(() => {
});
};
videoEl.onended = async () => {
if (runId !== currentRunId) return;
log.insertLog("视频播放完毕,即将跳转下一资源");
await sleep(3);
await processNext(runId);
};
} catch (e) {
log.insertLog(`未找到学习视频,即将跳转下一资源: ${e.message}`, "warning");
await sleep(3);
await processNext(runId);
}
};
const processPpt = async (runId) => {
try {
const container = document.querySelector(".preview-section .ppt-preview-container");
if (!container) {
await processPassiveResource(runId, "PPT资源");
return;
}
const getPageInfo = () => {
var _a, _b;
const current2 = parseInt((_a = container.querySelector(".page-current")) == null ? void 0 : _a.textContent, 10) || 0;
const total2 = parseInt((_b = container.querySelector(".page-total")) == null ? void 0 : _b.textContent, 10) || 0;
return [current2, total2];
};
let [current, total] = getPageInfo();
if (!current || !total) {
await processPassiveResource(runId, "PPT资源");
return;
}
log.insertLog(`PPT共 ${total} 页,从第 ${current} 页开始`);
while (runId === currentRunId && current < total) {
clickEl(container.querySelector(".next-page-btn"));
await sleep(4);
[current, total] = getPageInfo();
}
log.insertLog("PPT翻页完成");
await processNext(runId);
} catch (e) {
log.insertLog(`PPT处理出错: ${e.message}`, "warning");
await processNext(runId);
}
};
const processBook = async (runId) => {
try {
await waitFor(".preview-section .book-mark .book-container", 5e3);
const container = document.querySelector(
".preview-section .book-mark .book-container"
);
if (!container) {
await processPassiveResource(runId, "电子书资源");
return;
}
log.insertLog("发现电子书资源,正在阅读..");
while (runId === currentRunId && container.scrollTop + container.clientHeight < container.scrollHeight - 2) {
container.scrollTo({
top: Math.min(
container.scrollTop + Math.max(container.clientHeight * 0.8, 200),
container.scrollHeight
),
behavior: "smooth"
});
await sleep(1.5);
}
log.insertLog("电子书阅读完成");
await processNext(runId);
} catch {
await processPassiveResource(runId, "电子书资源");
}
};
const processPassiveResource = async (runId, label) => {
if (runId !== currentRunId) return;
log.insertLog(`${label}已处理,即将跳转下一资源`);
await sleep(3);
await processNext(runId);
};
async function doWork() {
const runId = ++currentRunId;
cleanupVideo();
await sleep(5);
if (runId !== currentRunId) return;
let card = getActiveCard();
if (card && isCardFinished(card)) {
log.insertLog("当前资源已完成,跳过..");
card = getNextCard();
if (!card) {
await nextSection(runId);
return;
}
await clickResourceCard(card);
await sleep(3);
card = getActiveCard() || card;
}
if (!card) {
card = getNextCard();
if (!card && getResourceCards().length) {
await nextSection(runId);
return;
}
if (card) {
await clickResourceCard(card);
await sleep(3);
card = getActiveCard() || card;
}
}
if (!card) {
log.insertLog("未找到学习资源,尝试跳转下一章节..", "warning");
await nextSection(runId);
return;
}
const type = getResourceType(card);
log.insertLog(`处理资源类型: ${type}`);
switch (type) {
case RESOURCE_TYPES.VIDEO:
await processVideo(runId);
break;
case RESOURCE_TYPES.BOOK:
await processBook(runId);
break;
case RESOURCE_TYPES.PPT:
await processPpt(runId);
break;
case RESOURCE_TYPES.TEXT:
await processPassiveResource(runId, "文本资源");
break;
case RESOURCE_TYPES.LINK:
case RESOURCE_TYPES.UNKNOWN:
default:
await processPassiveResource(runId, "当前资源");
}
}
async function nextSection(runId) {
var _a, _b;
if (runId !== currentRunId) return;
const next = getNextSection();
if (!next) {
reportCourseResult("done", "必学章节全部完成");
log.insertLog("所有必学章节已完成!");
return;
}
const title = ((_b = (_a = next.querySelector(".section-item-collapse-title")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) || "下一章节";
log.insertLog(`跳转到: ${title}`);
clickEl(next);
await sleep(5);
await doWork();
}
const dismissPopups = () => {
const noteDialog = document.querySelector(".read-note-dialog");
if (noteDialog && getComputedStyle(noteDialog).display !== "none") {
const btn = noteDialog.querySelector("button");
if (btn) {
clickEl(btn);
log.insertLog("已关闭阅读提示弹窗");
}
}
};
const watchUrlChange = () => {
if (urlWatcher) clearInterval(urlWatcher);
let prevUrl = location.href;
urlWatcher = setInterval(() => {
if (location.href === prevUrl) return;
prevUrl = location.href;
if (location.href.includes("/learnPage")) {
log.insertLog("检测到页面切换,重新初始化..");
doWork();
}
}, 1500);
};
const start = async () => {
log.insertLog("进入智慧树新形态课程学习页面,脚本开始工作..");
autoSetTab(setting, "1");
if (location.href.includes("/singleCourse")) {
log.insertLog("请点击任意章节开始学习,进入学习页后将自动处理..", "warning");
return;
}
await sleep(6);
dismissPopups();
try {
await waitFor(".section-item-collapse-info", 15e3);
} catch {
log.insertLog("等待章节列表加载超时..", "warning");
}
watchUrlChange();
await doWork();
};
start();
};
const studyPlusLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
let videoEl = null;
let quizTimer = null;
let finishTimer = null;
let visibilityTimer = null;
let isSwitching = false;
let isHandlingQuiz = false;
let visibilityWarned = false;
let isAllFinished = false;
const waitFor = (target, timeout = 1e4, interval = 300) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const result = typeof target === "function" ? target() : document.querySelector(target);
if (result) {
clearInterval(check);
resolve(result);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error("等待页面内容加载超时"));
}
}, interval);
});
};
const clickEl = async (el) => {
var _a, _b, _c;
if (!el) return false;
(_a = el.scrollIntoView) == null ? void 0 : _a.call(el, { behavior: "smooth", block: "center" });
await sleep(0.2);
(_b = el.click) == null ? void 0 : _b.call(el);
(_c = el.dispatchEvent) == null ? void 0 : _c.call(el, new MouseEvent("click", { bubbles: true }));
return true;
};
const getRate = () => {
const rate = Number(setting.config.basicConfig.videoPlayrate.value);
return Math.min(rate || 1, 16);
};
const getCourseName = () => {
var _a, _b;
const text = (_b = (_a = document.querySelector(".top-back-box > span:nth-child(2), .course-name")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim();
return (text == null ? void 0 : text.replace(/^课程名称[::]\s*/, "")) || "未知课程";
};
const getChapterItems = () => Array.from(document.querySelectorAll(".child-main")).filter(
(el) => {
var _a;
return (_a = el.parentElement) == null ? void 0 : _a.querySelector(".child-time");
}
);
const getCurrentChapter = () => getChapterItems().find((el) => el.classList.contains("current"));
const getChapterName = (item) => {
var _a, _b;
return ((_b = (_a = item == null ? void 0 : item.parentElement) == null ? void 0 : _a.textContent) == null ? void 0 : _b.replace(/\s+/g, " ").trim()) || "未知章节";
};
const isChapterFinished = (item) => {
var _a;
return !!((_a = item == null ? void 0 : item.parentElement) == null ? void 0 : _a.querySelector(".finish-icon"));
};
const getNextChapter = () => {
const items = getChapterItems().filter((item) => !isChapterFinished(item));
if (!items.length) return null;
const current = getCurrentChapter();
const currentIndex = items.indexOf(current);
return currentIndex >= 0 ? items[currentIndex + 1] : items[0];
};
const cleanupVideo = () => {
if (!videoEl) return;
videoEl.onpause = null;
videoEl.onended = null;
videoEl = null;
};
const handleQuizDialog = async () => {
if (isHandlingQuiz) return;
const dialog = document.querySelector(".ai-test-question-wrapper");
if (!dialog) return;
isHandlingQuiz = true;
try {
const done = dialog.querySelector(".done");
const closeBtn = dialog.querySelector(".close-box");
if (done) {
await clickEl(closeBtn);
return;
}
const options = Array.from(dialog.querySelectorAll(".options .option"));
if (options.length) {
// 随堂练习也可能弹验证码,等用户过完再点
const captchaPassed = await zhsWaitForYidun(log);
if (!captchaPassed) return;
const option = options[Math.floor(Math.random() * options.length)];
await clickEl(option);
await sleep(1);
}
await clickEl(document.querySelector(".submit-btn .submits"));
await sleep(1);
await clickEl(closeBtn);
log.insertLog("随堂练习弹窗已处理");
} finally {
await sleep(1);
isHandlingQuiz = false;
}
};
const fixControls = () => {
const bar = document.querySelector(".controlsBar");
if (!bar) return;
bar.style.display = "block";
bar.style.zIndex = "2";
};
const playVideo = async () => {
cleanupVideo();
try {
fixControls();
videoEl = await waitFor(() => {
const media = document.querySelector("video, audio");
return (media == null ? void 0 : media.src) ? media : null;
}, 15e3);
videoEl.currentTime = Math.max(videoEl.currentTime, 1);
videoEl.volume = 0;
videoEl.muted = true;
videoEl.playbackRate = getRate();
log.insertLog(`正在学习:${getChapterName(getCurrentChapter())}`);
log.insertLog(`已设置播放倍速: ${videoEl.playbackRate}x`);
videoEl.play().catch(() => {
});
videoEl.onpause = () => {
if (!videoEl || videoEl.ended || isSwitching || isHandlingQuiz) return;
setTimeout(() => {
var _a;
return (_a = videoEl == null ? void 0 : videoEl.play) == null ? void 0 : _a.call(videoEl).catch(() => {
});
}, 1e3);
};
videoEl.onended = async () => {
if (isSwitching) return;
await sleep(2);
await nextChapter();
};
} catch (error) {
log.insertLog(`${error.message || "视频加载失败"},尝试切换下一节..`, "warning");
await sleep(2);
await nextChapter();
}
};
const nextChapter = async () => {
if (isSwitching) return;
if (isAllFinished) return;
if (!setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(
"自动切换章节未开启,前往设置模块中更改",
"warning"
);
return;
}
isSwitching = true;
cleanupVideo();
const next = getNextChapter();
if (!next) {
// 记住已经刷完,避免每 2 秒重复检查、重复打印同一句日志
isAllFinished = true;
reportCourseResult("done", "章节视频全部完成");
log.insertLog("所有章节视频已完成!");
if (finishTimer) clearInterval(finishTimer);
finishTimer = null;
isSwitching = false;
return;
}
log.insertLog(`跳转到: ${getChapterName(next)}`);
await clickEl(next);
await sleep(5);
isSwitching = false;
playVideo();
};
const startPolling = () => {
quizTimer = setInterval(handleQuizDialog, 3e3);
finishTimer = setInterval(() => {
if (isSwitching) return;
const current = getCurrentChapter();
if (current && isChapterFinished(current)) nextChapter();
}, 2e3);
visibilityTimer = setInterval(() => {
if (document.visibilityState === "hidden" && !visibilityWarned) {
visibilityWarned = true;
log.insertLog("页面未保持前台,学习流程可能无法正常推进..", "warning");
} else if (document.visibilityState === "visible") {
visibilityWarned = false;
}
}, 1e3);
};
const start = async () => {
log.insertLog(`进入智慧树新共享课学习页面(${getCourseName()}),脚本开始工作..`);
autoSetTab(setting, "1");
await sleep(5);
try {
await waitFor(".child-main", 2e4);
} catch {
log.insertLog("等待章节列表加载超时,尝试继续处理..", "warning");
}
document.querySelectorAll(".el-overlay,.el-dialog").forEach((dialog) => {
dialog.style.display = "none";
});
startPolling();
playVideo();
};
start();
return () => {
cleanupVideo();
if (quizTimer) clearInterval(quizTimer);
if (finishTimer) clearInterval(finishTimer);
if (visibilityTimer) clearInterval(visibilityTimer);
};
};
const TYPE_TEXT_MAP = [
{ keywords: ["多选", "多项", "X型", "不定项"], type: "多选题" },
{ keywords: ["判断", "是非"], type: "判断题" },
{ keywords: ["填空", "完形", "完型"], type: "填空题" },
{ keywords: ["简答"], type: "简答题" },
{ keywords: ["论述"], type: "论述题" },
{ keywords: ["计算"], type: "计算题" },
{ keywords: ["单选", "单项"], type: "单选题" }
];
const QUESTION_TYPE_CODE_MAP = {
单选题: "0",
单项选择题: "0",
单选: "0",
多选题: "1",
多项选择题: "1",
多选: "1",
填空题: "2",
填空: "2",
判断题: "3",
判断: "3",
是非题: "3",
简答题: "4",
简答: "4",
名词解释: "5",
论述题: "6",
论述: "6",
计算题: "7",
计算: "7"
};
const PROFILES = [
{
name: "新形态正式考试",
root: ".question-area-content",
type: [
"div.flex.items-center.mb-\\[16px\\]",
"[class*='question-type']",
"[class*='mb-[16px]']"
],
title: [
"div.flex-1 .mb-\\[32px\\] .text-mainText.font-medium",
".text-mainText.font-medium",
"[class*='text-mainText'][class*='font-medium']",
"[class*='question'][class*='title']"
],
options: [
"label.user-select.group",
"label[class*='user-select']",
"label.group",
"[role='radio']",
"[role='checkbox']"
],
beforeParse: async () => {
var _a;
const firstButton = document.querySelector(".grid.grid-cols-7 > button") || Array.from(document.querySelectorAll("button")).find(
(button) => {
var _a2;
return ((_a2 = button.textContent) == null ? void 0 : _a2.trim()) === "1";
}
);
if (firstButton) {
firstButton.click();
}
const startTime = Date.now();
while (Date.now() - startTime < 1e4) {
const root = document.querySelector(".question-area-content");
if ((_a = root == null ? void 0 : root.textContent) == null ? void 0 : _a.trim()) return;
await sleep(0.3);
}
},
nextButton: () => Array.from(document.querySelectorAll("button")).find(
(button) => {
var _a;
return (_a = button.textContent) == null ? void 0 : _a.includes("下一题");
}
),
useAnswerSheet: true
},
{
name: "新形态作业考试",
root: ".questionBox",
type: ".questionTit",
title: ".questionContent",
options: ".optionUl label",
nextButton: "span.Topicswitchingbtn:nth-child(2)"
},
{
name: "新形态掌握度",
root: ".questionContent",
type: ".questionName",
title: ".questionName .centent-pre",
options: ".radio-view li.clearfix, .checkbox-views label.el-checkbox, .fillAnswer",
// 最后一题时按钮仍在,只是多了 noNext 类;以前没写翻页按钮,这页只答第一题就收工
nextButton: () => {
const button = document.querySelector(".pre-next .next-topic");
return button && !button.classList.contains("noNext") ? button : null;
}
},
{
name: "AI掌握度",
root: ".exam-item",
type: ".quest-type",
title: ".quest-title .option-name",
options: "label"
}
];
const smartAnswerLogic = async () => {
var _a, _b;
const log = useLogStore();
const setting = useSettingStore();
const questionStore = useQuestionStore();
if (!setting.config.basicConfig.autoAnswer.value) {
log.insertLog(
`自动答题已关闭,请前往设置模块中更改`
);
return;
}
const cleanText = (text = "") => String(text ?? "").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/\s+/g, " ").trim();
const normalize = (text = "") => cleanText(text).replace(/^[A-Z][.、.]\s*/i, "").replace(/[^\p{L}\p{N}√×]/gu, "").toLowerCase();
const htmlOf = (el) => (el == null ? void 0 : el.innerHTML) || (el == null ? void 0 : el.textContent) || "";
const getByKeys = (obj, keys) => {
if (!obj || typeof obj !== "object") return "";
const key = keys.find((item) => obj[item] != null && obj[item] !== "");
return key ? obj[key] : "";
};
const parseJsonLike = (value) => {
if (typeof value !== "string") return value;
const text = value.trim();
if (!/^[\[{]/.test(text)) return value;
try {
return JSON.parse(text);
} catch {
return value;
}
};
const normalizePayload = (payload) => {
const visited = /* @__PURE__ */ new WeakSet();
const normalizeNode = (node) => {
const parsed = parseJsonLike(node);
if (!parsed || typeof parsed !== "object") return parsed;
if (visited.has(parsed)) return parsed;
visited.add(parsed);
if (Array.isArray(parsed)) return parsed.map(normalizeNode);
Object.keys(parsed).forEach((key) => {
parsed[key] = normalizeNode(parsed[key]);
});
return parsed;
};
return normalizeNode(payload);
};
const normalizeOption = (option) => ({
text: cleanText(
getByKeys(option, [
"option",
"content",
"optionContent",
"optionText",
"optionName",
"optionTitle",
"answer",
"answerContent",
"answerText",
"name",
"text",
"title",
"label",
"value"
])
),
html: String(
getByKeys(option, ["content", "optionContent", "optionName", "name", "text"]) || ""
)
});
const extractOptionList = (obj) => {
const optionList = getByKeys(obj, [
"questionOptions",
"questionOption",
"options",
"optionList",
"questionOptionList",
"questionOptionDTOList",
"questionOptionsList",
"optionContentList",
"answerOptions",
"answerOptionList",
"answerList",
"answerDtos",
"answerDTOList",
"answers",
"optionVos",
"optionVOList",
"items"
]);
if (!Array.isArray(optionList)) return [];
return optionList.map(normalizeOption).filter((option) => option.text);
};
const normalizeApiType = (typeText) => {
const text = String(typeText || "");
const matched = TYPE_TEXT_MAP.find(
({ keywords }) => keywords.some((keyword) => text.includes(keyword))
);
if (matched) return matched.type;
if (QUESTION_TYPE_CODE_MAP[text]) return text;
const codeMap = {
1: "单选题",
2: "多选题",
3: "判断题",
4: "填空题",
5: "简答题",
14: "填空题"
};
return codeMap[text] || "单选题";
};
const collectAnswerSheetMetaQuestions = (payload) => {
var _a2;
const normalizedPayload = normalizePayload(payload);
const parts = (_a2 = normalizedPayload == null ? void 0 : normalizedPayload.data) == null ? void 0 : _a2.partSheetVos;
if (!Array.isArray(parts)) return [];
return parts.flatMap(
(part) => (part.questionSheetVos || []).map((question) => ({
type: normalizeApiType(question.questionType || part.name),
sort: Number(question.sort) || 0,
questionId: question.questionId,
version: question.version,
partName: part.name
}))
).sort((a, b) => a.sort - b.sort);
};
const collectAnswerSheetQuestions = (payload) => {
const metaQuestions = collectAnswerSheetMetaQuestions(payload);
if (metaQuestions.length) return metaQuestions;
const questions2 = [];
const visited = /* @__PURE__ */ new WeakSet();
const normalizedPayload = normalizePayload(payload);
const titleKeys = [
"questionName",
"questionTitle",
"questionContent",
"questionContentText",
"questionText",
"questionStem",
"questionStemContent",
"questionStemRich",
"questionStemRichText",
"questionDescribe",
"questionDesc",
"questionBody",
"stemContent",
"stemText",
"stem",
"title",
"name",
"content",
"contentText",
"topicContent",
"topicName"
];
const typeKeys = [
"questionTypeName",
"questionType",
"questionTypeStr",
"questionTypeText",
"questionTypeCode",
"questionTypeId",
"questionTypeDto",
"questionTypeDTO",
"typeName",
"type"
];
const walk = (node) => {
if (!node || typeof node !== "object") return;
if (visited.has(node)) return;
visited.add(node);
if (Array.isArray(node)) {
node.forEach(walk);
return;
}
const rawTitle = getByKeys(node, titleKeys);
const title = cleanText(
typeof rawTitle === "object" ? getByKeys(rawTitle, ["name", "title", "content", "text", "value"]) : rawTitle
);
const options = extractOptionList(node);
if (title && (options.length || title.length > 6)) {
const optionsObject = {};
options.forEach((option) => {
optionsObject[option.text] = option.html || option.text;
});
questions2.push({
type: normalizeApiType(
(() => {
const typeValue = getByKeys(node, typeKeys);
return typeof typeValue === "object" ? getByKeys(typeValue, ["name", "title", "text", "value", "code", "id"]) : typeValue;
})()
),
title,
optionsText: options.map((option) => option.text),
options: optionsObject
});
}
Object.values(node).forEach(walk);
};
walk(normalizedPayload);
const seen = /* @__PURE__ */ new Set();
return questions2.filter((question) => {
const key = `${question.title}|${question.optionsText.join("|")}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
};
const waitForAnswerSheetQuestions = async () => {
const pageWindow = _unsafeWindow || window;
if (pageWindow[ANSWER_SHEET_KEY]) {
return collectAnswerSheetQuestions(pageWindow[ANSWER_SHEET_KEY]);
}
return new Promise((resolve) => {
const timer = setTimeout(() => resolve([]), 1e4);
pageWindow.addEventListener(
"ABC_ZHS_ANSWER_SHEET",
(event) => {
clearTimeout(timer);
resolve(collectAnswerSheetQuestions(event.detail));
},
{ once: true }
);
});
};
const describePayloadShape = (payload) => {
const normalizedPayload = normalizePayload(payload);
const topKeys = normalizedPayload && typeof normalizedPayload === "object" ? Object.keys(normalizedPayload).slice(0, 12) : [];
const candidateKeys = [];
const visited = /* @__PURE__ */ new WeakSet();
const walk = (node) => {
if (!node || typeof node !== "object" || candidateKeys.length >= 5) return;
if (visited.has(node)) return;
visited.add(node);
if (Array.isArray(node)) {
node.slice(0, 5).forEach(walk);
return;
}
const keys = Object.keys(node);
if (keys.some((key) => /question|option|answer|stem|title|content/i.test(key))) {
candidateKeys.push(keys.slice(0, 16).join(","));
}
Object.values(node).forEach(walk);
};
walk(normalizedPayload);
return {
topKeys,
candidateKeys
};
};
const toSelectorList = (selectors) => Array.isArray(selectors) ? selectors : [selectors].filter(Boolean);
const queryAllSafe = (root, selectors) => {
const results = [];
toSelectorList(selectors).forEach((selector) => {
try {
results.push(...root.querySelectorAll(selector));
} catch {
}
});
return Array.from(new Set(results));
};
const queryOneSafe = (root, selectors) => queryAllSafe(root, selectors)[0] || null;
const resolveType = (root, profile2) => {
var _a2;
const rawText = ((_a2 = queryOneSafe(root, profile2.type)) == null ? void 0 : _a2.textContent) || root.textContent || "";
const matched = TYPE_TEXT_MAP.find(
({ keywords }) => keywords.some((keyword) => rawText.includes(keyword))
);
const domMulti = detectMultiChoiceDom(root);
if (matched == null ? void 0 : matched.type) {
// 题型文字写的是单选、页面却是复选框:以页面控件为准,否则多选只答一项
if (domMulti && matched.type === "单选题") return "多选题";
return matched.type;
}
if (domMulti) return "多选题";
return "单选题";
};
const toApiQuestion = (question) => ({
...question,
type: QUESTION_TYPE_CODE_MAP[question.type] || question.type || "999"
});
const getActiveProfile = async () => {
const startTime = Date.now();
// 页面异步加载,题目区可能晚一些才出现,等 45 秒
while (Date.now() - startTime < 4.5e4) {
const profile2 = PROFILES.find((item) => document.querySelector(item.root));
if (profile2) return profile2;
await sleep(0.3);
}
return null;
};
// 翻页后题干是异步渲染的,等标题出现再解析,避免解析到空题面
// 图片题没有文字标题,题目区里出现图片也算就绪
const waitForQuestionTitle = async (profile2, timeout = 1e4) => {
var _a2;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const root = document.querySelector(profile2.root);
if (root && cleanText((_a2 = queryOneSafe(root, profile2.title)) == null ? void 0 : _a2.textContent)) return;
if (root && root.querySelector && root.querySelector("img")) return;
await sleep(0.3);
}
};
const parseQuestions = (profile2) => {
const roots = Array.from(document.querySelectorAll(profile2.root));
return roots.map((root) => {
const optionEls = queryAllSafe(root, profile2.options).filter((option) => {
if (cleanText(option.textContent)) return true;
return !!(option.querySelector && option.querySelector("img"));
});
const optionsText = optionEls.map((option, index) => {
const t = cleanText(option.textContent);
if (t) return t;
const img = option.querySelector && option.querySelector("img");
const src = img ? (img.getAttribute("data-src") || img.currentSrc || img.src || "") : "";
return src ? `
` : `选项${String.fromCharCode(65 + index)}`;
});
const options = {};
optionEls.forEach((option, index) => {
options[optionsText[index]] = htmlOf(option);
});
const titleEl = queryOneSafe(root, profile2.title);
let title = cleanText(htmlOf(titleEl));
const titleHasImg = !!(titleEl && titleEl.querySelector && titleEl.querySelector("img"));
if (!title && titleHasImg) title = "[图片题]";
if (!title) {
const optionTextSet = new Set(optionsText.map((option) => normalize(option)));
title = queryAllSafe(root, "div, p, span").filter((el) => el.getClientRects().length > 0).map((el) => cleanText(el.textContent)).filter((text) => text.length > 4).filter((text) => !optionTextSet.has(normalize(text))).filter(
(text) => !TYPE_TEXT_MAP.some(
({ keywords }) => keywords.some((keyword) => text === keyword || text === `${keyword}题`)
)
).sort((a, b) => b.length - a.length)[0] || "";
}
return {
element: root,
type: resolveType(root, profile2),
title,
optionsText,
options,
optionEls,
optionSelector: profile2.options,
answer: { code: -1, answer: [] },
workType: "zhs",
refer: location.href,
id: "zhs"
};
});
};
const isChoiceType = (type) => ["单选题", "多选题", "判断题"].includes(type);
const fillBlank = (question, answers) => {
const inputs = Array.from(
question.element.querySelectorAll("input, textarea, .fillAnswer input, .fillAnswer textarea")
);
if (!inputs.length) return false;
let values = (answers || []).map((a) => String(a == null ? "" : a).trim()).filter(Boolean);
if (values.length < inputs.length) {
const flat = [];
values.forEach((v) => v.split(/[,,、;;|]+/).forEach((p) => {
if (p.trim()) flat.push(p.trim());
}));
if (flat.length > values.length) values = flat;
}
let filledCount = 0;
inputs.forEach((input, index) => {
const value = values[index];
if (!value) return;
input.value = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
if (input.value === value) filledCount += 1;
});
return filledCount > 0 && filledCount === inputs.length;
};
const isInputChecked = (i) => i.checked === true || (i.getAttribute && i.getAttribute("checked")) != null;
const choiceOptionSelected = (el) => {
var _b;
if (!el) return false;
const inputs = el.querySelectorAll ? [...el.querySelectorAll('input[type="radio"], input[type="checkbox"]')] : [];
if (inputs.some((i) => isInputChecked(i))) return true;
if (((_b = el.getAttribute) == null ? void 0 : _b.call(el, "aria-checked")) === "true") return true;
const tokens = String(el.className || "").toLowerCase().split(/\s+/);
return tokens.some((t) => t === "is-checked" || t === "ischecked" || t === "checked");
};
const choiceOptionSig = (el) => {
var _b;
const inputs = el.querySelectorAll ? [...el.querySelectorAll('input[type="radio"], input[type="checkbox"]')] : [];
return [
String(el.className || ""),
((_b = el.getAttribute) == null ? void 0 : _b.call(el, "aria-checked")) || "",
inputs.map((i) => isInputChecked(i) ? 1 : 0).join("")
].join("|");
};
// 答案可能是「A. 正文, B. 正文…」这样被拼成一条的:先按「字母+分隔符」拆成多项,
// 否则多选题只会认到其中一项(其余选项匹配不上,表现就是只勾一个)
const normalizeChoiceAnswerList = (answers) => {
const list = (Array.isArray(answers) ? answers : [answers]).map((a) => String(a == null ? "" : a).trim()).filter(Boolean);
// 每一项都试着拆:后端偶尔会把两三选项挤在同一条里("D.甲 E.乙"),只拆整条拼串的那种会漏
const splitItem = (text) => {
const parts = text.split(/(?=[A-Za-z]\s*[.、.))::]\s*\S)/)
.map((s) => s.replace(/^[,,、;;\s]+/, "").trim()).filter(Boolean);
const allPrefixed = parts.length > 1 && parts.every((p) => /^[A-Za-z]\s*[.、.))::]/.test(p));
return allPrefixed ? parts : [text];
};
const out = [];
list.forEach((item) => {
splitItem(item).forEach((part) => {
if (!out.includes(part)) out.push(part);
});
});
return out;
};
const fillChoice = async (question, answers) => {
var _a2;
answers = normalizeChoiceAnswerList(answers);
const optionSelector = question.optionSelector || "";
const pickOptions = (root) => queryAllSafe(root, optionSelector).filter((option) => {
if (cleanText(option.textContent)) return true;
return !!(option.querySelector && option.querySelector("img"));
});
const optionEls = ((_a2 = question.optionEls) == null ? void 0 : _a2.length) ? question.optionEls : Array.from(
(question.element || document).querySelectorAll(
".optionUl label, label.user-select.group, .radio-view li.clearfix, .checkbox-views label.el-checkbox, label, [role='radio'], [role='checkbox']"
)
);
const questionRoot = () => {
const root = question.element;
return root && root.isConnected !== false ? root : document;
};
// 页面每勾一项都可能重新渲染,点击前按索引重新取一次,避免点在被替换掉的旧节点上(旧节点点了没反应,就会漏选)
const resolveOption = (idx) => {
const root = questionRoot();
if (optionSelector && root && root.querySelectorAll) {
const live = pickOptions(root);
if (live.length === optionEls.length && live[idx]) return live[idx];
}
const cached = optionEls[idx];
if (cached && cached.isConnected !== false) return cached;
const want = normalize((cached && cached.textContent) || "");
if (optionSelector && want) {
const hit = queryAllSafe(document, optionSelector).find((node) => normalize(node.textContent) === want);
if (hit) return hit;
}
return null;
};
let selected = false;
const normalizedAnswers = answers.map((answer) => normalize(answer));
const judgementAnswer = normalizedAnswers[0];
// 收集答案中的图片 URL:提取
(超星 CDN 部分图 URL 无扩展名)+ 带扩展名的裸 URL
const answerImgs = [];
answers.forEach(a => {
const s = String(a || "");
const imgSrcRe = /
]+src=["']([^"']+)["']/gi;
let imgM;
while ((imgM = imgSrcRe.exec(s))) { if (!answerImgs.includes(imgM[1])) answerImgs.push(imgM[1]); }
const bareImg = s.match(/https?:\/\/[^\s"'<>]+\.(?:jpg|jpeg|png|gif|webp)/i);
if (bareImg && !answerImgs.includes(bareImg[0])) answerImgs.push(bareImg[0]);
});
// 保留原始答案文本用于子串匹配
const rawAnswers = answers.map(a => String(a).trim()).filter(Boolean);
// 纯字母答案收集:["A","B","D"] 或紧凑串 ["ABD"] → 按索引映射(A=第1个选项)
// 紧凑串仅在不等于任何选项文本时才按字母拆,避免把单词选项误拆成字母
const letterAnswers = [];
let letterItemCount = 0;
answers.forEach(a => {
const t = String(a || "").trim();
// "A. 以人民安全为宗旨" / "B、政治安全" 这类「字母 + 分隔符 + 选项正文」,
// 直接按字母取;不这么处理就只能靠正文匹配,多选题经常只中最后一项
const prefixed = t.match(/^([A-Za-z])\s*[.、.))::]\s*(\S[\s\S]*)$/);
if (prefixed) {
const idx = prefixed[1].toUpperCase().charCodeAt(0) - 65;
const opt = optionEls[idx];
const optText = opt ? normalize(opt.textContent) : "";
const body = normalize(prefixed[2]);
// 字母与选项正文对得上才按字母点,防止选项顺序被打乱时点错
if (!optText || !body || optText.includes(body) || body.includes(optText)) {
const letter = prefixed[1].toUpperCase();
if (!letterAnswers.includes(letter)) letterAnswers.push(letter);
letterItemCount += 1;
}
} else if (/^[A-Za-z]$/.test(t)) {
if (!letterAnswers.includes(t.toUpperCase())) letterAnswers.push(t.toUpperCase());
letterItemCount += 1;
} else if (/^[A-Z]{2,10}$/.test(t) && [...t].every((c, i) => i === 0 || c > t[i - 1])) {
const isOptionText = optionEls.some(op => normalize(op.textContent) === normalize(t));
if (!isOptionText) {
t.toUpperCase().split("").forEach(c => { if (!letterAnswers.includes(c)) letterAnswers.push(c); });
letterItemCount += 1;
}
}
});
// 逐字母映射:越界的那个字母匹配不到选项自然跳过,不再一票否决整组字母答案
const letterMappingOk = letterAnswers.length > 0;
// 答案每一条都自带字母时就以字母为准。否则文字包含匹配会把「C.社会组织和公民个人」也算进来
// (它的文字正好是 B 选项的一部分),单选题就会出现「答案是 B、脚本却勾了 C」
const useLettersOnly = letterItemCount > 0 && letterItemCount >= answers.length;
const isMultiChoice = question.type === "多选题" || /多选|X型|不定项/.test(question.type || "");
// 最长公共片段占较短一方的比例:答案和选项文字只有标点、截断、全半角这类小差异时也能对上
const lcsRatio = (a, b) => {
if (!a || !b) return 0;
const shortText = a.length <= b.length ? a : b;
const longText = a.length <= b.length ? b : a;
let best = 0;
for (let i = 0; i < shortText.length; i += 1) {
for (let j = i + best + 1; j <= shortText.length; j += 1) {
const piece = shortText.slice(i, j);
if (longText.includes(piece)) best = piece.length;
else break;
}
}
return best / shortText.length;
};
// 先算出「应该勾哪几项」,再统一去点:多选题点一下是勾、点两下是取消,
// 边点边判很容易把已经勾上的又点掉,最后就缺项
const targetIdx = new Set();
optionEls.forEach((option, idx) => {
const optionText = normalize(option.textContent);
const isLetterMatched = letterMappingOk && letterAnswers.includes(String.fromCharCode(65 + idx));
if (useLettersOnly) {
if (isLetterMatched) targetIdx.add(idx);
return;
}
const isJudgementMatched = question.type === "判断题" && (/^(正确|是|对|√|t|true|right)$/i.test(judgementAnswer) && /^(正确|是|对|√|t|true|right)$/i.test(optionText) || /^(错误|否|错|×|f|false|wrong)$/i.test(judgementAnswer) && /^(错误|否|错|×|f|false|wrong)$/i.test(optionText));
if (isLetterMatched || normalizedAnswers.includes(optionText) || isJudgementMatched) {
targetIdx.add(idx);
return;
}
// 子串匹配兜底:答案可能含任何标点无法拆分,用选项文本在答案中查找包含关系
if (optionText && optionText.length >= 2 && rawAnswers.some(raw => normalize(raw).includes(optionText))) {
targetIdx.add(idx);
return;
}
// 图片匹配:按 URL 最后一段(去查询串/扩展名)比对,兼容超星无扩展名 CDN 图
if (answerImgs.length > 0) {
const getImgKey = (url) => {
const clean = String(url || "").split(/[?#]/)[0].replace(/\/+$/, "");
const seg = (clean.split("/").pop() || clean).replace(/\.(jpg|jpeg|png|gif|webp)$/i, "");
return seg.toLowerCase();
};
// 短 key(如 latex 公式图的数字段)只允许完全相等,防止 includes 误撞(912 vs 9120)
const imgKeyMatch = (a, b) => a === b || (a.length >= 8 && b.length >= 8 && (a.includes(b) || b.includes(a)));
const optionImgs = Array.from(option.querySelectorAll('img')).map(img => img.src || img.getAttribute('data-src') || '').filter(Boolean);
let matched = false;
for (const answerImg of answerImgs) {
if (matched) break;
const answerKey = getImgKey(answerImg);
for (const optionImg of optionImgs) {
const optionKey = getImgKey(optionImg);
if (answerKey && optionKey && imgKeyMatch(answerKey, optionKey)) {
matched = true;
break;
}
}
}
if (matched) {
targetIdx.add(idx);
return;
}
}
// 模糊兜底:答案文字和选项文字差一点点(截断、标点、全半角、多空格)就漏选,
// 多选题最容易出现「答了 5 个只勾上 3 个」,这里按公共片段占比再认一次
if (optionText.length >= 4 && normalizedAnswers.some((answer) => answer.length >= 4 && lcsRatio(optionText, answer) >= 0.85)) {
targetIdx.add(idx);
}
});
const letterOf = (idx) => String.fromCharCode(65 + idx);
const lettersFrom = (set) => Array.from(set).map(letterOf).sort().join("");
const indexes = optionEls.map((_, idx) => idx);
// 页面到底能不能读出「已勾选」:只要能读到一个勾上的,就说明读得准,后面才敢补点
let stateReadable = false;
const readSelected = (idx) => {
const el = resolveOption(idx);
if (!el) return false;
const selectedNow = choiceOptionSelected(el);
if (selectedNow) stateReadable = true;
return selectedNow;
};
const clickOnce = (idx) => {
const el = resolveOption(idx);
if (!el) return false;
try {
el.click();
return true;
} catch (_) {
return false;
}
};
// 第一遍:每个该勾的项只点一次,绝不补点,避免多选题被点成取消
for (const idx of indexes) {
if (!targetIdx.has(idx)) continue;
if (readSelected(idx)) continue;
clickOnce(idx);
await sleep(0.15 + Math.random() * 0.1);
}
// 复核一遍:个别选项点了没反应(页面重绘把点击吃掉)时,只对「确实还是没勾上」的补点一次。
// 页面读取不出勾选态时不做补点,否则会把已经勾上的又点掉。
if (stateReadable) {
const missing = Array.from(targetIdx).filter((idx) => !readSelected(idx));
for (const idx of missing) {
clickOnce(idx);
await sleep(0.2);
}
}
// 多选题:把不该勾的取消掉(同样只对确实勾着的动手)
if (isMultiChoice) {
for (const idx of indexes) {
if (targetIdx.has(idx)) continue;
if (!readSelected(idx)) continue;
clickOnce(idx);
await sleep(0.15);
}
}
const targets = lettersFrom(targetIdx);
const selectedNow = indexes.filter((idx) => readSelected(idx)).map(letterOf).join("");
const actual = selectedNow;
if (targets && actual && actual !== targets) {
log.insertLog(`本题答案 ${targets},实际只点上 ${actual}${targets.length > actual.length ? "(有选项没点上去)" : ""}`, "warning", 6);
}
return Array.from(targetIdx).some((idx) => actual.includes(letterOf(idx))) || (targets.length > 0 && actual.length === 0);
};
const fillQuestion = async (question) => {
var _a2;
const answers = ((_a2 = question.answer) == null ? void 0 : _a2.answer) || [];
if (!answers.length) return false;
if (isChoiceType(question.type)) return fillChoice(question, answers);
return fillBlank(question, answers);
};
const searchAndFill = async (question) => {
const { answerMode } = setting.config.basicConfig;
const mode = answerMode.value;
let handled = false;
let bankFilled = false;
const apiQuestion = toApiQuestion(question);
if (mode !== "ai") {
const answerData = await getAccurateAnswer(apiQuestion);
if (answerData.code === 1 && answerData.data.multimatch) {
// 手动选择模式:题库有多道相似题,交给用户挑,不自动填也不转 AI
question.answer = { code: 0, answer: [], src: "pick", msg: "存在多道相似题,请先手动选择后再填写", candidates: answerData.data.candidates };
const _idx = useQuestionStore().questionList.indexOf(question);
if (getManualWaitMode() === "skip") {
useLogStore().insertLog(`本题有${answerData.data.candidates.length}道相似题,已按设置跳过(本轮不自动提交)`, "warning", 6);
return false;
}
useLogStore().insertLog(`本题有${answerData.data.candidates.length}道相似题,已暂停,等待你选择后继续…`, "warning", 8);
const picked = _idx >= 0 ? await waitForManualPick(_idx) : null;
if (!picked) {
useLogStore().insertLog("本题等待选择超时,已跳过(本轮不自动提交)", "warning", 6);
return false;
}
question.answer = Object.assign({}, question.answer, { code: 1, answer: picked });
if (await fillQuestion(question)) {
bankFilled = true;
handled = true;
}
return true;
}
if (answerData.code === 1) {
question.answer = { code: 1, answer: answerData.data.answer, src: answerData.data.src };
setting.userInfo.remainCount = formatLocalRemainCount(answerData.data.remainCount);
saveAnswerRecord({
question: apiQuestion,
answer: answerData.data.answer,
source: "accurate"
});
if (await fillQuestion(question)) {
bankFilled = true;
handled = true;
} else {
question.answer.code = 0;
question.answer.msg = "答案拿到了但没点上去";
}
} else if (mode === "questionBank" || answerData.code !== -1004) {
// 错误情况:不将 msg 设为 answer,避免将"包月权益生效"等提示当作答案显示
question.answer = { code: answerData.code, answer: [], msg: answerData.msg };
handled = true;
}
}
if (!handled) {
if (isChoiceType(question.type)) {
const aiAnswerData = await getAIAnswer(apiQuestion);
if (aiAnswerData.code === 1) {
question.answer = { code: 2, answer: aiAnswerData.data.answer, src: aiAnswerData.data.src || "ai" };
setting.userInfo.remainCount = formatLocalRemainCount(aiAnswerData.data.remainCount);
saveAnswerRecord({
question: apiQuestion,
answer: aiAnswerData.data.answer,
source: "ai"
});
} else if (!(question.answer && question.answer.code === 0 && Array.isArray(question.answer.answer) && question.answer.answer.length)) {
// 错误情况:不将 msg 设为 answer,避免将错误提示当作答案显示
question.answer = { code: aiAnswerData.code, answer: [], msg: aiAnswerData.msg };
} else if (!question.answer.msg) {
question.answer.msg = "答案拿到了但没点上去";
}
} else {
question.answer = { code: -1, answer: ["该题型不支持AI答题"] };
}
}
if (bankFilled) return true;
if ([1, 2].includes(question.answer.code)) {
const filled = await fillQuestion(question);
if (!filled) question.answer.code = 0;
return filled;
}
return false;
};
const profile = await getActiveProfile();
if (!profile) {
log.insertLog("未识别到支持的题目区域,请确认页面已加载完成..", "warning");
return;
}
log.insertLog(`进入${profile.name}页面,脚本开始工作..`);
autoSetTab(setting, "1");
questionStore.accuracy = -1;
await ((_a = profile.beforeParse) == null ? void 0 : _a.call(profile));
const answerSheetQuestions = profile.useAnswerSheet ? await waitForAnswerSheetQuestions() : [];
if (answerSheetQuestions.length) {
log.insertLog(`已从接口读取到${answerSheetQuestions.length}个题目元信息..`);
} else if (profile.useAnswerSheet) {
const answerSheetPayload = (_unsafeWindow || window)[ANSWER_SHEET_KEY];
const { topKeys, candidateKeys } = answerSheetPayload ? describePayloadShape(answerSheetPayload) : { topKeys: [], candidateKeys: [] };
log.insertLog(
answerSheetPayload ? `已捕获考试接口但未提取到题目,顶层字段:${topKeys.join(",") || "无"};候选字段:${candidateKeys.join(" | ") || "无"}` : "未捕获到考试题目信息接口,将回退到页面 DOM 解析..",
"warning"
);
}
const questions = [];
let correctNum = 0;
let emptyTries = 0;
const handleCurrentPage = async () => {
var _a2;
await waitForQuestionTitle(profile);
let rawQuestions = parseQuestions(profile);
if (answerSheetQuestions.length) {
const apiQuestion = answerSheetQuestions[questions.length];
const domQuestion = rawQuestions[0] || {};
rawQuestions = apiQuestion ? [
{
...domQuestion,
...apiQuestion,
type: detectMultiChoiceDom(domQuestion.element)
? "多选题"
: (normalizeQuestionTypeName(apiQuestion.type || domQuestion.type) || "单选题"),
title: apiQuestion.title || domQuestion.title,
optionsText: ((_a2 = apiQuestion.optionsText) == null ? void 0 : _a2.length) ? apiQuestion.optionsText : domQuestion.optionsText || [],
options: Object.keys(apiQuestion.options || {}).length ? apiQuestion.options : domQuestion.options || {},
element: domQuestion.element || document,
optionEls: domQuestion.optionEls || [],
answer: { code: -1, answer: [] },
workType: "zhs",
refer: location.href,
id: apiQuestion.questionId || "zhs"
}
] : [];
}
const pageQuestions = rawQuestions.filter((question) => question.title || (question.element && question.element.querySelector && question.element.querySelector("img")));
if (!pageQuestions.length) {
// 题目是接口异步拉来的,前面几次解析不到很正常,只在第一次记详细情况,别刷屏
if (emptyTries === 0) {
const rootTexts = Array.from(document.querySelectorAll(profile.root)).map(
(root) => {
var _a3;
return (_a3 = root.textContent) == null ? void 0 : _a3.replace(/\s+/g, " ").trim().slice(0, 80);
}
);
const buttons = Array.from(document.querySelectorAll("button")).map((button) => {
var _a3;
return (_a3 = button.textContent) == null ? void 0 : _a3.replace(/\s+/g, " ").trim();
}).filter(Boolean).slice(0, 20);
log.insertLog(
`题目区域数量:${rawQuestions.length},选项数量:${rawQuestions.reduce(
(sum, question) => sum + question.optionsText.length,
0
)},题目区域文本:${rootTexts.join(" | ") || "空"},按钮:${buttons.join(" / ") || "无"}`,
"warning"
);
}
emptyTries += 1;
}
if (!pageQuestions.length) return false;
questions.push(...pageQuestions);
questionStore.questionList = [...questions];
log.insertLog(`当前页面解析到${pageQuestions.length}个题目..`);
for (const question of pageQuestions) {
const questionIndex = questions.indexOf(question);
questionStore.currentQuestionIndex = questionIndex;
questionStore.questionList = [...questions];
const filled = await searchAndFill(question);
if (filled) correctNum += 1;
questionStore.currentQuestionIndex = questionIndex + 1;
questionStore.questionList = [...questions];
await sleep(1.5 + Math.random() * 2.5);
}
return true;
};
autoSetTab(setting, "2");
await sleep(1 + Math.random() * 2);
let previousTitle = "";
let parsedOnce = false;
// 这版考试页是进页面之后才用接口拉题的:题目区先出现、内容后到。
// 以前解析一次为空就直接收工,表现就是「明明有题却提示未检测到试题」,必须手动刷新页面。
// 现在首题没解析到就每秒重试,最多等 60 秒。
const parseDeadline = Date.now() + 6e4;
while (true) {
const parsed = await handleCurrentPage();
if (!parsed) {
if (!parsedOnce && Date.now() < parseDeadline) {
await sleep(1);
continue;
}
break;
}
parsedOnce = true;
if (!profile.nextButton) break;
const nextButton = typeof profile.nextButton === "function" ? profile.nextButton() : document.querySelector(profile.nextButton);
if (!nextButton || nextButton.classList.contains("disabled")) break;
const currentTitle = ((_b = questions[questions.length - 1]) == null ? void 0 : _b.title) || "";
if (currentTitle && currentTitle === previousTitle) break;
previousTitle = currentTitle;
nextButton.click();
await sleep(1.5 + Math.random() * 2);
}
if (!questions.length) {
log.insertLog("等了 60 秒仍未解析到题目,请确认页面已加载完成,或手动刷新一次..", "warning");
return;
}
questionStore.currentQuestionIndex = questions.length;
questionStore.accuracy = (correctNum / questions.length * 100).toFixed(1);
reportAnswerSummary("zhs-h5", questions);
log.insertLog(
`答题完毕,正确率为${questionStore.accuracy}%,详情请前往答题模块查看..`
);
// 智慧树作业/考试脚本从不代替交卷,提醒一句,免得以为已经交掉了
log.insertLog("已填完全部题目,请核对答案后手动提交", "warning");
setTimeout(() => {
autoSetTab(setting, "1");
}, 2e3);
};
const specialStudyLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
let videoEl = null;
let finishTimer = null;
let visibilityTimer = null;
let isSwitching = false;
let visibilityWarned = false;
const pageProfiles = [
{
name: "校内课",
list: ".file-item",
current: ".file-item.active",
title: "#sourceTit",
done: ".icon-finish",
media: "#mediaPlayer, video, audio",
needsStudy: (item) => !item.querySelector(".icon-finish")
},
{
name: "教学空间AI智慧课程",
list: ".source-icon",
current: ".active-file",
title: ".active-file",
done: "i.select",
media: "video, audio",
mapItem: (icon) => {
var _a;
return (_a = icon.parentElement) == null ? void 0 : _a.parentElement;
},
needsStudy: (item) => !item.querySelector("i.select")
},
{
name: "教学空间AI智慧课程",
list: ".el-tree-node",
current: ".el-tree-node.is-current",
title: ".file-name",
done: "label.success",
media: "video, audio",
filter: (item) => {
var _a;
return !((_a = item.querySelector(".el-tree-node__children")) == null ? void 0 : _a.children.length);
},
needsStudy: (item) => !item.querySelector("label.success")
}
];
const waitFor = (target, timeout = 1e4, interval = 300) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const result = typeof target === "function" ? target() : document.querySelector(target);
if (result) {
clearInterval(check);
resolve(result);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error("等待页面内容加载超时"));
}
}, interval);
});
};
const clickEl = async (el) => {
var _a, _b, _c;
if (!el) return false;
(_a = el.scrollIntoView) == null ? void 0 : _a.call(el, { behavior: "smooth", block: "center" });
await sleep(0.2);
(_b = el.click) == null ? void 0 : _b.call(el);
(_c = el.dispatchEvent) == null ? void 0 : _c.call(el, new MouseEvent("click", { bubbles: true }));
return true;
};
const getProfile = () => pageProfiles.find((profile) => document.querySelector(profile.list));
const getItems = (profile) => Array.from(document.querySelectorAll(profile.list)).map((item) => {
var _a;
return ((_a = profile.mapItem) == null ? void 0 : _a.call(profile, item)) || item;
}).filter(Boolean).filter((item) => !profile.filter || profile.filter(item));
const getCurrentItem = (profile) => {
var _a;
return ((_a = document.querySelector(profile.current)) == null ? void 0 : _a.closest(profile.list)) || getItems(profile).find((item) => {
var _a2;
return (_a2 = item.matches) == null ? void 0 : _a2.call(item, profile.current);
}) || null;
};
const getTitle = (item, profile) => {
var _a, _b, _c, _d, _e;
return ((_b = (_a = item == null ? void 0 : item.querySelector(profile.title)) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) || ((_d = (_c = document.querySelector(profile.title)) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) || ((_e = item == null ? void 0 : item.textContent) == null ? void 0 : _e.trim()) || "未知章节";
};
const getReadableDocuments = () => {
const docs = [document];
Array.from(document.querySelectorAll("iframe")).forEach((iframe) => {
try {
if (iframe.contentDocument) docs.push(iframe.contentDocument);
} catch {
}
});
return docs;
};
const isMediaElement = (el) => ["VIDEO", "AUDIO"].includes(el == null ? void 0 : el.tagName);
const hasPlayableSource = (media) => {
var _a;
return Boolean(
(media == null ? void 0 : media.currentSrc) || (media == null ? void 0 : media.src) || ((_a = media == null ? void 0 : media.querySelector) == null ? void 0 : _a.call(media, "source")) || (media == null ? void 0 : media.readyState) > 0
);
};
const findMediaElement = (selector) => {
var _a;
for (const doc of getReadableDocuments()) {
const candidates = Array.from(doc.querySelectorAll(selector));
for (const candidate of candidates) {
const media = isMediaElement(candidate) ? candidate : (_a = candidate.querySelector) == null ? void 0 : _a.call(candidate, "video, audio");
if (media && (hasPlayableSource(media) || isMediaElement(media))) {
return media;
}
}
const fallback = doc.querySelector("video, audio");
if (fallback && (hasPlayableSource(fallback) || isMediaElement(fallback))) {
return fallback;
}
}
return null;
};
const getNextItem = (profile) => {
const items = getItems(profile).filter((item) => profile.needsStudy(item));
if (!items.length) return null;
const current = getCurrentItem(profile);
const currentIndex = items.indexOf(current);
return currentIndex >= 0 ? items[currentIndex + 1] : items[0];
};
const cleanupVideo = () => {
if (!videoEl) return;
videoEl.onpause = null;
videoEl.onended = null;
videoEl = null;
};
const playMedia = async (profile) => {
var _a;
cleanupVideo();
try {
videoEl = await waitFor(() => {
return findMediaElement(profile.media);
}, 15e3);
videoEl.volume = 0;
videoEl.muted = true;
log.insertLog(`正在学习:${getTitle(getCurrentItem(profile), profile)}`);
(_a = videoEl.play) == null ? void 0 : _a.call(videoEl).catch(() => {
});
videoEl.onpause = () => {
if (!videoEl || videoEl.ended || isSwitching) return;
setTimeout(() => {
var _a2;
return (_a2 = videoEl == null ? void 0 : videoEl.play) == null ? void 0 : _a2.call(videoEl).catch(() => {
});
}, 1e3);
};
videoEl.onended = async () => {
await sleep(3);
await nextItem(profile);
};
} catch (error) {
log.insertLog(`${error.message || "未找到音视频"},尝试切换下一节..`, "warning");
await sleep(3);
await nextItem(profile);
}
};
const nextItem = async (profile) => {
if (isSwitching) return;
if (!setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(
"自动切换章节未开启,前往设置模块中更改",
"warning"
);
return;
}
isSwitching = true;
cleanupVideo();
const next = getNextItem(profile);
if (!next) {
reportCourseResult("done", "章节视频全部完成");
log.insertLog("所有章节视频已完成!");
isSwitching = false;
return;
}
log.insertLog(`跳转到: ${getTitle(next, profile)}`);
await clickEl(next);
await sleep(5);
isSwitching = false;
await playMedia(profile);
};
const startPolling = (profile) => {
finishTimer = setInterval(() => {
if (isSwitching) return;
const current = getCurrentItem(profile);
if (current && !profile.needsStudy(current)) nextItem(profile);
}, 2e3);
visibilityTimer = setInterval(() => {
if (document.visibilityState === "hidden" && !visibilityWarned) {
visibilityWarned = true;
log.insertLog("页面未保持前台,学习流程可能无法正常推进..", "warning");
} else if (document.visibilityState === "visible") {
visibilityWarned = false;
}
}, 1e3);
};
const start = async () => {
autoSetTab(setting, "1");
if (location.href.includes("ai-course-center") || !location.href.includes("resource") && location.href.includes("tools-hike")) {
log.insertLog("请手动进入具体学习资源页面,进入后将自动处理..", "warning");
return;
}
await sleep(5);
const profile = getProfile();
if (!profile) {
log.insertLog("未识别到支持的章节列表,请确认页面已加载完成..", "warning");
return;
}
log.insertLog(`进入智慧树${profile.name}学习页面,脚本开始工作..`);
startPolling(profile);
await playMedia(profile);
};
start();
return () => {
cleanupVideo();
if (finishTimer) clearInterval(finishTimer);
if (visibilityTimer) clearInterval(visibilityTimer);
};
};
const wisdomStudyLogic = () => {
const log = useLogStore();
const setting = useSettingStore();
let videoEl = null;
let videoCheckTimer = null;
let quizCheckTimer = null;
let progressBarTimer = null;
let visibilityTimer = null;
let finishMonitorTimer = null;
let lastReplayAt = 0;
let visibilityWarned = false;
let isSwitching = false;
let isHandlingQuiz = false;
let isStoppedByStudyTimeWarn = false;
let hasLoggedPlaybackRate = false;
const RELOAD_COUNT_KEY = "abc-zhs-wisdom-study-reload-count";
const waitFor = (target, timeout = 1e4, interval = 300) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = setInterval(() => {
const result = typeof target === "function" ? target() : document.querySelector(target);
if (result) {
clearInterval(check);
resolve(result);
return;
}
if (Date.now() - startTime > timeout) {
clearInterval(check);
reject(new Error("等待页面内容加载超时"));
}
}, interval);
});
};
const clickEl = async (el) => {
var _a;
if (!el) return false;
(_a = el.scrollIntoView) == null ? void 0 : _a.call(el, { behavior: "smooth", block: "center" });
await sleep(0.2);
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const target = document.elementFromPoint(x, y) || el;
const eventOptions = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0,
buttons: 1
};
target.dispatchEvent(new MouseEvent("mousemove", eventOptions));
target.dispatchEvent(new MouseEvent("mousedown", eventOptions));
target.dispatchEvent(new MouseEvent("mouseup", { ...eventOptions, buttons: 0 }));
target.dispatchEvent(new MouseEvent("click", { ...eventOptions, buttons: 0 }));
return true;
};
const clickNativeFirst = async (el) => {
var _a, _b, _c;
if (!el) return false;
(_a = el.scrollIntoView) == null ? void 0 : _a.call(el, { behavior: "smooth", block: "center" });
await sleep(0.2);
const beforeClass = el.className;
const beforeChecked = (_b = el.querySelector("input")) == null ? void 0 : _b.checked;
el.click();
await sleep(0.2);
const afterChecked = (_c = el.querySelector("input")) == null ? void 0 : _c.checked;
if (el.className !== beforeClass || afterChecked !== beforeChecked) return true;
return clickEl(el);
};
const getConfiguredRate = () => {
const rate = Number(setting.config.basicConfig.videoPlayrate.value);
return Math.min(rate || 1, 1.8);
};
const getReloadCount = () => Number(sessionStorage.getItem(RELOAD_COUNT_KEY) || 0);
const setReloadCount = (count) => {
sessionStorage.setItem(RELOAD_COUNT_KEY, String(count));
};
const fixProcessBar = () => {
const bar = document.querySelector(".controlsBar");
if (!bar) return;
bar.style.display = "block";
bar.style.zIndex = "2";
bar.style.overflow = "";
};
const waitForCaptcha = async () => {
if (!document.querySelector(".yidun_popup")) return true;
log.insertLog("检测到验证码,请完成验证后继续..", "warning");
try {
await waitFor(() => !document.querySelector(".yidun_popup"), 5 * 60 * 1e3, 1e3);
log.insertLog("验证码已完成,继续学习..");
return true;
} catch {
log.insertLog("等待验证码完成超时,请手动刷新页面后继续..", "warning");
return false;
}
};
const getCourseName = () => {
var _a, _b;
const text = (_b = (_a = document.querySelector(".course-name")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim();
return (text == null ? void 0 : text.replace(/^课程名称[::]\s*/, "")) || "未知课程";
};
const getChapterName = (item) => {
var _a;
return ((_a = item == null ? void 0 : item.textContent) == null ? void 0 : _a.trim()) || "未知章节";
};
const getChapterItems = () => {
let items = Array.from(document.querySelectorAll(".chapter-content .chapter-item"));
items = items.map((el) => {
const children = el.querySelectorAll(".chapter-content-second");
return children.length ? Array.from(children) : [el];
}).flat();
return items.filter((el) => !el.querySelector(".finish-icon"));
};
const getCurrentChapter = () => document.querySelector(".chapter-content-second.current, .chapter-item.current");
const getNextChapter = () => {
const items = getChapterItems();
if (!items.length) return null;
const current = getCurrentChapter();
const currentIndex = items.indexOf(current);
return currentIndex >= 0 ? items[currentIndex + 1] : items[0];
};
const closeMasteryDialog = async () => {
const dialog = document.querySelector(".masterylevel-body");
if (!dialog) return false;
await clickEl(document.querySelector(".header-box .close-box"));
try {
await waitFor(() => !document.querySelector(".masterylevel-body"), 1e4, 200);
} catch {
log.insertLog("等待掌握度弹窗关闭超时,尝试继续切换..", "warning");
}
return true;
};
const dismissBlockingDialogs = async (log2) => {
const overlays = Array.from(document.querySelectorAll(".el-overlay,.el-dialog"));
if (!overlays.length) return;
const closeSelectors = [
".el-dialog__headerbtn",
".el-dialog__header .el-dialog__close",
".header-box .close-box",
".header-icon"
];
for (const overlay of overlays) {
let clicked = false;
for (const sel of closeSelectors) {
const btn = overlay.querySelector(sel) || overlay.closest(".el-overlay")?.querySelector(sel);
if (btn && btn.isConnected) {
btn.click();
clicked = true;
break;
}
}
if (!clicked && overlay.style.display !== "none") {
overlay.style.display = "none";
log2?.insertLog("弹窗无关闭按钮,已隐藏以免遮挡..", "warning");
}
}
await new Promise((r) => setTimeout(r, 300));
};
const hasStudyTimeWarnDialog = () => Array.from(document.querySelectorAll(".el-overlay .content")).some(
(el) => {
var _a;
return (_a = el.textContent) == null ? void 0 : _a.includes("保持良好的学习习惯");
}
);
const handleStudyTimeWarnDialog = () => {
if (!hasStudyTimeWarnDialog()) return false;
isStoppedByStudyTimeWarn = true;
log.insertLog(
"检测到学习习惯提示弹窗,已暂停自动切换,请手动处理后刷新页面继续..",
"warning"
);
return true;
};
// AI 随堂练习:选项是自绘组件,点击后选中态可能落在 class / input / aria 上,逐个位置都查一遍
const isQuizOptionPicked = (el) => {
if (!el) return false;
const input = el.querySelector('input[type="radio"], input[type="checkbox"]');
if (input && input.checked) return true;
if (el.getAttribute("aria-checked") === "true") return true;
return /(^|\s)(active|checked|selected|is-checked|is-active|el-radio__input--checked|el-checkbox__input--checked)(\s|$)/.test(el.className || "");
};
const normQuizAnswer = (s) => String(s || "").replace(/^[A-Za-z][.、.)::]\s*/, "").replace(/[(())。,,、\s]/g, "").toLowerCase();
const pickQuizOptionIndexes = (answers, optionTexts, type) => {
const result = new Set();
const texts = optionTexts.map(normQuizAnswer);
const judgeIndex = (v) => {
const s = String(v || "").trim();
if (/^(对|正确|是|√|T|true|A)$/i.test(s)) return 0;
if (/^(错|错误|否|×|F|false|B)$/i.test(s)) return 1;
return -1;
};
if (type === "判断题") {
const j = judgeIndex(answers[0]);
if (j >= 0 && j < optionTexts.length) result.add(j);
return Array.from(result);
}
for (const answer of answers) {
const a = String(answer || "").trim();
if (!a) continue;
if (/^[A-Za-z]{1,8}$/.test(a)) {
a.toUpperCase().split("").forEach((ch) => {
const i = ch.charCodeAt(0) - 65;
if (i >= 0 && i < optionTexts.length) result.add(i);
});
continue;
}
const prefixed = a.match(/^([A-Za-z])[.、.))::]\s*(.+)$/);
if (prefixed) {
const i = prefixed[1].toUpperCase().charCodeAt(0) - 65;
if (i >= 0 && i < optionTexts.length) result.add(i);
continue;
}
const n = normQuizAnswer(a);
if (!n) continue;
const hit = texts.findIndex((t) => t && (t === n || t.includes(n) || n.includes(t)));
if (hit >= 0) result.add(hit);
}
return Array.from(result);
};
// 先点选项本身,没反应再点整行(自绘组件常把点击绑在外层 label 上)
const clickQuizOption = async (el) => {
if (!el) return;
await clickNativeFirst(el);
if (!isQuizOptionPicked(el)) {
const row = el.closest("label, li, .item-row, .option-row") || el.parentElement;
if (row && row !== el) await clickNativeFirst(row);
}
};
const handleQuizDialog = async () => {
if (isHandlingQuiz) return;
const dialog = document.querySelector(".ai-class-exercise-dialog");
if (!dialog) return;
isHandlingQuiz = true;
try {
const items = Array.from(dialog.querySelectorAll(".ques-list .item"));
const optionsAll = Array.from(dialog.querySelectorAll(".ques-list .item .option"));
log.insertLog(`检测到随堂练习弹窗${items.length > 1 ? `(${items.length} 题)` : ""}`);
if (optionsAll.length) {
const captchaPassed = await waitForCaptcha();
if (!captchaPassed) return;
const targets = items.length ? items : [dialog];
let answered = 0;
for (const item of targets) {
const options = Array.from(item.querySelectorAll(".option"));
if (!options.length) continue;
const optionTexts = options.map((el) => (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim());
const fullText = (item.innerText || item.textContent || "").replace(/\s+/g, " ").trim();
// 题干 = 整题文字去掉选项文字,避免把选项混进题目去搜答案
let title = fullText;
optionTexts.forEach((t) => {
if (t) title = title.replace(t, " ");
});
title = title.replace(/\s+/g, " ").trim();
const isMulti = /多选|多项选择|不定项/.test(fullText);
const type = isMulti ? "多选题" : /判断/.test(fullText) ? "判断题" : "单选题";
let picked = [];
try {
const data = await getAccurateAnswer({ title, options: optionTexts, type, workType: "zhs" });
if (data && data.code === 1 && Array.isArray(data.data.answer)) {
picked = pickQuizOptionIndexes(data.data.answer, optionTexts, type);
}
} catch (_) {}
if (!picked.length) {
// 实在查不到才随机点一个,至少不卡住视频
picked = [Math.floor(Math.random() * options.length)];
log.insertLog("这道随堂练习没查到答案,先随机选一个占位", "warning", 4);
} else {
answered++;
}
const pickedSet = new Set(picked);
const order = Array.from(pickedSet).sort((a, b) => a - b);
const list = isMulti ? order : [order[0]];
for (const idx of list) {
const el = options[idx];
if (!el) continue;
await clickQuizOption(el);
await sleep(0.25);
}
// 多选题补点:没点上的再点整行一次,避免只选上一个
if (isMulti) {
for (const idx of order) {
const el = options[idx];
if (el && !isQuizOptionPicked(el)) {
const row = el.closest("label, li, .item-row, .option-row") || el.parentElement;
if (row && row !== el) await clickNativeFirst(row);
}
}
}
await sleep(0.3);
}
if (answered) log.insertLog(`随堂练习已按题库答案作答(${answered} 题)`);
}
const submitBtn = dialog.querySelector(".el-dialog__footer .el-button.btn");
if (submitBtn) {
await clickNativeFirst(submitBtn);
await sleep(1);
}
const closeBtn = dialog.querySelector(".header-icon");
if (closeBtn) await clickNativeFirst(closeBtn);
log.insertLog("随堂练习弹窗已完成");
} finally {
await sleep(1);
isHandlingQuiz = false;
}
};
const switchPlaybackRate = async (rate) => {
if (!videoEl) return;
videoEl.playbackRate = rate;
if (!hasLoggedPlaybackRate) {
hasLoggedPlaybackRate = true;
log.insertLog(`已设置播放倍速: ${rate}x`);
}
};
const switchDefinition = async () => {
const controlsBar = document.querySelector(".controlsBar");
if (controlsBar) controlsBar.style.display = "block";
};
const cleanupVideo = () => {
if (videoCheckTimer) {
clearInterval(videoCheckTimer);
videoCheckTimer = null;
}
if (videoEl) {
videoEl.onpause = null;
videoEl.onended = null;
videoEl = null;
}
};
const replayCurrentVideo = async () => {
if (!(videoEl == null ? void 0 : videoEl.isConnected) || !videoEl.ended) return;
const now = Date.now();
if (now - lastReplayAt < 8e3) return;
lastReplayAt = now;
try {
videoEl.currentTime = 1;
await switchPlaybackRate(getConfiguredRate());
await videoEl.play();
log.insertLog("当前节点尚未出现完成标记,已重新播放当前视频..", "warning");
} catch {
log.insertLog("重新播放当前视频失败,等待下次检查..", "warning");
}
};
const handleLoadFailure = async (error) => {
log.insertLog(`${error.message || "视频加载失败"},即将刷新页面重试..`, "warning");
const reloadCount = getReloadCount();
if (reloadCount >= 3) {
setReloadCount(0);
log.insertLog("视频加载多次失败,请检查网络或手动刷新页面..", "warning");
return;
}
setReloadCount(reloadCount + 1);
setTimeout(() => {
location.reload();
}, 3e3);
};
const playVideo = async () => {
if (isStoppedByStudyTimeWarn) return;
const captchaPassed = await waitForCaptcha();
if (!captchaPassed) return;
cleanupVideo();
try {
fixProcessBar();
await switchDefinition();
await sleep(1);
videoEl = await waitFor(() => {
const media = document.querySelector("video, audio");
return (media == null ? void 0 : media.src) ? media : null;
}, 15e3, 300);
setReloadCount(0);
lastReplayAt = 0;
videoEl.currentTime = Math.max(videoEl.currentTime, 1);
videoEl.volume = 0;
videoEl.muted = true;
await switchPlaybackRate(getConfiguredRate());
const current = getCurrentChapter();
log.insertLog(`正在学习:${getChapterName(current)}`);
videoEl.play().catch(() => {
});
videoCheckTimer = setInterval(() => {
if (!(videoEl == null ? void 0 : videoEl.isConnected)) {
cleanupVideo();
setTimeout(playVideo, 1500);
}
}, 3e3);
videoEl.onpause = () => {
if (!videoEl || videoEl.ended || isSwitching) return;
setTimeout(() => {
var _a;
return (_a = videoEl == null ? void 0 : videoEl.play) == null ? void 0 : _a.call(videoEl).catch(() => {
});
}, 1e3);
};
videoEl.onended = async () => {
if (isSwitching) return;
log.insertLog("当前视频播放完毕,等待平台标记完成..");
};
} catch (error) {
await handleLoadFailure(error);
}
};
const nextChapter = async () => {
if (isSwitching || isStoppedByStudyTimeWarn) return;
if (!setting.config.basicConfig.autoChangeChapter.value) {
log.insertLog(
"自动切换章节未开启,前往设置模块中更改",
"warning"
);
return;
}
isSwitching = true;
cleanupVideo();
const hidden = document.querySelector(".chapter-wrapper.hidden");
if (hidden) await clickEl(document.querySelector(".collapse-box") || hidden);
document.querySelectorAll(".el-collapse-item__wrap").forEach((el) => {
el.style.display = "";
});
await sleep(0.2);
if (handleStudyTimeWarnDialog()) {
isSwitching = false;
return;
}
await closeMasteryDialog();
const next = getNextChapter();
if (!next) {
reportCourseResult("done", "章节视频全部完成");
log.insertLog("所有章节视频已完成!");
isSwitching = false;
return;
}
await clickEl(next);
next.scrollIntoView({ behavior: "smooth", block: "center" });
await sleep(5);
isSwitching = false;
playVideo();
};
const startPolling = () => {
quizCheckTimer = setInterval(async () => {
await closeMasteryDialog();
await handleQuizDialog();
}, 3e3);
progressBarTimer = setInterval(fixProcessBar, 1e3);
finishMonitorTimer = setInterval(() => {
if (isSwitching || isStoppedByStudyTimeWarn) return;
const current = getCurrentChapter();
if (!(current == null ? void 0 : current.querySelector(".finish-icon"))) {
replayCurrentVideo();
return;
}
nextChapter();
}, 2e3);
visibilityTimer = setInterval(() => {
if (document.visibilityState === "hidden" && !visibilityWarned) {
visibilityWarned = true;
log.insertLog("页面未保持前台,学习流程可能无法正常推进..", "warning");
} else if (document.visibilityState === "visible") {
visibilityWarned = false;
}
}, 1e3);
};
const start = async () => {
log.insertLog(`进入智慧树新智慧学习页面(${getCourseName()}),脚本开始工作..`);
autoSetTab(setting, "1");
await sleep(5);
try {
await waitFor(".video-play", 2e4);
} catch {
log.insertLog("等待视频区域加载超时,尝试继续处理..", "warning");
}
dismissBlockingDialogs(log);
startPolling();
playVideo();
};
start();
return () => {
cleanupVideo();
if (quizCheckTimer) clearInterval(quizCheckTimer);
if (progressBarTimer) clearInterval(progressBarTimer);
if (finishMonitorTimer) clearInterval(finishMonitorTimer);
if (visibilityTimer) clearInterval(visibilityTimer);
};
};
/* ==================== 青书学堂 / 出头科技(接口直通模块) ====================
* 接入方式:全部走平台自己的 HTTP 接口 + 页面上下文请求(带 Cookie),
* 不注入第三方依赖,不改动超星 / 智慧树 的任何逻辑。
* 青书:学习时长上报、作业取题与保存;默认不自动交卷。
* 出头:章节学习时长上报、考试取题与点选。
*/
const XS_EXT = (() => {
'use strict';
/* ------------------------------ 通用工具 ------------------------------ */
const EXT_CONF = {
minGapMs: 10000, // 两次接口请求最小间隔
requestTimeout: 30000,
studyStepSec: 120, // 青书每次上报的学习时长(秒)
studyScorePoll: 5, // 青书每上报 N 次读一次分数
studyMaxMinutes: 180, // 青书单次挂机上限
ctStepSec: 60, // 出头每个打包片段的学习时长(秒)
ctMaxChapters: 30, // 出头单次最多处理的章节数
ctMinGapMs: 1500, // 出头接口间隔(比青书宽松,平台限流不同)
autoSubmit: false, // 作业 / 考试是否自动交卷(默认关闭)
autoFillExam: true // 出头考试是否自动点选(点完逐题核对,点不中退回提示)
};
const extLog = (text, type) => {
try { useLogStore().insertLog(text, type || null); } catch (e) { /* ignore */ }
};
const extSleep = (ms) => new Promise((r) => setTimeout(r, ms));
const extText = (value) => String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
/** 页面上下文 XHR:带 Cookie,避免 GM_xmlhttpRequest 丢登录态 */
function extFetch(url, opts = {}) {
return new Promise((resolve, reject) => {
let Xhr;
try { Xhr = (_unsafeWindow && _unsafeWindow.XMLHttpRequest) || XMLHttpRequest; }
catch (e) { Xhr = XMLHttpRequest; }
const xhr = new Xhr();
xhr.open(opts.method || 'GET', url, true);
try { xhr.withCredentials = true; } catch (e) { /* ignore */ }
const headers = opts.headers || {};
Object.keys(headers).forEach((key) => {
try { if (headers[key] != null) xhr.setRequestHeader(key, headers[key]); } catch (e) { /* ignore */ }
});
xhr.timeout = opts.timeout || EXT_CONF.requestTimeout;
xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || '', raw: xhr });
xhr.onerror = () => reject(new Error('网络连接失败'));
xhr.ontimeout = () => reject(new Error('请求超时'));
xhr.onabort = () => reject(new Error('请求已取消'));
try { xhr.send(opts.body == null ? null : opts.body); }
catch (e) { reject(e); }
});
}
/** 全局串行 + 最小间隔,避免触发平台限流 */
let extChain = Promise.resolve();
let extNextAt = 0;
function extPaced(run, gapMs) {
const task = async () => {
const wait = extNextAt - Date.now();
if (wait > 0) await extSleep(wait);
try { return await run(); }
finally { extNextAt = Date.now() + (gapMs || EXT_CONF.minGapMs); }
};
const promise = extChain.then(task, task);
extChain = promise.catch(() => {});
return promise;
}
function extRandomId() {
const rand = () => Math.random().toString(16).slice(2, 10);
return `${rand()}-${rand()}-${rand()}`;
}
function extParseJson(text, label) {
try { return JSON.parse(String(text || '')); }
catch (e) { throw new Error(`${label || '接口'}返回内容不是 JSON`); }
}
function extIsHttpOk(status) {
return status >= 200 && status < 400;
}
/* ============================== 青书学堂 ============================== */
const QS = {
key: 'qs',
minGap: 10000
};
function qsOnSite() {
return /(^|\.)qingshuxuetang\.com$/i.test(location.hostname || '');
}
function qsOrgCode(pathOrUrl) {
let path = String(pathOrUrl || '');
try { path = new URL(path, location.href).pathname; } catch (e) { /* ignore */ }
const match = path.match(/^\/([^/?#]+)\/Student(?:\/|$)/i);
return match ? decodeURIComponent(match[1]) : '';
}
function qsQueryOf(url, names) {
try {
const u = new URL(url, location.href);
for (const name of names) {
const value = u.searchParams.get(name);
if (value) return value;
}
} catch (e) { /* ignore */ }
return '';
}
function qsContext() {
const ctx = {
siteHost: location.origin,
orgCode: qsOrgCode(location.pathname),
courseId: qsQueryOf(location.href, ['courseId', 'courseid']),
teachPlanId: qsQueryOf(location.href, ['teachPlanId', 'teachplanId']),
teachPlanCourseId: qsQueryOf(location.href, ['teachPlanCourseId', 'teachplanCourseId', 'teachingPlanCourseId']),
periodId: qsQueryOf(location.href, ['periodId']),
nodeId: qsQueryOf(location.href, ['nodeId', 'contentId', 'cw_nodeId']),
quizId: qsQueryOf(location.href, ['quizId'])
};
if (!ctx.orgCode) {
const first = String(location.pathname || '').split('/').filter(Boolean)[0];
ctx.orgCode = first && !/^Student$/i.test(first) ? first : '';
}
return ctx;
}
function qsApiBase(ctx) {
const origin = String(ctx.siteHost || location.origin).replace(/\/$/, '');
return `${origin}/${ctx.orgCode}`;
}
function qsMultipart(fields) {
const boundary = `----XsHelper${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
let body = '';
Object.keys(fields).forEach((key) => {
const value = fields[key];
body += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value == null ? '' : value}\r\n`;
});
body += `--${boundary}--\r\n`;
return { body, contentType: `multipart/form-data; boundary=${boundary}` };
}
async function qsRequest(ctx, endpoint, opts = {}) {
const target = new URL(String(endpoint || ''), qsApiBase(ctx) + '/');
const params = opts.params || {};
Object.keys(params).forEach((key) => {
const value = params[key];
if (value !== undefined && value !== null && value !== '') target.searchParams.set(key, String(value));
});
if (!target.searchParams.has('_t')) target.searchParams.set('_t', String(Date.now()));
const headers = {
Accept: opts.accept || '*/*',
'X-Requested-With': 'XMLHttpRequest',
'Device-Trace-Id-QS': qsDeviceId()
};
if (opts.referer) headers.Referer = opts.referer;
let body = opts.body;
if (opts.json !== undefined) {
headers['Content-Type'] = 'application/json; charset=utf-8';
body = JSON.stringify(opts.json);
} else if (opts.multipart) {
const packed = qsMultipart(opts.multipart);
headers['Content-Type'] = packed.contentType;
body = packed.body;
} else if (opts.form) {
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
body = new URLSearchParams(opts.form).toString();
}
const res = await extPaced(() => extFetch(target.href, {
method: opts.method || 'GET',
headers,
body,
timeout: opts.timeout || EXT_CONF.requestTimeout
}));
if (res.status === 401 || res.status === 403) throw new Error('青书登录已失效,请重新登录');
if (res.status === 429) throw new Error('青书请求过于频繁(429),请稍后再试');
if (!extIsHttpOk(res.status)) throw new Error(`青书接口返回 HTTP ${res.status}`);
return res.text;
}
let qsDevice = '';
function qsDeviceId() {
if (!qsDevice) {
try { qsDevice = GM_getValue('xs-qs-device-id', '') || ''; } catch (e) { qsDevice = ''; }
if (!qsDevice) {
qsDevice = extRandomId();
try { GM_setValue('xs-qs-device-id', qsDevice); } catch (e) { /* ignore */ }
}
}
return qsDevice;
}
function qsResponseOk(json) {
if (!json || typeof json !== 'object') return false;
if (Object.prototype.hasOwnProperty.call(json, 'hr')) return String(json.hr) === '0';
if (Object.prototype.hasOwnProperty.call(json, 'code')) return String(json.code) === '0';
if (Object.prototype.hasOwnProperty.call(json, 'success')) return json.success === true;
return extText(json.message) === '成功';
}
function qsNum(value) {
const match = String(value == null ? '' : value).match(/-?\d+(?:\.\d+)?/);
return match ? Number(match[0]) : 0;
}
async function qsCourseList(ctx) {
const text = await qsRequest(ctx, 'Student/Course/CourseData', { method: 'POST' });
const json = extParseJson(text, '青书课程列表');
if (!qsResponseOk(json)) throw new Error(json.message || '读取青书课程列表失败');
const rows = Array.isArray(json.data) ? json.data : [];
return rows.map((raw) => ({
courseId: String(raw.courseId || raw.id || ''),
courseName: String(raw.courseName || raw.name || '青书课程'),
teachPlanCourseId: String(raw.teachingPlanCourseId || raw.teachPlanCourseId || raw.teachplanCourseId || ''),
teachPlanId: String(raw.teachPlanId || raw.classId || ''),
periodId: String(raw.periodId || ''),
learnStatus: raw.learnStatus,
learnStatusName: String(raw.learnStatusName || ''),
score: raw.score,
allowLearn: ![false, 0, '0', 'false'].includes(raw.allowLearn)
})).filter((item) => item.courseId);
}
async function qsResolveContext(ctx) {
if (ctx.courseId && ctx.teachPlanId && ctx.periodId) {
if (ctx.teachPlanCourseId) return ctx;
}
const courses = await qsCourseList(ctx);
const matched = courses.filter((item) =>
(!ctx.courseId || item.courseId === String(ctx.courseId)) &&
(!ctx.teachPlanId || item.teachPlanId === String(ctx.teachPlanId)) &&
(!ctx.periodId || item.periodId === String(ctx.periodId)));
if (matched.length !== 1) {
throw new Error('无法确定当前青书课程,请先进入具体课程页面再运行');
}
const course = matched[0];
return Object.assign({}, ctx, {
courseId: course.courseId,
courseName: course.courseName,
teachPlanId: course.teachPlanId,
teachPlanCourseId: course.teachPlanCourseId,
periodId: course.periodId,
course
});
}
async function qsProgress(ctx) {
const text = await qsRequest(ctx, 'Student/Course/GetStudyRecordAndScore', {
params: { teachPlanCourseId: ctx.teachPlanCourseId, periodId: ctx.periodId, _: Date.now() - 100 }
});
const json = extParseJson(text, '青书学习进度');
if (!qsResponseOk(json)) throw new Error(json.message || '读取青书学习进度失败');
const data = json.data || {};
return {
score: qsNum(data.studyScoreStr || data.score),
studyTime: String(data.studyTimeStr || data.studyTime || ''),
learnStatus: String(data.learnStatusName || data.learnStatus || ''),
passed: String(data.learnStatusName || '').trim() === '已通过',
records: Array.isArray(data.studyRecordList) ? data.studyRecordList : []
};
}
async function qsScoreDetail(ctx) {
const text = await qsRequest(ctx, 'Student/Course/CourseScore', {
params: { courseId: ctx.courseId, teachplanCourseId: ctx.teachPlanCourseId, _: Date.now() - 100 },
accept: 'text/html,*/*'
});
const doc = new DOMParser().parseFromString(text || '', 'text/html');
let current = null;
let target = null;
const rows = Array.from(doc.querySelectorAll('tr')).filter((row) =>
Array.from(row.children).some((cell) => extText(cell.textContent) === '课件学习'));
if (rows.length) {
const match = extText(rows[0].lastElementChild && rows[0].lastElementChild.textContent)
.match(/^(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/);
if (match) {
current = Number(match[1]);
target = Number(match[2]);
}
}
if (current === null) {
const table = doc.querySelector('#studySummaryTbl');
if (table) {
current = qsNum((table.querySelector('.study-login') || {}).textContent || 0);
target = qsNum((table.querySelector('.title-class-score') || {}).textContent || 0);
}
}
if (current === null || !Number.isFinite(target) || target <= 0) {
return { known: false, currentScore: 0, targetScore: 80 };
}
return { known: true, currentScore: current, targetScore: target };
}
async function qsBeginStudy(ctx, contentId) {
const text = await qsRequest(ctx, 'Student/Course/UploadStudyRecordBegin', {
method: 'POST',
json: {
classId: ctx.teachPlanId,
courseId: ctx.courseId,
contentId: contentId || ctx.nodeId || 'kcjs_1_1',
contentType: 11,
periodId: ctx.periodId,
position: 0,
detectId: null
}
});
const json = extParseJson(text, '青书开始学习');
if (!qsResponseOk(json)) throw new Error(json.message || '青书开始学习失败');
const recordId = json.data && typeof json.data === 'object' ? (json.data.recordId || json.data.id) : json.data;
if (!recordId) throw new Error('青书未返回学习记录号');
return String(recordId);
}
async function qsContinueStudy(ctx, recordId, position, end) {
const text = await qsRequest(ctx, 'Student/Course/UploadStudyRecordContinue', {
method: 'POST',
multipart: {
recordId,
end: end ? 'true' : 'false',
position: Math.max(0, Math.round(position)),
timeOutConfirm: 'true'
}
});
const json = extParseJson(text, '青书学习进度上报');
if (!qsResponseOk(json)) throw new Error(json.message || '青书学习进度上报失败');
return json;
}
/** 青书:学习挂机 */
async function qsStudyLogic() {
if (!qsOnSite()) return;
if (!/\/Student\/Course\/CourseStudy/i.test(location.pathname)) return;
const startHref = location.href;
const setting = useSettingStore();
if (setting.config && setting.config.basicConfig && setting.config.basicConfig.token) {
if (!setting.config.basicConfig.token.value) {
extLog('青书:未配置密钥,学习挂机仍可运行,但作业答题需要密钥', 'warning');
}
}
try {
const ctx = await qsResolveContext(qsContext());
const detail = await qsScoreDetail(ctx).catch(() => ({ known: false, currentScore: 0, targetScore: 80 }));
const progress = await qsProgress(ctx).catch(() => ({ score: 0, passed: false, records: [] }));
if (progress.passed || (detail.known && detail.currentScore >= detail.targetScore)) {
extLog(`青书:课件学习已达标(${detail.currentScore}/${detail.targetScore} 分),无需挂机`, 'success');
return;
}
const target = detail.known ? detail.targetScore : 80;
extLog(`青书:开始挂课,当前 ${progress.score || detail.currentScore}/${target} 分;每 ${EXT_CONF.studyStepSec} 秒上报一次`, null);
const recent = (progress.records && progress.records[0]) || {};
const contentId = ctx.nodeId || recent.contentId || recent.nodeId || 'kcjs_1_1';
const recordId = await qsBeginStudy(ctx, contentId);
const deadline = Date.now() + EXT_CONF.studyMaxMinutes * 60 * 1000;
let position = 0;
let uploads = 0;
let score = progress.score || detail.currentScore || 0;
try {
while (Date.now() < deadline) {
await extSleep(EXT_CONF.studyStepSec * 1000);
if (location.href !== startHref) {
extLog('青书:页面已切换,挂机停止', 'warning');
return;
}
position += EXT_CONF.studyStepSec;
await qsContinueStudy(ctx, recordId, position, false);
uploads += 1;
extLog(`青书:已上报 ${uploads} 次,进度位置 ${Math.round(position / 60)} 分钟,分数 ${score}/${target}`, null);
if (uploads % EXT_CONF.studyScorePoll === 0) {
const fresh = await qsProgress(ctx).catch(() => null);
if (fresh) {
score = fresh.score;
extLog(`青书:最新分数 ${score}/${target}${fresh.studyTime ? `,学习时长 ${fresh.studyTime}` : ''}`, score >= target ? 'success' : null);
if (fresh.passed || score >= target) break;
}
}
}
} finally {
try { await qsContinueStudy(ctx, recordId, position, true); }
catch (e) { extLog('青书:学习记录关闭未确认,可能影响时长统计', 'warning'); }
}
extLog(`青书:挂机结束,共上报 ${uploads} 次,当前分数 ${score}/${target}`, 'success');
} catch (error) {
extLog(`青书挂课失败:${(error && error.message) || error}`, 'error');
}
}
/* ---------------------------- 青书作业答题 ---------------------------- */
const QS_TYPE_NAMES = {
0: '单选题', 1: '多选题', 2: '填空题', 3: '判断题', 4: '简答题'
};
function qsTypeFromText(text) {
const raw = String(text || '');
if (/多选/.test(raw)) return '1';
if (/填空/.test(raw)) return '2';
if (/判断/.test(raw)) return '3';
if (/单选/.test(raw)) return '0';
if (/简答|问答|论述|名词解释|计算/.test(raw)) return '4';
return 'unknown';
}
function qsCleanOption(text) {
return extText(String(text || '').replace(/<[^>]+>/g, '')).replace(/^[A-Z][.、.)\s]+/i, '').trim();
}
function qsParsePageQuestions() {
const containers = Array.from(document.querySelectorAll('.question-detail-container'));
const questions = [];
for (const container of containers) {
const typeText = (container.querySelector('.question-detail-type-desc') || {}).textContent || '';
const type = qsTypeFromText(typeText);
const descEl = container.querySelector('.question-detail-description') ||
container.querySelector('.detail-description-content');
const stem = descEl ? extText(descEl.textContent) : '';
const options = [];
Array.from(container.querySelectorAll('.question-detail-option')).forEach((optEl, index) => {
const input = optEl.querySelector('input.option-input, input[type="radio"], input[type="checkbox"]');
const preview = optEl.querySelector('.option-input-preview');
let label = (input && input.value) || (preview && preview.getAttribute('content')) || '';
label = String(label || '').trim().toUpperCase();
if (!label) label = String.fromCharCode(65 + index);
const desc = optEl.querySelector('.option-description');
options.push({
label,
content: desc ? extText(desc.textContent) : '',
element: optEl,
input
});
});
questions.push({
questionId: container.id || '',
type,
typeName: QS_TYPE_NAMES[type] || typeText.trim() || '未知题型',
stem,
stemHtml: descEl ? descEl.innerHTML : stem,
options,
element: container
});
}
return questions;
}
/** 把后端返回的答案数组转成青书的选项字母 */
function qsResolveLabels(question, answers) {
const labels = question.options.map((item) => item.label);
// 页面选项是复选框时按多选题处理,避免题型文字没识别出来把多选当成单选只填一个
const multiDom = question.options.some((opt) => opt.input && String(opt.input.type || '').toLowerCase() === 'checkbox');
const raw = (Array.isArray(answers) ? answers : [answers]).map((item) => extText(item)).filter(Boolean);
if (!raw.length) return [];
const joined = raw.join('');
const letterOnly = joined.replace(/^(参考答案|正确答案|答案)\s*[::]\s*/i, '').toUpperCase();
if (/^[A-Z]+$/.test(letterOnly) && letterOnly.split('').every((ch) => labels.includes(ch))) {
const unique = Array.from(new Set(letterOnly.split('')));
if (question.type !== '1' && !multiDom && unique.length > 1) return [];
return unique;
}
const hits = [];
for (const item of raw) {
const key = qsCleanOption(item);
const matched = question.options.find((opt) => qsCleanOption(opt.content) === key);
if (matched) hits.push(matched.label);
}
if (hits.length && (question.type === '1' || multiDom || hits.length === 1)) return Array.from(new Set(hits));
if (question.type === '3') {
const truth = /^(对|正确|是|√|✓|T|TRUE|Y|1)$/i.test(qsCleanOption(joined));
const falsey = /^(错|错误|否|×|✗|F|FALSE|N|0)$/i.test(qsCleanOption(joined));
if (truth || falsey) {
const found = question.options.find((opt) => {
const text = qsCleanOption(opt.content);
return truth
? /^(对|正确|是|√|✓|T|TRUE|Y|1)$/i.test(text)
: /^(错|错误|否|×|✗|F|FALSE|N|0)$/i.test(text);
});
if (found) return [found.label];
}
}
return [];
}
/** 提交给青书接口的答案字符串 */
function qsNativeAnswer(question, answers) {
if (question.type === '2' || question.type === '4') {
const list = (Array.isArray(answers) ? answers : [answers]).map((item) => String(item == null ? '' : item).trim()).filter(Boolean);
return list.join('||');
}
if (!['0', '1', '3'].includes(question.type) || !question.options.length) return '';
const labels = qsResolveLabels(question, answers);
if (!labels.length) return '';
return labels.join('');
}
function qsFillPage(question, labels) {
let filled = 0;
question.options.forEach((opt) => {
if (!opt.input) return;
const wanted = labels.includes(String(opt.input.value || '').trim().toUpperCase());
if (opt.input.checked !== wanted) {
try { (opt.element || opt.input).click(); } catch (e) { /* ignore */ }
}
if (opt.input.checked) filled += 1;
});
return filled;
}
function qsBuildQuestionPayload(question) {
return {
title: question.stemHtml || question.stem,
optionsText: question.options.map((opt) => opt.content),
type: question.type === 'unknown' ? '' : question.type,
refer: location.href,
element: question.element
};
}
/** 青书:作业页自动作答(默认只保存,不交卷) */
async function qsHomeworkLogic() {
if (!qsOnSite()) return;
const ctx = qsContext();
if (!ctx.quizId) {
extLog('青书:未识别到作业编号(quizId),请从作业列表进入具体作业', 'warning');
return;
}
const log = useLogStore();
const setting = useSettingStore();
const token = setting.config && setting.config.basicConfig && setting.config.basicConfig.token
? setting.config.basicConfig.token.value : '';
if (!token) {
extLog('青书:未配置密钥,无法答题。请到「设置」配置后再试', 'warning');
return;
}
const questions = qsParsePageQuestions();
if (!questions.length) {
extLog('青书:当前页面没有解析到题目,可能不是作业作答页', 'warning');
return;
}
extLog(`青书:解析到 ${questions.length} 道题,开始作答(默认只保存,不自动交卷)`, null);
let answered = 0;
let skipped = 0;
for (let index = 0; index < questions.length; index += 1) {
const question = questions[index];
const rowText = `青书 第${index + 1}/${questions.length}题`;
if (question.type === 'unknown') {
extLog(`${rowText}:题型未识别,已跳过`, 'warning');
skipped += 1;
continue;
}
if (question.type !== '2' && question.type !== '4' && !question.options.length) {
extLog(`${rowText}:没有选项,已跳过`, 'warning');
skipped += 1;
continue;
}
let result;
try {
result = await getAccurateAnswer(qsBuildQuestionPayload(question));
} catch (error) {
extLog(`${rowText}:查询答案失败(${(error && error.message) || error})`, 'error');
skipped += 1;
continue;
}
if (!result || result.code !== 1 || !result.data || !result.data.answer) {
extLog(`${rowText}:未查到答案(${(result && result.msg) || '题库与 AI 都没有结果'})`, 'warning');
skipped += 1;
continue;
}
const answers = Array.isArray(result.data.answer) ? result.data.answer : [result.data.answer];
const native = qsNativeAnswer(question, answers);
if (!native) {
extLog(`${rowText}:答案与选项对不上,已跳过(原始答案:${extText(answers.join('、')).slice(0, 40)})`, 'warning');
skipped += 1;
continue;
}
if (question.type === '2' || question.type === '4') {
extLog(`${rowText}:主观题,答案已取到但需你在页面填写:${native.slice(0, 60)}`, 'warning');
} else {
const labels = qsResolveLabels(question, answers);
qsFillPage(question, labels);
extLog(`${rowText}:已选择 ${labels.join('')}`, 'success');
}
try {
await qsRequest(ctx, 'Svc/SaveQuestionAnswer', {
method: 'POST',
json: { quizId: ctx.quizId, questionAnswers: [{ questionId: question.questionId, answer: native }] }
});
} catch (error) {
extLog(`${rowText}:保存失败(${(error && error.message) || error})`, 'error');
skipped += 1;
continue;
}
answered += 1;
log.insertLog(`${rowText}:已作答并保存`, 'success', 2);
}
extLog(`青书:作答结束,已保存 ${answered} 题,跳过 ${skipped} 题;请检查后手动交卷`, 'success');
if (EXT_CONF.autoSubmit && skipped === 0) {
extLog('青书:自动交卷已开启,请在面板确认', 'warning');
}
}
/* ============================== 出头科技 ============================== */
function ctOnSite() {
return /(^|\.)web2\.superchutou\.com$/i.test(location.hostname || '');
}
function ctApiBase() {
return `${location.origin}/service`;
}
function ctUser() {
try { return JSON.parse(localStorage.getItem('user') || '{}') || {}; }
catch (e) { return {}; }
}
async function ctApi(method, path, opts = {}) {
const url = new URL(ctApiBase() + (String(path || '').startsWith('/') ? path : '/' + path));
const params = opts.params || {};
Object.keys(params).forEach((key) => {
const value = params[key];
if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, String(value));
});
const headers = { Accept: '*/*' };
let body;
if (opts.body !== undefined) {
headers['Content-Type'] = 'application/json; charset=utf-8';
body = JSON.stringify(opts.body);
}
const res = await extPaced(() => extFetch(url.href, { method, headers, body }), EXT_CONF.ctMinGapMs);
if (res.status === 401 || res.status === 403) throw new Error('出头登录已失效,请重新登录');
if (!extIsHttpOk(res.status)) throw new Error(`出头接口返回 HTTP ${res.status}`);
const json = extParseJson(res.text, '出头接口');
if (!json || typeof json !== 'object') throw new Error('出头接口返回格式异常');
return json;
}
async function ctCourseTree(user) {
const res = await ctApi('GET', '/eduSuper/Specialty/GetStuSpecialtyCurriculumList', {
params: { StuDetail_ID: user.StuDetail_ID, IsStudyYear: 1, StuID: user.StuID }
});
if (!res.SuccessResponse) throw new Error(res.Message || '读取出头课程失败');
const data = res.Data || {};
let list = data.list || data.rows || data;
if (!Array.isArray(list)) list = [];
return { all: list, meta: { curStudyYear: data.CurStudyYear } };
}
async function ctChapters(user, courseId, curriculumId) {
const res = await ctApi('GET', '/eduSuper/Question/GetCourse_ChaptersNodeList', {
params: {
Valid: 1,
Course_ID: courseId,
StuID: user.StuID,
Curriculum_ID: curriculumId,
Examination_ID: 0,
StuDetail_ID: user.StuDetail_ID
}
});
if (!res.SuccessResponse) return [];
return Array.isArray(res.Data) ? res.Data : [];
}
function ctToInt(value, fallback) {
const num = parseInt(value, 10);
return Number.isFinite(num) ? num : fallback;
}
function ctChapterDuration(chapter) {
const direct = ctToInt(chapter.Duration, 0);
if (direct > 0) return direct;
return ctToInt(chapter.VideoDuration || chapter.WareDuration || chapter.TimeLength, 0);
}
function ctChapterLearned(chapter) {
const total = Number(chapter.TotalSecond);
if (Number.isFinite(total) && total > 0) return Math.floor(total);
return ctToInt(chapter.PlayRecord || chapter.LookDuration || chapter.LastWatchPos || chapter.StudyDuration, 0);
}
function ctChapterDone(chapter) {
const id = ctToInt(chapter.ID, 0);
if (id > 0 && ctToInt(chapter.IsLook, 0) === id) return true;
if (ctToInt(chapter.LookEndTime, 0) > 0) return true;
const duration = ctChapterDuration(chapter);
const learned = ctChapterLearned(chapter);
if (duration > 0 && learned >= duration) return true;
const progress = chapter.Progress != null ? chapter.Progress : chapter.StudyProgress;
if (progress != null && ctToInt(progress, 0) >= 100) return true;
return false;
}
function ctLeafChapters(nodes, out) {
const result = out || [];
for (const node of nodes || []) {
const children = node.ChildNodeList;
if (children && children.length) {
ctLeafChapters(children, result);
continue;
}
const wareType = node.CourseWareType_ID != null ? node.CourseWareType_ID : node.WareType;
if (wareType != null && ![0, 1, 2].includes(ctToInt(wareType, -1))) continue;
if (node.ID != null) result.push(node);
}
return result;
}
async function ctSaveLook(chapterId, lookTime, lastWatchPos, lookType) {
return ctApi('POST', '/datastore/WebCourse/SaveCourse_Look', {
body: {
CourseChapters_ID: chapterId,
LookType: lookType || 0,
LookTime: Math.max(0, Math.round(lookTime)),
LastWatchPos: Math.max(0, Math.round(lastWatchPos)),
IP: (window.returnCitySN && window.returnCitySN.cip) || '127.0.0.1'
}
});
}
/** 出头:刷课(章节学习时长上报) */
async function ctStudyLogic() {
if (!ctOnSite()) return;
const user = ctUser();
if (!user.StuID || !user.StuDetail_ID) {
extLog('出头:未读取到登录信息,请先登录后再打开课程页', 'warning');
return;
}
const startHref = location.href;
try {
const tree = await ctCourseTree(user);
const courses = tree.all.filter((course) => {
const courseId = ctToInt(course.Course_ID || course.spcuCourseId, 0);
const chapters = ctToInt(course.CourseChapters, 0);
return courseId > 0 && chapters > 0;
});
if (!courses.length) {
extLog('出头:当前账号没有可学习的网课', 'warning');
return;
}
const route = ctRoute();
const scoped = route.curriculumId
? courses.filter((course) => String(course.Curriculum_ID) === String(route.curriculumId))
: courses;
const targets = scoped.length ? scoped : courses;
extLog(`出头:共 ${targets.length} 门网课待检查`, null);
let handled = 0;
let skippedCourse = 0;
for (const course of targets) {
if (handled >= EXT_CONF.ctMaxChapters) break;
if (location.href !== startHref) {
extLog('出头:页面已切换,刷课停止', 'warning');
return;
}
const courseId = ctToInt(course.Course_ID || course.spcuCourseId, 0);
const curriculumId = ctToInt(course.Curriculum_ID, 0);
const courseName = course.CuName || course.Name || String(courseId);
let chapters = [];
try { chapters = ctLeafChapters(await ctChapters(user, courseId, curriculumId)); }
catch (error) {
extLog(`出头:${courseName} 章节读取失败(${(error && error.message) || error})`, 'warning');
skippedCourse += 1;
continue;
}
const todo = chapters.filter((chapter) => !ctChapterDone(chapter));
if (!todo.length) continue;
extLog(`出头:${courseName} 有 ${todo.length} 个章节待学习`, null);
for (const chapter of todo) {
if (handled >= EXT_CONF.ctMaxChapters) break;
if (location.href !== startHref) return;
const chapterId = ctToInt(chapter.ID, 0);
const duration = ctChapterDuration(chapter) || Math.max(ctChapterLearned(chapter), 300);
const learned = Math.min(ctChapterLearned(chapter), duration);
const title = chapter.Name || chapter.ChapterName || chapter.CourseWare_Name || `章节${chapterId}`;
let position = learned;
if (position >= duration) continue;
while (position < duration) {
const step = Math.min(EXT_CONF.ctStepSec, duration - position);
position += step;
await ctSaveLook(chapterId, position, position, ctToInt(chapter.LookType, 0));
}
handled += 1;
extLog(`出头:${courseName} / ${title} 已上报至 ${Math.round(position / 60)} 分钟`, 'success');
await extSleep(300);
}
}
extLog(`出头:本轮刷课结束,处理 ${handled} 个章节${skippedCourse ? `,${skippedCourse} 门课程读取失败` : ''}`, 'success');
} catch (error) {
extLog(`出头刷课失败:${(error && error.message) || error}`, 'error');
}
}
function ctRoute() {
const path = (location.hash || '').replace(/^#/, '') || location.pathname;
let match = path.match(/\/onlineclass\/paper\/(\d+)\/(\d+)\/(\d+)\/(\d+)/i);
if (match) {
return {
kind: 'paper',
examPaperId: ctToInt(match[1], 0),
examinationId: ctToInt(match[2], 0),
modeType: ctToInt(match[3], 0),
curriculumId: ctToInt(match[4], 0)
};
}
match = path.match(/\/onlineclass\/curriculum\/(\d+)\/(\d+)\/(\d+)\/(\d+)/i);
if (match) {
return {
kind: 'curriculum',
curriculumId: ctToInt(match[1], 0),
courseId: ctToInt(match[2], 0),
examinationId: ctToInt(match[3], 0),
tabIndex: ctToInt(match[4], 0)
};
}
return { kind: 'other' };
}
const CT_TYPES = {
1: '0', 2: '1', 3: '2', 4: '3', 5: '4', 6: '7',
9: '1', 10: '5', 11: '6'
};
function ctParseBody(bodyText) {
if (!bodyText) return [];
try {
const obj = typeof bodyText === 'string' ? JSON.parse(bodyText) : bodyText;
if (!obj || typeof obj !== 'object') return [];
return Object.keys(obj).map((key) => ({
label: String(key).toUpperCase(),
content: extText(String(obj[key]).replace(/<[^>]+>/g, ''))
}));
} catch (e) { return []; }
}
function ctAnswerLetters(answer, options) {
const labels = options.map((opt) => opt.label);
const srcList = (Array.isArray(answer) ? answer : [answer]).map((x) => String(x == null ? '' : x).trim()).filter(Boolean);
const raw = srcList.join(',').toUpperCase().replace(/^(参考答案|正确答案|答案)\s*[::]\s*/i, '');
if (/^[A-Z](?:\s*[,,、;;|]?\s*[A-Z])*$/.test(raw)) {
const letters = Array.from(new Set(raw.match(/[A-Z]/g) || []));
if (letters.every((letter) => labels.includes(letter))) return letters;
}
const hits = [];
const addLabel = (label) => { if (label && !hits.includes(label)) hits.push(label); };
const matchPart = (part) => {
const key = qsCleanOption(part);
if (!key) return false;
const found = options.find((opt) => qsCleanOption(opt.content) === key);
if (!found) return false;
addLabel(found.label);
return true;
};
srcList.forEach((item) => {
if (matchPart(item)) return;
// "A. 选项正文":字母与选项正文都对得上就按字母点,避免多选题只认到一项
const m = item.match(/^([A-Za-z])\s*[.、.))::]\s*(\S[\s\S]*)$/);
if (m && labels.includes(m[1].toUpperCase())) {
const letter = m[1].toUpperCase();
const opt = options.find((o) => o.label === letter);
const optClean = opt ? qsCleanOption(opt.content) : '';
const bodyClean = qsCleanOption(m[2]);
if (!optClean || !bodyClean || optClean.includes(bodyClean) || bodyClean.includes(optClean)) {
addLabel(letter);
return;
}
}
// 同一条里塞了多个答案("A、B"):最后才按分隔符拆
item.split(/[\s,,、;;|]+/).filter(Boolean).forEach((part) => { matchPart(part); });
});
return hits;
}
function ctVisible(el) {
if (!el || !el.getBoundingClientRect) return false;
const rect = el.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) return false;
const style = window.getComputedStyle ? window.getComputedStyle(el) : null;
if (style && (style.visibility === 'hidden' || style.display === 'none' || Number(style.opacity) === 0)) return false;
return true;
}
function ctClick(el) {
if (!el) return false;
try {
['mousedown', 'mouseup', 'click'].forEach((type) => {
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
});
return true;
} catch (e) {
try { el.click(); return true; } catch (_) { return false; }
}
}
function ctOptionChecked(node) {
if (!node) return false;
const input = node.matches && node.matches('input')
? node
: (node.querySelector ? node.querySelector('input[type="radio"], input[type="checkbox"], input') : null);
if (input && input.checked) return true;
let host = node;
try { host = (node.closest && node.closest('.ant-radio-wrapper, .ant-checkbox-wrapper, label, li')) || node; }
catch (e) { host = node; }
const cls = String((host && host.className) || '');
return /checked|selected|active/i.test(cls);
}
/** 在当前题目范围内按选项字母点选,返回是否点中并确认 */
function ctClickLetters(letters) {
if (!letters || !letters.length) return false;
const scopes = [
document.querySelector('.question-content, .exam-question, [class*="questionContent"], [class*="QuestionContent"], .ant-card-body'),
document.body
].filter(Boolean);
let allOk = true;
for (const letter of letters) {
const lower = letter.toLowerCase();
let input = null;
for (const scope of scopes) {
const list = Array.from(scope.querySelectorAll(
`input[type="radio"][value="${letter}"], input[type="checkbox"][value="${letter}"], ` +
`input[type="radio"][value="${lower}"], input[type="checkbox"][value="${lower}"]`
)).filter(ctVisible);
if (list.length) { input = list[list.length - 1]; break; }
}
if (input) {
const host = input.closest('label') || input.parentElement || input;
ctClick(host);
if (!ctOptionChecked(input)) ctClick(input);
if (!ctOptionChecked(input)) allOk = false;
continue;
}
let hit = null;
for (const scope of scopes) {
const nodes = Array.from(scope.querySelectorAll('.ant-radio-wrapper, .ant-checkbox-wrapper, label, li, span'))
.filter((el) => ctVisible(el) && el.children.length === 0);
hit = nodes.find((el) => new RegExp(`^\\s*${letter}\\s*[.、.))]`).test((el.textContent || '').trim()));
if (hit) break;
}
if (hit) ctClick(hit);
else allOk = false;
}
return allOk;
}
/** 点答题卡上的题号,切到指定题 */
function ctClickAnswerCard(num) {
const label = String(num);
const roots = Array.from(document.querySelectorAll(
'[class*="answer"], [class*="Answer"], [class*="sheet"], [class*="Sheet"], [class*="card"], [class*="Card"]'
));
for (const root of roots) {
const nodes = Array.from(root.querySelectorAll('div, span, li, a, button'))
.filter((el) => ctVisible(el) && el.children.length === 0 && (el.textContent || '').trim() === label);
const hit = nodes.find((el) => {
const rect = el.getBoundingClientRect();
return rect.width >= 12 && rect.width <= 90 && rect.height >= 12;
});
if (hit) { ctClick(hit); return true; }
}
return false;
}
async function ctExamLogic() {
if (!ctOnSite()) return;
const startHref = location.href;
const route = ctRoute();
if (route.kind !== 'paper' || !route.examPaperId || !route.examinationId) return;
const user = ctUser();
if (!user.StuID) {
extLog('出头:未读取到登录信息,无法读取试卷', 'warning');
return;
}
const setting = useSettingStore();
const token = setting.config && setting.config.basicConfig && setting.config.basicConfig.token
? setting.config.basicConfig.token.value : '';
if (!token) {
extLog('出头:未配置密钥,无法答题', 'warning');
return;
}
try {
const res = await ctApi('GET', '/eduSuper/Question/GetExamPaperQuestions', {
params: {
examPaperId: route.examPaperId,
StuID: user.StuID,
StuDetail_ID: user.StuDetail_ID,
Examination_ID: route.examinationId,
Curriculum_ID: route.curriculumId,
SignId: 0,
type: 2
}
});
if (!res.SuccessResponse || !res.Data) throw new Error(res.Message || '读取试卷失败');
const blocks = res.Data.QuestionType || [];
const questions = [];
blocks.forEach((block) => {
(block.Question || []).forEach((q) => questions.push(q));
});
if (!questions.length) {
extLog('出头:试卷里没有题目', 'warning');
return;
}
extLog(`出头:试卷共 ${questions.length} 道题,开始作答`, null);
let done = 0;
let skipped = 0;
const pending = [];
for (let index = 0; index < questions.length; index += 1) {
const q = questions[index];
const typeId = ctToInt(q.QuestionType_ID, 0);
const type = CT_TYPES[typeId];
const rowText = `出头 第${index + 1}/${questions.length}题`;
if (!type) {
extLog(`${rowText}:题型(${typeId})暂不支持,已跳过`, 'warning');
skipped += 1;
continue;
}
const options = ctParseBody(q.Body);
const title = extText(String(q.Title || '').replace(/<[^>]+>/g, ''));
if (!title) {
skipped += 1;
continue;
}
let result;
try {
result = await getAccurateAnswer({
title,
optionsText: options.map((opt) => opt.content),
type,
refer: location.href
});
} catch (error) {
extLog(`${rowText}:查询答案失败(${(error && error.message) || error})`, 'error');
skipped += 1;
continue;
}
if (!result || result.code !== 1 || !result.data || !result.data.answer) {
extLog(`${rowText}:未查到答案`, 'warning');
skipped += 1;
continue;
}
const answers = Array.isArray(result.data.answer) ? result.data.answer : [result.data.answer];
const letters = ctAnswerLetters(answers, options);
pending.push({ index, letters, textAnswer: extText(answers.join('、')).slice(0, 60) });
done += 1;
}
if (!pending.length) {
extLog(`出头:没有可作答的题目(跳过 ${skipped} 题)`, 'warning');
return;
}
if (!EXT_CONF.autoFillExam) {
pending.forEach((item) => {
extLog(`出头 第${item.index + 1}题:答案 ${item.letters.length ? item.letters.join('') : item.textAnswer}`,
item.letters.length ? 'success' : 'warning');
});
extLog('出头:自动点选已关闭,请按上面的答案手动选择', 'warning');
return;
}
extLog(`出头:答案已就绪(${done} 题),开始自动点选;每题点完会核对,交卷仍需你手动确认`, null);
let filled = 0;
let manual = 0;
for (const item of pending) {
if (location.href !== startHref) {
extLog('出头:页面已切换,自动点选停止', 'warning');
break;
}
if (!item.letters.length) {
manual += 1;
extLog(`出头 第${item.index + 1}题:答案不是选项字母,请手动填 ${item.textAnswer}`, 'warning');
continue;
}
if (pending.length > 1) {
ctClickAnswerCard(item.index + 1);
await extSleep(260);
}
const ok = ctClickLetters(item.letters);
await extSleep(140);
if (ok) {
filled += 1;
extLog(`出头 第${item.index + 1}题:已选 ${item.letters.join('')}`, 'success');
} else {
manual += 1;
extLog(`出头 第${item.index + 1}题:自动点选未确认,请手动选 ${item.letters.join('')}`, 'warning');
}
}
extLog(`出头:自动点选完成,已选 ${filled} 题,需手动 ${manual} 题;请核对后手动交卷`, 'success');
} catch (error) {
extLog(`出头考试处理失败:${(error && error.message) || error}`, 'error');
}
}
/* ------------------------------ 路由表 ------------------------------ */
const qsRoutes = {
match: [
{ keyword: 'ExercisePaper', tab: '1', logic: qsHomeworkLogic },
{ keyword: '/Student/Course/CourseStudy', tab: '0', logic: qsStudyLogic },
{ keyword: '/Student/', tab: '0' }
],
defaultRoute: { show: true, tab: '0' }
};
const ctRoutes = {
match: [
{ keyword: '/onlineclass/paper/', tab: '1', logic: ctExamLogic },
{ keyword: '/onlineclass/curriculum/', tab: '0', logic: ctStudyLogic },
{ keyword: 'superchutou', tab: '0' }
],
defaultRoute: { show: true, tab: '0' }
};
return { qsRoutes, ctRoutes, qsOnSite, ctOnSite };
})();
// ===== 智慧树课程队列 =====
// 目标:在「队列」里勾几门课,刷完一门自动进下一门。
// 状态存 GM,多个标签页共用一份;同一时刻只有一个页面真正在推进队列。
const QUEUE_STORAGE_PREFIX = "xsCourseQueue:";
const QUEUE_HEARTBEAT_SECONDS = 5;
const QUEUE_LOCK_STALE_MS = 30e3;
const QUEUE_WATCHDOG_SECONDS = 30;
const QUEUE_IDLE_TIMEOUT_MS = 30 * 60e3;
const QUEUE_SWITCH_DELAY_SECONDS = 3;
const QUEUE_ORPHAN_CHECK_SECONDS = 20;
const QUEUE_STRUCTURE_FAILURE_LIMIT = 2;
const QUEUE_PROGRESS_THROTTLE_MS = 10e3;
const QUEUE_RESULT_STATUS = { done: "done", stuck: "skipped", locked: "skipped", error: "failed" };
const QUEUE_PAGE_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
const queueStorageKey = (platform) => `${QUEUE_STORAGE_PREFIX}${platform}`;
const queueReadValue = (key, fallback) => {
try {
if (typeof _GM_getValue === "function") return _GM_getValue(key, fallback);
const raw = window.localStorage.getItem(key);
return raw === null ? fallback : raw;
} catch (e) {
return fallback;
}
};
const queueWriteValue = (key, value) => {
try {
if (typeof _GM_setValue === "function") {
_GM_setValue(key, value);
return;
}
window.localStorage.setItem(key, value);
} catch (e) {
}
};
const createEmptyQueueState = () => ({
status: "idle",
items: [],
currentKey: null,
ownerId: null,
heartbeat: 0,
structureFailures: 0,
updatedAt: 0
});
const parseQueueState = (raw) => {
const empty = createEmptyQueueState();
if (!raw) return empty;
try {
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
if (!parsed || typeof parsed !== "object") return empty;
return {
...empty,
...parsed,
items: Array.isArray(parsed.items) ? parsed.items : []
};
} catch (e) {
return empty;
}
};
const loadQueueState = (platform) => parseQueueState(queueReadValue(queueStorageKey(platform), ""));
const saveQueueState = (platform, state) => {
queueWriteValue(queueStorageKey(platform), JSON.stringify(state));
};
// 另一个页面改了队列(开始/暂停/换课)时同步过来
const watchQueueState = (platform, callback) => {
if (typeof GM_addValueChangeListener !== "function") return () => {
};
const listenerId = GM_addValueChangeListener(
queueStorageKey(platform),
(_name, _oldValue, newValue, remote) => {
if (remote) callback(parseQueueState(newValue));
}
);
return () => {
if (typeof GM_removeValueChangeListener === "function") {
GM_removeValueChangeListener(listenerId);
}
};
};
let activeQueueEngine = null;
// 学习逻辑刷完一门课时调用,告诉队列这门课的结论
const reportCourseResult = (result, reason = "") => {
activeQueueEngine?.report(result, reason);
};
const reportCourseProgress = () => {
activeQueueEngine?.reportProgress();
};
class CourseQueueEngine {
constructor(adapter, options) {
this.pageId = QUEUE_PAGE_ID;
this.heartbeatTimer = null;
this.watchdogTimer = null;
this.lastProgressSavedAt = 0;
this.navigating = false;
this.spaWatching = false;
this.adapter = adapter;
this.logger = options.logger;
this.onChange = options.onChange;
this.state = loadQueueState(adapter.platform);
watchQueueState(adapter.platform, (state) => {
this.state = state;
if (!this.isOwner()) this.stopTimers();
this.onChange(this.snapshot());
});
window.addEventListener("beforeunload", () => this.releaseOwnership());
}
getState() {
return this.snapshot();
}
isOwner() {
return this.state.ownerId === this.pageId;
}
isOwnedByOtherPage() {
const { ownerId, heartbeat } = this.state;
return Boolean(ownerId) && ownerId !== this.pageId && !this.isLockStale(heartbeat);
}
getCurrentItem() {
const { currentKey, items } = this.state;
return items.find((item) => item.key === currentKey) || null;
}
// 每次页面加载调用一次,按当前页面角色决定队列下一步
async onPageLoad() {
this.state = loadQueueState(this.adapter.platform);
this.onChange(this.snapshot());
if (this.adapter.isSpa) this.watchSpaNavigation();
if (this.state.status !== "running") return;
const currentUrl = location.href;
const role = this.adapter.resolvePageRole(currentUrl);
const current = this.getCurrentItem();
if (this.isOwnedByOtherPage()) {
this.logger("课程队列正在其他页面运行中");
return;
}
if (!current) {
this.claimOwnership();
await this.advance();
return;
}
const isQueuePage = (role === "courseHome" || role === "study") && this.adapter.getCourseKeyFromUrl(currentUrl) === current.key;
if (isQueuePage) {
this.claimOwnership();
await this.handleQueuePage(role, current);
return;
}
this.scheduleOrphanCheck();
}
start(candidates) {
if (!this.checkBlockers()) return false;
if (!candidates.length) {
this.logger("请先勾选要加入队列的课程", "warning");
return false;
}
this.update((state) => {
Object.assign(state, createEmptyQueueState(), {
status: "running",
items: candidates.map((candidate) => ({
...candidate,
status: "pending",
reason: "",
startedAt: 0,
lastProgressAt: 0
}))
});
});
this.claimOwnership();
this.logger(`课程队列已开始,共 ${candidates.length} 门课程`);
void this.advance();
return true;
}
pause(reason = "课程队列已暂停", type = null) {
this.stopTimers();
this.update((state) => {
state.status = "paused";
state.ownerId = null;
});
this.logger(reason, type);
}
resume() {
if (this.state.status !== "paused") return false;
if (!this.checkBlockers()) return false;
this.update((state) => {
state.status = "running";
state.structureFailures = 0;
});
this.claimOwnership();
this.logger("课程队列已继续");
const current = this.getCurrentItem();
if (!current) {
void this.advance();
return true;
}
const currentUrl = location.href;
const role = this.adapter.resolvePageRole(currentUrl);
if (this.adapter.getCourseKeyFromUrl(currentUrl) === current.key && role !== "other") {
void this.handleQueuePage(role, current);
} else {
void this.navigateTo(current.entryUrl);
}
return true;
}
// 只允许移出还没开始的课程,正在学的和已有结论的保留,免得打断流程
removeItem(key) {
const target = this.state.items.find((item) => item.key === key);
if (!target || target.status !== "pending") return false;
let removed = false;
this.update((state) => {
const index = state.items.findIndex((item) => item.key === key && item.status === "pending");
if (index === -1) return;
state.items.splice(index, 1);
removed = true;
if (!state.items.length) Object.assign(state, createEmptyQueueState());
});
if (removed) this.logger(`已将「${target.name}」移出课程队列`);
return removed;
}
clear() {
this.stopTimers();
this.update((state) => {
Object.assign(state, createEmptyQueueState());
});
this.logger("课程队列已清空");
}
// 只在当前页面就是队列课程时才认这个结论
report(result, reason = "") {
if (!this.isOwner() || this.state.status !== "running") return;
const current = this.getCurrentItem();
if (!current) return;
if (this.adapter.getCourseKeyFromUrl(location.href) !== current.key) return;
void this.finishCurrent(QUEUE_RESULT_STATUS[result], reason);
}
reportProgress() {
if (!this.isOwner() || this.state.status !== "running") return;
const now = Date.now();
if (now - this.lastProgressSavedAt < QUEUE_PROGRESS_THROTTLE_MS) return;
this.lastProgressSavedAt = now;
this.update((state) => {
const item = state.items.find((course) => course.key === state.currentKey);
if (item) item.lastProgressAt = now;
});
}
async handleQueuePage(role, current) {
if (role === "study") {
this.startWatchdog();
return;
}
if (role !== "courseHome") return;
let result;
try {
result = await this.adapter.enterStudy(current);
} catch (error) {
result = { type: "failed", reason: "进入学习页时出现异常" };
}
if (!this.isOwner() || this.state.status !== "running") return;
if (result.type === "navigate") {
this.logger(result.label ? `进入未完成章节:${result.label}` : "正在进入学习页面..");
await this.navigateTo(result.url);
} else if (result.type === "finished") {
await this.finishCurrent("done", result.reason);
} else {
await this.finishCurrent("failed", result.reason, Boolean(result.structural));
}
}
async finishCurrent(status, reason = "", structural = false) {
const current = this.getCurrentItem();
if (!current) return;
let structureFailures = 0;
this.update((state) => {
const item = state.items.find((course) => course.key === current.key);
if (item) {
item.status = status;
item.reason = reason;
}
state.structureFailures = structural ? state.structureFailures + 1 : 0;
structureFailures = state.structureFailures;
});
const statusText = { done: "已完成", skipped: "已跳过", failed: "处理失败" }[status];
this.logger(
`队列课程「${current.name}」${statusText}${reason ? `:${reason}` : ""}`,
status === "done" ? null : "warning"
);
if (structureFailures >= QUEUE_STRUCTURE_FAILURE_LIMIT) {
this.pause("连续多门课程都读不到页面数据,平台页面可能已改版,队列已暂停", "warning");
return;
}
await this.advance();
}
async advance() {
if (!this.isOwner() || this.state.status !== "running") return;
const next = this.state.items.find((item) => item.status === "pending");
if (!next) {
this.stopTimers();
const summary = this.summarize();
this.update((state) => {
state.status = "finished";
state.currentKey = null;
state.ownerId = null;
});
this.logger(`课程队列已全部处理完毕。${summary}`);
return;
}
const now = Date.now();
this.update((state) => {
const item = state.items.find((course) => course.key === next.key);
if (!item) return;
item.status = "running";
item.startedAt = now;
item.lastProgressAt = now;
state.currentKey = item.key;
});
const index = this.state.items.findIndex((item) => item.key === next.key) + 1;
this.logger(`即将开始队列课程(${index}/${this.state.items.length}):${next.name}`);
await this.navigateTo(next.entryUrl);
}
async navigateTo(targetUrl) {
if (this.navigating) return;
this.navigating = true;
await sleep(QUEUE_SWITCH_DELAY_SECONDS);
if (!this.isOwner() || this.state.status !== "running") {
this.navigating = false;
return;
}
this.stopTimers();
this.releaseOwnership();
this.adapter.navigate(targetUrl);
}
// 学习页一直在放视频就持续记进度;30 分钟没动静才算这门课卡住
startWatchdog() {
if (this.watchdogTimer) return;
this.watchdogTimer = setInterval(() => {
if (!this.isOwner() || this.state.status !== "running") {
this.stopTimers();
return;
}
if (this.adapter.isActivelyStudying()) {
this.reportProgress();
return;
}
const current = this.getCurrentItem();
if (!current) return;
if (Date.now() - current.lastProgressAt > QUEUE_IDLE_TIMEOUT_MS) {
void this.finishCurrent("skipped", "超过 30 分钟没有学习进度");
}
}, QUEUE_WATCHDOG_SECONDS * 1e3);
}
claimOwnership() {
this.update((state) => {
state.ownerId = this.pageId;
state.heartbeat = Date.now();
});
if (this.heartbeatTimer) return;
this.heartbeatTimer = setInterval(() => {
if (!this.isOwner()) {
this.stopTimers();
return;
}
this.update((state) => {
state.heartbeat = Date.now();
});
}, QUEUE_HEARTBEAT_SECONDS * 1e3);
}
releaseOwnership() {
if (!this.isOwner()) return;
this.update((state) => {
state.ownerId = null;
});
}
scheduleOrphanCheck() {
setTimeout(() => {
this.state = loadQueueState(this.adapter.platform);
if (this.state.status !== "running") return;
if (this.state.ownerId && !this.isLockStale(this.state.heartbeat)) return;
this.pause("检测到已离开队列中的课程页面,队列已暂停,可在「队列」里点继续", "warning");
}, QUEUE_ORPHAN_CHECK_SECONDS * 1e3);
}
watchSpaNavigation() {
if (this.spaWatching) return;
this.spaWatching = true;
let lastUrl = location.href;
setInterval(() => {
if (location.href === lastUrl) return;
lastUrl = location.href;
this.stopTimers();
void this.onPageLoad();
}, 1e3);
}
checkBlockers() {
const blockers = this.adapter.getStartBlockers();
blockers.forEach((text) => this.logger(text, "warning"));
return blockers.length === 0;
}
summarize() {
const count = (status) => this.state.items.filter((item) => item.status === status).length;
const parts = [`完成 ${count("done")} 门`];
if (count("skipped")) parts.push(`跳过 ${count("skipped")} 门`);
if (count("failed")) parts.push(`失败 ${count("failed")} 门`);
return parts.join(",");
}
isLockStale(heartbeat) {
return Date.now() - heartbeat > QUEUE_LOCK_STALE_MS;
}
stopTimers() {
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
if (this.watchdogTimer) clearInterval(this.watchdogTimer);
this.heartbeatTimer = null;
this.watchdogTimer = null;
}
// 其他页面可能刚写过队列,改之前先读最新一份,避免覆盖
update(mutator) {
const latest = loadQueueState(this.adapter.platform);
mutator(latest);
latest.updatedAt = Date.now();
this.state = latest;
saveQueueState(this.adapter.platform, latest);
this.onChange(this.snapshot());
}
snapshot() {
return JSON.parse(JSON.stringify(this.state));
}
}
class BasicCourseQueueAdapter {
constructor() {
this.isSpa = false;
}
getStartBlockers() {
return [];
}
isActivelyStudying() {
return false;
}
navigate(targetUrl) {
const topWindow = (_unsafeWindow && _unsafeWindow.top) || window.top;
topWindow.location.href = targetUrl;
}
readParam(targetUrl, ...names) {
try {
const params = new URL(targetUrl, location.href).searchParams;
const lowerNames = names.map((name) => name.toLowerCase());
for (const [key, value] of params) {
if (lowerNames.includes(key.toLowerCase()) && value) return value;
}
} catch (e) {
}
return null;
}
}
const ZHS_QUEUE_CONFIG = {
courseList: {
host: "onlineweb.zhihuishu.com",
pagePath: "/onlinestuh5",
pageUrl: "https://onlineweb.zhihuishu.com/onlinestuh5",
componentName: "courseList",
pageSize: 50,
maxPages: 10,
// 进行中的共享课
shareStatus: 0,
// 「智慧课程」板块的进行中标签
smartStatus: 3
},
study: {
courseParam: "recruitAndCourseId",
fallbackUrl: "https://studyvideoh5.zhihuishu.com/stuStudy?recruitAndCourseId="
},
// 智慧课程:主页 /singleCourse/{模块}/{courseId}/{classId},学习页 /learnPage/{courseId}/{nodeUid}/{classId}
smart: {
hosts: ["ai-smart-course-student-pro.zhihuishu.com", "smartcoursestudent.zhihuishu.com"],
homePrefix: "/singleCourse/",
studyPrefix: "/learnPage/",
knowledgeItem: ".item-content",
knowledgeProgress: ".progress-num"
},
resolveTimeout: 6,
smartHomeTimeout: 20
};
const queueParseUrl = (targetUrl) => {
try {
return new URL(targetUrl, location.href);
} catch (e) {
return null;
}
};
const queueIsSmartUrl = (parsed) => Boolean(parsed) && ZHS_QUEUE_CONFIG.smart.hosts.includes(parsed.hostname);
const queueParsePercent = (text) => {
const value = parseInt(String(text ?? "").replace(/[^\d]/g, ""), 10);
return Number.isNaN(value) ? null : value;
};
// 课程列表接口的参数由平台加密,只能借页面里课程列表组件自己的请求方法
const queueFindVueComponent = (name) => {
const pageDocument = (_unsafeWindow && _unsafeWindow.document) || document;
for (const el of pageDocument.querySelectorAll("*")) {
const vm = el.__vue__;
if (vm?.$options?.name === name) return vm;
}
return null;
};
class ZhsCourseQueueAdapter extends BasicCourseQueueAdapter {
constructor() {
super();
this.platform = "zhs";
this.platformName = "智慧树";
this.isSpa = true;
}
// 共享课从课程列表直接进学习页;智慧课程要先进课程主页挑未完成的知识点
resolvePageRole(targetUrl) {
const parsed = queueParseUrl(targetUrl);
if (!parsed) return "other";
const list = ZHS_QUEUE_CONFIG.courseList;
const smart = ZHS_QUEUE_CONFIG.smart;
if (parsed.hostname === list.host && parsed.pathname.startsWith(list.pagePath)) return "courseList";
if (queueIsSmartUrl(parsed)) {
if (!this.getCourseKeyFromUrl(targetUrl)) return "other";
if (parsed.pathname.startsWith(smart.studyPrefix)) return "study";
if (parsed.pathname.startsWith(smart.homePrefix)) return "courseHome";
return "other";
}
return this.getCourseKeyFromUrl(targetUrl) ? "study" : "other";
}
getCourseKeyFromUrl(targetUrl) {
const parsed = queueParseUrl(targetUrl);
if (queueIsSmartUrl(parsed)) {
const segments = parsed.pathname.split("/").filter(Boolean);
const prefix = `/${segments[0]}/`;
if (prefix === ZHS_QUEUE_CONFIG.smart.studyPrefix) return segments[1] || null;
if (prefix === ZHS_QUEUE_CONFIG.smart.homePrefix) return segments[2] || null;
return null;
}
return this.readParam(targetUrl, ZHS_QUEUE_CONFIG.study.courseParam) || null;
}
getStartBlockers() {
const setting = useSettingStore();
const blockers = [];
if (!setting.config.basicConfig.assistVideo.value) blockers.push("课程队列需要先打开「视频助手(智慧树)」");
if (!setting.config.basicConfig.autoChangeChapter.value) blockers.push("课程队列需要开启「自动切换章节」");
return blockers;
}
isActivelyStudying() {
return Array.from(document.querySelectorAll("video, audio")).some((media) => !media.paused && !media.ended);
}
async fetchCourseList() {
const list = ZHS_QUEUE_CONFIG.courseList;
if (this.resolvePageRole(location.href) !== "courseList") {
return { courses: [], error: `请先打开智慧树在线学堂学生首页(${list.pageUrl})再读取课程列表` };
}
const vm = await this.waitForCourseListComponent();
if (!vm?.$api) return { courses: [], error: "未找到课程列表组件,平台页面可能已更新" };
const sources = [
{ name: "共享课", load: () => this.loadShareCourses(vm) },
{ name: "智慧课程", load: () => this.loadSmartCourses(vm) }
];
const courses = [];
const loadedSources = [];
const errors = [];
const seen = new Set();
for (const source of sources) {
let sourceCourses;
try {
sourceCourses = await source.load();
} catch (error) {
errors.push(`${source.name}读取失败:${error.message}`);
continue;
}
if (sourceCourses.length) loadedSources.push(source.name);
for (const course of sourceCourses) {
if (seen.has(course.key)) continue;
seen.add(course.key);
courses.push(course);
}
}
if (!courses.length) {
return { courses: [], error: errors.join(";") || "没有进行中的共享课或智慧课程" };
}
return { courses, source: loadedSources.join("、") };
}
async enterStudy(item) {
if (!queueIsSmartUrl(queueParseUrl(item.entryUrl))) return { type: "navigate", url: item.entryUrl };
return this.enterSmartStudy();
}
// 知识点卡片没有跳转地址,只能点了让平台自己路由到学习页
async enterSmartStudy() {
const timeout = ZHS_QUEUE_CONFIG.smartHomeTimeout;
const smart = ZHS_QUEUE_CONFIG.smart;
let items = [];
for (let i = 0; i < timeout * 2; i++) {
items = Array.from(document.querySelectorAll(smart.knowledgeItem));
if (items.length) break;
await sleep(0.5);
}
if (!items.length) return { type: "failed", reason: "未读取到课程知识点列表", structural: true };
const target = items.find((item) => {
const percent = queueParsePercent(item.querySelector(smart.knowledgeProgress)?.textContent);
return percent === null || percent < 100;
});
if (!target) return { type: "finished", reason: "全部知识点学习进度已达 100%" };
const label = target.innerText.split("\n")[0].trim();
target.click();
for (let i = 0; i < timeout * 2; i++) {
if (location.pathname.startsWith(smart.studyPrefix)) return { type: "navigate", url: location.href, label };
await sleep(0.5);
}
return { type: "failed", reason: "点击知识点后未进入学习页" };
}
async waitForCourseListComponent() {
for (let i = 0; i < 20; i++) {
const vm = queueFindVueComponent(ZHS_QUEUE_CONFIG.courseList.componentName);
if (vm) return vm;
await sleep(0.5);
}
return null;
}
async loadShareCourses(vm) {
if (typeof vm.$api.queryShareCourseInfo !== "function") throw new Error("平台接口已变更");
const rawCourses = await this.fetchShareCourses(vm);
const courses = [];
for (const course of rawCourses) {
if (!course.secret) continue;
const resolvedUrl = await this.resolveEntryUrl(vm, course);
const entryUrl = resolvedUrl || ZHS_QUEUE_CONFIG.study.fallbackUrl + course.secret;
courses.push({
key: this.getCourseKeyFromUrl(entryUrl) || course.secret,
name: course.courseName || `课程 ${course.courseId}`,
desc: [course.teacherName, course.schoolName, course.progress && `进度 ${course.progress}`].filter(Boolean).join(" · "),
cover: course.courseImg || "",
entryUrl,
meta: {
type: "share",
courseId: String(course.courseId ?? ""),
recruitId: String(course.recruitId ?? "")
}
});
}
return courses;
}
async fetchShareCourses(vm) {
const list = ZHS_QUEUE_CONFIG.courseList;
const courses = [];
for (let pageNo = 1; pageNo <= list.maxPages; pageNo++) {
const response = await vm.$api.queryShareCourseInfo({ status: list.shareStatus, pageNo, pageSize: list.pageSize });
if (response?.code !== 200) throw new Error(response?.message || `接口返回异常(${response?.code ?? "无响应"})`);
const pageCourses = response.result?.courseOpenDtos || [];
courses.push(...pageCourses);
const total = Number(response.result?.totalCount) || 0;
if (!pageCourses.length || courses.length >= total) break;
}
return courses;
}
async loadSmartCourses(vm) {
if (typeof vm.$api.getAiCourseList !== "function") throw new Error("平台接口已变更");
const response = await vm.$api.getAiCourseList({ status: ZHS_QUEUE_CONFIG.courseList.smartStatus });
if (Number(response?.status) !== 200) throw new Error(response?.msg || `接口返回异常(${response?.status ?? "无响应"})`);
const courses = [];
for (const course of response.rt || []) {
const entryUrl = course.linkUrl;
const key = entryUrl && this.getCourseKeyFromUrl(entryUrl);
if (!key) continue;
courses.push({
key,
name: course.courseName || `课程 ${course.courseId}`,
desc: ["智慧课程", course.teacherUserName, course.schoolName, course.schedule != null && `进度 ${course.schedule}%`].filter(Boolean).join(" · "),
cover: course.imgUrl || "",
entryUrl,
meta: {
type: "smart",
courseId: String(course.courseId ?? ""),
classId: String(course.classId ?? "")
}
});
}
return courses;
}
// 学习页地址由平台按课程状态动态算,借平台自己的跳转方法,拦下这次跳转拿到地址
resolveEntryUrl(vm, course) {
const navigation = _unsafeWindow && _unsafeWindow.navigation;
if (!navigation?.addEventListener || typeof vm.goCourse !== "function") return Promise.resolve("");
return new Promise((resolve) => {
let timer = null;
const finish = (targetUrl) => {
clearTimeout(timer);
navigation.removeEventListener("navigate", onNavigate);
resolve(targetUrl);
};
const onNavigate = (event) => {
if (!event.cancelable) return;
event.preventDefault();
finish(event.destination?.url || "");
};
navigation.addEventListener("navigate", onNavigate);
timer = setTimeout(() => finish(""), ZHS_QUEUE_CONFIG.resolveTimeout * 1e3);
try {
vm.goCourse(course, 1);
} catch (error) {
finish("");
}
});
}
}
const zhsCourseQueueAdapter = new ZhsCourseQueueAdapter();
const useCourseQueueStore = pinia.defineStore("courseQueue", {
state: () => ({
enabled: false,
platformName: "",
status: "idle",
items: [],
currentKey: null,
runningElsewhere: false,
candidates: [],
selectedKeys: [],
loadingCandidates: false,
candidateError: "",
candidateSource: ""
}),
getters: {
currentItem: (state) => state.items.find((item) => item.key === state.currentKey) || null,
finishedCount: (state) => state.items.filter((item) => item.status !== "pending" && item.status !== "running").length,
hasQueue: (state) => state.items.length > 0
},
actions: {
init(adapter) {
if (this.enabled) return;
const log = useLogStore();
const engine = new CourseQueueEngine(adapter, {
logger: (text, type = null) => log.insertLog(text, type),
onChange: (queueState) => this.applyState(queueState)
});
activeQueueEngine = engine;
this.enabled = true;
this.platformName = adapter.platformName;
this.applyState(engine.getState());
engine.onPageLoad();
},
applyState(queueState) {
this.status = queueState.status;
this.items = queueState.items;
this.currentKey = queueState.currentKey;
this.runningElsewhere = Boolean(activeQueueEngine?.isOwnedByOtherPage());
},
async loadCandidates() {
if (!activeQueueEngine || this.loadingCandidates) return;
this.loadingCandidates = true;
this.candidateError = "";
try {
const { courses, error, source } = await activeQueueEngine.adapter.fetchCourseList();
this.candidates = courses;
this.candidateSource = source || "";
this.candidateError = error || "";
this.selectedKeys = this.selectedKeys.filter((key) => courses.some((course) => course.key === key));
} finally {
this.loadingCandidates = false;
}
},
// 勾选顺序就是队列里的先后顺序
toggleCandidate(key) {
this.selectedKeys = this.selectedKeys.includes(key) ? this.selectedKeys.filter((item) => item !== key) : [...this.selectedKeys, key];
},
clearSelection() {
this.selectedKeys = [];
},
start() {
const selected = this.selectedKeys.map((key) => this.candidates.find((course) => course.key === key)).filter(Boolean);
return activeQueueEngine?.start(selected) ?? false;
},
pause() {
activeQueueEngine?.pause();
},
resume() {
activeQueueEngine?.resume();
},
removeItem(key) {
return activeQueueEngine?.removeItem(key) ?? false;
},
clear() {
activeQueueEngine?.clear();
}
}
});
// 队列界面:读取进行中的课程 → 勾选 → 开始,之后由脚本按顺序一门门刷
const CourseQueuePanel = {
name: "CourseQueuePanel",
setup() {
const store = useCourseQueueStore();
const statusText = vue.computed(() => ({
idle: "未开始",
running: "运行中",
paused: "已暂停",
finished: "已结束"
}[store.status] || "未开始"));
const statusLabel = (item) => ({
pending: "等待中",
running: "学习中",
done: "已完成",
skipped: "已跳过",
failed: "处理失败"
}[item.status] || "等待中");
const load = () => store.loadCandidates();
const start = () => store.start();
return { store, statusText, statusLabel, load, start };
},
// 用渲染函数而不是模板字符串:油猴沙箱里 new Function 编译出来的代码看不到 Vue 变量
render() {
const h = vue.h;
const store = this.store;
const statusText = this.statusText;
const plain = { padding: "5px 12px", borderRadius: "6px", border: "1px solid var(--t-border2)", background: "var(--t-surface)", color: "var(--t-text)", cursor: "pointer" };
const primary = Object.assign({}, plain, { border: "1px solid var(--t-primary)", background: "var(--t-primary)", color: "#fff" });
const soft = { alignSelf: "flex-start", padding: "6px 14px", borderRadius: "6px", border: "1px solid var(--t-primary-border)", background: "var(--t-primary-bg)", color: "var(--t-primary)", cursor: "pointer" };
const row = (style, children) => h("div", { style }, children);
const children = [
row({ display: "flex", alignItems: "center", justifyContent: "space-between" }, [
h("div", { style: { fontSize: "15px", fontWeight: 600 } }, "课程队列"),
h("div", { style: { color: "var(--t-text3)" } }, statusText)
])
];
if (!store.hasQueue) {
const loadButton = h("button", { onClick: this.load, disabled: store.loadingCandidates, style: soft }, store.loadingCandidates ? "读取中.." : "读取进行中的课程");
const list = store.candidates.map((course) => h("label", {
key: course.key,
style: { display: "flex", alignItems: "flex-start", gap: "8px", padding: "8px 10px", border: "1px solid var(--t-border)", borderRadius: "6px", background: "var(--t-surface2)", cursor: "pointer" }
}, [
h("input", { type: "checkbox", checked: store.selectedKeys.includes(course.key), onChange: () => store.toggleCandidate(course.key), style: { marginTop: "3px" } }),
h("span", { style: { display: "flex", flexDirection: "column", gap: "2px" } }, [
h("span", { style: { color: "var(--t-text)" } }, course.name),
course.desc ? h("span", { style: { color: "var(--t-text3)", fontSize: "12px" } }, course.desc) : null
])
]));
children.push(row({ display: "flex", flexDirection: "column", gap: "10px" }, [
h("div", { style: { color: "var(--t-text3)", lineHeight: 1.7 } }, "先在浏览器打开智慧树「在线学堂」学生首页,再点下面的按钮读取进行中的课程;勾选的顺序就是自动学习的顺序。"),
loadButton,
store.candidateError ? h("div", { style: { color: "var(--t-warning)", lineHeight: 1.6 } }, store.candidateError) : null,
list.length ? row({ display: "flex", flexDirection: "column", gap: "6px" }, list) : null,
store.selectedKeys.length ? h("button", { onClick: this.start, style: Object.assign({}, soft, { border: "1px solid var(--t-primary)", background: "var(--t-primary)", color: "#fff" }) }, `开始学习(已选 ${store.selectedKeys.length} 门)`) : null
]));
} else {
const items = store.items.map((item, index) => h("div", {
key: item.key,
style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", padding: "8px 10px", border: "1px solid var(--t-border)", borderRadius: "6px", background: "var(--t-surface2)" }
}, [
h("span", { style: { color: "var(--t-text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, `${index + 1}. ${item.name}`),
h("span", { style: { color: "var(--t-text3)", flex: "none" } }, this.statusLabel(item))
]));
children.push(row({ display: "flex", flexDirection: "column", gap: "10px" }, [
h("div", { style: { color: "var(--t-text3)" } }, `已完成 ${store.finishedCount} / ${store.items.length} 门`),
row({ display: "flex", flexDirection: "column", gap: "6px" }, items),
row({ display: "flex", gap: "8px" }, [
store.status === "running" ? h("button", { onClick: () => store.pause(), style: plain }, "暂停") : null,
store.status === "paused" ? h("button", { onClick: () => store.resume(), style: primary }, "继续") : null,
h("button", { onClick: () => store.clear(), style: plain }, "清空队列")
])
]));
}
return row({ padding: "14px", display: "flex", flexDirection: "column", gap: "12px", height: "100%", overflow: "auto", color: "var(--t-text)", fontSize: "13px" }, children);
}
};
// ===== 智慧树掌握度满分 =====
// 参照「知到智慧树掌握度满分助手」的作答思路移植:
// 在掌握度热力图里逐个知识点进入练习,答对记住答案,答错换下一组选项重答,
// 直到知识点达到 100%;答案按课程存在本地,可导出 / 导入题库。
// 只接管智慧树掌握度页面,超星 / 青书 / 出头 不受影响。
const zhsMastery = (() => {
const STORAGE_PREFIX = "xsMastery:";
const SETTINGS_KEY = STORAGE_PREFIX + "settings";
const COURSES_KEY = STORAGE_PREFIX + "courses";
const COURSE_KEY = (id) => STORAGE_PREFIX + "course:" + id;
const SESSION_KEY = "xsMastery:run";
const SESSION_TTL_MS = 10 * 60e3;
const SELECTORS = {
cards: ".mastery-content-right .chunk-list .item-box",
cardName: ".item-box-name",
popover: ".mastery-box-custom-popover",
questions: ".exam .exam-item, .exam-preview .exam-item",
title: ".quest-title .option-name",
type: ".quest-type",
options: "label.el-radio, label.el-checkbox",
answerCards: ".exam .answer-card .list .item",
submit: ".exam .header .submit",
retry: ".exam-preview .header .submit",
pointAnalysis: ".point .line1-count-link"
};
const DEFAULT_SETTINGS = {
activity: "practice",
mode: "all",
ignoreAnswers: false,
delay: 200,
maxRounds: 4096,
importMatch: "loose"
};
// ---------- 本地存储 ----------
const readJson = (key, fallback) => {
try {
const raw = typeof _GM_getValue === "function" ? _GM_getValue(key, "") : window.localStorage.getItem(key);
if (!raw) return fallback;
return typeof raw === "string" ? JSON.parse(raw) : raw;
} catch (error) {
return fallback;
}
};
const writeJson = (key, value) => {
try {
const text = JSON.stringify(value);
if (typeof _GM_setValue === "function") {
_GM_setValue(key, text);
return true;
}
window.localStorage.setItem(key, text);
return true;
} catch (error) {
return false;
}
};
const settings = Object.assign({}, DEFAULT_SETTINGS, readJson(SETTINGS_KEY, {}) || {});
if (!["practice", "bank"].includes(settings.activity)) settings.activity = "practice";
if (!["all", "current"].includes(settings.mode)) settings.mode = "all";
if (!["loose", "strict"].includes(settings.importMatch)) settings.importMatch = "loose";
const saveSettings = () => writeJson(SETTINGS_KEY, settings);
const improving = () => settings.activity === "bank";
const coldReview = () => improving() && db && db.review && db.review.ignoreAnswers === true;
// ---------- 纯算法 ----------
const normalize = (value) => String(value == null ? "" : value).normalize("NFKC").replace(/\s+/g, " ").trim();
const canonical = (values) => [...values].sort();
const same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
function combinations(tokens, multiple) {
if (!tokens.length || tokens.length > 12) throw new Error("选项数量不支持(范围 1~12)");
const sorted = canonical(tokens);
if (new Set(sorted).size !== sorted.length) throw new Error("选项无法唯一识别");
if (!multiple) return sorted.map((token) => [token]);
const result = [];
function collect(start, size, chosen) {
if (!size) {
result.push(chosen);
return;
}
for (let i = start; i <= sorted.length - size; i++) collect(i + 1, size - 1, [...chosen, sorted[i]]);
}
// 先枚举真正的多选组合,再把单项垫在最后,覆盖所有非空子集
for (let size = 2; size <= sorted.length; size++) collect(0, size, []);
collect(0, 1, []);
return result;
}
function questionKey(scope, question) {
return JSON.stringify([scope, question.type, question.title, canonical(question.options.map((option) => option.token))]);
}
function binomial(n, k) {
let value = 1;
for (let i = 1; i <= k; i++) value = value * (n - i + 1) / i;
return value;
}
function answerModel(records) {
const model = {};
const seen = new Set();
for (const [key, record] of Object.entries(records || {})) {
if (record.type !== "多选题" || !record.correct || !record.correct.length) continue;
let parts;
try {
parts = JSON.parse(key);
} catch (error) {
continue;
}
const options = parts && parts[3];
if (!Array.isArray(options) || options.length < 1 || options.length > 12 || new Set(options).size !== options.length) continue;
if (new Set(record.correct).size !== record.correct.length || !record.correct.every((token) => options.includes(token))) continue;
const identity = JSON.stringify([parts[1], parts[2], canonical(options)]);
if (seen.has(identity)) continue;
seen.add(identity);
const counts = model[options.length] || (model[options.length] = {});
counts[record.correct.length] = (counts[record.correct.length] || 0) + 1;
}
return model;
}
function rankedCombinations(question, model = {}) {
const candidates = combinations(question.options.map((option) => option.token), question.multiple);
const n = question.options.length;
if (!question.multiple) return candidates;
// 冷启动按常见正确项数量排个先后,课程里确认过的题逐步覆盖这个先验
const prior = n === 5 ? { 2: 1, 3: 28, 4: 16, 5: 1 } : {};
const priorTotal = Object.values(prior).reduce((sum, count) => sum + count, 0);
const counts = Object.assign({}, model[n] || {});
for (const [size, count] of Object.entries(prior)) counts[size] = (counts[size] || 0) + 20 * count / priorTotal;
if (Object.values(counts).reduce((sum, count) => sum + count, 0) < 12) return candidates;
const score = (choice) => (counts[choice.length] || 0) / binomial(n, choice.length) + 1 / (2 ** n - 1);
return candidates
.map((choice, index) => ({ choice, index, score: score(choice) }))
.sort((a, b) => b.score - a.score || a.index - b.index)
.map((item) => item.choice);
}
function parseMasteryRate(value) {
const text = normalize(value);
if (!/^\d+(?:\.\d+)?\s*%?$/.test(text)) throw new Error("掌握度百分比无法识别,请检查知识点弹窗");
const rate = Number(text.replace("%", "").trim());
if (rate < 0 || rate > 100) throw new Error("掌握度百分比超出范围");
return rate;
}
function masteryProgress(colors, rates = []) {
const total = colors.length;
const exempt = colors.filter((color) => color === "blue").length;
const eligible = total - exempt;
let perfect = 0;
let verified = 0;
colors.forEach((color, index) => {
if (color === "blue") return;
const rate = rates[index];
if (typeof rate === "number" && Number.isFinite(rate) && rate >= 0 && rate <= 100) {
verified++;
if (rate === 100) perfect++;
}
});
return {
total,
exempt,
eligible,
perfect,
verified,
unchecked: eligible - verified,
remaining: eligible - perfect,
percent: eligible ? Math.round(1000 * perfect / eligible) / 10 : total ? 100 : 0
};
}
function nextAnswer(record, question, model, preferred = []) {
const candidates = rankedCombinations(question, model);
if (record.correct && record.correct.length) {
if (!candidates.some((choice) => same(choice, record.correct))) throw new Error("已知答案与当前选项不一致");
return record.correct;
}
const candidateKeys = new Set(candidates.map((choice) => JSON.stringify(canonical(choice))));
const eligible = preferred.filter((choice) => Array.isArray(choice) && candidateKeys.has(JSON.stringify(canonical(choice))));
const choice = [...eligible, ...candidates].find((candidate) => !(record.wrong || []).some((wrong) => same(wrong, candidate)));
if (!choice) throw new Error("所有组合均被判错,已暂停;请检查题目、批改或其他脚本干扰");
return choice;
}
function remember(record, choice, correct) {
if (!choice.length) throw new Error("批改题目没有已选答案,不能记录");
if (correct) {
record.correct = canonical(choice);
record.wrong = (record.wrong || []).filter((wrong) => !same(wrong, choice));
} else {
if (record.correct && same(record.correct, choice)) delete record.correct;
record.wrong = record.wrong || [];
if (!record.wrong.some((wrong) => same(wrong, choice))) record.wrong.push(canonical(choice));
}
}
function newReview(ignoreAnswers = false, id = `${Date.now()}-${Math.random().toString(36).slice(2)}`) {
return {
id,
startedAt: new Date().toISOString(),
ignoreAnswers,
completed: [],
items: {},
trials: {},
rounds: {},
activeCard: "",
readyScope: "",
finishedAt: null
};
}
function questionIdentity(question) {
return JSON.stringify([question.type, question.title, canonical(question.options.map((option) => option.token))]);
}
function reviewProgress(review, eligible) {
const completed = new Set((review && review.completed) || []).size;
const entries = Object.values((review && review.items) || {});
return {
completed,
eligible,
percent: eligible ? Math.round(1000 * Math.min(completed, eligible) / eligible) / 10 : 0,
added: entries.filter((item) => item.correct && !item.hadAnswer).length,
verified: entries.filter((item) => item.correct && item.hadAnswer && !item.changed).length,
corrected: entries.filter((item) => item.correct && item.changed).length,
unresolved: entries.filter((item) => !item.correct).length
};
}
function trackReview(review, question, record, choice, correct, imported = false) {
const identity = questionIdentity(question);
const item = review.items[identity] || (review.items[identity] = { hadAnswer: Boolean(record.correct && record.correct.length), changed: false, correct: false });
if (!correct && (imported || (record.correct && same(record.correct, choice)))) item.changed = true;
item.correct = correct;
if (review.ignoreAnswers) remember(review.trials[identity] || (review.trials[identity] = { wrong: [] }), choice, correct);
}
function reconcileFeedback(records, question, choice, correct) {
// 同一道题在不同知识点出现时,用这次批改同步修正旧记录;不做模糊覆盖
const identity = questionIdentity(question);
for (const [key, record] of Object.entries(records)) {
let parts;
try {
parts = JSON.parse(key);
} catch (error) {
continue;
}
if (!Array.isArray(parts) || !Array.isArray(parts[3])) continue;
if (JSON.stringify(parts.slice(1)) !== identity) continue;
remember(record, choice, correct);
}
}
function masteryURL(currentURL) {
const current = new URL(currentURL);
const course = current.searchParams.get("recruitAndCourseId");
if (!course || !/^https?:$/.test(current.protocol)) throw new Error("无法识别本课程热力图地址");
const target = new URL("/study/mastery", current.origin);
target.searchParams.set("recruitAndCourseId", course);
return target.href;
}
function identifyPage(currentURL, markers) {
if (markers.analysis) return "analysis";
if (markers.exam) return "exam";
if (markers.point) return "point";
if (markers.mastery) return "mastery";
const url = new URL(currentURL);
const path = url.pathname.replace(/\/+$/, "");
return ["/examAnalysis", "/exam", "/study/mastery"].includes(path) || path.startsWith("/pointOfMastery/") ? "loading" : "unknown";
}
function extractCourseName(label) {
const match = normalize(label).match(/^课程名称\s*[::]\s*(.+)$/);
return match ? match[1].split(/[||]/)[0].trim() : "";
}
const matchText = (value) => normalize(value).toLowerCase().replace(/[\p{P}\p{Z}\s]/gu, "");
function truthToken(value) {
const text = normalize(value).toLowerCase();
if (/^(对|正确|是|真|true|t|√|✓|1)$/.test(text)) return "true";
if (/^(错|错误|不正确|否|假|false|f|×|✗|0)$/.test(text)) return "false";
return null;
}
function textSimilarity(a, b) {
a = matchText(a);
b = matchText(b);
if (!a || !b) return 0;
if (a === b) return 1;
if (Math.min(a.length, b.length) < 4) return 0;
const grams = (value) => {
const counts = new Map();
for (let i = 0; i < value.length - 1; i++) {
const pair = value.slice(i, i + 2);
counts.set(pair, (counts.get(pair) || 0) + 1);
}
return counts;
};
const left = grams(a);
const right = grams(b);
let overlap = 0;
for (const [pair, count] of left) overlap += Math.min(count, right.get(pair) || 0);
return 2 * overlap / (a.length + b.length - 2);
}
function importedItem(type, title, options, answers) {
title = normalize(title);
if (!["单选题", "多选题", "判断题"].includes(type) || !title) throw new Error("题型或题干缺失");
options = options.map(normalize);
if (type === "判断题") {
options = options.length ? options.map((option) => truthToken(option) || option) : ["true", "false"];
answers = answers.map((answer) => answer.map((option) => truthToken(option) || normalize(option)));
} else {
answers = answers.map((answer) => answer.map(normalize));
}
if (!options.length || options.length > 12 || options.some((option) => !option) || new Set(options).size !== options.length) throw new Error("选项缺失、重复或超过12项");
if (!answers.length || answers.some((answer) => !answer.length || (type !== "多选题" && answer.length !== 1) || new Set(answer).size !== answer.length || !answer.every((value) => options.includes(value)))) throw new Error("答案缺失或不能唯一对应选项");
const unique = [...new Map(answers.map((answer) => [JSON.stringify(canonical(answer)), canonical(answer)])).values()];
return { type, title, options, answers: unique };
}
function parseTextBank(text) {
const lines = String(text).replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").split("\n");
const blocks = [];
let block;
for (const raw of lines) {
const line = raw.trim();
const typed = line.match(/^(?:\d{1,6}\s*[.、.))]\s*)?[【\[((]\s*(单选题?|多选题?|判断题?)\s*[】\]))]\s*(.*)$/);
const numbered = !typed && line.match(/^\d{1,6}\s*[.、.]\s*(?!\d)(.+)$/);
if (typed || numbered) {
if (blocks.length >= 20000) throw new Error("单次最多识别20000道题,请拆分题库");
block = { type: typed ? typed[1].replace(/题?$/, "题") : "", lines: [typed ? typed[2] : numbered[1]], number: blocks.length + 1 };
blocks.push(block);
} else if (block && line) block.lines.push(line);
}
const items = [];
const excluded = [];
for (const entry of blocks) {
try {
let body = [...entry.lines];
let answer = "";
const explicit = body.findIndex((line) => /^(?:参考答案|正确答案|答案)\s*[::]/.test(line));
if (explicit >= 0) {
answer = body[explicit].replace(/^(?:参考答案|正确答案|答案)\s*[::]\s*/, "");
body = body.slice(0, explicit);
}
const optionPattern = /^([A-La-l])\s*[.、.::))]\s*(.*)$/;
const optionStart = body.findIndex((line) => optionPattern.test(line));
let title;
let options = [];
const labels = [];
if (optionStart >= 0) {
title = body.slice(0, optionStart).join(" ");
if (!answer) {
if (optionPattern.test(body[body.length - 1])) throw new Error("没有答案行");
answer = body.pop();
}
for (const line of body.slice(optionStart)) {
const option = line.match(optionPattern);
if (option) {
labels.push(option[1].toUpperCase());
options.push(option[2]);
} else {
options[options.length - 1] += " " + line;
}
}
if (new Set(labels).size !== labels.length) throw new Error("选项字母重复");
} else {
if (!answer) answer = body.pop();
title = body.join(" ");
}
answer = normalize(answer);
let choice;
const exact = options.filter((option) => normalize(option) === answer);
if (exact.length === 1) choice = exact;
else if (labels.length && /^[A-La-l\s,,;;、|]+$/.test(answer)) {
const letters = answer.toUpperCase().replace(/[\s,,;;、|]/g, "").split("");
if (!letters.every((letter) => labels.includes(letter))) throw new Error("答案字母不在选项中");
choice = letters.map((letter) => options[labels.indexOf(letter)]);
} else choice = answer.split(/[;;]/).map(normalize);
if (choice.length === 1 && !exact.length && /[,,、|]/.test(answer)) {
const parts = answer.split(/[,,、|]/).map(normalize);
if (parts.every((part) => options.some((option) => normalize(option) === part))) choice = parts;
}
const type = entry.type || (!options.length && truthToken(answer) ? "判断题" : choice.length > 1 ? "多选题" : "单选题");
items.push(importedItem(type, title, options, [choice]));
} catch (error) {
excluded.push({ number: entry.number, title: entry.lines[0] || "", reason: error.message });
}
}
if (!blocks.length) throw new Error("未识别到题目。请使用“1.【单选题】题干”或“1.题干”开头,每个选项和答案各占一行");
return { items, excluded, detected: blocks.length };
}
function parseJSONBank(text) {
const data = JSON.parse(String(text).replace(/^\uFEFF/, ""));
const records = (data && data.records && data.records.questions) || (data && data.questions);
if (!records || typeof records !== "object" || Array.isArray(records)) throw new Error("JSON不是本脚本导出的记录或题库格式");
const entries = Object.entries(records);
if (entries.length > 20000) throw new Error("单次最多识别20000道题,请拆分题库");
const items = [];
const excluded = [];
for (const [key, record] of entries) {
try {
if (!record || typeof record !== "object") throw new Error("题目记录无效");
let parts;
try {
parts = JSON.parse(key);
} catch (error) {
parts = [];
}
const sourceOptions = record.latestOptions || record.options || (parts && parts[3]) || [];
const options = sourceOptions.map((option) => typeof option === "string" ? { token: option, content: option } : option);
const tokens = record.correctAnswerTokens || record.correct;
if (!Array.isArray(tokens) || !tokens.length) throw new Error("没有已确认答案");
const choice = tokens.map((token) => {
const matches = options.filter((option) => option && option.token === token);
if (matches.length !== 1) throw new Error("答案与选项不一致");
return matches[0].content != null ? matches[0].content : matches[0].token;
});
items.push(importedItem(record.type || (parts && parts[1]), record.title || (parts && parts[2]), options.map((option) => (option ? option.content != null ? option.content : option.token : "")), [choice]));
} catch (error) {
excluded.push({ number: excluded.length + items.length + 1, title: (record && record.title) || "", reason: error.message });
}
}
return { items, excluded, detected: entries.length };
}
function parseBankFile(text, filename = "") {
return /\.json$/i.test(filename) || /^[\s\uFEFF]*\{/.test(text) ? parseJSONBank(text) : parseTextBank(text);
}
function mergeImported(existing, incoming) {
const map = new Map();
let added = 0;
let duplicates = 0;
let addedAnswers = 0;
for (const [batch, items] of [existing || [], incoming].entries()) {
for (const raw of items) {
const item = importedItem(raw.type, raw.title, raw.options, raw.answers);
const key = JSON.stringify([item.type, item.title, canonical(item.options)]);
if (!map.has(key)) {
map.set(key, Object.assign({}, item, { answers: [] }));
if (batch === 1) added++;
}
const target = map.get(key);
for (const answer of item.answers) {
if (target.answers.some((previous) => same(previous, answer))) {
if (batch === 1) duplicates++;
} else {
target.answers.push(answer);
if (batch === 1) addedAnswers++;
}
}
}
}
if (map.size > 20000) throw new Error("合并后超过20000道题,请分到不同课程或先清除导入题库");
const items = [...map.values()];
return { items, added, addedAnswers, duplicates, conflicts: items.filter((item) => item.answers.length > 1).length };
}
function importedCandidates(items, question, mode = "loose") {
const live = question.options.map((option) => ({
token: option.token,
text: question.type === "判断题" ? truthToken(option.content != null ? option.content : option.token) || normalize(option.content != null ? option.content : option.token) : normalize(option.content != null ? option.content : option.token)
}));
const matches = [];
for (const item of items || []) {
if (item.type !== question.type) continue;
let score;
if (mode === "strict") {
if (normalize(item.title) !== normalize(question.title) || !same(item.options.map(normalize), live.map((option) => option.text))) continue;
score = 2;
} else {
if (/\[图片:/.test(item.title + question.title) && normalize(item.title) !== normalize(question.title)) continue;
score = textSimilarity(item.title, question.title);
if (score < 0.8) continue;
}
for (const answer of item.answers) {
const mapped = [];
let valid = true;
for (const value of answer) {
let options = live.filter((option) => mode === "strict" || /\[图片:/.test(value + option.text) ? option.text === normalize(value) : matchText(option.text) === matchText(value));
if (!options.length && mode !== "strict" && !/\[图片:/.test(value)) {
const ranked = live.filter((option) => !/\[图片:/.test(option.text)).map((option) => ({ option, similarity: textSimilarity(value, option.text) })).sort((a, b) => b.similarity - a.similarity);
if (ranked[0] && ranked[0].similarity >= 0.85 && ranked[0].similarity - ((ranked[1] && ranked[1].similarity) || 0) >= 0.08) options = [ranked[0].option];
}
if (options.length !== 1) {
valid = false;
break;
}
mapped.push(options[0].token);
}
if (valid && new Set(mapped).size === mapped.length && (question.multiple || mapped.length === 1)) matches.push({ choice: canonical(mapped), score });
}
}
return [...new Map(matches.sort((a, b) => b.score - a.score).map((item) => [JSON.stringify(item.choice), item.choice])).values()];
}
function questionBankFromRecords(records) {
const bank = { questions: {} };
for (const [key, record] of Object.entries(records || {})) {
let parts;
try {
parts = JSON.parse(key);
} catch (error) {
continue;
}
if (!parts || !Array.isArray(parts[3])) continue;
const [knowledgeScope, type, title, tokens] = parts;
bank.questions[key] = {
type: record.type || type,
title: record.title || title,
scope: knowledgeScope,
knowledgeTitle: record.knowledgeTitle || "",
multiple: type === "多选题",
latestOptions: record.options || tokens.map((token) => ({ token, content: token, images: [] })),
correctAnswerTokens: record.correct && record.correct.length ? canonical(record.correct) : null
};
}
return bank;
}
function formatTextBank(bank) {
const line = (value) => String(value == null ? "" : value).replace(/\s+/g, " ").trim();
const contentOf = (option) => {
let content = line(option.content != null ? option.content : option.token);
if (content === option.token) content = content.replace(/\s+\[ID:[^\]]*\]$/, "");
const images = (option.images || []).map((image) => image.src).filter(Boolean);
if (images.length && !content.includes("[图片:")) content += ` [图片:${images.join("|")}]`;
return content;
};
const groups = new Map();
let skipped = 0;
let duplicates = 0;
let conflicts = 0;
let unconfirmed = 0;
let incomplete = 0;
const excluded = [];
const exclude = (key, question, reason) => {
skipped++;
if (reason === "unconfirmed") unconfirmed++;
else incomplete++;
excluded.push(Object.assign({ key }, question, { reason }));
};
for (const [key, question] of Object.entries(bank.questions || {})) {
const options = question.latestOptions || [];
const tokens = question.correctAnswerTokens;
if (!question.title || !["单选题", "多选题", "判断题"].includes(question.type) || !Array.isArray(options) || !options.length || options.some((option) => !option || typeof option.token !== "string" || !option.token)) {
exclude(key, question, "incomplete");
continue;
}
if (tokens === null || tokens === void 0 || (Array.isArray(tokens) && !tokens.length)) {
exclude(key, question, "unconfirmed");
continue;
}
if (!Array.isArray(tokens) || (!question.multiple && tokens.length !== 1) || new Set(tokens).size !== tokens.length || new Set(options.map((option) => option.token)).size !== options.length || !tokens.every((token) => options.some((option) => option.token === token))) {
exclude(key, question, "incomplete");
continue;
}
const contents = options.map(contentOf);
let answers = options.filter((option) => tokens.includes(option.token)).map(contentOf);
if (contents.some((content) => !content)) {
exclude(key, question, "incomplete");
continue;
}
let answer = answers.join(";");
if (question.type === "判断题") {
if (/^(对|正确|是|真|true|t|√|✓|1)$/i.test(normalize(answer))) answer = "true";
else if (/^(错|错误|不正确|否|假|false|f|×|✗|0)$/i.test(normalize(answer))) answer = "false";
}
const identity = JSON.stringify([question.type, normalize(question.title), canonical(contents)]);
const signature = question.type === "判断题" ? answer : JSON.stringify(canonical(answers));
if (groups.has(identity)) {
const previous = groups.get(identity);
if (previous.signature !== signature) previous.conflict = true;
else duplicates++;
continue;
}
groups.set(identity, { type: question.type, title: line(question.title), contents, answer, signature, key, question });
}
const blocks = [];
for (const item of groups.values()) {
if (item.conflict) {
conflicts++;
excluded.push(Object.assign({ key: item.key }, item.question, { reason: "conflict" }));
continue;
}
const lines = [`${blocks.length + 1}.【${item.type}】${item.title}`];
if (item.type !== "判断题") lines.push(...item.contents.map((content, index) => `${String.fromCharCode(65 + index)}.${content}`));
lines.push(item.answer);
blocks.push(lines.join("\r\n"));
}
return {
text: blocks.length ? blocks.join("\r\n\r\n") + "\r\n" : "",
exported: blocks.length,
skipped,
duplicates,
conflicts,
unconfirmed,
incomplete,
excluded
};
}
function formatExcludedBankReport(bank) {
const reasons = { unconfirmed: "尚未取得本题判对答案", incomplete: "题干、选项或答案匹配数据异常", conflict: "同题已确认答案互相冲突" };
const blocks = bank.excluded.map((question, index) => {
const lines = [
`${index + 1}.【${question.type || "未知题型"}】${question.title || "题干缺失"}`,
`未入库原因:${reasons[question.reason]}`,
`知识点:${question.knowledgeTitle || question.scope || "未知"}`
];
if (Array.isArray(question.latestOptions)) lines.push(...question.latestOptions.map((option, position) => `${String.fromCharCode(65 + position)}.${(option && option.content) || (option && option.token) || "选项内容缺失"}`));
return lines.join("\r\n");
});
return `未入库题目清单\r\n已入库 ${bank.exported} 题;未确认答案 ${bank.unconfirmed} 题;数据异常 ${bank.incomplete} 题;答案冲突 ${bank.conflicts} 组。\r\n以下题目未写入正式题库;这份清单供排查,不要导入搜题软件作为答案。\r\n\r\n${blocks.join("\r\n\r\n")}\r\n`;
}
// ---------- UI 状态 ----------
const useMasteryStore = pinia.defineStore("zhsMastery", {
state: () => ({
visible: false,
status: "idle",
message: "未开始",
courseId: "",
courseName: "",
activeName: "",
activity: settings.activity,
mode: settings.mode,
ignoreAnswers: settings.ignoreAnswers,
importMatch: settings.importMatch,
delay: settings.delay,
logs: [],
progress: null,
reviewStats: null,
bankCount: 0,
wrongCount: 0,
importedCount: 0,
courses: [],
exportCourse: "",
importPreview: "",
importReady: false,
bankPending: 0,
bankBroken: 0,
bankConflicts: 0,
canStart: false,
pageKind: "unknown"
}),
getters: {
statusText: (state) => ({
idle: "未开始",
starting: "启动中",
running: "运行中",
paused: "已暂停",
finished: "已完成"
}[state.status] || "未开始"),
percentText: (state) => state.progress ? `${state.progress.percent}%` : "—",
remainText: (state) => state.progress ? `已满分 ${state.progress.perfect} / 可练 ${state.progress.eligible},免考 ${state.progress.exempt},剩余 ${state.progress.remaining}` : "等待核实掌握度"
},
actions: {
start() {
moduleStart(false);
},
pause() {
moduleStop("已手动暂停");
},
setActivity(value) {
if (running || starting) return;
settings.activity = value === "bank" ? "bank" : "practice";
saveSettings();
syncState();
refreshProgress();
},
setMode(value) {
if (running || starting) return;
settings.mode = value === "current" ? "current" : "all";
saveSettings();
syncState();
},
setIgnoreAnswers(value) {
if (running || starting) return;
settings.ignoreAnswers = !!value;
saveSettings();
if (db && db.review) {
db.review.ignoreAnswers = settings.ignoreAnswers;
save();
}
syncState();
},
setDelay(value) {
if (running || starting) return;
const ms = Number(value);
if (!Number.isFinite(ms) || ms < 0 || ms > 5000) return;
settings.delay = Math.round(ms);
saveSettings();
syncState();
},
exportRecords() {
exportRecordsFile();
},
setImportMatch(value) {
if (running || starting) return;
settings.importMatch = value === "strict" ? "strict" : "loose";
saveSettings();
syncState();
},
refresh() {
refreshProgress();
},
setExportCourse(id) {
if (this.exportCourse === id) return;
this.exportCourse = id;
// 换了目标课程,之前预览过的题库文件就不能再用来导入了
stagedImport = null;
this.importPreview = "";
this.importReady = false;
refreshImportSummary();
},
exportBank() {
exportBankFile();
},
exportExcluded() {
exportExcludedFile();
},
importFile(file) {
previewBankFile(file);
},
confirmImport() {
changeImportedBank(false);
},
cancelImport() {
stagedImport = null;
this.importPreview = "";
this.importReady = false;
},
clearImportBank() {
const id = this.exportCourse || courseId();
if (!id) return;
if (!window.confirm("确认清除该课程已导入的候选题库?已确认的答案不受影响。")) return;
changeImportedBank(true);
}
}
});
const masteryStore = () => {
try {
return useMasteryStore();
} catch (error) {
return null;
}
};
// ---------- 运行时状态 ----------
let db = null;
let dbCourse = "";
let running = false;
let starting = false;
let busy = false;
let navigating = false;
let generation = 0;
let releaseLock = null;
let runCourse = "";
let lastRoute = "";
let routeSince = Date.now();
let routeReady = false;
let statusMessage = "待启动";
let activeName = "";
let selectedCard = "";
let checks = null;
let checksCourse = "";
let metadataRoute = "";
let metadataSince = Date.now();
let nameScanned = false;
let stagedImport = null;
let importBusy = false;
let bankQualitySignature = "";
let stepTimer = null;
let everStarted = false;
let refreshTimer = null;
let pagehideHooked = false;
let candidateSignature = "";
const candidateCache = new Map();
const backendCache = new Map();
const q = (selector, root = document) => root.querySelector(selector);
const qa = (selector, root = document) => [...root.querySelectorAll(selector)];
const textOf = (el) => normalize(el && el.textContent);
const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function visible(el, viewport = false) {
if (!el || !el.isConnected) return false;
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
const style = getComputedStyle(node);
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0 || node.getAttribute("aria-hidden") === "true") return false;
}
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && (!viewport || (rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth));
}
const shown = (selector) => qa(selector).find((el) => visible(el));
const courseId = () => new URL(location.href).searchParams.get("recruitAndCourseId") || "";
const DONE_KEY = (id) => STORAGE_PREFIX + "done:" + id;
function readDoneName() {
try {
return window.sessionStorage.getItem(DONE_KEY(courseId()));
} catch (error) {
return null;
}
}
function clearDoneName() {
try {
window.sessionStorage.removeItem(DONE_KEY(courseId()));
} catch (error) {
}
}
function scope() {
const url = new URL(location.href);
const knowledge = url.searchParams.get("knowledgeId") || (url.pathname.startsWith("/pointOfMastery/") ? url.pathname.split("/")[4] : "");
if (!courseId() || !knowledge) throw new Error("无法识别课程或知识点,请从掌握度热力图进入");
return `${courseId()}|${knowledge}`;
}
function pageKind() {
return identifyPage(location.href, {
analysis: Boolean(q(".exam-preview .exam-item .question-result")),
exam: Boolean(q(".exam .exam-item .quest-title")),
point: qa(".point .line1-count-total-num").length === 2,
mastery: Boolean(q(SELECTORS.cards))
});
}
// ---------- 日志 ----------
function log(message, course) {
statusMessage = message;
const line = `${new Date().toLocaleTimeString("zh-CN", { hour12: false })} ${message}`;
const store = masteryStore();
if (store) {
store.logs.push(line);
if (store.logs.length > 60) store.logs.splice(0, store.logs.length - 60);
store.message = message;
}
try {
useLogStore().insertLog(message);
} catch (error) {
}
console.info("[掌握度满分]", message);
}
// ---------- 记录存取 ----------
function registerCourse(id, name = "") {
const courses = readJson(COURSES_KEY, {}) || {};
const previous = courses[id];
const next = { id, name: name || (previous && previous.name) || "" };
if (!previous || previous.id !== id || previous.name !== next.name) {
courses[id] = next;
writeJson(COURSES_KEY, courses);
}
return next;
}
function loadDB(course) {
if (!course) throw new Error("请打开课程页面");
if (dbCourse === course && db) return;
db = readJson(COURSE_KEY(course), { version: 1, questions: {}, rounds: {}, pending: null });
if (!db || db.version !== 1 || !db.questions || !db.rounds) throw new Error("本地记录格式异常,请导出后清除记录");
dbCourse = course;
const known = registerCourse(course, db.course && db.course.id === course ? db.course.name : "");
if (!db.course || db.course.name !== known.name) db.course = known;
}
function save() {
if (!db || !dbCourse) return;
const latest = readJson(COURSE_KEY(dbCourse), null);
if (latest && latest.imported) {
db.imported = latest.imported;
db.importRevision = latest.importRevision;
}
writeJson(COURSE_KEY(dbCourse), db);
}
function preferredFor(question) {
if (coldReview()) return [];
const signature = JSON.stringify([dbCourse, (db && db.importRevision) || 0, settings.importMatch]);
if (candidateSignature !== signature) {
candidateSignature = signature;
candidateCache.clear();
}
const key = questionKey(scope(), question);
if (!candidateCache.has(key)) candidateCache.set(key, importedCandidates(db.imported, question, settings.importMatch));
return candidateCache.get(key);
}
function currentModel() {
if (!coldReview()) return answerModel(db.questions);
const trials = Object.fromEntries(Object.entries(db.review.trials).map(([identity, record]) => {
const parts = JSON.parse(identity);
return [JSON.stringify(["review", ...parts]), Object.assign({}, record, { type: parts[0] })];
}));
return answerModel(trials);
}
const answerRecord = (question, record) => coldReview() ? (db.review.trials[questionIdentity(question)] || (db.review.trials[questionIdentity(question)] = { wrong: [] })) : record;
// ---------- 掌握度缓存(同一次运行内) ----------
function loadMasteryChecks(course) {
if (checksCourse === course && checks) return checks;
checksCourse = course;
try {
checks = JSON.parse(window.sessionStorage.getItem(STORAGE_PREFIX + "checks:" + course) || "null");
} catch (error) {
checks = null;
}
if (!checks || checks.version !== 1 || !checks.entries || typeof checks.entries !== "object") checks = { version: 1, entries: {} };
return checks;
}
function saveMasteryChecks() {
try {
window.sessionStorage.setItem(STORAGE_PREFIX + "checks:" + checksCourse, JSON.stringify(checks));
} catch (error) {
}
}
function cardColor(card) {
return ["blue", "green", "gray", "origin", "pink"].find((color) => card.classList.contains(color)) || "unknown";
}
function cardIdentity(card, index = qa(SELECTORS.cards).indexOf(card)) {
const section = textOf(q(".mastery-name", card.closest(".mastery-content")));
return JSON.stringify([courseId(), section, textOf(q(SELECTORS.cardName, card)), index]);
}
function checkedRate(card, index) {
const entry = loadMasteryChecks(courseId()).entries[cardIdentity(card, index)];
return entry && entry.color === cardColor(card) && typeof entry.rate === "number" && entry.rate >= 0 && entry.rate <= 100 ? entry.rate : null;
}
function recordMasteryRate(key, rate, color) {
loadMasteryChecks(courseId()).entries[key] = { rate, color };
saveMasteryChecks();
}
// ---------- 面板数据 ----------
const assignIfChanged = (store, key, value) => {
if (JSON.stringify(store[key]) !== JSON.stringify(value)) store[key] = value;
};
function syncState() {
const store = masteryStore();
if (!store) return;
store.activity = settings.activity;
store.mode = settings.mode;
store.ignoreAnswers = settings.ignoreAnswers;
store.importMatch = settings.importMatch;
if (store.delay !== settings.delay) store.delay = settings.delay;
store.status = running ? "running" : starting ? "starting" : store.status === "finished" ? "finished" : everStarted ? "paused" : "idle";
store.canStart = !running && !starting && Boolean(courseId());
}
function refreshCourses() {
const store = masteryStore();
if (!store) return;
const id = courseId();
if (location.href !== metadataRoute) {
metadataRoute = location.href;
metadataSince = Date.now();
nameScanned = false;
}
// 扫全页找课程名很吃性能(每个元素都要算样式和位置),每页只做一次,且限制元素数量
if (id && dbCourse === id && !nameScanned && Date.now() - metadataSince >= 1000 && pageKind() === "mastery") {
nameScanned = true;
const candidates = qa("body *")
.slice(0, 1500)
.filter((el) => el.children.length < 4 && el.textContent.length < 180 && visible(el))
.map((el) => textOf(el))
.filter((text) => extractCourseName(text))
.sort((a, b) => a.length - b.length);
const name = candidates.length ? extractCourseName(candidates[0]) : "";
const course = registerCourse(id, name);
if (JSON.stringify(db.course) !== JSON.stringify(course)) {
db.course = course;
if (running) save();
}
}
const courses = readJson(COURSES_KEY, {}) || {};
const current = courses[id] || { id, name: "" };
store.courseId = id;
store.courseName = id ? current.name || "课程名称待识别" : "当前未打开课程";
assignIfChanged(store, "courses", Object.values(courses));
if (!store.exportCourse) store.exportCourse = id;
store.pageKind = pageKind();
refreshImportSummary();
}
function refreshProgress() {
const store = masteryStore();
if (!store || !courseId()) return;
try {
loadDB(courseId());
if (!running && !starting && !busy && !importBusy && dbCourse) {
const latest = readJson(COURSE_KEY(dbCourse), null);
if (latest && latest.version === 1 && latest.questions && latest.rounds) db = latest;
}
refreshCourses();
const cards = qa(SELECTORS.cards);
if (cards.length) {
const colors = cards.map(cardColor);
const snapshot = masteryProgress(colors, cards.map(checkedRate));
if (JSON.stringify(db.progress) !== JSON.stringify(snapshot)) {
db.progress = snapshot;
if (running) save();
}
}
let progress = db.progress && db.progress.perfect !== void 0 ? db.progress : null;
let stats = null;
if (improving() && progress) {
stats = reviewProgress(db.review, progress.eligible);
progress = Object.assign({}, progress, {
perfect: stats.completed,
percent: stats.percent,
remaining: Math.max(0, stats.eligible - stats.completed),
unchecked: 0
});
}
assignIfChanged(store, "progress", progress);
assignIfChanged(store, "reviewStats", stats);
const records = Object.values(db.questions || {});
store.bankCount = records.filter((record) => record.correct && record.correct.length).length;
store.wrongCount = records.filter((record) => !record.correct || !record.correct.length).length;
// 导入候选数和(换过课程时的)题数按「题库课程」下拉框选的那门算
refreshImportSummary();
} catch (error) {
store.progress = null;
store.message = `记录读取失败:${error.message}`;
}
}
function refreshImportSummary() {
const store = masteryStore();
if (!store) return;
const id = store.exportCourse || courseId();
if (!id) {
store.importedCount = 0;
store.bankPending = 0;
store.bankBroken = 0;
store.bankConflicts = 0;
return;
}
const records = readJson(COURSE_KEY(id), null);
store.importedCount = (records && records.imported ? records.imported.length : 0);
const questions = (records && records.questions) || null;
if (questions && id !== dbCourse) {
// 看的不是当前页面这门课时,题数按所选课程的记录算,别拿当前课程的顶替
const list = Object.values(questions);
store.bankCount = list.filter((record) => record.correct && record.correct.length).length;
}
// 题库质量(待确认 / 数据异常 / 答案冲突):只在内容真的变了时重算,别每次刷新都跑一遍全量
const sample = questions ? Object.keys(questions).length : 0;
const weight = questions ? Object.values(questions).reduce((sum, record) => sum + ((record.correct || []).length + (record.wrong || []).length), 0) : 0;
const signature = `${id}|${sample}|${weight}|${store.importedCount}|${(records && records.importRevision) || 0}`;
if (signature !== bankQualitySignature) {
bankQualitySignature = signature;
const bank = formatTextBank(questionBankFromRecords(questions || {}));
store.bankPending = bank.unconfirmed;
store.bankBroken = bank.incomplete;
store.bankConflicts = bank.conflicts;
}
}
// ---------- 题库导入导出 ----------
function exportSource() {
const store = masteryStore();
const id = (store && store.exportCourse) || courseId();
if (!id) throw new Error("没有可导出的课程记录");
const records = readJson(COURSE_KEY(id), null);
if (!records || !records.questions || !records.rounds) throw new Error("所选课程没有有效记录");
const course = (readJson(COURSES_KEY, {}) || {})[id] || { id, name: "" };
return { course, records };
}
function download(filename, data, mime = "application/json;charset=utf-8") {
const url = URL.createObjectURL(new Blob([data], { type: mime }));
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
setTimeout(() => URL.revokeObjectURL(url), 5000);
}
const safeName = (value) => String(value || "").replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_").replace(/[.\s]+$/, "").slice(0, 80);
function exportBankFile() {
try {
const { course, records } = exportSource();
const bank = formatTextBank(questionBankFromRecords(records.questions));
if (!bank.text) throw new Error("还没有确认过的题,暂时没有可导出的题库");
const at = new Date().toISOString().slice(0, 10);
download(`${safeName("智慧树纯文字题库")}-${safeName(course.name) || "未识别课程"}-${at}.txt`, "\uFEFF" + bank.text, "text/plain;charset=utf-8");
log(`已导出题库 ${bank.exported} 题(未确认 ${bank.unconfirmed}、数据异常 ${bank.incomplete}、冲突 ${bank.conflicts})`);
} catch (error) {
log(`导出失败:${error.message}`);
}
}
function exportExcludedFile() {
try {
const { course, records } = exportSource();
const bank = formatTextBank(questionBankFromRecords(records.questions));
if (!bank.excluded.length) {
log("没有未入库的题目");
return;
}
const at = new Date().toISOString().slice(0, 10);
download(`${safeName("智慧树未入库题目")}-${safeName(course.name) || "未识别课程"}-${at}.txt`, "\uFEFF" + formatExcludedBankReport(bank), "text/plain;charset=utf-8");
log(`已导出未入库清单 ${bank.excluded.length} 条`);
} catch (error) {
log(`导出失败:${error.message}`);
}
}
function exportRecordsFile() {
try {
const { course, records } = exportSource();
const at = new Date().toISOString();
download(`${safeName("智慧树练习记录")}-${safeName(course.name) || "未识别课程"}-${at.slice(0, 10)}.json`, JSON.stringify({ version: 1, course, exportedAt: at, records }, null, 2));
log("已导出运行记录 JSON");
} catch (error) {
log(`导出失败:${error.message}`);
}
}
async function previewBankFile(file) {
const store = masteryStore();
if (!file || !store) return;
if (running || starting || busy || importBusy) {
log("请先暂停,待当前操作结束后再导入题库");
return;
}
const id = store.exportCourse || courseId();
if (!id) {
log("请先打开课程,再选择导入目标");
return;
}
stagedImport = null;
store.importPreview = "";
store.importReady = false;
importBusy = true;
try {
if (file.size > 20 * 1024 * 1024) throw new Error("题库文件超过20 MB,请拆分后导入");
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
let text;
if (bytes[0] === 0xff && bytes[1] === 0xfe) text = new TextDecoder("utf-16le", { fatal: true }).decode(buffer);
else if (bytes[0] === 0xfe && bytes[1] === 0xff) text = new TextDecoder("utf-16be", { fatal: true }).decode(buffer);
else {
try {
text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
} catch (error) {
text = new TextDecoder("gb18030", { fatal: true }).decode(buffer);
}
}
const parsed = parseBankFile(text, file.name);
const records = readJson(COURSE_KEY(id), { version: 1, questions: {}, rounds: {}, pending: null });
const merged = mergeImported(records.imported, parsed.items);
const course = (readJson(COURSES_KEY, {}) || {})[id] || { id, name: "" };
stagedImport = { id, parsed, filename: file.name };
const sample = parsed.items.slice(0, 3).map((item, index) => `${index + 1}.【${item.type}】${item.title}\n候选:${item.answers.map((answer) => answer.join(";")).join(" / ")}`).join("\n\n");
store.importPreview = `目标课程:${course.name || id}\n文件:${file.name}\n识别 ${parsed.detected} 题,可用 ${parsed.items.length} 题,无法识别 ${parsed.excluded.length} 题\n新增 ${merged.added} 题 / ${merged.addedAnswers} 个候选答案,重复 ${merged.duplicates} 个,合并后冲突 ${merged.conflicts} 组` + (sample ? `\n\n${sample}` : "");
store.importReady = parsed.items.length > 0;
log(`题库预览已准备:可用 ${parsed.items.length} 题,核对后点「确认导入」`);
} catch (error) {
store.importPreview = `读取题库失败:${error.message}`;
log(`读取题库失败:${error.message}`);
} finally {
importBusy = false;
}
}
async function changeImportedBank(clear = false) {
const store = masteryStore();
if (!store) return;
if (running || starting || busy || importBusy) {
log("请先暂停,待当前操作结束后再修改导入题库");
return;
}
const id = store.exportCourse || courseId();
if (!id || (!clear && (!stagedImport || stagedImport.id !== id))) {
log("目标课程已改变,请重新选择题库文件");
return;
}
const course = (readJson(COURSES_KEY, {}) || {})[id] || { id, name: "" };
const incoming = clear ? [] : stagedImport.parsed.items;
importBusy = true;
try {
const applyImport = () => {
const records = readJson(COURSE_KEY(id), { version: 1, questions: {}, rounds: {}, pending: null });
const merged = mergeImported(clear ? [] : records.imported, incoming);
records.imported = merged.items;
records.importRevision = (Number(records.importRevision) || 0) + 1;
writeJson(COURSE_KEY(id), records);
if (dbCourse === id) db = records;
registerCourse(id, course.name);
candidateCache.clear();
return merged;
};
const merged = navigator.locks
? await navigator.locks.request(STORAGE_PREFIX + "lock:" + id, { ifAvailable: true }, (lock) => {
if (!lock) throw new Error("该课程正在其他标签页运行,请先暂停后再导入");
return applyImport();
})
: applyImport();
stagedImport = null;
store.importPreview = "";
store.importReady = false;
log(clear ? `[${course.name || id}] 导入候选已清除,已确认答案保留` : `[${course.name || id}] 已合并导入:新增 ${merged.added} 题、${merged.addedAnswers} 个候选答案`);
} catch (error) {
log(`题库修改失败:${error.message}`);
} finally {
importBusy = false;
refreshImportSummary();
}
}
// ---------- 页面守卫 ----------
function challenge() {
return qa('iframe[src*="captcha"], iframe[src*="turing.captcha"], .yidun_popup, .geetest_panel, .tcaptcha-transform, [id*="captcha"][role="dialog"]').find((el) => visible(el, true));
}
function dialogs() {
const matches = qa(".el-message-box, .el-dialog, [role=\"dialog\"]").filter((el) => visible(el));
return matches.filter((el) => !matches.some((other) => other !== el && el.contains(other)));
}
function guard(ctx, navigation = false, allowDialog = false) {
if (!running || ctx.generation !== generation) throw new Error("操作已取消");
if (courseId() !== runCourse) throw new Error("课程已改变,请重新开始");
if (!navigation && location.href !== ctx.url) throw new Error("页面已改变,已停止当前操作");
if (challenge()) throw new Error("页面出现需要手动处理的提示,请检查后点击继续");
if (!allowDialog && dialogs().length) throw new Error("出现弹窗,请手动处理后点击继续");
}
async function pauseMs(ms, ctx, navigation = false, allowDialog = false) {
const end = Date.now() + ms;
do {
guard(ctx, navigation, allowDialog);
await sleepMs(Math.min(100, Math.max(0, end - Date.now())));
} while (Date.now() < end);
guard(ctx, navigation, allowDialog);
}
async function waitFor(fn, ctx, timeout = 20000, navigation = false, allowDialog = false) {
const end = Date.now() + timeout;
do {
guard(ctx, navigation, allowDialog);
const result = fn();
if (result) return result;
await pauseMs(80, ctx, navigation, allowDialog);
} while (Date.now() < end);
throw new Error("等待页面响应超时,已暂停;请检查页面后继续");
}
async function click(el, ctx, allowDialog = false) {
guard(ctx, false, allowDialog);
if (!el || !visible(el) || el.disabled || el.classList.contains("disabled") || el.getAttribute("aria-disabled") === "true") throw new Error("按钮不存在、不可见或不可点击");
el.scrollIntoView({ block: "center", inline: "nearest" });
await pauseMs(settings.delay, ctx, false, allowDialog);
guard(ctx, false, allowDialog);
if (!el.isConnected) throw new Error("页面元素已刷新,请继续重试");
el.click();
}
async function navigateClick(el, ctx, expectedPage) {
await click(el, ctx);
await waitFor(() => location.href !== ctx.url && expectedPage.includes(pageKind()), ctx, 25000, true);
await pauseMs(settings.delay, ctx, true);
}
// ---------- 题目读取 ----------
function mediaKey(el) {
return qa("img", el).map((img) => img.getAttribute("src") || img.getAttribute("data-src") || img.getAttribute("alt") || "").join("|");
}
function readQuestion(el, index) {
const titleEl = q(SELECTORS.title, el);
const title = textOf(titleEl) + (titleEl && mediaKey(titleEl) ? ` [图片:${mediaKey(titleEl)}]` : "");
const type = textOf(q(SELECTORS.type, el));
if (!title || !/^(单选题|多选题|判断题)$/.test(type)) throw new Error(`第 ${index + 1} 题题型或题干不支持:${type || "未知"}`);
const labels = qa(SELECTORS.options, el);
const base = labels.map((label) => textOf(q(".preStyle", label)) || textOf(label).replace(/^[A-Z][.、.]\s*/i, ""));
const options = labels.map((label, i) => {
const input = q("input", label);
const content = `${base[i]}${mediaKey(label) ? " [图片:" + mediaKey(label) + "]" : ""}`;
const duplicate = base.filter((value) => value === base[i]).length > 1;
const token = content + (duplicate ? ` [ID:${(input && input.value) || ""}]` : "");
if (!input || !content || (duplicate && !input.value)) throw new Error(`第 ${index + 1} 题选项无法识别`);
return {
label,
input,
token,
letter: String.fromCharCode(65 + i),
content: base[i],
inputValue: input.value,
images: qa("img", label).map((img) => ({ src: img.getAttribute("src") || img.getAttribute("data-src") || "", alt: img.getAttribute("alt") || "" }))
};
});
const multiple = type === "多选题";
if (options.some((option) => option.input.type !== (multiple ? "checkbox" : "radio"))) throw new Error("题型与选项控件不一致");
combinations(options.map((option) => option.token), multiple);
const number = Number((textOf(q(".option-index", el)).match(/\d+/) || [])[0]) || index + 1;
return { el, title, type, multiple, options, number };
}
const selectedChoice = (question) => question.options.filter((option) => option.input.checked).map((option) => option.token);
function questions() {
const roots = qa(SELECTORS.questions);
if (!roots.length) throw new Error("没有识别到题目,请等待加载后继续");
return roots.map(readQuestion);
}
const answerLabel = (question, choice) => question.options.filter((option) => choice.includes(option.token)).map((option) => option.letter).join("");
// ---------- 题库 / AI 先答 ----------
function mapBackendAnswer(question, answers) {
const picked = [];
const list = typeof normalizeLocalBackendAnswers === "function" ? normalizeLocalBackendAnswers(answers) : [].concat(answers || []);
for (const raw of list) {
const text = String(raw == null ? "" : raw).trim();
if (!text) continue;
if (/^[A-Za-z]{1,8}$/.test(text)) {
const letters = text.toUpperCase().split("");
const mapped = letters.map((letter) => question.options[letter.charCodeAt(0) - 65]);
if (mapped.every(Boolean)) {
mapped.forEach((option) => picked.push(option.token));
continue;
}
}
let hit = question.options.find((option) => option.content === text);
if (!hit) hit = question.options.find((option) => normalize(option.content) === normalize(text));
if (!hit) hit = question.options.find((option) => option.content && (option.content.includes(text) || text.includes(option.content)));
if (!hit) return null;
picked.push(hit.token);
}
const unique = [...new Set(picked)];
if (!unique.length) return null;
if (!question.multiple && unique.length !== 1) return null;
return canonical(unique);
}
async function backendChoice(question, trial, options = {}) {
let token = "";
try {
token = useSettingStore().config.basicConfig.token.value || "";
} catch (error) {
token = "";
}
if (!token) return null;
const identity = questionIdentity(question);
let choice = backendCache.get(identity);
if (choice === void 0) {
let result = null;
try {
result = await callLocalBackendAnswer({
title: question.title,
optionsText: question.options.map((option) => option.content),
type: QUESTION_TYPE_CODE_MAP[question.type] || question.type,
element: question.el,
refer: location.href
}, options);
} catch (error) {
result = null;
}
if (result && result.code === 1 && result.data && Array.isArray(result.data.answer)) {
choice = mapBackendAnswer(question, result.data.answer);
backendCache.set(identity, choice);
} else if (result && result.code === -1004) {
// 服务器明确回复「没有答案」:本轮不再重复问
choice = null;
backendCache.set(identity, choice);
} else {
// 网络失败、限流、未登录等:不缓存,稍后可重试,避免一次抖动就被当成没有答案去猜
choice = null;
}
}
if (!choice) return null;
if ((trial.wrong || []).some((wrong) => same(wrong, choice))) return null;
return choice;
}
// 整页题库批量查询:一次请求把题库里已有的答案全部取回,没命中的题再逐题问 AI
async function batchBankAnswers(list) {
let token = "";
try {
token = useSettingStore().config.basicConfig.token.value || "";
} catch (error) {
token = "";
}
if (!token || !list.length) return [];
const answers = new Array(list.length).fill(null);
const CHUNK = 20;
for (let start = 0; start < list.length; start += CHUNK) {
const slice = list.slice(start, start + CHUNK);
const payload = JSON.stringify(slice.map((question) => ({
question: question.title || "",
options: question.options.map((option) => option.content),
type: QUESTION_TYPE_CODE_MAP[question.type] || question.type,
refer: location.href
})));
const result = await new Promise((resolve) => {
GM_xmlhttpRequest({
url: LOCAL_BACKEND_ANSWER_URL + "/bank?v=xs-6",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
Authorization: "Bearer " + token
},
data: "questions=" + encodeURIComponent(payload) + "&match_mode=auto",
timeout: 60000,
onload: (response) => {
try {
resolve(JSON.parse(response.responseText || "{}"));
} catch (error) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null)
});
});
if (!result || result.code !== 200 || !result.data || !Array.isArray(result.data.items)) continue;
if (result.data.profile) updateUserInfoFromProfile(result.data.profile);
for (const entry of result.data.items) {
const index = Number(entry && entry.index);
if (!Number.isInteger(index) || index < 0 || index >= slice.length) continue;
if (!entry.answer) continue;
const mapped = mapBackendAnswer(slice[index], [entry.answer]);
if (mapped && mapped.length) answers[start + index] = mapped;
}
}
return answers;
}
// ---------- 各页面处理 ----------
async function finishKnowledge(ctx) {
guard(ctx);
const target = masteryURL(ctx.url);
if (selectedCard) recordMasteryRate(selectedCard, 100, "green");
if (!selectedCard && activeName) {
// 答题过程中换过页面,卡片身份丢了:先按知识点名字记下来,回到热力图再回填成 100%
try {
window.sessionStorage.setItem(DONE_KEY(runCourse || courseId()), activeName);
} catch (error) {
}
}
if (improving()) {
const card = db.review.activeCard || selectedCard;
if (!card) throw new Error("本轮知识点身份缺失,请回到热力图后继续");
if (!db.review.completed.includes(card)) db.review.completed.push(card);
db.review.activeCard = "";
db.review.readyScope = "";
}
save();
if (!improving() && settings.mode === "current") {
stopMastery("当前知识点已全部答对,返回热力图并停止");
location.assign(target);
return;
}
saveRun();
log(`${activeName} 本轮全部答对,跳转热力图继续下一项`);
goHome(target);
}
function goHome(target) {
navigating = true;
const attempt = generation;
location.assign(target);
setTimeout(() => {
if (navigating && running && generation === attempt) log("正在等待热力图载入;若页面仍未变化,请刷新,或暂停后重试");
}, 25000);
}
async function handleExam(ctx) {
const quiz = questions();
const cards = qa(SELECTORS.answerCards);
if (cards.length !== quiz.length) throw new Error("题目数量与答题卡不一致,暂停等待页面完整加载");
activeName = textOf(q(".exam .nodename"));
const knowledge = scope();
const rounds = improving() ? db.review.rounds : db.rounds;
if ((rounds[knowledge] || 0) >= settings.maxRounds) throw new Error(`此知识点已达到 ${settings.maxRounds} 轮,请检查记录后调整上限`);
const model = currentModel();
const plan = [];
const entries = quiz.map((item) => {
const { key, record } = recordFor(item);
return { key, item, preferred: preferredFor(item), trial: answerRecord(item, record) };
});
// 先用一次批量请求取回题库里已有的答案,题库没命中的题再逐题问 AI(保持原来的准确率)
const pending = entries.map((entry, index) => index).filter((index) => !(entries[index].trial.correct && entries[index].trial.correct.length));
let batchAnswers = [];
if (pending.length) {
try {
batchAnswers = await batchBankAnswers(pending.map((index) => entries[index].item));
} catch (error) {
batchAnswers = [];
}
}
const bankPicked = new Map();
pending.forEach((entryIndex, position) => {
const mapped = batchAnswers[position];
if (mapped && mapped.length) {
bankPicked.set(entryIndex, mapped);
backendCache.set(questionIdentity(entries[entryIndex].item), mapped);
}
});
for (let index = 0; index < entries.length; index++) {
const entry = entries[index];
const item = entry.item;
const trial = entry.trial;
let choice = null;
let source = "尝试";
if (trial.correct && trial.correct.length) {
choice = nextAnswer(trial, item, model, entry.preferred);
source = "沿用正确答案";
} else if (bankPicked.has(index)) {
choice = bankPicked.get(index);
source = "题库/AI";
} else {
const backend = await backendChoice(item, trial);
if (backend) {
choice = backend;
source = "题库/AI";
} else {
choice = nextAnswer(trial, item, model, entry.preferred);
source = entry.preferred.some((candidate) => same(candidate, choice)) ? "题库候选" : "尝试";
}
}
plan.push({ q: item, key: entry.key, choice, known: Boolean(trial.correct && trial.correct.length), imported: source === "题库候选", source });
}
// 填答可以中断;只有所有题目的实际勾选状态符合计划才会提交
for (let index = 0; index < plan.length; index++) {
const item = plan[index];
if (!visible(item.q.el)) {
const card = cards.find((node) => Number(textOf(node)) === item.q.number);
await click(card, ctx);
await waitFor(() => visible(item.q.el), ctx);
}
log(`第 ${index + 1}/${plan.length} 题:${item.source} ${answerLabel(item.q, item.choice)}`);
for (const option of item.q.options) {
const want = item.choice.includes(option.token);
if ((item.q.multiple && option.input.checked !== want) || (!item.q.multiple && want && !option.input.checked)) {
if (option.input.disabled) throw new Error("选项不可编辑,请确认当前是答题页");
await click(option.label, ctx);
await waitFor(() => option.input.checked === want, ctx, 3000);
}
}
if (!same(selectedChoice(item.q), item.choice)) throw new Error(`第 ${index + 1} 题选择未生效,可能有其他脚本修改答案`);
}
await pauseMs(settings.delay, ctx);
if (plan.some((item) => !same(selectedChoice(item.q), item.choice))) throw new Error("提交前答案被改变,请关闭其他自动答题脚本后继续");
db.pending = {
scope: knowledge,
title: activeName,
entries: plan.map(({ key, choice, imported }) => ({ key, choice, imported })),
at: Date.now(),
reviewId: improving() ? db.review.id : null
};
save();
log(`本轮已作答 ${plan.length} 题,准备提交(第 ${(db.rounds[knowledge] || 0) + 1} 轮)`);
await click(q(SELECTORS.submit), ctx);
const end = Date.now() + 25000;
const accepted = new WeakSet();
while (Date.now() < end) {
guard(ctx, true, true);
if (location.href !== ctx.url && ["point", "analysis"].includes(pageKind())) {
db.rounds[knowledge] = (db.rounds[knowledge] || 0) + 1;
if (improving()) db.review.rounds[knowledge] = (db.review.rounds[knowledge] || 0) + 1;
save();
await pauseMs(settings.delay, ctx, true);
return;
}
for (const dialog of dialogs()) {
if (accepted.has(dialog)) continue;
const message = textOf(dialog);
if (/未[作答回答]|未完成|漏答|验证码/.test(message)) throw new Error("提交提示有未作答题目或验证,请手动检查");
if (/次数|已用完|上限|无法继续|不足/.test(message)) throw new Error("平台提示答题次数受限,已暂停");
if (!/(确认提交|确定.{0,6}提交|是否.{0,6}提交|提交后|交卷|提交.{0,6}[??])/.test(message)) throw new Error("出现未识别的提交弹窗,请手动处理后继续");
const confirm = qa("button", dialog).find((button) => /^(确定|确认|提交|确认提交|确定提交)$/.test(textOf(button)));
if (!confirm) throw new Error("未识别提交确认按钮");
accepted.add(dialog);
await click(confirm, ctx, true);
}
await pauseMs(100, ctx, true, true);
}
throw new Error("提交没有返回批改结果;已暂停,避免重复提交");
}
function recordFor(question) {
const key = questionKey(scope(), question);
const record = db.questions[key] || (db.questions[key] = { title: question.title, type: question.type, wrong: [] });
record.options = question.options.map((option, index) => ({
token: option.token,
content: option.content,
letter: option.letter || String.fromCharCode(65 + index),
index: index + 1,
inputValue: option.inputValue,
images: (option.images || []).map((image) => Object.assign({}, image))
}));
record.knowledgeTitle = activeName;
return { key, record };
}
async function handleAnalysis(ctx) {
if (improving() && !db.pending) {
if (db.review.readyScope === scope()) await finishKnowledge(ctx);
else await navigateClick(q(SELECTORS.retry), ctx, ["exam"]);
return;
}
if (improving() && db.pending.reviewId !== db.review.id) throw new Error("存在上一轮未处理的答卷,请从热力图开始新一轮");
const quiz = questions();
activeName = textOf(q(".exam-preview .nodename"));
const outcomes = quiz.map((item) => {
const result = q(".question-result", item.el);
const message = textOf(result);
const correct = /回答正确/.test(message) ? true : /回答错误/.test(message) ? false : null;
if (correct === null) throw new Error("批改尚未完成,或反馈格式改变,请等待后继续");
const choice = selectedChoice(item);
if (!choice.length || (!item.multiple && choice.length !== 1)) throw new Error("解析页没有有效的已选答案");
const { key, record } = recordFor(item);
return { q: item, key, record, choice, correct, message };
});
if (db.pending) {
if (db.pending.scope !== scope()) throw new Error("批改知识点与待提交记录不同,请返回原知识点继续");
if (db.pending.entries.length !== outcomes.length || outcomes.some((outcome) => {
const entry = db.pending.entries.find((node) => node.key === outcome.key);
return !entry || !same(entry.choice, outcome.choice);
})) throw new Error("批改答案与本轮提交记录不一致,请检查其他脚本或是否查看了旧记录");
}
outcomes.forEach((outcome) => {
if (improving()) trackReview(db.review, outcome.q, outcome.record, outcome.choice, outcome.correct, (db.pending && db.pending.entries.find((entry) => entry.key === outcome.key) || {}).imported);
reconcileFeedback(db.questions, outcome.q, outcome.choice, outcome.correct);
});
const count = outcomes.filter((outcome) => outcome.correct).length;
if (improving()) db.review.readyScope = count === outcomes.length ? scope() : "";
db.pending = null;
save();
log(`已记住批改结果:${count}/${outcomes.length} 正确`);
if (count === outcomes.length) {
await finishKnowledge(ctx);
} else {
const model = currentModel();
outcomes.forEach((outcome) => nextAnswer(answerRecord(outcome.q, outcome.record), outcome.q, model, preferredFor(outcome.q)));
await navigateClick(q(SELECTORS.retry), ctx, ["exam"]);
}
}
async function handlePoint(ctx) {
activeName = textOf(q(".point .backup-title"));
const counts = qa(".point .line1-count-total-num").map((el) => Number(textOf(el)));
if (counts.length !== 2 || !Number.isInteger(counts[0]) || counts[0] <= 0 || !Number.isInteger(counts[1]) || counts[1] < 0 || counts[1] > counts[0]) throw new Error("结果页题数未加载或没有练习题");
if (db.pending && db.pending.scope === scope()) {
await navigateClick(q(SELECTORS.pointAnalysis), ctx, ["analysis"]);
return;
}
if (counts[1] === counts[0] && (!improving() || db.review.readyScope === scope())) {
await finishKnowledge(ctx);
} else await navigateClick(q(SELECTORS.pointAnalysis), ctx, ["analysis"]);
}
function associatedPopover(card) {
const name = q(SELECTORS.cardName, card);
const ids = ((name && name.getAttribute("aria-describedby")) || "").split(/\s+/);
const linked = ids.map((id) => document.getElementById(id)).find((el) => el && el.matches(SELECTORS.popover));
if (linked) return linked;
const matches = qa(SELECTORS.popover).filter((el) => textOf(q("strong.name", el)) === textOf(name));
return matches.length === 1 ? matches[0] : null;
}
async function openPopover(card, ctx) {
const name = q(SELECTORS.cardName, card);
if (!name) throw new Error("知识点名称未识别");
card.scrollIntoView({ block: "center" });
await pauseMs(settings.delay, ctx);
const rect = name.getBoundingClientRect();
for (const type of ["pointerenter", "mouseenter", "mouseover", "mousemove"]) {
guard(ctx);
const EventClass = type.startsWith("pointer") ? PointerEvent : MouseEvent;
name.dispatchEvent(new EventClass(type, { bubbles: type !== "mouseenter" && type !== "pointerenter", clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 }));
}
return waitFor(() => {
const popover = associatedPopover(card);
return popover && visible(popover) && textOf(q("strong.name", popover)) === textOf(name) ? popover : null;
}, ctx, 3500);
}
async function handleMastery(ctx) {
if (!improving() && settings.mode === "current") {
stopMastery("当前知识点模式:请手动进入一个知识点后继续");
return;
}
const cards = qa(SELECTORS.cards);
if (!cards.length) throw new Error("热力图知识点尚未加载,请等待后继续");
const doneName = readDoneName();
if (doneName) {
const index = cards.findIndex((card) => textOf(q(SELECTORS.cardName, card)) === doneName);
if (index >= 0) recordMasteryRate(cardIdentity(cards[index], index), 100, cardColor(cards[index]));
clearDoneName();
}
let target;
let targetIndex;
for (const [index, card] of cards.entries()) {
const color = cardColor(card);
if (color === "blue") continue;
if (color === "unknown") throw new Error("热力图出现未知颜色状态");
let rate = checkedRate(card, index);
if (rate === null) {
log(`核实掌握度 ${index + 1}/${cards.length}:${textOf(q(SELECTORS.cardName, card))}`);
const popover = await openPopover(card, ctx);
rate = parseMasteryRate(textOf(q(".text > span", popover)));
recordMasteryRate(cardIdentity(card, index), rate, color);
}
const needed = improving() ? !db.review.completed.includes(cardIdentity(card, index)) : rate !== 100;
if (needed && !target) {
target = card;
targetIndex = index;
}
}
if (!target) {
if (improving()) {
db.review.finishedAt = new Date().toISOString();
save();
}
refreshProgress();
stopMastery(improving() ? "本轮题库完善已完成,可导出题库;开始新一轮可继续收集新题" : "所有非免考知识点已确认100%掌握度,任务完成");
return;
}
selectedCard = cardIdentity(target, targetIndex);
activeName = textOf(q(SELECTORS.cardName, target));
log(`进入知识点:${activeName}`);
const popover = await openPopover(target, ctx);
const rate = parseMasteryRate(textOf(q(".text > span", popover)));
recordMasteryRate(selectedCard, rate, cardColor(target));
if (!improving() && rate === 100) {
log(`${activeName} 已100%,跳过`);
return;
}
if (improving()) {
db.review.activeCard = selectedCard;
db.review.readyScope = "";
save();
}
const button = qa("button", popover).find((el) => textOf(el) === "提升掌握度");
await navigateClick(button, ctx, ["exam", "point", "analysis"]);
}
async function step() {
if (!running || busy || navigating) return;
if (location.href !== lastRoute) {
lastRoute = location.href;
routeSince = Date.now();
routeReady = false;
}
if (Date.now() - routeSince < 1000) return;
busy = true;
const ctx = { generation, url: location.href };
try {
guard(ctx);
const kind = pageKind();
if (kind === "unknown" || kind === "loading") {
if (Date.now() - routeSince < 25000) return;
throw new Error("当前页面不是掌握度练习,或加载超时,请返回热力图");
}
if (!routeReady) {
await pauseMs(600, ctx);
routeReady = true;
}
loadDB(runCourse);
if (kind === "exam") await handleExam(ctx);
else if (kind === "analysis") await handleAnalysis(ctx);
else if (kind === "point") await handlePoint(ctx);
else await handleMastery(ctx);
} catch (error) {
if (ctx.generation === generation && running) stopMastery(error.message || String(error));
} finally {
busy = false;
syncState();
}
}
// ---------- 启动 / 暂停 ----------
function saveRun() {
try {
window.sessionStorage.setItem(SESSION_KEY, JSON.stringify({ course: runCourse, activity: settings.activity, mode: settings.mode, at: Date.now() }));
} catch (error) {
}
}
function clearRun() {
try {
window.sessionStorage.removeItem(SESSION_KEY);
} catch (error) {
}
}
function resumeMarker() {
try {
const raw = window.sessionStorage.getItem(SESSION_KEY);
if (!raw) return null;
const data = JSON.parse(raw);
if (!data || !data.course || Date.now() - data.at > SESSION_TTL_MS) return null;
if (data.course !== courseId()) return null;
return data;
} catch (error) {
return null;
}
}
async function moduleStart(resume = false) {
if (running || starting || busy || importBusy) return;
const requestedCourse = courseId();
if (!requestedCourse || pageKind() === "unknown") {
log(`请打开掌握度热力图或知识点练习页面(当前路径:${location.pathname})`);
return;
}
starting = true;
syncState();
const attempt = ++generation;
log("等待热力图知识点列表或练习题目加载…");
try {
const deadline = Date.now() + 30000;
while (starting && attempt === generation) {
if (courseId() !== requestedCourse) {
stopMastery("课程已改变,请在当前课程重新开始");
return;
}
const kind = pageKind();
if (kind !== "loading" && kind !== "unknown") break;
if (Date.now() >= deadline) {
stopMastery("课程页面加载超时,请确认知识点列表或题目已显示后继续");
return;
}
await sleepMs(200);
}
if (!starting || attempt !== generation) return;
runCourse = requestedCourse;
const lock = await acquireLock(requestedCourse, attempt);
if (!lock) return;
dbCourse = "";
loadDB(runCourse);
if (improving()) {
db.review = db.review || newReview(settings.ignoreAnswers);
if (db.review.finishedAt) {
starting = false;
releaseLock && releaseLock();
releaseLock = null;
log("本轮已完成,请点「开始新一轮」继续完善题库");
return;
}
save();
}
loadMasteryChecks(runCourse);
if (resume !== true) {
checks = { version: 1, entries: {} };
saveMasteryChecks();
}
selectedCard = improving() ? db.review.activeCard : "";
running = true;
starting = false;
everStarted = true;
generation++;
lastRoute = location.href;
routeSince = Date.now();
routeReady = false;
saveRun();
if (stepTimer) clearInterval(stepTimer);
stepTimer = setInterval(() => {
step();
}, 350);
if (improving() && !selectedCard && pageKind() !== "mastery") {
log("返回热力图,建立本轮知识点列表");
goHome(masteryURL(location.href));
} else {
log(pageKind() === "mastery" ? "已识别热力图,开始处理下一知识点" : "已识别练习页面,逐题尝试并保存批改结果");
}
refreshProgress();
syncState();
} catch (error) {
if (attempt === generation || running) stopMastery(`启动失败:${error.message}`);
}
}
function acquireLock(course, attempt) {
return new Promise((resolve) => {
if (!navigator.locks) {
resolve(true);
return;
}
navigator.locks.request(STORAGE_PREFIX + "lock:" + course, { ifAvailable: true }, (lock) => {
return new Promise((holdRelease) => {
if (!starting || attempt !== generation) {
resolve(false);
holdRelease();
return;
}
if (!lock) {
starting = false;
syncState();
log("此课程已有另一个标签页在运行,请先暂停它");
resolve(false);
return;
}
releaseLock = holdRelease;
resolve(true);
});
}).catch(() => resolve(attempt === generation));
});
}
function stopMastery(reason = "已手动暂停") {
navigating = false;
running = false;
starting = false;
selectedCard = "";
generation++;
if (stepTimer) {
clearInterval(stepTimer);
stepTimer = null;
}
clearRun();
if (releaseLock) {
releaseLock();
releaseLock = null;
}
const store = masteryStore();
if (store) {
store.status = reason.includes("完成") || reason.includes("任务完成") ? "finished" : "paused";
}
log(reason);
syncState();
refreshProgress();
}
function resetReview() {
const id = courseId();
if (!id || running || starting) return;
dbCourse = "";
loadDB(id);
db.review = newReview(settings.ignoreAnswers);
db.pending = null;
save();
log("已开始新一轮题库完善");
moduleStart(false);
}
// ---------- 面板 ----------
const MasteryPanel = {
name: "MasteryPanel",
setup() {
const store = useMasteryStore();
const onFile = (event) => {
const file = event.target.files && event.target.files[0];
if (file) store.importFile(file);
event.target.value = "";
};
const start = () => store.start();
const reset = () => resetReview();
return { store, onFile, start, reset };
},
// 用渲染函数而不是模板字符串:油猴沙箱里 new Function 编译出来的代码看不到 Vue 变量
render() {
const h = vue.h;
const store = this.store;
const locked = store.status === "running" || store.status === "starting";
const smallText = { color: "var(--t-text3)", fontSize: "12px", lineHeight: 1.6 };
const selectStyle = { flex: 1, maxWidth: "200px", padding: "4px 8px", borderRadius: "6px", border: "1px solid var(--t-border2)", background: "var(--t-surface2)", color: "var(--t-text)" };
const buttonStyle = { padding: "5px 12px", borderRadius: "6px", border: "1px solid var(--t-border2)", background: "var(--t-surface)", color: "var(--t-text)", cursor: "pointer" };
const primaryButton = Object.assign({}, buttonStyle, { border: "1px solid var(--t-primary)", background: "var(--t-primary)", color: "#fff" });
const labeled = (label, control) => h("label", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px" } }, [h("span", { style: { color: "var(--t-text3)" } }, label), control]);
const parts = [
h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } }, [
h("div", { style: { fontSize: "15px", fontWeight: 600 } }, "掌握度满分"),
h("div", { style: { color: "var(--t-text3)" } }, store.statusText)
])
];
if (!store.courseId) {
parts.push(h("div", { style: { color: "var(--t-text3)", lineHeight: 1.7 } }, "请先打开智慧树的课程掌握度页面(热力图),再回到这里开始。"));
} else {
const info = [
h("div", { style: { color: "var(--t-text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, store.courseName)
];
if (store.activeName) info.push(h("div", { style: { color: "var(--t-text3)", fontSize: "12px" } }, `当前知识点:${store.activeName}`));
const percent = store.progress ? store.progress.percent : 0;
info.push(h("div", { style: { display: "flex", alignItems: "center", gap: "10px" } }, [
h("div", { style: { flex: 1, height: "6px", borderRadius: "3px", background: "var(--t-border)", overflow: "hidden" } }, [
h("div", { style: { width: `${percent}%`, height: "100%", background: "var(--t-primary)", transition: "width .3s" } })
]),
h("div", { style: { color: "var(--t-text)", fontVariantNumeric: "tabular-nums" } }, store.percentText)
]));
info.push(h("div", { style: { color: "var(--t-text3)", fontSize: "12px" } }, store.remainText));
if (store.reviewStats) {
info.push(h("div", { style: { color: "var(--t-text3)", fontSize: "12px" } }, `本轮新增 ${store.reviewStats.added} · 复核 ${store.reviewStats.verified} · 纠正 ${store.reviewStats.corrected} · 未定 ${store.reviewStats.unresolved}`));
}
parts.push(h("div", { style: { display: "flex", flexDirection: "column", gap: "6px" } }, info));
const actions = [
locked
? h("button", { onClick: () => store.pause(), style: { padding: "5px 14px", borderRadius: "6px", border: "1px solid var(--t-border2)", background: "var(--t-surface)", color: "var(--t-text)", cursor: "pointer" } }, store.status === "starting" ? "取消启动" : "暂停")
: h("button", { onClick: this.start, style: { padding: "5px 14px", borderRadius: "6px", border: "1px solid var(--t-primary)", background: "var(--t-primary)", color: "#fff", cursor: "pointer" } }, "开始 / 继续")
];
if (store.activity === "bank") actions.push(h("button", { onClick: this.reset, style: buttonStyle }, "开始新一轮"));
parts.push(h("div", { style: { display: "flex", gap: "8px" } }, actions));
parts.push(h("div", { style: { display: "flex", flexDirection: "column", gap: "6px" } }, [
labeled("模式", h("select", {
value: store.activity,
disabled: locked,
onChange: (event) => store.setActivity(event.target.value),
style: selectStyle
}, [h("option", { value: "practice" }, "满分练习"), h("option", { value: "bank" }, "题库完善")])),
h("div", { style: { color: "var(--t-text3)", fontSize: "12px", lineHeight: 1.6 } }, store.activity === "bank" ? "每个知识点都实际答一轮,不拿旧的满分顶替,用来补充题库。" : "跳过免考和已 100% 的知识点,其余答到全对为止。")
]));
parts.push(h("div", { style: { display: "flex", gap: "10px", alignItems: "center" } }, [
h("label", { style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" } }, [
h("span", { style: { color: "var(--t-text3)" } }, "范围"),
h("select", {
value: store.mode,
disabled: locked,
onChange: (event) => store.setMode(event.target.value),
style: Object.assign({}, selectStyle, { maxWidth: "140px" })
}, [h("option", { value: "all" }, "全部知识点"), h("option", { value: "current" }, "仅当前知识点")])
]),
h("label", { style: { display: "flex", alignItems: "center", gap: "6px", color: "var(--t-text3)" } }, [
"间隔",
h("input", {
type: "number",
min: 0,
max: 5000,
step: 50,
value: store.delay,
disabled: locked,
onChange: (event) => store.setDelay(event.target.value),
style: { width: "70px", padding: "3px 6px", borderRadius: "6px", border: "1px solid var(--t-border2)", background: "var(--t-surface2)", color: "var(--t-text)" }
}),
h("span", null, "毫秒")
])
]));
if (store.mode === "current") {
parts.push(h("div", { style: smallText }, "手动进入一个知识点后再点开始,这个知识点全对就停;不会自动往下一个走。"));
}
parts.push(h("label", { style: { display: "flex", alignItems: "center", gap: "8px", color: "var(--t-text3)" } }, [
h("input", {
type: "checkbox",
checked: store.ignoreAnswers,
disabled: locked,
onChange: (event) => store.setIgnoreAnswers(event.target.checked)
}),
h("span", null, "忽略已确认答案(重新核实本题)"),
h("span", { style: { color: "var(--t-text4, var(--t-text3))", fontSize: "12px" } }, "题库换过、答案可疑时勾上")
]));
const bank = [
labeled("题库课程", h("select", {
value: store.exportCourse,
onChange: (event) => store.setExportCourse(event.target.value),
style: selectStyle
}, store.courses.map((course) => h("option", { key: course.id, value: course.id }, course.name || course.id)))),
labeled("导入匹配", h("select", {
value: store.importMatch,
disabled: locked,
onChange: (event) => store.setImportMatch(event.target.value),
style: Object.assign({}, selectStyle, { maxWidth: "140px" })
}, [h("option", { value: "loose" }, "宽松(推荐)"), h("option", { value: "strict" }, "严格")])),
h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } }, [
h("span", null, `已确认答案 ${store.bankCount} 题`),
h("span", { style: { color: "var(--t-text3)", fontSize: "12px" } }, `导入候选 ${store.importedCount} 题`)
]),
h("div", { style: { color: "var(--t-text3)", fontSize: "12px" } }, `待确认 ${store.bankPending} · 数据异常 ${store.bankBroken} · 答案冲突 ${store.bankConflicts} 组`),
h("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" } }, [
h("button", { onClick: () => store.exportBank(), style: buttonStyle }, "导出题库"),
h("button", { onClick: () => store.exportExcluded(), style: buttonStyle }, "导出未入库清单"),
h("button", { onClick: () => store.exportRecords(), style: buttonStyle }, "导出记录"),
h("label", { style: Object.assign({}, buttonStyle, { display: "inline-block" }) }, [
"导入题库",
h("input", { type: "file", accept: ".txt,.json", style: { display: "none" }, onChange: this.onFile })
]),
h("button", {
onClick: () => store.clearImportBank(),
style: { padding: "5px 12px", borderRadius: "6px", border: "1px solid var(--t-border)", background: "transparent", color: "var(--t-text3)", cursor: "pointer" }
}, "清除导入库")
])
];
if (store.importPreview) {
bank.push(h("div", { style: { display: "flex", flexDirection: "column", gap: "6px" } }, [
h("div", { style: { whiteSpace: "pre-wrap", color: "var(--t-text3)", fontSize: "12px", lineHeight: 1.6, maxHeight: "120px", overflow: "auto" } }, store.importPreview),
h("div", { style: { display: "flex", gap: "8px" } }, [
h("button", { disabled: !store.importReady, onClick: () => store.confirmImport(), style: primaryButton }, "确认导入"),
h("button", { onClick: () => store.cancelImport(), style: buttonStyle }, "取消预览")
])
]));
}
parts.push(h("div", { style: { borderTop: "1px solid var(--t-border)", paddingTop: "10px", display: "flex", flexDirection: "column", gap: "8px" } }, bank));
parts.push(h("div", { style: { borderTop: "1px solid var(--t-border)", paddingTop: "10px", display: "flex", flexDirection: "column", gap: "4px" } }, [
h("div", { style: { color: "var(--t-text3)", fontSize: "12px" } }, "运行记录"),
h("div", {
style: { whiteSpace: "pre-wrap", fontSize: "12px", lineHeight: 1.6, color: "var(--t-text3)", maxHeight: "180px", overflow: "auto", fontFamily: "var(--t-mono, monospace)" }
}, store.logs.length ? store.logs.slice(-12).join("\n") : "暂无记录")
]));
}
return h("div", {
style: { padding: "14px", display: "flex", flexDirection: "column", gap: "12px", height: "100%", overflow: "auto", color: "var(--t-text)", fontSize: "13px" }
}, parts);
}
};
// ---------- 对外入口 ----------
async function logic() {
let enabled = true;
try {
enabled = useSettingStore().config.basicConfig.masteryFull.value;
} catch (error) {
enabled = true;
}
if (!enabled) {
await smartAnswerLogic();
return;
}
// 给页面一点时间渲染,再判断是不是掌握度页面
const pendingRun = resumeMarker();
const deadline = Date.now() + (pendingRun ? 30000 : 6000);
let kind = pageKind();
while ((kind === "loading" || kind === "unknown") && Date.now() < deadline) {
await sleepMs(300);
kind = pageKind();
}
if (kind === "loading" || kind === "unknown") {
await smartAnswerLogic();
return;
}
const store = masteryStore();
if (store) {
store.visible = true;
syncState();
}
refreshProgress();
if (!pagehideHooked) {
pagehideHooked = true;
window.addEventListener("pagehide", () => {
if (releaseLock) releaseLock();
});
}
if (!refreshTimer) {
refreshTimer = setInterval(() => {
try {
refreshProgress();
syncState();
} catch (error) {
}
}, 1500);
}
const marker = pendingRun;
if (marker) {
if (marker.activity) settings.activity = marker.activity;
await moduleStart(true);
} else if (store) {
store.message = "点「开始 / 继续」按知识点刷满分";
log("已识别到掌握度页面,切到「答题」页的「掌握度满分」点「开始」即可自动刷满分");
}
}
return {
logic,
useStore: useMasteryStore,
Panel: MasteryPanel,
resetReview
};
})();
const zhsRoutes = {
match: [
{ keyword: "/studyvideoh5.zhihuishu.com", tab: "1", logic: videoStudyLogic, assist: "video" },
{ keyword: "ai-smart-course-student-pro.zhihuishu.com/learnPage", tab: "1", logic: smartStudyLogic, assist: "video" },
{ keyword: "ai-smart-course-student-pro.zhihuishu.com/singleCourse", tab: "1", logic: smartStudyLogic, assist: "video" },
{ keyword: "smartcoursestudent.zhihuishu.com/learnPage", tab: "1", logic: smartStudyLogic, assist: "video" },
{ keyword: "smartcoursestudent.zhihuishu.com/singleCourse", tab: "1", logic: smartStudyLogic, assist: "video" },
{ keyword: "studyplush5.zhihuishu.com", tab: "1", logic: studyPlusLogic, assist: "video" },
{ keyword: "studywisdomh5.zhihuishu.com/study/index", tab: "1", logic: studyPlusLogic, assist: "video" },
{ keyword: "smartcourseexam.zhihuishu.com/ReviewExam", tab: "1", logic: smartAnswerLogic, assist: "answer" },
{ keyword: "studentexamcomh5.zhihuishu.com/studentReviewTestOrExam", tab: "1", logic: smartAnswerLogic, assist: "answer" },
{ keyword: "fusioncourseh5.zhihuishu.com/exam", tab: "1", logic: smartAnswerLogic, assist: "answer" },
// 掌握度练习:热力图 / 知识点 / 解析页都交给掌握度满分模块,页面上点「开始」才动手
{ keyword: "studywisdomh5.zhihuishu.com/study/mastery", tab: "2", logic: () => zhsMastery.logic() },
{ keyword: "studywisdomh5.zhihuishu.com/pointOfMastery", tab: "2", logic: () => zhsMastery.logic() },
{ keyword: "studywisdomh5.zhihuishu.com/examAnalysis", tab: "2", logic: () => zhsMastery.logic() },
{ keyword: "studywisdomh5.zhihuishu.com/exam", tab: "2", logic: () => zhsMastery.logic() },
{ keyword: "wisdom-mooc.zhihuishu.com/exam", tab: "1", logic: smartAnswerLogic, assist: "answer" },
{ keyword: "examloop.zhihuishu.com/exam", tab: "1", logic: smartAnswerLogic, assist: "answer" },
{ keyword: "zhihuishu.com/atHomeworkExam/stu/homeworkQ/exerciseList", tab: "1", logic: smartAnswerLogic, assist: "answer" },
{ keyword: "zhihuishu.com/atHomeworkExam/stu/examQ/examexercise", tab: "1", logic: smartAnswerLogic, assist: "answer" },
{ keyword: "zhihuishu.com/aidedteaching/sourceLearning", tab: "1", logic: specialStudyLogic, assist: "video" },
{ keyword: "hike-teaching-center.polymas.com/stu-hike/agent-course-hike/ai-course-center", tab: "1", logic: specialStudyLogic, assist: "video" },
{ keyword: "tools-hike/studentStudyResource", tab: "1", logic: specialStudyLogic, assist: "video" },
{ keyword: "fusioncourseh5.zhihuishu.com", tab: "1", logic: aiSmartLogic, assist: "video" },
{ keyword: "zhihuishu.com/learnPage", tab: "1", logic: learnPageLogic, assist: "video" },
{ keyword: "wisdom-mooc.zhihuishu.com/study/index", tab: "1", logic: wisdomStudyLogic, assist: "video" },
{ keyword: "/webExamList?", tab: "0" },
{ keyword: "/stuExamWeb.html", tab: "1", logic: autoAnswerLogic }
],
defaultRoute: {
show: true,
tab: "0"
}
};
// ===== 通用视频助手:新增平台共用的自动播放 / 自动翻页 =====
// 只在雨课堂、云班课、智慧职教、国家开放大学、中国大学MOOC、U校园、学堂在线、
// 绎通云、山东专技、西财在线 这些平台上生效;超星 / 智慧树 / 青书 / 出头 不经过这里
const XS_WATCH_STORAGE_KEY = "xsWatch:prefs";
const xsWatchDefaults = () => ({ speed: 1.5, autoNext: true, autoScroll: true, autoRun: false, segments: 0 });
const xsWatchReadPrefs = () => {
try {
const raw = typeof _GM_getValue === "function" ? _GM_getValue(XS_WATCH_STORAGE_KEY, "") : window.localStorage.getItem(XS_WATCH_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : null;
return Object.assign(xsWatchDefaults(), parsed && typeof parsed === "object" ? parsed : {});
} catch (e) {
return xsWatchDefaults();
}
};
const xsWatchWritePrefs = (prefs) => {
try {
const text = JSON.stringify(prefs);
if (typeof _GM_setValue === "function") _GM_setValue(XS_WATCH_STORAGE_KEY, text);
else window.localStorage.setItem(XS_WATCH_STORAGE_KEY, text);
} catch (e) {
}
};
// 各平台的页面特征。verified 表示按钮选择器来自公开实现、有据可依;
// 其余按页面常见结构写,第一次用如果点不动,把课程页地址发过来再调。
const XS_WATCH_PROFILES = [
{
id: "ykt",
name: "雨课堂",
test: (host) => /(^|\.)yuketang\.cn$/.test(host),
play: [".xt_video_bit_play_btn", ".xt_video_player_common_icon", ".xt_video_player_play_btn"],
next: [".btn-next", ".next-btn"],
verified: true
},
{
id: "mskt",
name: "云班课",
test: (host) => /(^|\.)mosoteach\.cn$/.test(host),
play: [".vjs-big-play-button", ".plyr__control--overlaid", "[class*=big-play]"],
next: ["[class*=next]:not([disabled])", ".ant-pagination-next button"],
verified: false
},
{
id: "icve-ai",
name: "智慧职教 AI",
test: (host) => host === "ai.icve.com.cn",
play: [".el-message-box .el-button--primary", "[class*=play-btn]", "[class*=video-play]"],
next: [".next .el-link--inner", "[class*=next]:not([disabled])"],
verified: true
},
{
id: "icve2",
name: "智慧职教",
test: (host) => host === "zjy2.icve.com.cn" || host === "zyk.icve.com.cn",
play: [".el-message-box .el-button--primary", "[class*=play-btn]"],
next: [".next .el-link--inner", "[class*=next]:not([disabled])"],
verified: true
},
{
id: "icve",
name: "智慧职教",
test: (host) => /(^|\.)icve\.com\.cn$/.test(host),
play: [".coursespace.screen-player-btn-icon.icon-play-sp-fill", ".screen-player-btn-icon.icon-play-sp-fill", "[class*=play]"],
next: [".item_done_pos", ".btn-next"],
verified: true
},
{
id: "ouchn-syxy",
name: "国家开放大学",
test: (host) => /(^|\.)syxy\.ouchn\.cn$/.test(host),
play: [".vjs-big-play-button", ".vjs-play-control.vjs-paused", ".vjs-poster"],
next: ["[class*=newgk-next]", "a.newgk-prenext"],
nextText: "下一页",
doc: true,
verified: true
},
{
id: "ouchn",
name: "国家开放大学",
test: (host) => /(^|\.)ouchn\.(cn|edu\.cn)$/.test(host),
play: [".mvp-fonts-play", "[class*=play]"],
next: [".next-btn"],
doc: true,
verified: true
},
{
id: "mooc",
name: "中国大学MOOC",
test: (host) => /(^|\.)icourse163\.org$/.test(host),
play: [".vjs-big-play-button", ".prism-big-play-btn", "[class*=big-play]"],
next: ["[class*=next]:not([disabled])"],
verified: false
},
{
id: "uschool",
name: "U 校园",
test: (host) => /(^|\.)unipus\.cn$/.test(host),
play: ["[class*=play]", ".vjs-big-play-button"],
next: ["[class*=next]:not([disabled])"],
verified: false
},
{
id: "xtzx",
name: "学堂在线",
test: (host) => /(^|\.)xuetangx\.com$/.test(host),
play: ["[class*=play]"],
next: [".next", "[class*=next]:not([disabled])"],
verified: true
},
{
id: "ytccr",
name: "绎通云",
test: (host) => /(^|\.)ytccr\.com$/.test(host),
play: ["[class*=play]", ".vjs-big-play-button"],
next: ["[class*=next]:not([disabled])"],
verified: false
},
{
id: "sdzj",
name: "山东专技",
test: (host) => /(^|\.)treewises\.com$/.test(host),
play: ["#replaybtn", ".bplayer-play-btn"],
next: [".btn-next"],
quizPopup: true,
verified: true
},
{
id: "xczx",
name: "西财在线",
test: (host) => /(^|\.)swufe-online\.com$/.test(host),
play: [".screen-player-btn-icon.icon-play-sp-fill", "[class*=play]"],
next: [".btn-next"],
verified: true
}
];
const xsWatchProfileFor = (hostname) => XS_WATCH_PROFILES.find((profile) => profile.test(hostname || location.hostname)) || null;
const xsWatchSupported = () => Boolean(xsWatchProfileFor(location.hostname));
// 倍速、自动翻页、开关都直接用「设置」里的现成项,视频页不再单独放一份
const readWatchSettings = () => {
const fallback = { speed: 1.5, autoNext: true, autoScroll: true, enabled: true };
try {
const basic = useSettingStore().config.basicConfig;
const speed = Number(basic.videoPlayrate && basic.videoPlayrate.value) || 1.5;
const autoNext = basic.autoChangeChapter ? !!basic.autoChangeChapter.value : true;
return {
speed: Math.min(4, Math.max(0.5, speed)),
autoNext,
autoScroll: autoNext,
enabled: basic.watchAssist ? !!basic.watchAssist.value : true
};
} catch (e) {
return fallback;
}
};
const xsWatch = (() => {
const prefs = vue.reactive(xsWatchReadPrefs());
const state = vue.reactive({
running: false,
status: "空闲",
page: "",
media: "",
progress: "",
segments: Number(prefs.segments) || 0,
logs: []
});
let timer = null;
let tickBusy = false;
let originalHasFocus = null;
// 点过「下一节」后先等页面换掉视频,避免同一节被反复点
const advanceGuard = { at: 0, href: "", src: "" };
const log = (text) => {
const line = `${(/* @__PURE__ */ new Date()).toLocaleTimeString()} ${text}`;
state.logs.unshift(line);
if (state.logs.length > 20) state.logs.length = 20;
try {
useLogStore().insertLog(text);
} catch (e) {
}
};
// 同一套逻辑要能穿透同源 iframe(智慧职教、西财在线把播放器放在 iframe 里)
const collectDocuments = () => {
const list = [document];
const seen = /* @__PURE__ */ new Set([document]);
for (let i = 0; i < list.length && list.length < 8; i++) {
let frames = [];
try {
frames = Array.from(list[i].querySelectorAll("iframe"));
} catch (e) {
frames = [];
}
for (const frame of frames) {
let inner = null;
try {
inner = frame.contentDocument;
} catch (e) {
inner = null;
}
if (inner && !seen.has(inner)) {
seen.add(inner);
list.push(inner);
}
}
}
return list;
};
const queryAll = (selector) => {
const found = [];
for (const doc of collectDocuments()) {
try {
found.push(...doc.querySelectorAll(selector));
} catch (e) {
}
}
return found;
};
const isVisible = (el) => {
if (!el || !el.isConnected) return false;
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (rect && (rect.width < 2 || rect.height < 2)) return false;
const style = window.getComputedStyle ? window.getComputedStyle(el) : null;
return !style || (style.display !== "none" && style.visibility !== "hidden");
};
const clickFirst = (selectors) => {
for (const selector of selectors || []) {
for (const el of queryAll(selector)) {
if (!isVisible(el)) continue;
try {
el.click();
return el;
} catch (e) {
}
}
}
return null;
};
const textOf = (el) => String((el && (el.innerText || el.textContent)) || "").replace(/\s+/g, "").trim();
const findVideo = () => {
// 有些播放器刚插入时尺寸还是 0,只要拿到了时长就当作有效视频
const videos = queryAll("video").filter((video) => isVisible(video) || Number(video.duration) > 0);
if (!videos.length) return null;
return videos.find((video) => !video.paused) || videos[0];
};
const setSpeed = (video, speed) => {
if (!video || !speed) return;
try {
if (Math.abs(video.playbackRate - speed) > 0.01) video.playbackRate = speed;
} catch (e) {
}
};
const safePlay = (video) => {
try {
const result = video.play();
if (result && result.catch) result.catch(() => {
});
} catch (e) {
}
};
const formatTime = (seconds) => {
const value = Math.max(0, Math.floor(Number(seconds) || 0));
const min = String(Math.floor(value / 60)).padStart(2, "0");
const sec = String(value % 60).padStart(2, "0");
return `${min}:${sec}`;
};
// 山东专技这类平台会在播放中弹一道验证题,随便选一个提交才能继续播
let lastQuizAt = 0;
const handleQuizPopup = (profile) => {
if (!profile || !profile.quizPopup) return false;
// 提交后弹窗要过一会儿才消失,这段时间别再重复点
if (Date.now() - lastQuizAt < 6e3) return false;
const options = queryAll(".ccQuestion li");
if (options.length) {
try {
options[Math.floor(Math.random() * options.length)].click();
clickFirst(["#ccQuestionSubmit"]);
lastQuizAt = Date.now();
log("弹窗验证题已作答");
} catch (e) {
}
return true;
}
const playerOptions = queryAll(".bplayer-question-content .option-item");
if (playerOptions.length && playerOptions[0].style && playerOptions[0].style.display !== "none") {
try {
playerOptions[Math.floor(Math.random() * playerOptions.length)].click();
clickFirst([".commit.bplayer-btn"]);
lastQuizAt = Date.now();
log("播放器弹窗题已作答");
} catch (e) {
}
return true;
}
return false;
};
const scrollDocument = async () => {
const el = document.scrollingElement || document.documentElement;
if (!el) return;
const step = Math.max(120, Math.floor(window.innerHeight * 0.5));
window.scrollBy(0, step);
await sleep(600);
if (el.scrollTop + window.innerHeight >= el.scrollHeight - 4) {
await sleep(2500);
}
};
const start = (profile) => {
const active = profile || xsWatchProfileFor(location.hostname);
if (!active) {
log("当前网站不在通用视频助手的支持范围内");
return false;
}
const settings = readWatchSettings();
if (!settings.enabled) {
log("设置里的「视频助手(新增平台)」是关闭状态,先到设置中打开");
return false;
}
if (timer) clearInterval(timer);
timer = null;
state.running = true;
state.status = "运行中";
prefs.autoRun = true;
xsWatchWritePrefs(prefs);
if (originalHasFocus === null) {
originalHasFocus = document.hasFocus;
try {
document.hasFocus = () => true;
} catch (e) {
}
}
log(`${active.name}:视频助手已启动,倍速 ${settings.speed}x`);
timer = setInterval(() => {
void tick(active);
}, 1e3);
void tick(active);
return true;
};
const stop = (reason) => {
if (timer) clearInterval(timer);
timer = null;
state.running = false;
state.status = reason || "已暂停";
prefs.autoRun = false;
xsWatchWritePrefs(prefs);
if (originalHasFocus !== null) {
try {
document.hasFocus = originalHasFocus;
} catch (e) {
}
originalHasFocus = null;
}
log(reason || "视频助手已暂停");
};
const clickNext = (profile) => {
for (const selector of profile.next || []) {
for (const el of queryAll(selector)) {
if (!isVisible(el)) continue;
if (profile.nextText && textOf(el) !== profile.nextText) continue;
try {
el.click();
return el;
} catch (e) {
}
}
}
return null;
};
const tick = async (profile) => {
if (!state.running || tickBusy) return;
tickBusy = true;
try {
if (!document.body) return;
const settings = readWatchSettings();
if (!settings.enabled) {
stop("设置里的「视频助手(新增平台)」已关闭");
return;
}
state.page = location.pathname || "/";
if (handleQuizPopup(profile)) return;
const video = findVideo();
if (!video) {
state.progress = "";
if (settings.autoScroll && profile.doc) {
state.media = "文档页:正在向下滚动";
await scrollDocument();
} else {
state.media = "当前页面没有视频";
}
return;
}
state.media = "视频";
const duration = Number(video.duration) || 0;
const current = Number(video.currentTime) || 0;
state.progress = duration > 0 ? `${formatTime(current)} / ${formatTime(duration)}(${Math.floor(current / duration * 100)}%)` : "";
if (!video.muted) {
try {
video.muted = true;
} catch (e) {
}
}
setSpeed(video, settings.speed);
const finished = video.ended || (duration > 0 && current >= duration - 0.8);
if (finished) {
if (!settings.autoNext) {
state.status = "本节已播完(自动翻页已关闭)";
return;
}
state.progress = "本节已播完,正在切换";
const videoSrc = String(video.currentSrc || video.src || "");
const pageChanged = location.href !== advanceGuard.href || videoSrc !== advanceGuard.src;
if (!pageChanged && Date.now() - advanceGuard.at < 1e4) {
state.status = "等待下一页加载";
return;
}
const clicked = clickNext(profile);
if (!clicked) {
stop("没有找到下一节按钮,已停止");
return;
}
advanceGuard.at = Date.now();
advanceGuard.href = location.href;
advanceGuard.src = videoSrc;
state.segments += 1;
prefs.segments = state.segments;
xsWatchWritePrefs(prefs);
log(`第 ${state.segments} 节播完,已点下一节`);
return;
}
if (video.paused) {
clickFirst(profile.play || []);
safePlay(video);
}
state.status = "运行中";
} catch (error) {
log(`运行出错:${error && error.message ? error.message : error}`);
} finally {
tickBusy = false;
}
};
return { prefs, state, start, stop, tick, profileFor: xsWatchProfileFor, supported: xsWatchSupported, autoStart: () => {
const profile = xsWatchProfileFor(location.hostname);
if (!profile) return;
// 设置里的「视频助手」开着才自动开始,关掉后连进入页面都不动
if (!readWatchSettings().enabled) return;
start(profile);
} };
})();
// 新增平台共用同一套路由:在这些站点的任何页面都显示面板,
// 进页面后由设置里的「视频助手」开关决定是否自动开始
// ===== 新平台通用答题:脚本负责抓题和填答案,答案由后端接口给 =====
// 不写死各平台的 class:靠页面结构特征找题目,平台改版后一般还能用;
// 找不到题目时把页面结构记进日志,方便按真实页面补选择器
const xsQuiz = (() => {
const TEXT = (el) => String((el && (el.innerText || el.textContent)) || "").replace(/\s+/g, " ").trim();
const LETTER_RE = /^\s*([A-H])\s*[.、,,::))]\s*/;
const letterOf = (text) => {
const m = String(text || "").match(LETTER_RE);
return m ? m[1] : "";
};
const stripLetter = (text) => String(text || "").replace(LETTER_RE, "").trim();
const norm = (s) => String(s || "").replace(/[\s "“”'‘’]/g, "").toLowerCase();
const judgeNorm = (s) => {
const t = String(s || "").trim();
if (/^(对|正确|是|√|t|true)$/i.test(t)) return "对";
if (/^(错|错误|否|×|f|false)$/i.test(t)) return "错";
return "";
};
// 同源 iframe 也要扫:不少平台的答题区在 iframe 里
const collectDocuments = (root, depth) => {
root = root || document;
depth = depth || 0;
const docs = [root];
if (depth >= 6) return docs;
let frames = [];
try { frames = Array.from(root.querySelectorAll("iframe")); } catch (_) {}
for (const frame of frames) {
let inner = null;
try { inner = frame.contentDocument; } catch (_) { inner = null; }
if (inner && inner.body) docs.push(...collectDocuments(inner, depth + 1));
}
return docs;
};
// 同一题的控件归成一组:radio 按 name,checkbox 按共同父级
const groupControls = (controls) => {
const groups = [];
const byName = new Map();
const rest = [];
for (const input of controls) {
if (input.type === "radio" && input.name) {
if (!byName.has(input.name)) {
const group = { controls: [], type: "radio", name: input.name };
byName.set(input.name, group);
groups.push(group);
}
byName.get(input.name).controls.push(input);
} else {
rest.push(input);
}
}
for (const input of rest) {
let host = input.parentElement;
let target = null;
for (let i = 0; i < 6 && host; i++, host = host.parentElement) {
if (host.querySelectorAll('input[type="checkbox"]').length > 1) { target = host; break; }
}
if (!target) target = input.parentElement;
let group = groups.find((x) => x.host === target);
if (!group) { group = { controls: [], type: "checkbox", host: target }; groups.push(group); }
group.controls.push(input);
}
return groups;
};
// 题目块 = 包含全部选项控件的最近祖先,再往上找带题干的那层
const resolveBlock = (group) => {
const first = group.controls[0];
let container = first.parentElement;
for (let i = 0; i < 10 && container; i++, container = container.parentElement) {
if (group.controls.every((c) => container.contains(c))) break;
}
if (!container) container = first.parentElement;
const baseText = TEXT(container);
let best = container;
let up = container.parentElement;
for (let i = 0; i < 4 && up; i++, up = up.parentElement) {
if (up === document.body || up === document.documentElement) break;
const extra = TEXT(up).length - baseText.length;
if (extra >= 6) { best = up; break; }
if (extra > 0) best = up;
}
return best;
};
const collectOptions = (controls) => controls.map((input) => {
const label = input.closest("label") || input.parentElement;
let text = "";
if (label) {
const clone = label.cloneNode(true);
clone.querySelectorAll("input, textarea, select").forEach((node) => node.remove());
text = TEXT(clone);
}
if (!text) text = TEXT(input.nextElementSibling);
return { el: label || input, input, text, letter: letterOf(text) };
});
// 没有 input 的选项块(前端框架常直接渲染 div 当选项)
const findClickOptions = (scope) => {
const SELECTORS = [
'[class*="option"]', '[class*="Option"]', '[class*="opt"]', '[class*="Opt"]',
'[class*="choice"]', '[class*="Choice"]', '[class*="answer-item"]'
];
for (const sel of SELECTORS) {
const nodes = Array.from(scope.querySelectorAll(sel)).filter((el) => {
if (el.querySelector(sel)) return false;
const text = TEXT(el);
return text.length >= 2 && text.length <= 300 && /^\s*[A-H]\s*[.、,,::))]/.test(text);
});
const byParent = new Map();
for (const node of nodes) {
const parent = node.parentElement;
if (!byParent.has(parent)) byParent.set(parent, []);
byParent.get(parent).push(node);
}
for (const list of byParent.values()) if (list.length >= 2) return list;
}
return [];
};
const clickBlockOf = (list) => {
const baseText = TEXT(list[0].parentElement);
let up = list[0].parentElement;
let best = up;
for (let i = 0; i < 4 && up; i++, up = up.parentElement) {
if (up === document.body || up === document.documentElement) break;
const extra = TEXT(up).length - baseText.length;
if (extra >= 6) { best = up; break; }
if (extra > 0) best = up;
}
return best;
};
const blockOrder = (a, b) => {
const pos = a.el.compareDocumentPosition(b.el);
if (pos & 4) return -1;
if (pos & 2) return 1;
return 0;
};
// 各平台精确选择器:参考 OCS 网课助手公开的结构整理,能对上就用它,对不上再退回通用识别
const PROFILES = [
{
id: "ykt",
domains: ["yuketang.cn"],
roots: [".container-problem .subject-item"],
title: [".problem-body"],
type: [".item-type"],
options: ['ul[class*="list-unstyled"] li label'],
optionText: [".radioText", ".checkboxText"],
blanks: ['input.blank-item-dynamic', 'input[placeholder="输入答案"]', "textarea"],
captcha: "#homework-automation-risk-challenge"
},
{
id: "mooc",
domains: ["icourse163.org"],
roots: [".u-questionItem", "[class*=questionBody]"],
title: [".j-title .j-richTxt", "[class*=questionInfo]"],
type: [],
options: [".choices li", "[class*=index-module__optionBody]"],
optionText: [".f-richEditorText"],
blanks: ["textarea"],
captcha: ""
},
// 职教云(zjy2.icve.com.cn 等)
{
id: "zjy",
domains: ["icve.com.cn"],
roots: [".subjectDet"],
title: ["h2, h3, h4, h5, h6", ".titleTest span"],
type: [],
options: [".optionList div", ".tkInput .el-input", ".tkInput .el-textarea"],
optionText: [],
blanks: ['.tkInput input[type="text"]', ".tkInput textarea"],
captcha: ""
},
// 智慧职教(老版)
{
id: "icve-a",
domains: ["icve.com.cn"],
roots: [".q_content"],
title: [".divQuestionTitle", '[name="fillblankTitle"]'],
type: [],
options: [".questionOptions .q_option", ".questionOptions .answerOption", ".answerOption"],
optionText: [],
blanks: [".fillblank_input input"],
captcha: ""
},
// 智慧职教(新版,Element 风格)
{
id: "icve-b",
domains: ["icve.com.cn"],
roots: [".content-item"],
title: [".questions-content [class*=title-content]"],
type: [],
options: ["label[class*='group-item']"],
optionText: [],
blanks: [".ivu-input-wrapper input"],
captcha: ""
},
// 云班课(新版 topic-subject,旧版 t-subject)
{
id: "mosoteach",
domains: ["mosoteach.cn"],
roots: [".pt-10.d-flex.list-item", ".topic-item"],
title: [".topic-subject", ".t-subject"],
type: [".v-chip__content", ".t-type"],
options: [".topic-answer-align", "label.d-flex.align-center", ".t-option.t-item label", ".t-judge.t-item label"],
optionText: ["[class*='opt-content']", ".topic-answer-align"],
blanks: ["textarea", 'input[type="text"]'],
captcha: ""
},
// 绎通云
{
id: "ytccr",
domains: ["ytccr.com"],
roots: [".border-item"],
title: [".title.qa-title", ".qa-title"],
type: [".qtype", ".question-box .tag"],
options: [".opts-list .opt-title-cnt", ".opts-list li"],
optionText: [],
blanks: ["textarea", 'input[type="text"]'],
captcha: ""
},
// 国家开放大学(学习网老版 + 电大考试站)
{
id: "ouchn-lms",
domains: ["ouchn.cn", "ouchn.edu.cn"],
roots: ["li.subject", ".everyQuest"],
title: [".summary-title .subject-description", ".topicTitle"],
type: [".subject-point > span", ".question-box .tag"],
options: [".subject-options li .option-content", ".optionList .topicTitle", ".optionList li"],
optionText: [],
blanks: ["textarea", 'input[type="text"]'],
captcha: ""
},
// 西财在线
{
id: "xczx",
domains: ["swufe-online.com"],
roots: [".test_item_theme", ".timu_title"],
title: [".timu_title"],
type: [],
options: ["ul > li", ".question-item-opt"],
optionText: [],
blanks: ["textarea", 'input[type="text"]'],
captcha: ""
}
];
// 同一个域名下可能有几套页面结构,依次尝试
const profilesForHost = (host) => PROFILES.filter((p) => p.domains.some((d) => host === d || host.endsWith("." + d)));
const pickText = (el, selectors) => {
for (const sel of selectors || []) {
const node = el.querySelector(sel);
if (node && TEXT(node)) return TEXT(node);
}
return "";
};
// 按平台选择器抓题;抓不到返回空数组,交给通用识别
const findBlocksByProfile = (scope, profile) => {
const roots = Array.from(scope.querySelectorAll(profile.roots.join(",")));
if (!roots.length) return [];
const blocks = [];
for (const root of roots) {
const options = [];
Array.from(root.querySelectorAll(profile.options.join(","))).forEach((el, index) => {
const input = el.querySelector('input[type="radio"], input[type="checkbox"]');
let text = pickText(el, profile.optionText);
// 判断题接口常把选项值设成 true / false,没有文字时用对错兜底
if (!text && input && (input.value === "true" || input.value === "false")) {
text = input.value === "true" ? "对" : "错";
}
if (!text) {
const clone = el.cloneNode(true);
clone.querySelectorAll("input, textarea, select, img").forEach((node) => node.remove());
text = TEXT(clone);
}
if (!text) return;
// 选项没有 A/B/C 前缀时按顺序补,题库给字母答案也能对上
options.push({ el, input, text, letter: letterOf(text) || String.fromCharCode(65 + index) });
});
const blanks = [];
for (const sel of profile.blanks || []) {
for (const el of Array.from(root.querySelectorAll(sel))) {
if (!el.disabled && !el.readOnly && !blanks.includes(el)) blanks.push(el);
}
}
if (!options.length && !blanks.length) continue;
options.sort((a, b) => {
const pos = a.el.compareDocumentPosition(b.el);
return pos & 4 ? -1 : pos & 2 ? 1 : 0;
});
const typeText = pickText(root, profile.type);
let type = options.length ? "单选" : "填空";
if (/多选|多项选择|不定项/.test(typeText)) type = "多选";
else if (/判断/.test(typeText)) type = "判断";
else if (/填空|简答|问答|论述/.test(typeText)) type = "填空";
if (options.some((o) => o.input && o.input.type === "checkbox")) type = "多选";
blocks.push({
el: root,
kind: options.length ? "choice" : "input",
type,
options,
controls: options.map((o) => o.input).filter(Boolean),
inputs: blanks,
title: (pickText(root, profile.title) || TEXT(root)).replace(/\s+/g, " ").slice(0, 300)
});
}
return blocks;
};
// 扫描当前页面(含 iframe),返回题目块
const findBlocks = (scope) => {
scope = scope || document;
// 先按平台精确选择器找,找不到再走通用结构识别
const host = (scope.location && scope.location.hostname) || location.hostname;
for (const profile of profilesForHost(host)) {
try {
const exact = findBlocksByProfile(scope, profile);
if (exact.length) return exact;
} catch (_) {}
}
const blocks = [];
const controls = Array.from(scope.querySelectorAll('input[type="radio"], input[type="checkbox"]'))
.filter((el) => !el.disabled);
for (const group of groupControls(controls)) {
blocks.push({
el: resolveBlock(group),
kind: "choice",
type: group.type === "radio" ? "单选" : "多选",
options: collectOptions(group.controls),
controls: group.controls,
inputs: [],
title: ""
});
}
const clickOpts = findClickOptions(scope);
if (clickOpts.length) {
const blockEl = clickBlockOf(clickOpts);
const overlapped = blocks.some((b) => b.el === blockEl || b.el.contains(blockEl) || blockEl.contains(b.el));
if (!overlapped) {
blocks.push({
el: blockEl,
kind: "click",
type: "单选",
options: clickOpts.map((el) => ({ el, input: null, text: TEXT(el), letter: letterOf(TEXT(el)) })),
controls: [],
inputs: [],
title: ""
});
}
}
// 填空、简答:不属于任何选择题块的输入框
const textInputs = Array.from(scope.querySelectorAll('input[type="text"], input:not([type]), textarea'))
.filter((el) => !el.disabled && !el.readOnly)
.filter((el) => !blocks.some((b) => b.el.contains(el)));
const seen = new Set();
for (const input of textInputs) {
let host = input.parentElement;
for (let i = 0; i < 5 && host; i++, host = host.parentElement) {
if (TEXT(host).length >= 8) break;
}
const blockEl = host || input.parentElement;
if (seen.has(blockEl)) continue;
seen.add(blockEl);
const group = textInputs.filter((item) => blockEl.contains(item));
blocks.push({
el: blockEl,
kind: "input",
type: group.some((item) => item.tagName === "TEXTAREA") ? "简答" : "填空",
options: [],
controls: [],
inputs: group,
title: ""
});
}
// 题干:去掉选项和控件后的文字
for (const block of blocks) {
if (block.title) continue;
const clone = block.el.cloneNode(true);
clone.querySelectorAll("input, textarea, select, button").forEach((node) => node.remove());
let raw = TEXT(clone);
if (block.options.length) {
const firstText = stripLetter(block.options[0].text);
const at = firstText ? raw.indexOf(firstText) : -1;
if (at > 0) raw = raw.slice(0, at);
}
raw = raw.replace(/\s*[A-H]\s*[.、,,::))]\s*$/, "");
block.title = raw.replace(/\s+/g, " ").trim().slice(0, 300);
if (block.kind === "click") block.type = /多选|多项选择|不定项/.test(block.title) ? "多选" : "单选";
else if (block.type === "单选" && /多选|多项选择|不定项/.test(block.title)) block.type = "多选";
}
return blocks.sort(blockOrder);
};
// 答案匹配选项:先按字母,再按文字包含,避免选项文字有前后缀时匹配不上
const isLetterAnswer = (a) => /^[A-H]$/i.test(String(a).trim());
// 匹配不上的答案单独返回,由调用方标出来,绝不硬凑一个选项
const pickOptions = (block, answer) => {
const answers = Array.isArray(answer) ? answer : [answer];
const picked = [];
const unmatched = [];
for (const item of answers) {
const raw = String(item == null ? "" : item).trim();
if (!raw) continue;
if (isLetterAnswer(raw)) {
const hit = block.options.find((o) => String(o.letter).toUpperCase() === raw.toUpperCase());
if (hit) { picked.push(hit); continue; }
unmatched.push(raw);
continue;
}
const target = norm(raw);
if (!target) continue;
const hits = block.options.filter((o) => {
const text = norm(o.text);
const body = norm(stripLetter(o.text));
// 判断题的同义写法:对/正确/√/T 与 错/错误/×/F 互相认
if (judgeNorm(raw) && judgeNorm(raw) === judgeNorm(body)) return true;
return text.includes(target) || target.includes(body) || (body.length >= 4 && target.includes(body));
});
if (hits.length) {
// 多个选项都包含答案文字时优先取完全相同的,其次取最短的,避免把长选项当答案
const exact = hits.find((o) => norm(stripLetter(o.text)) === target);
if (exact) { picked.push(exact); continue; }
hits.sort((x, y) => stripLetter(x.text).length - stripLetter(y.text).length);
picked.push(hits[0]);
} else {
unmatched.push(raw);
}
}
return { picked: picked.filter((value, index, arr) => arr.indexOf(value) === index), unmatched };
};
const fill = (block, answer) => {
if (!block) return { ok: false, reason: "没有题目块" };
if (block.kind === "input") {
const list = Array.isArray(answer) ? answer : [answer];
let count = 0;
block.inputs.forEach((input, index) => {
const value = String(list[index] == null ? "" : list[index]).trim();
if (!value) return;
input.focus();
input.value = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
count++;
});
const missing = Math.max(0, block.inputs.length - list.filter((x) => String(x == null ? "" : x).trim()).length);
return { ok: count > 0, filled: count, missing };
}
const matched = pickOptions(block, answer);
const picked = matched.picked.slice();
if (!picked.length) return { ok: false, reason: "答案没匹配到任何选项", unmatched: matched.unmatched };
if (block.type === "单选" || block.type === "判断") picked.splice(1);
picked.forEach((opt) => {
try { (opt.input || opt.el).click(); } catch (_) {}
});
// 回读:点完真的选中了才算填上;有些平台要整行点,点不动就再点一次整行
let checkedCount = 0;
if (block.controls.length) checkedCount = block.controls.filter((input) => input.checked).length;
if (block.controls.length && checkedCount === 0) {
picked.forEach((opt) => {
try { opt.el.click(); } catch (_) {}
});
checkedCount = block.controls.filter((input) => input.checked).length;
}
if (block.controls.length && checkedCount === 0) {
return { ok: false, reason: "点了选项但页面没反应,可能要点整行选项", unmatched: matched.unmatched };
}
return {
ok: true,
filled: picked.length,
letters: picked.map((o) => o.letter),
unmatched: matched.unmatched,
extraDropped: matched.picked.length > picked.length ? matched.picked.length - picked.length : 0
};
};
const isAnswered = (block) => {
if (!block) return false;
if (block.kind === "input") return block.inputs.some((input) => String(input.value || "").trim());
if (block.controls.length) return block.controls.some((input) => input.checked);
return block.options.some((opt) => /(^|[\s-])(active|selected|checked|is-checked|choose|chosen)([\s-]|$)/i.test(String(opt.el.className || "")));
};
const registry = [];
const state = vue.reactive({
scanning: false,
running: false,
total: 0,
done: 0,
items: [],
logs: []
});
const log = (text) => {
state.logs.unshift({ at: Date.now(), text: String(text) });
if (state.logs.length > 30) state.logs.length = 30;
};
// 扫一遍页面,题目有变化才更新列表
const scan = (silent) => {
// 答题过程中不要重建列表,否则会把手里的进度和已答结果冲掉
if (state.running) return [];
state.scanning = true;
try {
const blocks = [];
for (const doc of collectDocuments()) {
try { blocks.push(...findBlocks(doc)); } catch (_) {}
}
const usable = blocks.filter((block) => (block.options && block.options.length >= 2) || (block.inputs && block.inputs.length));
state.total = usable.length;
if (!silent) {
if (usable.length) log(`当前页面识别到 ${usable.length} 道题`);
else log("没有识别到题目:请确认已经进入作业或考试页面");
}
// 题目节点单独放,不塞进响应式数据里,避免 Vue 去代理整棵 DOM
registry.length = 0;
registry.push(...usable);
state.items = usable.map((block, index) => ({
index,
title: block.title || `第 ${index + 1} 题`,
type: block.type,
answer: "",
status: isAnswered(block) ? "done" : "pending",
msg: ""
}));
return usable;
} finally {
state.scanning = false;
}
};
// 整条替换,确保列表一定重渲染
const setItem = (index, patch) => {
const next = state.items.slice();
if (!next[index]) return;
next[index] = Object.assign({}, next[index], patch);
state.items = next;
};
const toQuestion = (block) => ({
title: block.title || "",
optionsText: (block.options || []).map((opt) => stripLetter(opt.text)),
type: block.kind === "input"
? (block.type === "简答" ? "简答题" : "填空题")
: (block.type === "多选" ? "多选题" : "单选题"),
refer: location.href
});
// 逐题取答案并填上;默认不点提交
const start = async () => {
if (state.running) return;
const usable = scan(true);
if (!usable.length) {
log("没有识别到题目,先打开作业或考试页面再试");
return;
}
state.running = true;
state.done = 0;
log(`开始作答,共 ${usable.length} 道题`);
for (const item of state.items.slice()) {
if (!state.running) break;
if (item.status === "done") { state.done++; continue; }
const block = registry[item.index];
if (!block) continue;
setItem(item.index, { status: "running" });
try {
const question = toQuestion(block);
let res = await getAccurateAnswer(question);
let answer = res && res.data && res.data.answer;
if (!Array.isArray(answer) || !answer.length) {
res = await getAIAnswer(question);
answer = res && res.data && res.data.answer;
}
if (!Array.isArray(answer) || !answer.length) {
setItem(item.index, { status: "fail", msg: (res && res.msg) || "未搜到答案" });
log(`第 ${item.index + 1} 题未搜到答案`);
continue;
}
const filled = fill(block, answer);
const fromAI = !!(res && res.data && res.data.src === "ai");
if (filled.ok) {
const notes = [];
if (filled.unmatched && filled.unmatched.length) notes.push(`有 ${filled.unmatched.length} 个答案没对上选项`);
if (filled.missing) notes.push(`还有 ${filled.missing} 个空没有答案`);
if (filled.extraDropped) notes.push("题目是单选,多余的答案没填");
if (fromAI) notes.push("这条是 AI 作答,建议核对");
const needCheck = notes.length > 0;
setItem(item.index, {
status: needCheck ? "warn" : "done",
answer: answer.join(" "),
msg: needCheck ? notes.join(";") : `已填 ${filled.filled} 项`
});
log(`第 ${item.index + 1} 题${needCheck ? "已填,需核对" : "已作答"}:${answer.join(" ")}`);
} else {
setItem(item.index, { status: "fail", answer: answer.join(" "), msg: filled.reason || "填写失败" });
log(`第 ${item.index + 1} 题 ${filled.reason || "填写失败"}`);
}
state.done++;
} catch (e) {
setItem(item.index, { status: "fail", msg: String((e && e.message) || e).slice(0, 80) });
log(`第 ${item.index + 1} 题出错`);
}
// sleep 的单位是秒,这里每题之间停 1 秒,给页面留出更新时间
await sleep(1);
}
state.running = false;
log("本轮作答结束,请检查后手动提交");
};
const stop = () => {
state.running = false;
log("已停止");
};
// 页面结构变了、识别不到题时,把关键结构打进日志
const inspect = () => {
try {
const docs = collectDocuments();
const radios = docs.reduce((n, doc) => n + doc.querySelectorAll('input[type="radio"], input[type="checkbox"]').length, 0);
const inputs = docs.reduce((n, doc) => n + doc.querySelectorAll("input, textarea").length, 0);
const buttons = docs.reduce((n, doc) => n + doc.querySelectorAll("button, [role='button']").length, 0);
log(`页面结构:选项控件 ${radios} 个,输入框 ${inputs} 个,按钮 ${buttons} 个,共 ${docs.length} 个文档`);
} catch (e) {
log("读取页面结构失败");
}
};
const Panel = {
name: "XsQuizPanel",
setup() {
return { state, start, stop, scan: () => scan(false), inspect };
},
render() {
const h = vue.h;
const state = this.state;
const plain = { padding: "5px 12px", borderRadius: "6px", border: "1px solid var(--t-border2)", background: "var(--t-surface)", color: "var(--t-text)", cursor: "pointer" };
const primary = Object.assign({}, plain, { border: "1px solid var(--t-primary)", background: "var(--t-primary)", color: "#fff" });
const statusColor = {
pending: "var(--t-text3)", running: "var(--t-primary)",
done: "var(--t-success, #12b76a)", warn: "var(--t-warning, #dc6803)", fail: "var(--t-danger, #d92d20)"
};
const statusText = { pending: "待答", running: "作答中", done: "已答", warn: "核对", fail: "失败" };
const rows = state.items.map((item) => h("div", {
key: item.index,
style: { display: "flex", gap: "8px", padding: "6px 0", borderBottom: "1px dashed var(--t-border)" }
}, [
h("span", { style: { flex: "0 0 34px", color: statusColor[item.status] || "var(--t-text3)", fontSize: "12px" } }, statusText[item.status] || ""),
h("div", { style: { flex: "1", minWidth: 0 } }, [
h("div", { style: { color: "var(--t-text)", fontSize: "12.5px", lineHeight: 1.6, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, item.title),
item.answer ? h("div", { style: { color: "var(--t-primary)", fontSize: "12px" } }, item.answer) : null,
item.msg ? h("div", { style: { color: "var(--t-text3)", fontSize: "11.5px" } }, item.msg) : null
])
]));
const logs = state.logs.slice(0, 8).map((item, index) => h("div", { key: index, style: { color: "var(--t-text3)", fontSize: "11.5px", lineHeight: 1.7 } }, item.text));
return h("div", { style: { display: "flex", flexDirection: "column", gap: "10px" } }, [
h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } }, [
h("div", { style: { fontSize: "15px", fontWeight: 600 } }, "自动答题"),
h("div", { style: { color: "var(--t-text3)", fontSize: "12px" } },
state.total ? `识别到 ${state.total} 题,已完成 ${state.done}` : "未识别到题目")
]),
h("div", { style: { color: "var(--t-text3)", fontSize: "12px", lineHeight: 1.7 } },
"进入作业或考试页面后点「扫描题目」。答案由你的题库和 AI 提供,只自动填答案,不会替你点提交,检查完自己提交。"),
h("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" } }, [
h("button", { onClick: this.scan, disabled: state.scanning, style: plain }, state.scanning ? "扫描中" : "扫描题目"),
h("button", { onClick: this.start, disabled: state.running || !state.total, style: primary }, state.running ? "正在作答" : "开始答题"),
state.running ? h("button", { onClick: this.stop, style: plain }, "停止") : null,
h("button", { onClick: this.inspect, style: plain }, "诊断")
]),
rows.length ? h("div", { style: { display: "flex", flexDirection: "column" } }, rows) : null,
logs.length ? h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", paddingTop: "4px" } }, logs) : null
]);
}
};
// 进入页面后自动扫一次,隔几秒再扫,页面异步渲染也能跟上
let booted = false;
const boot = () => {
if (booted) return;
booted = true;
setTimeout(() => { try { scan(true); } catch (_) {} }, 2500);
setTimeout(() => { try { scan(true); } catch (_) {} }, 6000);
};
return { state, scan, start, stop, inspect, Panel, boot, findBlocks, fill };
})();
const xsWatchRoutes = {
match: [{
keyword: "",
tab: "0",
logic: () => {
xsWatch.autoStart();
// 作业、考试页先扫一遍有没有题目,有就显示在「答题」页
xsQuiz.boot();
}
}],
defaultRoute: { show: true, tab: "0" }
};
const platformConfigs = [
{
key: "cx",
name: "超星学习通",
domains: [
"chaoxing.com",
"edu.cn",
"org.cn",
"xueyinonline.com",
"hnsyu.net",
"qutjxjy.cn",
"ynny.cn",
"hnvist.cn",
"fjlecb.cn",
"gdhkmooc.com",
"cugbonline.cn",
"zjelib.cn",
"cqrspx.cn",
"neauce.com",
"zhihui-yun.com",
"cqie.cn",
"ccqmxx.com",
"jxgmxy.com",
"sslibrary.com"
],
routes: cxRoutes,
showAi: true
},
{
key: "zhs",
name: "智慧树",
domains: ["zhihuishu.com", "polymas.com"],
routes: zhsRoutes,
showAi: false,
queueAdapter: zhsCourseQueueAdapter
},
{
key: "qs",
name: "青书学堂",
domains: ["qingshuxuetang.com"],
routes: XS_EXT.qsRoutes,
showAi: true
},
{
key: "ct",
name: "出头科技",
domains: ["web2.superchutou.com"],
routes: XS_EXT.ctRoutes,
showAi: true
},
// ===== 以下平台走通用视频助手:只做视频/文档进度,不抓题作答 =====
{
key: "ykt",
name: "雨课堂",
domains: ["yuketang.cn"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "mskt",
name: "云班课",
domains: ["mosoteach.cn"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "icve",
name: "智慧职教",
domains: ["icve.com.cn"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "ouchn",
name: "国家开放大学",
domains: ["ouchn.cn", "ouchn.edu.cn"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "mooc",
name: "中国大学MOOC",
domains: ["icourse163.org"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "uschool",
name: "U 校园",
domains: ["unipus.cn"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "xtzx",
name: "学堂在线",
domains: ["xuetangx.com"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "ytccr",
name: "绎通云",
domains: ["ytccr.com"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "sdzj",
name: "山东专技",
domains: ["treewises.com"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
},
{
key: "xczx",
name: "西财在线",
domains: ["swufe-online.com"],
routes: xsWatchRoutes,
showAi: false,
watchSite: true
}
];
// 设置页要按当前平台决定显示哪些项
function resolvePlatformKey(hostname) {
const host = hostname || (typeof location !== "undefined" ? location.hostname : "");
const found = platformConfigs.find((item) => item.domains.some((domain) => host === domain || host.endsWith("." + domain)));
return found ? found.key : null;
}
// 面板内容兜底:某个页签渲染出错时显示原因,不再整块空白
const PanelBoundary = {
name: "PanelBoundary",
data: () => ({ error: "" }),
errorCaptured(error) {
this.error = String((error && error.message) || error || "未知错误");
try {
console.error("[学神助手] 面板渲染出错", error);
} catch (ignore) {
}
return false;
},
render() {
if (this.error) {
return vue.h("div", { class: "tab-content", style: "padding:14px;font-size:12px;line-height:1.7;color:var(--t-text);white-space:pre-wrap;word-break:break-all" }, ["面板渲染出错:" + this.error]);
}
return vue.h("div", { style: "display:contents" }, this.$slots.default ? this.$slots.default() : []);
}
};
const _hoisted_1$1 = { class: "box-content" };
const _sfc_main$2 = {
__name: "index",
emits: ["customEvent"],
setup(__props, { emit: __emit }) {
const isShow = vue.ref(false);
const log = useLogStore();
const setting = useSettingStore();
const emit = __emit;
const url2 = window.location.href;
const hostname = window.location.hostname;
const messageStore = useMessageStore();
const currentPlatform = platformConfigs.find(
(item) => item.domains.some((domain) => hostname === domain || hostname.endsWith("." + domain))
) ?? null;
const settingTabKey = "4";
// 智慧树的「答题」和「掌握度满分」本质是同一件事,合并成一个页签:
// 页面在答题 / 考试 / 测验就显示答题,停在掌握度相关页面才显示掌握度,不额外放切换按钮
const isZhsMasteryPage = () => {
try {
return /studywisdomh5\.zhihuishu\.com\/(study\/mastery|pointOfMastery|examAnalysis|exam)/i.test(window.location.href);
} catch (_) {
return false;
}
};
const ZhiHuiShuAnswerPanel = {
setup() {
const masteryMode = vue.ref(isZhsMasteryPage());
let timer = null;
vue.onMounted(() => {
timer = setInterval(() => {
masteryMode.value = isZhsMasteryPage();
}, 1200);
});
vue.onUnmounted(() => {
if (timer) clearInterval(timer);
});
return { masteryMode };
},
render() {
const h = vue.h;
return h(this.masteryMode ? zhsMastery.Panel : _sfc_main$7);
}
};
const contentMap = [
{
key: "0",
tab: "◉ 首页",
component: ScriptHome
},
{
key: "1",
tab: "▤ 日志",
component: _sfc_main$9
},
{
key: "record",
tab: "▣ 记录",
component: _sfc_main$3
},
...(currentPlatform?.queueAdapter ? [{
key: "queue",
tab: "◫ 队列",
component: CourseQueuePanel
}] : []),
{
key: "2",
tab: "◈ 答题",
// 新平台用通用答题面板(抓题 + 填答案),旧平台仍用原来的题目列表;
// 智慧树在原来答题页里加一个「掌握度满分」切换,不再单独占页签
component: currentPlatform?.watchSite
? xsQuiz.Panel
: currentPlatform?.key === "zhs"
? ZhiHuiShuAnswerPanel
: _sfc_main$7
},
{
key: "3",
tab: "◎ 搜题",
component: _sfc_main$5
},
{
key: settingTabKey,
tab: "◆ 设置",
component: _sfc_main$6
}
];
const availableKeys = contentMap.map((item) => item.key);
const normalizeTabKey = (tabKey) => availableKeys.includes(tabKey) ? tabKey : "0";
const activeKey = vue.ref(normalizeTabKey(setting.tabIndex));
const visibleContentMap = vue.computed(() => contentMap.filter((item) => !item.hidden));
const currentContent = vue.computed(() => contentMap.find((item) => item.key === activeKey.value));
const refreshTimer = vue.ref(null);
const examIframeTimer = vue.ref(null);
const examIframeKeywords = [
"/examcode/examnotes",
"/exam/test/examcode",
"/exam/test/reVersionTestStartNew",
"exam-ans/exam/test/reVersionTestStartNew",
"mooc-ans/exam/test/reVersionTestStartNew",
"/mooc2/exam/preview",
"exam/preview"
];
const normalizeIframeUrl = (iframeSrc) => {
if (!iframeSrc) return "";
try {
return new URL(iframeSrc, window.location.href).href;
} catch (error) {
return iframeSrc;
}
};
const tryOpenExamIframe = () => {
if (!url2.includes("/base")) return false;
const examIframeUrl = Array.from(document.querySelectorAll("iframe")).map((iframe) => normalizeIframeUrl(iframe.getAttribute("src") || iframe.src)).find(
(iframeUrl) => examIframeKeywords.some((keyword) => iframeUrl.includes(keyword))
);
if (!examIframeUrl) return false;
const storageKey = `abc-opened-exam-iframe:${examIframeUrl}`;
if (sessionStorage.getItem(storageKey)) return true;
sessionStorage.setItem(storageKey, "1");
window.open(examIframeUrl, "_blank");
log.insertLog("检测到考试页面已嵌入 iframe,已尝试在新窗口打开..");
return true;
};
const startExamIframeWatcher = () => {
if (!url2.includes("/base") || examIframeTimer.value) return;
tryOpenExamIframe();
examIframeTimer.value = setInterval(() => {
tryOpenExamIframe();
}, 1e3);
};
const chooseLogicByUrl = () => {
var _a;
const platformRoutes = currentPlatform == null ? void 0 : currentPlatform.routes;
if (!platformRoutes) {
isShow.value = false;
return;
}
const route = platformRoutes.match.find((item) => url2.includes(item.keyword));
const shouldShow = route || ((_a = platformRoutes.defaultRoute) == null ? void 0 : _a.show);
if (!shouldShow) {
isShow.value = false;
return;
}
if (currentPlatform?.queueAdapter) {
useCourseQueueStore().init(currentPlatform.queueAdapter);
}
fetchBasicInfo().then((res) => {
console.log("fetchBasicInfo", res);
if (res.code) {
const { shopLink, version } = res.data;
setting.shopLink = shopLink;
setting.latestVersion = version;
}
});
updateTokenInfo();
startExamIframeWatcher();
if (route) {
if (!setting.config.basicConfig.token.value) {
messageStore.insertMessage(
"检测到未配置密钥,无法使用自动答题功能,请前往 “设置” 模块配置",
"warning"
);
}
setting.tabIndex = route.tab;
if (route.log) log.insertLog(route.log);
let routeRunning = false;
const runRouteLogic = async () => {
if (!route.logic || routeRunning) return;
routeRunning = true;
try { await route.logic(); }
catch (error) { log.insertLog("当前操作失败:" + (error.message || error), "warning"); }
finally { routeRunning = false; }
};
// 助手开关只管智慧树:路由上带 assist 的页面,开关关着就先挂起,等用户打开再开始
const assistKind = route.assist || "";
const assistLabel = assistKind === "video" ? "视频助手" : "答题助手";
const assistValue = () => assistKind === "video"
? setting.config.basicConfig.assistVideo.value
: setting.config.basicConfig.assistAnswer.value;
if (assistKind) {
vue.watch(assistValue, (enabled) => {
if (enabled) return;
log.insertLog(`${assistLabel}已关闭,当前页面不再开始新任务`, "warning");
if (assistKind === "video") {
document.querySelectorAll("video, audio").forEach((media) => media.pause());
}
});
}
const startRoute = async () => {
if (assistKind && !assistValue()) {
log.insertLog(`${assistLabel}未开启,开启后将自动开始当前页面的任务`, "warning");
await new Promise((resolve) => {
const stop = vue.watch(assistValue, (enabled) => {
if (!enabled) return;
stop();
resolve();
});
});
log.insertLog(`${assistLabel}已开启,开始执行当前页面的任务`);
}
runRouteLogic();
};
startRoute();
if ([cxHomeworkLogic, cxExamLogic, enterCxExamPreviewLogic].includes(route.logic)) {
vue.watch(() => setting.config.basicConfig.autoAnswer.value, enabled => { if (enabled) runRouteLogic(); });
}
if (setting.config.searchConfig.searchBySelectedText.value) {
log.insertLog("划词搜题已开启..");
}
autoRefreshPage();
selectTextSearchLogic(document, window);
} else {
setting.tabIndex = platformRoutes.defaultRoute.tab;
}
isShow.value = true;
};
const updateTokenInfo = () => {
if (setting.config.basicConfig.token.value) {
getTokenInfo().then((res) => {
if (res.code && res.code === 1) {
const { count, usedCount, remainCount } = res.data;
console.log("getTokenInfo", res.data);
setting.userInfo.remainCount = remainCount !== void 0 ? formatLocalRemainCount(remainCount) : formatLocalRemainCount(count - usedCount);
} else {
setting.userInfo.remainCount = "密钥无效";
messageStore.insertMessage(
(res && res.msg) || "密钥无效或已过期,请到用户中心重新生成脚本密钥",
"warning"
);
}
});
} else {
setting.userInfo.remainCount = "密钥未配置";
}
};
const changeTabIndex = (newActiveKey) => {
const nextTabKey = normalizeTabKey(newActiveKey);
activeKey.value = nextTabKey;
setting.tabIndex = nextTabKey;
};
vue.watch(
() => setting.tabIndex,
(newVal) => {
const nextTabKey = normalizeTabKey(newVal);
activeKey.value = nextTabKey;
if (nextTabKey === settingTabKey) {
updateTokenInfo();
}
}
);
const getPageContexts = () => {
const contexts = [{ document, window }];
const collectIframeContexts = (rootDocument) => {
const iframes = Array.from(rootDocument.querySelectorAll("iframe"));
for (const iframe of iframes) {
try {
if (iframe.contentDocument && iframe.contentWindow) {
contexts.push({
document: iframe.contentDocument,
window: iframe.contentWindow
});
collectIframeContexts(iframe.contentDocument);
}
} catch (error) {
console.warn("读取页面框架失败", error);
}
}
};
collectIframeContexts(document);
return contexts;
};
const tryChangeNewVersion = () => {
var _a;
for (const pageContext of getPageContexts()) {
const changeBtn = pageContext.document.querySelector("a.experience");
if (!changeBtn) continue;
const onclickText = changeBtn.getAttribute("onclick") || "";
const isNewVersionBtn = ((_a = changeBtn.textContent) == null ? void 0 : _a.trim().includes("体验新版")) || onclickText.includes("toNewFyCourseList");
if (!isNewVersionBtn) continue;
const pageWindow = pageContext.window;
if (typeof pageWindow.toNewFyCourseList === "function") {
pageWindow.toNewFyCourseList();
} else if (typeof changeBtn.onclick === "function") {
changeBtn.onclick.call(changeBtn);
} else {
changeBtn.click();
}
return true;
}
return false;
};
const changeNewVersion = async () => {
for (let i = 0; i < 10; i++) {
if (tryChangeNewVersion()) return;
await sleep(0.5);
}
};
const autoRefreshPage = () => {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
const timerStatus = setting.config.basicConfig.autoRefresh.value;
if (timerStatus) {
canRefreshStudyPage(document);
refreshTimer.value = setInterval(() => {
if (!setting.config.basicConfig.autoRefresh.value || !canRefreshStudyPage(document)) return;
log.insertLog("定时刷新已开启:页面空闲或视频长期无进展,执行恢复刷新");
window.location.reload();
}, 18e5);
}
};
vue.watch(() => setting.config.basicConfig.autoRefresh.value, autoRefreshPage);
vue.onUnmounted(() => clearInterval(refreshTimer.value));
changeNewVersion();
chooseLogicByUrl();
emit("customEvent", isShow.value);
return (_ctx, _cache) => {
const _component_a_tab_pane = vue.resolveComponent("a-tab-pane");
const _component_a_tabs = vue.resolveComponent("a-tabs");
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [
vue.createVNode(_component_a_tabs, {
activeKey: activeKey.value,
"onUpdate:activeKey": _cache[0] || (_cache[0] = ($event) => activeKey.value = $event),
onChange: changeTabIndex
}, {
default: vue.withCtx(() => [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(visibleContentMap.value, (item) => {
return vue.openBlock(), vue.createBlock(_component_a_tab_pane, {
tab: item.tab,
key: item.key
}, null, 8, ["tab"]);
}), 128))
]),
_: 1
}, 8, ["activeKey"]),
currentContent.value ? (vue.openBlock(), vue.createBlock(PanelBoundary, { key: 0 }, {
default: vue.withCtx(() => [
(vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent(currentContent.value.component), {
class: "tab-content",
onAutoRefreshPage: autoRefreshPage,
onChangeTab: changeTabIndex
}, null, 32))
]),
_: 1
})) : vue.createCommentVNode("", true)
]);
};
}
};
const _hoisted_1 = {
key: 0,
class: "message-container"
};
const _hoisted_2 = { class: "script-info" };
const _sfc_main$1 = {
__name: "index",
setup(__props) {
const scriptInfo = getScriptInfo();
const isMini = vue.ref(false);
const mainWrapRef = vue.ref(null);
const headerLeftRef = vue.ref(null);
const boxSize = vue.ref({ width: 400, height: 700 });
const setting = useSettingStore();
const remembered = getRememberedLogin();
const loginUser = vue.ref(remembered.username || "");
const loginPwd = vue.ref("");
const loginMsg = vue.ref("");
const loginBusy = vue.ref(false);
const rememberLogin = vue.ref(remembered.remember !== false);
const isAutoLoggedIn = vue.ref(!!remembered.token);
const openUserCenter = () => {
const token = (remembered && remembered.token) || "";
const user = (remembered && remembered.user) || "";
const userStr = typeof user === "string" ? user : JSON.stringify(user);
const url = token
? `${LOCAL_USER_CENTER_URL}&token=${encodeURIComponent(token)}&user=${encodeURIComponent(userStr)}`
: setting.shopLink || LOCAL_USER_CENTER_URL;
window.open(url, "_blank");
};
const openPricing = () => {
const token = (remembered && remembered.token) || "";
const user = (remembered && remembered.user) || "";
const userStr = typeof user === "string" ? user : JSON.stringify(user);
const baseUrl = __billHref; // FX-CH:pricing-base-url
if (!baseUrl) {
cardMsg.value = "提示:" + __noShopTip;
return;
}
const url = token && !__DIST.shop // FX-CH:pricing-login-param
? `${baseUrl}&token=${encodeURIComponent(token)}&user=${encodeURIComponent(userStr)}`
: baseUrl;
window.open(url, "_blank");
};
const openRegister = () => {
window.open("https://xs.openget.cn/register", "_blank");
};
const openForgot = () => {
window.open("https://xs.openget.cn/forgot", "_blank");
};
const loginLocalBackend = async () => {
if (!loginUser.value.trim() || !loginPwd.value.trim()) {
loginMsg.value = "请填写用户名/邮箱和密码";
return;
}
loginBusy.value = true;
loginMsg.value = "正在登录用户中心...";
try {
const password_hash = await sha256Text(loginPwd.value.trim());
const loginRes = await localBackendJson("https://xs.openget.cn/api/auth/login", {
username: loginUser.value.trim(),
password_hash
});
if (loginRes.code !== 200 || !loginRes.token) {
loginMsg.value = loginRes.msg || "登录失败";
return;
}
const syncRes = await syncLoginTokenToScriptKey(setting, loginRes.token);
if (syncRes.ok) {
if (rememberLogin.value) {
setRememberedLogin({
remember: true,
username: loginUser.value.trim(),
token: loginRes.token,
savedAt: Date.now()
});
} else {
setRememberedLogin({ remember: false, username: loginUser.value.trim() });
}
isAutoLoggedIn.value = true;
loginMsg.value = "登录成功,已记住登录状态并同步密钥";
} else {
loginMsg.value = syncRes.msg || "登录成功,但脚本密钥获取失败,请点用户中心手动复制";
}
} catch (e) {
loginMsg.value = "登录失败,请检查本地后端是否运行";
} finally {
loginBusy.value = false;
}
};
const autoLoginByRememberedToken = async () => {
if (!remembered.token) return;
loginBusy.value = true;
loginMsg.value = "正在恢复上次登录状态...";
const profile = await localBackendJson("https://xs.openget.cn/api/user/profile", null, remembered.token);
if (profile.code === 401 || profile.code === 403) {
loginMsg.value = "登录状态已过期,请重新登录";
setRememberedLogin({ remember: rememberLogin.value, username: remembered.username || "" });
isAutoLoggedIn.value = false;
loginBusy.value = false;
return;
}
if (profile.code !== 200) {
loginMsg.value = "网络异常,暂未恢复登录状态(已保留登录记忆)";
isAutoLoggedIn.value = false;
loginBusy.value = false;
return;
}
const syncRes = await syncLoginTokenToScriptKey(setting, remembered.token);
loginMsg.value = syncRes.ok ? "已恢复登录状态并同步密钥" : syncRes.msg || "自动登录失败";
loginBusy.value = false;
};
const clearRememberedLogin = () => {
setRememberedLogin({ remember: rememberLogin.value, username: loginUser.value.trim() });
isAutoLoggedIn.value = false;
loginMsg.value = "已清除脚本页登录记忆";
};
const cardCode = vue.ref("");
const cardBusy = vue.ref(false);
const cardMsg = vue.ref("");
const activateCardKey = async () => {
const code = cardCode.value.trim().toUpperCase();
if (code.length !== 16) {
cardMsg.value = "请输入 16 位卡密";
return;
}
const token = getLocalBackendToken() || getRememberedLogin().token || "";
if (!token) {
cardMsg.value = "请先登录";
return;
}
cardBusy.value = true;
cardMsg.value = "正在激活...";
try {
const res = await localBackendJson("https://xs.openget.cn/api/user/card-key/activate", { code }, token);
if (res.code === 200) {
cardCode.value = "";
cardMsg.value = res.msg || "卡密激活成功,权益已到账";
if (res.profile) {
setting.userInfo.remainCount = getProfileRemainingCount(res.profile);
setting.userInfo.activeMember = !!res.profile.active_member;
setting.userInfo.memberUntil = res.profile.member_until || "";
setting.userInfo.pointsBalance = Number(res.profile.points_balance || 0);
persistSettingState(setting);
}
} else if (res.code === 401) {
cardMsg.value = "登录状态已失效,请重新登录";
} else {
cardMsg.value = res.msg || "激活失败,请稍后再试";
}
} catch (_) {
cardMsg.value = "激活失败,请检查网络";
} finally {
cardBusy.value = false;
}
};
vue.onMounted(autoLoginByRememberedToken);
const isShow = vue.ref(true);
const messageStore = useMessageStore();
const messageList = vue.ref(messageStore.messageList);
const backendTabKeys = ["1", "record"];
const isBackendActive = vue.computed(() => backendTabKeys.includes(setting.tabIndex));
const THEME_OPTIONS = [
{ value: "dark", label: "暗色", dot: "dot-dark", icon: "☾" },
{ value: "light", label: "亮色", dot: "dot-light", icon: "☀" },
{ value: "glass", label: "毛玻璃", dot: "dot-glass", icon: "◐" },
{ value: "graphite", label: "深空灰", dot: "dot-graphite", icon: "◑" },
{ value: "mist", label: "雾蓝", dot: "dot-mist", icon: "◔" },
{ value: "sand", label: "暖砂", dot: "dot-sand", icon: "◕" }
];
const THEME_CLASS = {
dark: "theme-dark",
light: "theme-light",
graphite: "theme-graphite",
mist: "theme-mist",
sand: "theme-sand"
};
const savedTheme = (() => {
try { return GM_getValue("themeMode") || "dark"; } catch (_) { return "dark"; }
})();
const themeMode = vue.ref(THEME_OPTIONS.some((t) => t.value === savedTheme) ? savedTheme : "dark");
const themeMenuOpen = vue.ref(false);
const toggleThemeMenu = () => {
themeMenuOpen.value = !themeMenuOpen.value;
};
const chooseTheme = (mode) => {
themeMode.value = mode;
themeMenuOpen.value = false;
try { GM_setValue("themeMode", mode); } catch (_) {}
};
const onThemePointerDown = (event) => {
if (!themeMenuOpen.value) return;
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
const inside = path.some((el) => el && el.classList && (el.classList.contains("theme-menu") || el.classList.contains("theme-toggle")));
if (!inside) themeMenuOpen.value = false;
};
const onThemeKeydown = (event) => {
if (event.key === "Escape" && themeMenuOpen.value) themeMenuOpen.value = false;
};
vue.onMounted(() => {
window.addEventListener("pointerdown", onThemePointerDown, true);
window.addEventListener("keydown", onThemeKeydown, true);
});
vue.onUnmounted(() => {
window.removeEventListener("pointerdown", onThemePointerDown, true);
window.removeEventListener("keydown", onThemeKeydown, true);
});
const supportsGlassBlur = (() => {
try {
const css = window.CSS;
return !!(css && css.supports && (css.supports("backdrop-filter", "blur(2px)") || css.supports("-webkit-backdrop-filter", "blur(2px)")));
} catch (_) {
return false;
}
})();
const themeClass = vue.computed(() => {
if (themeMode.value === "glass") return supportsGlassBlur ? "theme-glass" : "theme-glass theme-glass-flat";
return THEME_CLASS[themeMode.value] || "theme-dark";
});
const themeIcon = vue.computed(() => {
const hit = THEME_OPTIONS.find((t) => t.value === themeMode.value);
return hit ? hit.icon : "☾";
});
const isPinned = vue.ref(GM_getValue("isPinned") || false);
const togglePin = () => {
isPinned.value = !isPinned.value;
GM_setValue("isPinned", isPinned.value);
};
const getPopupContainer = () => {
return headerLeftRef.value || mainWrapRef.value || document.body;
};
const handleBackendMenuClick = ({ key }) => {
if (key === "log") {
autoSetTab(setting, "1");
return;
}
if (key === "record") {
setting.tabIndex = "record";
}
};
vue.watch(
setting,
(newVal) => {
GM_setValue("setting", JSON.stringify(newVal));
const token = setting.config && setting.config.basicConfig && setting.config.basicConfig.token && setting.config.basicConfig.token.value;
scheduleSaveAbcCloudConfig(setting, token);
},
{ deep: true }
);
const position = vue.ref({
right: setting.position.right || "0px",
top: setting.position.top || "0px"
});
const isDragging = vue.ref(false);
const dragOffset = vue.ref({ x: 0, y: 0 });
const persistPosition = () => {
setting.position.right = position.value.right;
setting.position.top = position.value.top;
};
const syncPositionWithRect = () => {
if (!mainWrapRef.value) return;
const rect = mainWrapRef.value.getBoundingClientRect();
position.value = {
right: Math.max(0, window.innerWidth - rect.right) + "px",
top: Math.max(0, rect.top) + "px"
};
};
const changeBoxSize = () => {
isMini.value = !isMini.value;
};
const miniBallStatus = vue.ref({ text: "待命中", icon: "" });
const miniBallLogSummary = vue.ref("暂无日志");
const updateMiniBallInfo = () => {
let lastLogText = "";
try {
const logStore2 = useLogStore();
const logs = logStore2.logList;
if (logs.length > 0) {
const last = logs[logs.length - 1];
lastLogText = String(last.text || "");
miniBallLogSummary.value = lastLogText.length > 18 ? lastLogText.slice(0, 18) + "..." : lastLogText;
} else {
miniBallLogSummary.value = "暂无日志";
}
} catch (e) {
miniBallLogSummary.value = "暂无日志";
}
try {
if (/视频|播放|倍速|video/.test(lastLogText)) {
miniBallStatus.value = { text: "视频播放中", icon: "" };
} else if (/答题|测验|作业|考试|交卷|提交答案|quiz|homework|exam/.test(lastLogText)) {
miniBallStatus.value = { text: "答题中", icon: "" };
} else if (/直播|live|zhibo/.test(lastLogText)) {
miniBallStatus.value = { text: "直播中", icon: "" };
} else if (/阅读|PPT|电子书|document|ebook/.test(lastLogText)) {
miniBallStatus.value = { text: "阅读中", icon: "" };
} else {
const url = location.href;
if (/zhibo\.chaoxing\.com/.test(url)) {
miniBallStatus.value = { text: "直播中", icon: "" };
} else if (/chaoxing\.com/.test(url)) {
miniBallStatus.value = { text: "学习中", icon: "" };
} else {
miniBallStatus.value = { text: "待命中", icon: "" };
}
}
} catch (e) {
miniBallStatus.value = { text: "待命中", icon: "" };
}
};
let miniBallTimer = null;
vue.onMounted(() => {
updateMiniBallInfo();
miniBallTimer = setInterval(updateMiniBallInfo, 1500);
});
vue.onUnmounted(() => {
if (miniBallTimer) clearInterval(miniBallTimer);
});
const miniBallRemainCount = vue.computed(() => {
try {
const rc = setting.userInfo.remainCount;
if (rc === "无限" || rc === Infinity) return "∞";
const n = Number(rc);
if (isNaN(n) || n <= 0) return String(rc || "");
return n > 9999 ? "∞" : String(n);
} catch (e) {
return "";
}
});
const startDrag = (event) => {
if (isPinned.value) return;
event.preventDefault();
if (!mainWrapRef.value) return;
const rect = mainWrapRef.value.getBoundingClientRect();
boxSize.value = { width: rect.width, height: rect.height };
isDragging.value = true;
dragOffset.value = {
x: rect.right - event.clientX,
y: event.clientY - rect.top
};
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", endDrag);
};
const drag = (e) => {
if (!isDragging.value) return;
let newRight = window.innerWidth - e.clientX - dragOffset.value.x;
let newY = e.clientY - dragOffset.value.y;
if (newRight < 0) {
newRight = 0;
}
if (newRight + boxSize.value.width > window.innerWidth) {
newRight = window.innerWidth - boxSize.value.width;
}
if (newY < 0) {
newY = 0;
}
if (newY + boxSize.value.height > window.innerHeight) {
newY = window.innerHeight - boxSize.value.height;
}
position.value = {
right: newRight + "px",
top: newY + "px"
};
};
const endDrag = () => {
isDragging.value = false;
persistPosition();
document.removeEventListener("mousemove", drag);
document.removeEventListener("mouseup", endDrag);
};
const updateBoxSize = () => {
if (mainWrapRef.value) {
const rect = mainWrapRef.value.getBoundingClientRect();
boxSize.value = {
width: rect.width,
height: rect.height
};
}
};
const checkAndFixPosition = () => {
if (!mainWrapRef.value) return;
const rect = mainWrapRef.value.getBoundingClientRect();
const currentRight = parseInt(position.value.right) || 0;
const currentTop = parseInt(position.value.top) || 0;
let needFix = false;
let newRight = currentRight;
let newTop = currentTop;
if (currentRight + rect.width > window.innerWidth) {
newRight = Math.max(0, window.innerWidth - rect.width);
needFix = true;
}
if (currentRight < 0) {
newRight = 0;
needFix = true;
}
if (currentTop < 0) {
newTop = 0;
needFix = true;
}
if (currentTop + rect.height > window.innerHeight) {
newTop = Math.max(0, window.innerHeight - rect.height);
needFix = true;
}
if (needFix) {
position.value = {
right: newRight + "px",
top: newTop + "px"
};
persistPosition();
console.log("位置已修正,防止超出屏幕边界");
}
};
vue.onMounted(async () => {
await vue.nextTick();
const token = setting.config && setting.config.basicConfig && setting.config.basicConfig.token && setting.config.basicConfig.token.value;
if (token) {
abcCloudApplying = true;
await fetchAndApplyAbcCloudConfig(setting, token);
abcCloudApplying = false;
}
updateBoxSize();
syncPositionWithRect();
persistPosition();
checkAndFixPosition();
if (window.ResizeObserver) {
const resizeObserver = new ResizeObserver(() => {
updateBoxSize();
checkAndFixPosition();
});
if (mainWrapRef.value) {
resizeObserver.observe(mainWrapRef.value);
}
}
});
return (_ctx, _cache) => {
const _component_a_alert = vue.resolveComponent("a-alert");
const _component_a_menu_item = vue.resolveComponent("a-menu-item");
const _component_a_menu = vue.resolveComponent("a-menu");
const _component_a_dropdown = vue.resolveComponent("a-dropdown");
return isShow.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 0,
class: vue.normalizeClass(["main-wrap", themeClass.value]),
style: vue.normalizeStyle(position.value),
ref_key: "mainWrapRef",
ref: mainWrapRef
}, [
!isMini.value && themeMode.value === "glass" ? (vue.openBlock(), vue.createElementBlock("div", {
key: 3,
class: "glass-aura"
})) : vue.createCommentVNode("", true),
isMini.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 0,
class: "mini-ball",
onClick: vue.withModifiers(changeBoxSize, ["stop"]),
onMousedown: _cache[2] || (_cache[2] = vue.withModifiers(startDrag, ["stop"]))
}, [
vue.createElementVNode("div", { class: "mini-ball-icon" }, [
miniBallRemainCount.value ? (vue.openBlock(), vue.createElementBlock("span", {
key: 0,
class: "mini-ball-count"
}, vue.toDisplayString(miniBallRemainCount.value), 1)) : vue.createCommentVNode("", true),
vue.createTextVNode(" 学神")
]),
vue.createElementVNode("div", { class: "mini-ball-body" }, [
vue.createElementVNode("span", { class: "mini-ball-text" }, vue.toDisplayString(miniBallStatus.value.icon ? miniBallStatus.value.icon + " " + miniBallStatus.value.text : miniBallStatus.value.text), 1),
vue.createElementVNode("span", { class: "mini-ball-sub" }, vue.toDisplayString(miniBallLogSummary.value), 1)
]),
vue.createElementVNode("div", { class: "mini-ball-shine" }),
vue.createElementVNode("div", { class: "mini-ball-glow" }),
vue.createElementVNode("div", { class: "mini-ball-orbits" }, [
vue.createElementVNode("i", null, " "),
vue.createElementVNode("i", null, " ")
])
], 32)) : (vue.openBlock(), vue.createElementBlock("div", {
key: 1,
class: vue.normalizeClass(["main-box", themeClass.value])
}, [
vue.createElementVNode("div", {
class: "box-header",
onMousedown: startDrag
}, [
vue.createElementVNode("div", {
class: "header-left",
ref_key: "headerLeftRef",
ref: headerLeftRef
}, [
vue.createElementVNode("span", _hoisted_2, "学神助手 " + vue.toDisplayString(vue.unref(scriptInfo).version), 1),
!isMini.value ? (vue.openBlock(), vue.createBlock(_component_a_dropdown, {
key: 0,
placement: "bottomLeft",
trigger: ["hover"],
overlayClassName: "backend-dropdown-overlay",
getPopupContainer
}, {
overlay: vue.withCtx(() => [
vue.createVNode(_component_a_menu, { onClick: handleBackendMenuClick }, {
default: vue.withCtx(() => [
vue.createVNode(_component_a_menu_item, { key: "log" }, {
default: vue.withCtx(() => _cache[4] || (_cache[4] = [
vue.createTextVNode("≡ 日志")
])),
_: 1
}),
vue.createVNode(_component_a_menu_item, { key: "record" }, {
default: vue.withCtx(() => _cache[5] || (_cache[5] = [
vue.createTextVNode("▤ 记录")
])),
_: 1
})
]),
_: 1
})
]),
default: vue.withCtx(() => [
vue.createElementVNode("span", {
class: vue.normalizeClass(["backend-entry", { active: isBackendActive.value }]),
onClick: _cache[0] || (_cache[0] = vue.withModifiers(() => {
}, ["stop"])),
onMousedown: _cache[1] || (_cache[1] = vue.withModifiers(() => {
}, ["stop"]))
}, " ▤ ", 34)
]),
_: 1
})) : vue.createCommentVNode("", true)
], 512),
vue.createElementVNode("div", { style: { "display": "flex", "gap": "6px", "align-items": "center" } }, [
vue.createElementVNode("div", {
class: vue.normalizeClass(["pin-btn", { pinned: isPinned.value }]),
tabindex: "0",
role: "button",
onClick: vue.withModifiers(togglePin, ["stop"]),
onKeydown: ($event) => {
if ($event.key === "Enter" || $event.key === " ") {
$event.preventDefault();
togglePin();
}
},
onMousedown: _cache[6] || (_cache[6] = vue.withModifiers(() => {
}, ["stop"])),
title: isPinned.value ? "解除固定" : "固定位置"
}, "⌖", 10, ["title"]),
vue.createElementVNode("div", {
class: vue.normalizeClass(["theme-toggle", { active: themeMenuOpen.value }]),
tabindex: "0",
role: "button",
onClick: vue.withModifiers(toggleThemeMenu, ["stop"]),
onKeydown: ($event) => {
if ($event.key === "Enter" || $event.key === " ") {
$event.preventDefault();
toggleThemeMenu();
}
},
onMousedown: _cache[6] || (_cache[6] = vue.withModifiers(() => {
}, ["stop"])),
title: "切换界面主题"
}, vue.toDisplayString(themeIcon.value), 3),
vue.createElementVNode("div", {
class: "shrink-btn",
tabindex: "0",
role: "button",
onClick: vue.withModifiers(changeBoxSize, ["stop"]),
onKeydown: ($event) => {
if ($event.key === "Enter" || $event.key === " ") {
$event.preventDefault();
changeBoxSize();
}
},
onMousedown: _cache[7] || (_cache[7] = vue.withModifiers(() => {
}, ["stop"])),
title: "收起"
}, "—")
]),
], 32),
!isMini.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 1,
class: "login-bar",
style: { "padding": "8px 10px", "background": "var(--t-surface2)", "border-bottom": "1px solid var(--t-border)", "display": "flex", "flex-direction": "column", "gap": "6px" }
}, [
vue.createElementVNode("div", { style: { "display": "flex", "gap": "6px", "align-items": "center" } }, [
vue.createElementVNode("input", {
value: loginUser.value,
onInput: ($event) => loginUser.value = $event.target.value,
placeholder: "用户名/邮箱",
style: { "flex": "1", "min-width": "0", "height": "28px", "padding": "0 8px", "border": "1px solid var(--t-border2)", "border-radius": "4px", "background": "var(--t-surface)", "color": "var(--t-text)", "font-size": "12px" }
}, null, 40, ["value"]),
vue.createElementVNode("input", {
value: loginPwd.value,
onInput: ($event) => loginPwd.value = $event.target.value,
onKeydown: vue.withKeys(loginLocalBackend, ["enter"]),
type: "password",
placeholder: "密码",
style: { "flex": "1", "min-width": "0", "height": "28px", "padding": "0 8px", "border": "1px solid var(--t-border2)", "border-radius": "4px", "background": "var(--t-surface)", "color": "var(--t-text)", "font-size": "12px" }
}, null, 40, ["value"]),
vue.createElementVNode("button", {
onClick: loginLocalBackend,
disabled: loginBusy.value,
style: { "height": "28px", "padding": "0 12px", "border": "none", "border-radius": "4px", "background": "var(--t-primary)", "color": "#fff", "cursor": "pointer", "font-size": "12px", "font-weight": "500", "flex": "none" }
}, vue.toDisplayString(loginBusy.value ? "登录中..." : "登录"), 9, ["disabled"])
]),
vue.createElementVNode("div", { style: { "display": "flex", "gap": "10px", "align-items": "center", "color": "var(--t-text3)", "font-size": "11px", "flex-wrap": "wrap" } }, [
vue.createElementVNode("label", { style: { "display": "inline-flex", "align-items": "center", "gap": "4px", "cursor": "pointer" } }, [
vue.createElementVNode("input", {
type: "checkbox",
checked: rememberLogin.value,
onChange: ($event) => {
rememberLogin.value = $event.target.checked;
if (!rememberLogin.value) clearRememberedLogin();
}
}, null, 40, ["checked"]),
vue.createTextVNode("记住登录")
]),
vue.createElementVNode("a", {
href: "javascript:void(0)",
onClick: vue.withModifiers(openForgot, ["stop", "prevent"]),
style: { "color": "var(--t-primary)", "cursor": "pointer", "text-decoration": "none" }
}, "找回密码"),
vue.createElementVNode("a", {
href: "javascript:void(0)",
onClick: vue.withModifiers(openRegister, ["stop", "prevent"]),
style: { "color": "var(--t-primary)", "cursor": "pointer", "text-decoration": "none" }
}, "注册"),
vue.createElementVNode("a", {
href: "javascript:void(0)",
onClick: vue.withModifiers(openPricing, ["stop", "prevent"]),
style: { "color": "var(--t-primary)", "cursor": "pointer", "text-decoration": "none" }
}, "套餐"),
isAutoLoggedIn.value ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
onClick: clearRememberedLogin,
style: { "height": "20px", "padding": "0 6px", "border": "1px solid var(--t-danger)", "border-radius": "4px", "background": "var(--t-surface2)", "color": "var(--t-danger)", "cursor": "pointer", "font-size": "11px" }
}, "清除记忆")) : vue.createCommentVNode("", true),
loginMsg.value ? (vue.openBlock(), vue.createElementBlock("span", {
key: 2,
style: { "color": loginMsg.value.includes("成功") ? "var(--t-success)" : "var(--t-danger)" }
}, vue.toDisplayString(loginMsg.value), 1)) : vue.createCommentVNode("", true)
]),
__DIST.code ? (vue.openBlock(), vue.createElementBlock("div", { // FX-CH:card-key-activate
key: 3,
style: { "display": "flex", "gap": "6px", "align-items": "center", "flex-wrap": "wrap" }
}, [
vue.createElementVNode("span", { style: { "font-size": "12px", "color": "var(--t-text3)", "font-weight": "600", "flex": "none" } }, "🎫"),
vue.createElementVNode("input", {
value: cardCode.value,
onInput: ($event) => cardCode.value = $event.target.value,
onKeydown: vue.withKeys(activateCardKey, ["enter"]),
maxlength: "16",
placeholder: "卡密(16 位)",
style: { "flex": "1", "min-width": "0", "height": "28px", "padding": "0 8px", "border": "1px solid var(--t-border2)", "border-radius": "4px", "background": "var(--t-surface)", "color": "var(--t-text)", "font-size": "12px", "letter-spacing": "1px", "text-transform": "uppercase" }
}, null, 40, ["value"]),
vue.createElementVNode("button", {
onClick: activateCardKey,
disabled: cardBusy.value,
style: { "height": "28px", "padding": "0 12px", "border": "none", "border-radius": "4px", "background": "var(--t-primary)", "color": "#fff", "cursor": "pointer", "font-size": "12px", "font-weight": "500", "flex": "none" }
}, vue.toDisplayString(cardBusy.value ? "激活中..." : "激活卡密"), 9, ["disabled"]),
cardMsg.value ? (vue.openBlock(), vue.createElementBlock("span", {
key: 0,
style: { "color": cardBusy.value || cardMsg.value.indexOf("提示:") === 0 ? "var(--t-text3)" : (cardMsg.value.includes("成功") || cardMsg.value.includes("已到账") ? "var(--t-success)" : "var(--t-danger)"), "flex": "1 1 100%", "font-size": "11px" }
}, vue.toDisplayString(cardMsg.value), 5)) : vue.createCommentVNode("", true)
])) : vue.createCommentVNode("", true)
])) : vue.createCommentVNode("", true),
vue.createVNode(_sfc_main$2, {
class: vue.normalizeClass(`box-content ${isMini.value ? "mini" : ""}`),
onCustomEvent: _cache[8] || (_cache[8] = (newValue) => isShow.value = newValue)
}, null, 8, ["class"])
], 2)),
themeMenuOpen.value && !isMini.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 2,
class: "theme-menu",
onMousedown: _cache[9] || (_cache[9] = vue.withModifiers(() => {
}, ["stop"]))
}, [
vue.createElementVNode("div", { class: "theme-menu-title" }, "界面主题"),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(THEME_OPTIONS, (opt) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: opt.value,
class: vue.normalizeClass(["theme-menu-item", { active: themeMode.value === opt.value }]),
tabindex: "0",
role: "button",
onClick: vue.withModifiers(($event) => chooseTheme(opt.value), ["stop"]),
onKeydown: ($event) => {
if ($event.key === "Enter" || $event.key === " ") {
$event.preventDefault();
chooseTheme(opt.value);
}
}
}, [
vue.createElementVNode("i", { class: vue.normalizeClass(["dot", opt.dot]) }, null, 2),
vue.createTextVNode(" " + vue.toDisplayString(opt.label), 1)
], 10, ["onClick"]);
}), 128))
])) : vue.createCommentVNode("", true)
], 6)) : vue.createCommentVNode("", true);
};
}
};
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "App",
setup(__props) {
const shadowRoot = vue.inject("shadowRoot");
const getPopupContainer = () => {
var _a;
return ((_a = shadowRoot == null ? void 0 : shadowRoot.querySelector) == null ? void 0 : _a.call(shadowRoot, "[data-xs-abc-app]")) ?? document.body;
};
return (_ctx, _cache) => {
const _component_a_style_provider = vue.resolveComponent("a-style-provider");
const _component_a_config_provider = vue.resolveComponent("a-config-provider");
return vue.openBlock(), vue.createBlock(_component_a_config_provider, { getPopupContainer }, {
default: vue.withCtx(() => [
vue.createVNode(_component_a_style_provider, { container: vue.unref(shadowRoot) }, {
default: vue.withCtx(() => [
vue.createVNode(_sfc_main$1)
]),
_: 1
}, 8, ["container"])
]),
_: 1
});
};
}
});
const hookError = () => {
const oldset = _unsafeWindow.setInterval;
const oldout = _unsafeWindow.setTimeout;
_unsafeWindow.setInterval = function(...args) {
const err = new Error("");
if (err.stack && err.stack.indexOf("checkoutNotTrustScript") !== -1) {
return -1;
}
return oldset.call(this, ...args);
};
_unsafeWindow.setTimeout = function(...args) {
const err = new Error("");
if (err.stack && err.stack.indexOf("checkoutNotTrustScript") !== -1) {
return -1;
}
return oldout.call(this, ...args);
};
};
const url = _unsafeWindow.location.href;
const isTopWindow = _unsafeWindow.self === _unsafeWindow.top;
const ABC_LOADED_FLAG = Symbol.for("__xsLoaded");
if (Object.prototype.hasOwnProperty.call(_unsafeWindow, ABC_LOADED_FLAG)) ;
else {
let injectCSSIntoShadow = function(shadow) {
const pendingCSS = globalThis.__abcCSS;
if (pendingCSS) {
const style = document.createElement("style");
style.textContent = pendingCSS;
shadow.appendChild(style);
// 记住这块样式,后面页面新注入的样式都会追加到它后面,需要把它挪回最后,否则同权重会被页面样式盖掉
shadow.__xsPanelCss = style;
delete globalThis.__abcCSS;
}
}, observeHeadStyles = function(shadow) {
const clonedMap = /* @__PURE__ */ new WeakMap();
const cloneStyle = (node) => {
if (!(node instanceof HTMLStyleElement)) return;
if (node.dataset.abcIgnore) return;
if (clonedMap.has(node)) return;
const clone = node.cloneNode(true);
clone.dataset.abcIgnore = "true";
shadow.appendChild(clone);
if (shadow.__xsPanelCss) shadow.appendChild(shadow.__xsPanelCss);
clonedMap.set(node, clone);
new MutationObserver(() => {
clone.textContent = node.textContent;
}).observe(node, { childList: true, characterData: true, subtree: true });
};
new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) cloneStyle(node);
}
}).observe(document.head, { childList: true });
};
Object.defineProperty(_unsafeWindow, ABC_LOADED_FLAG, { value: true, configurable: true, enumerable: false, writable: true });
if (url.includes("zhihuishu.com") && isTopWindow) {
if (url.includes("studyvideoh5.zhihuishu.com")) {
hookError();
}
// 作业页(atHomeworkExam)也会拉同一份试卷接口,只在几个考试域名上装钩子会漏抓,
// 全站装一次,没命中接口就什么都不做,代价可以忽略
installAnswerSheetHook();
}
const timer = setInterval(() => {
if (document.readyState === "complete") {
clearInterval(timer);
if (!isTopWindow) return;
if (document.querySelector("[data-xs-abc-root]")) return;
const container = document.createElement("div");
container.setAttribute("data-xs-abc-root", "");
container.style.cssText = "all:initial!important;position:static!important;display:block!important;width:0!important;height:0!important;overflow:visible!important;opacity:1!important;visibility:visible!important;transform:none!important;filter:none!important;pointer-events:none!important";
document.body.appendChild(container);
const shadow = container.attachShadow({ mode: "open" });
const resetStyle = document.createElement("style");
resetStyle.textContent = [
":host{all:initial!important;display:block!important}",
".no-select{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}",
"input{outline:none;border:none}",
"div{box-sizing:border-box}",
"ul,ol{padding-inline-start:20px}",
".pin-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px;cursor:pointer;font-size:14px;opacity:0.5;transition:all 0.2s;user-select:none}",
".pin-btn:hover{opacity:0.8}",
".pin-btn.pinned{opacity:1;color:var(--t-primary,#1677ff)}",
".update-notify-bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:linear-gradient(135deg,var(--t-primary,#2563eb),var(--t-primary-2,#1d4ed8));color:#fff;font-size:13px;border-radius:8px 8px 0 0;pointer-events:auto}",
".update-notify-btn{padding:3px 12px;background:rgba(255,255,255,.25);border-radius:4px;cursor:pointer;white-space:nowrap;font-weight:600;transition:background .2s}",
".update-notify-btn:hover{background:rgba(255,255,255,.4)}",
".update-dismiss-btn{cursor:pointer;opacity:.6;font-size:16px;padding:0 4px;transition:opacity .2s}",
".update-dismiss-btn:hover{opacity:1}"
].join("\n");
shadow.appendChild(resetStyle);
injectCSSIntoShadow(shadow);
observeHeadStyles(shadow);
const appDiv = document.createElement("div");
appDiv.setAttribute("data-xs-abc-app", "");
shadow.appendChild(appDiv);
const app = vue.createApp(_sfc_main);
const pinia$1 = pinia.createPinia();
app.provide("shadowRoot", shadow);
app.use(pinia$1);
app.use(Antd);
app.mount(appDiv);
// >>> FX-STRIP:auto-update
// === 自动检测更新 + 顶部提示条 ===
(function() {
var _showUpdateBar = function(remote) {
var _insertBar = function() {
if (shadow.querySelector('.update-notify-bar')) return;
var mainBox = shadow.querySelector('.main-box');
if (!mainBox) return false;
var bar = document.createElement('div');
bar.className = 'update-notify-bar';
bar.innerHTML = '🎉 发现新版本 v' + remote.version + ',点击右侧按钮更新' +
'立即更新' +
'✕';
mainBox.insertBefore(bar, mainBox.firstChild);
bar.querySelector('.update-notify-btn').addEventListener('click', function(e) {
e.stopPropagation();
if (typeof GM_openInTab === 'function') {
GM_openInTab(remote.download_url, { active: true, insert: true });
} else if (typeof GM !== 'undefined' && typeof GM.openInTab === 'function') {
GM.openInTab(remote.download_url, { active: true });
} else {
window.open(remote.download_url, '_blank');
}
});
bar.querySelector('.update-dismiss-btn').addEventListener('click', function(e) {
e.stopPropagation();
bar.remove();
});
return true;
};
if (_insertBar()) return;
var _obs = new MutationObserver(function(mutations, obs) {
if (_insertBar()) obs.disconnect();
});
_obs.observe(shadow, { childList: true, subtree: true });
};
var currentVersion = (_GM_info && _GM_info.script && _GM_info.script.version) || "0.0.0";
GM_xmlhttpRequest({
method: 'GET',
url: 'https://xs.openget.cn/scripts/version.json?t=' + Date.now(),
onload: function(res) {
try {
var data = JSON.parse(res.responseText);
var remote = data['xueshen-sc'];
if (!remote) return;
var rv = remote.version.split('.').map(Number);
var lv = currentVersion.split('.').map(Number);
var newer = false;
for (var i = 0; i < Math.max(rv.length, lv.length); i++) {
var r = rv[i] || 0, l = lv[i] || 0;
if (r > l) { newer = true; break; }
if (r < l) break;
}
if (newer) _showUpdateBar(remote);
} catch(e) {}
},
onerror: function() {}
});
})();
// <<< FX-STRIP:auto-update
}
}, 100);
}
})(Vue, antd, Pinia, marked, rxjs, md5, DOMPurify);
/* ==================== 景德镇艺术职业学院(o-learn.cn)课程助手 ==================== */
(function () {
'use strict';
if (!/o-learn\.cn$/i.test(location.hostname)) return;
/* ======================== 配置 ======================== */
const CONFIG = {
defaultSpeed: 2, // 默认倍速
autoPlay: true, // 默认开启自动播放
autoMute: true, // 默认静音播放(浏览器策略要求,播放后自动取消)
autoNext: true, // 视频结束自动进入下一节
retryInterval: 3000, // 定期扫描兜底间隔(ms)
speedLockInterval: 1000, // 倍速锁定间隔(ms)
playRetryMax: 10, // 自动播放最大重试次数
playRetryDelay: 1000, // 自动播放重试间隔(ms)
quizInterval: 2000, // 答题轮询间隔(ms)
logMax: 200, // 日志面板最大条数
backendBase: 'https://xs.openget.cn',
};
/* ======================== 持久化存储 ======================== */
const STORE_KEY = 'jdz-art-settings-v4';
let store = {
account: { username: '', token: '', scriptKey: '' },
tasks: { exam: true, quiz: true, video: true }, // 任务开关:考试 / 答题 / 视频
speed: CONFIG.defaultSpeed,
autoPlay: CONFIG.autoPlay,
autoMute: CONFIG.autoMute,
autoNext: CONFIG.autoNext,
};
function loadStore() {
try {
const raw = GM_getValue(STORE_KEY, '');
if (raw) store = Object.assign(store, JSON.parse(raw));
} catch (e) { /* ignore */ }
}
function saveStore() {
try { GM_setValue(STORE_KEY, JSON.stringify(store)); } catch (e) { /* ignore */ }
}
/* ======================== 状态 ======================== */
let state = {
currentVideo: null,
examStarted: false,
examAnswered: false,
};
const SPEED_PRESETS = [0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4, 8, 16];
/* ======================== 日志 ======================== */
const logStore = { list: [] };
function log(...args) {
const msg = args.map(a => {
if (typeof a === 'string') return a;
if (a instanceof Element) {
const id = a.id ? `#${a.id}` : '';
const cls = typeof a.className === 'string' && a.className.trim() ? `.${a.className.trim().split(/\s+/).join('.')}` : '';
return `<${a.tagName.toLowerCase()}${id}${cls}>`;
}
if (a instanceof Error) return `${a.name}: ${a.message}`;
if (a && typeof a === 'object') {
const s = JSON.stringify(a);
return s && s.length > 120 ? s.slice(0, 120) + '…' : s;
}
return String(a);
}).join(' ');
logStore.list.push({ time: new Date(), msg });
if (logStore.list.length > CONFIG.logMax) logStore.list.shift();
console.log('%c[景德镇艺术助手]', 'color:#4CAF50;font-weight:bold', msg);
renderLogPanel();
}
/* ======================== 学习记录 ======================== */
const RECORD_KEY = 'jdz-art-records';
let records = [];
function loadRecords() {
try { records = JSON.parse(localStorage.getItem(RECORD_KEY)) || []; }
catch (e) { records = []; }
}
function saveRecords() {
try { localStorage.setItem(RECORD_KEY, JSON.stringify(records)); }
catch (e) { /* ignore */ }
}
function getRecordKey(video) {
return video.currentSrc || video.src || location.href;
}
function ensureRecord(video) {
const key = getRecordKey(video);
let rec = records.find(r => r.key === key);
if (!rec) {
rec = {
key,
title: document.title || key,
firstTime: Date.now(),
watched: 0,
done: false,
lastSeen: Date.now(),
};
records.unshift(rec);
saveRecords();
} else {
rec.lastSeen = Date.now();
}
return rec;
}
function formatDuration(sec) {
sec = Math.floor(sec);
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
return h ? `${h}时${m}分` : m ? `${m}分${s}秒` : `${s}秒`;
}
function formatTime(ts) {
const d = new Date(ts);
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
function safeText(s) {
return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
/* ======================== 后端 API ======================== */
function backendRequest(path, { method = 'GET', data = null, token = '' } = {}) {
return new Promise((resolve) => {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = 'Bearer ' + token;
GM_xmlhttpRequest({
url: CONFIG.backendBase + path,
method,
headers,
data: data ? JSON.stringify(data) : undefined,
timeout: 20000,
onload: (res) => {
try { resolve(JSON.parse(res.responseText || '{}')); }
catch (e) { resolve({ code: 500, msg: '响应解析失败' }); }
},
onerror: () => resolve({ code: 500, msg: '无法连接本地后端' }),
ontimeout: () => resolve({ code: 500, msg: '后端请求超时' }),
});
});
}
async function sha256Text(text) {
const enc = new TextEncoder();
const buf = await crypto.subtle.digest('SHA-256', enc.encode(text || ''));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function backendLogin(username, password) {
const hash = await sha256Text(password);
const res = await backendRequest('/api/auth/login', { method: 'POST', data: { username, password_hash: hash } });
if (res.code !== 200 || !res.token) return { ok: false, msg: res.msg || '登录失败' };
// 同步脚本密钥
const keyRes = await backendRequest('/api/user/script-key', { token: res.token });
if (keyRes.code === 200 && keyRes.script_key) {
store.account = { username, token: res.token, scriptKey: keyRes.script_key };
saveStore();
return { ok: true, msg: '登录成功并已同步密钥', scriptKey: keyRes.script_key };
}
return { ok: false, msg: '登录成功但密钥获取失败,请到用户中心手动复制' };
}
async function backendProfile() {
const token = store.account.scriptKey || store.account.token;
if (!token) return null;
const res = await backendRequest('/api/user/profile', { token });
if (res.code === 200 && res.profile) return res.profile;
return null;
}
async function refreshAccountBadge() {
try {
const prof = await backendProfile();
if (prof) {
store.account.profile = {
points_balance: prof.active_member ? null : Number(prof.points_balance || 0),
active_member: !!prof.active_member,
member_until: prof.member_until || '',
};
saveStore();
}
} catch (e) { /* ignore */ }
if (loginStatusEl) updateLoginStatus();
}
function formatQuotaText() {
const p = store.account.profile;
if (!p) return '';
if (p.active_member) return ` · 🎉包月不限题数`;
if (p.points_balance === null || p.points_balance === undefined) return '';
return ` · 剩余 ${p.points_balance} 题`;
}
function callAnswer(questionPayload, { u = '', skipBank = false } = {}) {
return new Promise((resolve) => {
const token = store.account.scriptKey || store.account.token || '';
const data = 'question=' + encodeURIComponent(JSON.stringify(questionPayload))
+ '&u=' + encodeURIComponent(u)
+ '&model_mode=auto'
+ (skipBank ? '&skip_bank=1' : '');
GM_xmlhttpRequest({
url: CONFIG.backendBase + '/api/v1/cx?v=xs-5.0',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'Authorization': token ? 'Bearer ' + token : '',
},
data,
timeout: 300000,
onload: (res) => {
try {
const obj = JSON.parse(res.responseText || '{}');
if (obj.code === 200 && obj.data) {
// 同步权益信息(剩余题数 / 包月状态),无需额外请求
if (obj.data.remainCount !== undefined || obj.data.profile) {
const prof = obj.data.profile || {};
const isActive = !!(prof.active_member !== undefined ? prof.active_member : store.account.profile && store.account.profile.active_member);
store.account.profile = {
points_balance: isActive ? null : Number(prof.points_balance !== undefined ? prof.points_balance : obj.data.remainCount),
active_member: isActive,
member_until: prof.member_until || (store.account.profile && store.account.profile.member_until) || '',
};
saveStore();
if (loginStatusEl) updateLoginStatus();
}
resolve({ ok: true, data: obj.data, msg: obj.msg || (obj.data.bank ? '题库命中' : obj.data.cache ? '缓存命中' : 'AI回答') });
} else if (res.status === 401 || obj.code === 401) {
resolve({ ok: false, msg: '登录态失效,请重新登录用户中心' });
} else {
resolve({ ok: false, msg: obj.msg || '未命中' });
}
} catch (e) {
resolve({ ok: false, msg: '响应解析失败' });
}
},
onerror: () => resolve({ ok: false, msg: '无法连接后端' }),
ontimeout: () => resolve({ ok: false, msg: '后端请求超时' }),
});
});
}
/* ======================== 图片转 base64 ======================== */
function downloadImageAsDataURL(url) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
url,
method: 'GET',
responseType: 'arraybuffer',
timeout: 30000,
onload: (res) => {
try {
const bytes = new Uint8Array(res.response);
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
const type = (res.responseHeaders.match(/content-type:\s*([^\s;\r\n]+)/i) || [])[1] || 'image/jpeg';
resolve('data:' + type + ';base64,' + btoa(binary));
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
}
/* ======================== 倍速控制 ======================== */
function applySpeed(video, rate) {
if (!video) return;
try {
if (video.__rateHijacked) restorePlaybackRate(video);
video.playbackRate = rate;
video.defaultPlaybackRate = rate;
} catch (e) { /* ignore */ }
hijackPlaybackRate(video);
if (video.__vjsPlayer) {
try { video.__vjsPlayer.playbackRate(rate); } catch (e) { /* ignore */ }
}
}
let speedLockTimer = null;
function startSpeedLock(video) {
stopSpeedLock();
speedLockTimer = setInterval(() => {
if (!video || !state.currentVideo) return;
try {
if (video.playbackRate !== Number(store.speed)) {
video.__realRate = store.speed;
video.defaultPlaybackRate = store.speed;
try { video.playbackRate = store.speed; } catch (e) {}
}
} catch (e) { /* ignore */ }
}, CONFIG.speedLockInterval);
}
function stopSpeedLock() {
if (speedLockTimer) { clearInterval(speedLockTimer); speedLockTimer = null; }
}
function hijackPlaybackRate(video) {
if (video.__rateHijacked) return;
video.__realRate = Number(store.speed);
try {
let proto = Object.getPrototypeOf(video), desc;
while (proto && !desc) {
desc = Object.getOwnPropertyDescriptor(proto, 'playbackRate');
proto = Object.getPrototypeOf(proto);
}
if (!desc || !desc.get || !desc.set) return;
Object.defineProperty(video, 'playbackRate', {
configurable: true,
get: function () { return desc.get.call(video); },
set: function (v) {
const numV = Number(v);
if (isNaN(numV)) return;
if (numV !== store.speed) {
log('🛡️ 网页想修改倍速,已保持为', store.speed + 'x');
if (desc && desc.set) desc.set.call(video, store.speed);
video.__realRate = store.speed;
return;
}
if (desc && desc.set) desc.set.call(video, numV);
video.__realRate = numV;
}
});
video.__rateHijacked = true;
} catch (e) {
video.__rateHijacked = false;
log('⚠️ 倍速锁定遇到小问题,已自动改用备用方式', e);
}
}
function restorePlaybackRate(video) {
if (!video.__rateHijacked) return;
try { delete video.playbackRate; } catch (e) { /* ignore */ }
video.__rateHijacked = false;
}
/* ======================== 拦截 pause ======================== */
function hijackPause(video) {
if (video.__pauseHijacked) return;
video.__pauseHijacked = true;
const originalPause = video.pause.bind(video);
video.__originalPause = originalPause;
video.pause = function () {
if (store.autoPlay && !video.ended && !video.__userPaused) {
log('🛡️ 网页想暂停视频,已拦截,继续播放');
return;
}
return originalPause();
};
video.addEventListener('click', () => {
video.__userPaused = video.paused;
});
}
/* ======================== 自动播放 ======================== */
function autoPlayVideo(video, fromUserGesture) {
if (!video || !store.autoPlay) return;
let retryCount = 0;
const allowSound = fromUserGesture || store.autoMute === false;
function attemptPlay() {
if (!store.autoPlay || !video || video.ended) return;
if (retryCount >= CONFIG.playRetryMax) {
log('⚠️ 自动播放多次失败,请在页面上手动点击一次播放按钮');
statusEl && (statusEl.textContent = '请手动点击播放一次');
return;
}
retryCount++;
log(`▶️ 自动播放尝试 #${retryCount}(共 ${CONFIG.playRetryMax} 次)`);
if (!video.src && !video.currentSrc) {
log('⏳ 视频还在加载中,稍等一下...');
return;
}
video.muted = !allowSound;
video.volume = allowSound ? 1 : 0;
const doPlay = (target) => {
const p = target.play();
return p || Promise.reject(new Error('no promise'));
};
doPlay(video)
.then(() => {
log('✅ 自动播放成功' + (allowSound ? '(带声)' : '(静音)'));
setTimeout(() => {
if (allowSound) { video.muted = false; video.volume = 1; }
}, 500);
applySpeed(video, store.speed);
updateUI();
})
.catch((e) => {
log(`❌ 浏览器阻止了自动播放,正在尝试其他方式...`);
if (video.__vjsPlayer) {
try {
video.__vjsPlayer.play().then(() => {
log('✅ 自动播放成功' + (allowSound ? '(带声)' : '(静音)'));
applySpeed(video, store.speed);
updateUI();
}).catch(() => { fallbackMuted(); });
return;
} catch (err) { /* ignore */ }
}
fallbackMuted();
});
function fallbackMuted() {
if (!allowSound) { setTimeout(attemptPlay, CONFIG.playRetryDelay); return; }
video.muted = true;
video.volume = 0;
const p = (video.__vjsPlayer ? video.__vjsPlayer.play() : video.play());
if (p && p.then) {
p.then(() => log('✅ 静音播放成功'))
.catch(() => { log('静音播放也失败,等待用户交互后重试'); setTimeout(attemptPlay, CONFIG.playRetryDelay); });
} else {
setTimeout(attemptPlay, CONFIG.playRetryDelay);
}
}
}
const events = ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'durationchange'];
const handler = (e) => {
if (store.autoPlay && video.paused) { log('▶️ 视频已就绪,自动开始播放'); attemptPlay(); }
};
events.forEach(evt => video.addEventListener(evt, handler));
attemptPlay();
const retryTimer = setInterval(() => {
if (video.ended || !store.autoPlay) { clearInterval(retryTimer); return; }
if (video.paused && video.readyState >= 2) { attemptPlay(); }
else if (!video.paused) { clearInterval(retryTimer); }
}, CONFIG.playRetryDelay);
}
/* ======================== 绑定视频 ======================== */
function getVjsPlayer(video) {
try {
if (window.videojs) {
const id = (video.id || '').replace(/_html5_api$/, '');
if (window.videojs.getPlayer) {
const p = window.videojs.getPlayer(id);
if (p) return p;
}
const players = window.videojs.getAllPlayers ? window.videojs.getAllPlayers() : [];
for (const p of players) {
try {
if (p.tech_ && p.tech_.el && p.tech_.el() === video) return p;
if (p.el && p.el() && p.el().contains(video)) return p;
} catch (e) { /* ignore */ }
}
}
} catch (e) { /* ignore */ }
return null;
}
function bindVideo(video) {
if (video.__bound) return;
video.__bound = true;
state.currentVideo = video;
video.__vjsPlayer = getVjsPlayer(video);
video.__record = ensureRecord(video);
log('🎬 发现视频,开始接管', video.__vjsPlayer ? '' : '', `时长=${video.duration ? formatDuration(video.duration) : '未知'}`);
video.autoplay = true;
video.preload = 'auto';
hijackPause(video);
hijackPlaybackRate(video);
applySpeed(video, store.speed);
startSpeedLock(video);
if (store.autoPlay) autoPlayVideo(video);
video.addEventListener('pause', () => {
if (store.autoPlay && !video.ended && !video.__userPaused) {
log('🔄 视频被暂停了,马上自动恢复播放');
setTimeout(() => {
if (store.autoPlay && !video.ended) {
video.muted = true;
video.play().catch(() => {});
}
}, 800);
}
});
video.addEventListener('ended', () => {
log('🏁 视频播放结束');
if (video.__record) {
video.__record.done = true;
saveRecords();
renderRecordPanel();
}
if (store.autoNext) gotoNextCourseware();
});
video.addEventListener('ratechange', () => {
if (video.__realRate !== store.speed) {
log('🔁 网页改了播放速度,已自动改回', store.speed + 'x');
applySpeed(video, store.speed);
}
});
updateUI();
}
/* ======================== 自动下一节 ======================== */
function gotoNextCourseware() {
log('🔜 视频播完了,正在自动进入下一节...');
const nextTexts = ['下一节', '下一个', '下一页', '下一章', 'next', 'Next', '继续学习'];
const allClickable = document.querySelectorAll('button, a, [role="button"], [class*="next"], [class*="Next"]');
for (const btn of allClickable) {
const text = (btn.textContent || '').trim();
if (text && nextTexts.some(t => text.includes(t))) {
log('📍 找到"下一节"按钮,自动点击:', text);
btn.click();
return;
}
}
const outlineItems = document.querySelectorAll(
'[class*="courseware"] li, [class*="chapter"] li, [class*="lesson"] li, [class*="catalog"] li, [class*="tree"] li, [class*="outline"] li, [class*="menu-item"], [class*="node"]'
);
let foundCurrent = false;
for (const item of outlineItems) {
if (foundCurrent) {
log('📍 从课程目录中点击下一项:', item.textContent?.trim()?.substring(0, 20));
item.click();
return;
}
if (item.classList.contains('active') || item.classList.contains('current') ||
item.getAttribute('aria-current') === 'page' ||
item.querySelector('.active, .current, [class*="active"], [class*="current"]')) {
foundCurrent = true;
}
}
log('⌨️ 没找到"下一节"按钮,尝试用键盘切换');
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', keyCode: 39, bubbles: true }));
}
/* ======================== 考试自动答题 ======================== */
const JUDGE_OPTIONS = { 0: '正确', 1: '错误' };
function isExamPage() {
return /examflow_index|examflow|examPaper|examQuestion/i.test(location.href)
|| !!document.getElementById('paperSheet')
|| !!document.querySelector('.exam_page, #examPage');
}
/* ---- 答案解析:把后端返回文本解析为 字母列表 或 判断题 目标值 ---- */
function parseAnswerText(text, type) {
text = String(text || '').trim();
if (!text) return null;
const isJudge = type === '判断题' || type === 'judge';
if (isJudge) {
const correct = /正确|true|对|yes|right|^1\b|^1$/.test(text);
const wrong = /错误|false|错|no|^0\b|^0$/.test(text);
const target = correct && !wrong ? '0' : wrong ? '1' : null;
return target === null ? null : { judge: target };
}
if (text.startsWith('[')) {
try {
const arr = JSON.parse(text);
if (Array.isArray(arr) && arr.length) {
const items = arr.map((x) => String(x).trim()).filter(Boolean);
const asLetters = items.filter((x) => /^[A-Z]$/i.test(x)).map((x) => x.toUpperCase());
if (asLetters.length === items.length) return { letters: asLetters };
return { texts: items };
}
} catch (_) {}
}
// 多选题/单选题:仅在整段是字母答案时提取,避免把 "DNA"/"Option A is correct" 误拆成字母
const letterOnly = text.replace(/^(答案|正确答案|选择?)[是::.\s]*/i, "").trim();
const compact = letterOnly.replace(/[\s,,、;;|+/&和]+/g, "").toUpperCase();
if (/^[A-H]{1,8}$/.test(compact)) {
const unique = [];
for (const l of compact) {
if (!unique.includes(l)) unique.push(l);
}
return { letters: unique };
}
// 数字索引 1,2,3
const nums = text.match(/\d+/g) || [];
if (nums.length) return { letters: nums.map(n => String.fromCharCode(64 + parseInt(n, 10))) };
return null;
}
/* ---- 双结构题目解析 ----
结构A(纯文本): #qContent{N} > .divQuestionTitle .q-title-text + .questionOptions .q_option(input + .a11y-option)
结构B(图片/旧): #paperSheet > .set-group > .set-item (.score + a.option / label.exam_radio + 题干图片)
*/
function getExamQuestions() {
const result = [];
// 结构A:qContent{N}
const qContents = document.querySelectorAll('.exam_page .q_content, #examPage .q_content, div.q_content[id^="qContent"]');
if (qContents.length) {
qContents.forEach(qc => {
const idMatch = (qc.id || '').match(/qContent(\d+)/);
const titleEl = qc.querySelector('.q-title-text');
const title = titleEl ? (titleEl.textContent || '').replace(/\s+/g, ' ').trim() : '';
const qidInput = qc.querySelector('input[name="quesId"]');
const qid = qidInput ? qidInput.value : '';
const typeEl = qc.querySelector('.divQuestionTitle');
const typeName = typeEl ? (typeEl.getAttribute('data-q-type-name') || '') : '';
const hasCheckbox = !!qc.querySelector('input[type=checkbox]');
const resolvedType = hasCheckbox || /多选|不定项|X型/.test(typeName) ? '多选题' : typeName;
const opts = [];
qc.querySelectorAll('.q_option').forEach(op => {
const input = op.querySelector('input[type=radio], input[type=checkbox]');
const clickable = op.querySelector('.a11y-option') || op.querySelector('.radio_off') || op.querySelector('.checkbox_off') || op;
const labelEl = clickable;
const labelText = (labelEl.textContent || '').replace(/\s+/g, ' ').trim();
const optLetter = clickable.getAttribute && (clickable.getAttribute('data-option-label') || '');
const inputVal = input ? input.value : '';
const checked = input ? !!input.checked : false;
const img = op.querySelector && op.querySelector("img");
const imgSrc = img ? (img.getAttribute("data-src") || img.currentSrc || img.src || "") : "";
const finalText = labelText || (imgSrc ? `
` : "");
opts.push({ text: finalText, el: clickable, input, value: inputVal, letter: optLetter, checked });
});
const imgs = Array.from(qc.querySelectorAll('img'));
const imgSrcs = imgs.map(img => img.src || img.getAttribute('data-src') || '').filter(Boolean);
if (idMatch) {
result.push({
num: parseInt(idMatch[1], 10),
type: resolvedType,
title,
qid,
opts,
judges: typeName === '判断题' ? opts.map((o, i) => ({ value: o.value, text: o.text, el: o.el })) : [],
stemText: title,
imgSrcs,
el: qc,
mode: 'A',
isJudge: typeName === '判断题',
});
}
});
if (result.length) return result;
}
// 结构B:paperSheet set-item
const sheet = document.getElementById('paperSheet');
if (!sheet) return [];
const items = sheet.querySelectorAll('.set-item');
items.forEach((item, i) => {
const scoreEl = item.querySelector('.score');
const numMatch = scoreEl ? (scoreEl.textContent || '').match(/(\d+)/) : null;
if (!numMatch) return;
const num = parseInt(numMatch[1], 10);
const opts = [];
item.querySelectorAll('a.option').forEach(o => {
opts.push({
text: (o.textContent || '').trim(),
el: o,
letter: (o.getAttribute('data-option-label') || (o.textContent || '').trim()).toUpperCase(),
checked: /r_on|selected|checked/.test(o.className || '')
});
});
const checkboxEls = item.querySelectorAll('input[type=checkbox], label.exam_checkbox');
checkboxEls.forEach(el => {
const input = el.tagName === 'INPUT' ? el : el.querySelector('input[type=checkbox]');
const label = el.tagName === 'LABEL' ? el : (el.closest('label') || el);
const text = (label.textContent || '').replace(/\s+/g, ' ').trim();
if (!text || opts.some(o => o.text === text)) return;
opts.push({
text,
el: label,
input,
letter: ((label.getAttribute && label.getAttribute('data-option-label')) || (text.match(/^[A-Z]/i) || [''])[0]).toUpperCase(),
checked: !!(input && input.checked)
});
});
const radioEls = item.querySelectorAll('label.exam_radio');
const judges = [];
radioEls.forEach(label => {
const input = label.querySelector('input[type=radio]');
judges.push({ value: input ? input.value : '', text: (label.textContent || '').trim(), el: label });
});
const stemEl = item.querySelector('.stem, [class*="stem"], .fl, .question-content, [class*="question"]');
const stemText = stemEl ? (stemEl.textContent || '').replace(/\s+/g, ' ').trim() : '';
const imgs = item.querySelectorAll('img');
const imgSrcs = Array.from(imgs).map(img => img.src || img.getAttribute('data-src') || '').filter(Boolean);
const headText = ((scoreEl && scoreEl.textContent) || '') + (item.getAttribute('data-q-type-name') || '');
const isMulti = checkboxEls.length > 0 || /多选题|多项选择|不定项|X型/.test(headText) || /多选题|多项选择/.test((item.textContent || '').slice(0, 120));
result.push({ num, opts, judges, stemText, imgSrcs, el: item, mode: 'B', isJudge: judges.length > 0 && !opts.length, type: judges.length && !opts.length ? '判断题' : (isMulti ? '多选题' : (opts.length ? '单选题' : '')) });
});
return result;
}
/* ---- 选项文本匹配:模型可能直接返回选项内容(如"发展")而非字母 ---- */
function matchOptionTextToLetters(q, answer) {
const clean = (s) => String(s || '')
.replace(/^[A-F][、..))::]\s*/i, '')
.replace(/^答案[::]\s*/i, '')
.replace(/^选项[::]\s*/i, '')
.replace(/\s+/g, '').trim();
const ans = clean(answer).toLowerCase();
const answerImgs = collectImgSrcs(answer);
if (!ans && !answerImgs.length) return [];
const hasCheckbox = (q.opts || []).some(o => o.input && o.input.type === 'checkbox');
const isSingle = !hasCheckbox && !/多选/.test(q.type || '');
const found = [];
q.opts.forEach((o, idx) => {
const optText = clean(o.text || o.textContent || '').toLowerCase();
const letter = o.letter ? o.letter.toUpperCase() : String.fromCharCode(65 + idx);
let hit = false;
if (ans && optText) {
hit = optText === ans || optText.includes(ans) || (ans.length >= 2 && ans.includes(optText));
}
if (!hit && answerImgs.length) {
hit = imgSrcsMatch(answerImgs, collectImgSrcs(o.el).concat(collectImgSrcs(o.text)));
}
if (hit && !found.includes(letter)) found.push(letter);
});
return isSingle ? found.slice(0, 1) : found;
}
/* ---- 单题填答:兼容两套结构 ---- */
function fillExamAnswer(q, answer) {
const parsed = parseAnswerText(answer, q.type);
// 判断题
if (q.isJudge) {
let targetValue = parsed ? parsed.judge : null;
// 模型可能返回字母(A=正确/第一个选项,B=错误/第二个选项),按选项顺序映射
if (targetValue === null) {
const letterMatch = String(answer || '').match(/\b([A-F])\b/i);
if (letterMatch) {
const idx = letterMatch[1].toUpperCase().charCodeAt(0) - 65;
const list = q.mode === 'A' ? q.opts : q.judges;
if (list && list[idx] !== undefined) targetValue = String(list[idx].value);
}
}
if (targetValue === null) return false;
if (q.mode === 'A') {
// 结构A: 通过全局 renderCheckboxOrRadioByLabel(num, qid, idx) 或点击
let clicked = false;
q.opts.forEach((o, idx) => {
if (String(o.value) === targetValue && !o.input.checked) {
const fn = window.renderCheckboxOrRadioByLabel;
if (typeof fn === 'function') {
try { fn(String(q.num), q.qid, String(idx)); clicked = true; } catch (e) {}
} else {
o.el.click();
clicked = true;
}
}
});
return clicked;
}
// 结构B
for (const j of q.judges) {
if (String(j.value) === targetValue) {
j.el.classList.add('r_on');
if (typeof renderCheckboxOrRadio === 'function') {
try { renderCheckboxOrRadio(q.num, q.el.querySelector('.wrap') ? q.el.querySelector('.wrap').id : '', JSON.stringify(targetValue)); } catch (e) {}
}
const input = j.el.querySelector('input[type=radio]');
if (input) input.checked = true;
return true;
}
}
return false;
}
// 选择题(单选/多选)
let letters = parsed ? (parsed.letters || []) : [];
if (!letters.length && parsed && parsed.texts && parsed.texts.length) {
parsed.texts.forEach((t) => {
matchOptionTextToLetters(q, t).forEach((l) => {
if (!letters.includes(l)) letters.push(l);
});
});
}
// 答案没含字母时,尝试按选项文本匹配(模型可能直接返回选项内容)
if (!letters.length) {
letters = matchOptionTextToLetters(q, answer);
}
if (!letters.length) return false;
let clicked = 0;
q.opts.forEach((o, idx) => {
// 计算该选项对应的字母(数据里可能已有 letter,否则用索引)
const letter = o.letter ? o.letter.toUpperCase() : String.fromCharCode(65 + idx);
const want = letters.includes(letter);
if (want && !o.checked) {
if (q.mode === 'A' && typeof window.renderCheckboxOrRadioByLabel === 'function') {
try { window.renderCheckboxOrRadioByLabel(String(q.num), q.qid, String(idx)); clicked++; } catch (e) {}
} else {
o.el.click();
clicked++;
}
}
});
return clicked > 0;
}
function formatAnswerForLog(raw) {
// 清理答案文本:只保留有效字母(或 正确/错误),去掉 "答案:" 等前缀
const s = String(raw || '').trim();
if (!s) return '';
const judge = s.match(/正确|错误|对|错/);
if (judge) return judge[0];
const letters = s.match(/[A-F]/gi);
if (letters && letters.length) return letters.map(l => l.toUpperCase()).join('');
return s.slice(0, 20);
}
function formatModelForLog(model) {
const m = String(model || '');
if (!m) return '';
// 去掉厂商前缀 tencent/ qwen/ deepseek/ 等,只留模型名
const seg = m.split('/').pop();
return seg.slice(0, 24);
}
async function answerExamQuestion(q, index, extra = {}) {
const hasCheckbox = (q.opts || []).some(o => o.input && o.input.type === 'checkbox');
let payload = { question: q.stemText || q.title, type: q.type || (q.isJudge ? 'judge' : (hasCheckbox ? 'multiple' : 'single')) };
if (/多选|不定项|X型/.test(payload.type) || hasCheckbox) payload.type = 'multiple';
else if (/单选/.test(payload.type)) payload.type = 'single';
else if (/判断/.test(payload.type)) payload.type = 'judge';
if (q.opts.length) payload.options = q.opts.map(o => o.text);
// 图片题:下载图片
const dataURLs = [];
for (const src of q.imgSrcs) {
const dataUrl = await downloadImageAsDataURL(src);
if (dataUrl) dataURLs.push(dataUrl);
}
if (dataURLs.length) payload.images = dataURLs;
if (!q.stemText && !dataURLs.length) return { ok: false, msg: '题目为空' };
log(`📝 [${q.num}题] 调后端获取答案...`);
let res = await callAnswer(payload, { u: store.account.username || '', skipBank: !!extra.skipBank });
// 超时/网络失败时自动重试一次(后端冷却机制会让下一次换模型)
if (!res.ok && /超时|无法连接|未命中|响应解析/.test(res.msg || '')) {
log(`⚠️ [${q.num}题] ${res.msg},3秒后自动重试...`);
await new Promise(r => setTimeout(r, 3000));
res = await callAnswer(payload, { u: store.account.username || '', skipBank: !!extra.skipBank });
if (res.ok) log(`✅ [${q.num}题] 重试成功`);
}
if (!res.ok) return res;
return {
ok: true,
answer: res.data.answer,
model: res.data.model,
msg: res.msg || '',
src: res.data.bank ? 'bank' : res.data.cache ? 'cache' : 'ai'
};
}
/* ---- 一卷一报:本场答题小结 ----
这里读不到主脚本的 pinia / 日志队列,所以自带一份,走同一个 /api/script-logs。
字段口径与主脚本的 buildAnswerSummary 保持一致,后台按这两个拼一起统计。 */
async function reportAnswerSummary(rows) {
try {
const list = rows || [];
if (!list.length) return false;
const token = store.account.scriptKey || store.account.token;
if (!token) return false;
let filled = 0;
let skippedQuota = 0;
const src = { bank: 0, cache: 0, ai: 0, unknown: 0 };
const unanswered = [];
const isQuota = (msg) => /余额不足|题数不足|点数不足|insufficient.?quota|请到用户中心购买|请联系您的服务方/i.test(String(msg || ''));
for (const row of list) {
if (row.ok) {
filled += 1;
if (row.src === 'bank' || row.src === 'cache' || row.src === 'ai') src[row.src] += 1;
else src.unknown += 1;
continue;
}
if (isQuota(row.reason) || Number(row.code) === 402) {
skippedQuota += 1;
continue;
}
// 指纹逐题 await 一次 sha256,而后台只吃前 40 条:到量就停,
// 别让一场全卡住的整卷在答完之后再排几百次哈希。total/filled 另算,不受影响。
if (unanswered.length >= 40) continue;
unanswered.push({
type: String(row.type || '').slice(0, 20),
hash: await sha256Text(JSON.stringify({ title: row.title || '', type: row.type || '' })).catch(() => ''),
code: Number(row.code) || 0,
reason: String(row.reason || (Number(row.code) === 0 ? '答案拿到了但没点上去' : '')).slice(0, 80),
title: String(row.title || '').replace(/\s+/g, ' ').trim().slice(0, 60)
});
}
const total = list.length - skippedQuota;
if (!total) return false;
const version = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) || '';
const res = await backendRequest('/api/script-logs', {
method: 'POST',
token,
data: {
logs: [{
event_type: 'answer_summary',
level: 'info',
message: `[答题小结] olearn 共${total}题 填上${filled}题`.slice(0, 4000),
page_url: location.href,
extra: {
platform: 'olearn',
time: new Date().toTimeString().slice(0, 8),
script_version: version,
total,
filled,
unfilled: Math.max(0, total - filled),
src,
unanswered: unanswered.slice(0, 40),
accuracy: Math.round(filled / Math.max(total, 1) * 1000) / 10,
threshold: 0,
// 平台交卷后不回吐分数,判对率采不到;留 null,后台也不显示分数
score: null
},
timestamp: Date.now()
}]
}
});
return !!(res && res.code === 200);
} catch (e) {
return false;
}
}
/* ---- 整页图片试卷模式 ----
试卷题干是整页大图(#docImagesViewArea 下 .cont > img),一张图含多道题。
逐张图调用后端视觉模型,返回 JSON: [{"num":1,"answer":"A"}, ...],再逐题填答。
扣点按实际 API 调用次数(每张图一次调用扣1点,包月不扣)。
*/
function extractImagePages() {
const area = document.getElementById('docImagesViewArea');
if (!area) return [];
const srcs = [];
area.querySelectorAll('img').forEach(img => {
const s = img.src || img.getAttribute('data-src') || '';
if (s && /(\.jpe?g|\.png|\.gif|\.webp)(\?|$)/i.test(s)) srcs.push(s);
});
// 若 docImagesViewArea 为空,退回找页面里非主题的整页大图
if (!srcs.length) {
document.querySelectorAll('img').forEach(img => {
const s = img.src || '';
if (s && /preview-office-convert|resourcePath|upload/.test(s) && img.naturalWidth > 300) srcs.push(s);
});
}
return srcs;
}
function parseImagePageAnswers(text) {
// 兼容: [{"num":1,"answer":"A"},...] / {1:"A",...} / 逐行 "1.A"
const raw = String(text || '').trim();
if (!raw) return null;
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) {
const map = {};
arr.forEach(item => {
const num = parseInt(item.num !== undefined ? item.num : item.index, 10);
if (num && item.answer !== undefined) map[num] = String(item.answer).trim();
});
return map;
}
if (typeof arr === 'object') {
const map = {};
Object.keys(arr).forEach(k => {
const num = parseInt(k, 10);
if (num) map[num] = String(arr[k]).trim();
});
return map;
}
} catch (e) { /* not pure json */ }
const map = {};
// 兼容 "1.A 2.B 3.C" 或 "1、A" 等
const re = /(\d{1,3})\s*[.、::))]\s*([A-F]+|正确|错误)/gi;
let m;
while ((m = re.exec(raw))) map[parseInt(m[1], 10)] = m[2].toUpperCase();
return Object.keys(map).length ? map : null;
}
async function answerImagePages() {
const pages = extractImagePages();
if (!pages.length) return false;
log(`🖼️ 检测到整页图片型试卷,共 ${pages.length} 张整页图,开始识别...`);
// 等待 set-item 渲染(填答需要)
let items = [];
for (let i = 0; i < 10; i++) {
items = getExamQuestions();
if (items.length) break;
await new Promise(r => setTimeout(r, 1000));
}
if (!items.length) {
log('⚠️ 图片已找到但题目选项未渲染');
reportAnswerSummary(pages.map((s, i) => ({ ok: false, type: '整页图片', title: '第' + (i + 1) + '张', reason: '选项未渲染' })));
return false;
}
const byNum = {};
items.forEach(q => { byNum[q.num] = q; });
let filledCount = 0;
const summaryRows = [];
for (let p = 0; p < pages.length; p++) {
log(`🖼️ [第${p + 1}/${pages.length}张图] 下载并识别...`);
const dataUrl = await downloadImageAsDataURL(pages[p]);
if (!dataUrl) { log(`❌ 第${p + 1}张图下载失败`); summaryRows.push({ ok: false, type: '整页图片', title: '第' + (p + 1) + '张', reason: '图片下载失败' }); continue; }
const payload = {
question: '请识别这张试卷图片中的全部题目(题干、选项),并按题号逐题给出答案。只输出 JSON 数组,格式:[{"num":1,"answer":"A"},{"num":2,"answer":"B,C"},...]。判断题用"正确"或"错误"。不要输出其他任何内容。',
images: [dataUrl],
type: 'image',
platform: 'jdz-exam-image',
refer: location.href,
};
const res = await callAnswer(payload, { u: store.account.username || '' });
if (!res.ok) { log(`❌ 第${p + 1}张图识别失败: ${res.msg}`); summaryRows.push({ ok: false, type: '整页图片', title: '第' + (p + 1) + '张', reason: res.msg }); continue; }
const answers = parseImagePageAnswers(res.data.answer);
if (!answers) { log(`⚠️ 第${p + 1}张图返回无法解析: ${String(res.data.answer).slice(0, 120)}`); summaryRows.push({ ok: false, type: '整页图片', title: '第' + (p + 1) + '张', reason: '模型返回无法解析' }); continue; }
const nums = Object.keys(answers).map(Number);
log(`✅ 第${p + 1}张图识别出 ${nums.length} 题答案 (${res.msg})`);
for (const num of nums) {
const q = byNum[num];
if (!q) { log(`⚠️ 题${num}未在页面中找到选项,跳过`); continue; }
const ok = fillExamAnswer(q, answers[num]);
if (ok) filledCount++;
summaryRows.push({ ok, src: 'ai', type: q.type, title: q.stemText || q.title });
log(`${ok ? '✅' : '⚠️'} [${num}题] ${ok ? '已填写' : '未能填写'} 答案=${formatAnswerForLog(answers[num]) || answers[num]}`);
await new Promise(r => setTimeout(r, 800));
}
await new Promise(r => setTimeout(r, 1500));
}
reportAnswerSummary(summaryRows);
log(`🏁 整页图片答题完成,共填写 ${filledCount} 题`);
return filledCount > 0;
}
async function runExamAutoAnswer() {
if (state.examStarted) return;
state.examStarted = true;
log('📖 检测到考试页面,开始自动答题');
// 整页图片型试卷优先处理
if (extractImagePages().length) {
const ok = await answerImagePages();
state.examStarted = false;
if (ok && store.tasks.examAutoSubmit) {
setTimeout(() => { trySubmitExam(); }, 2000);
}
return;
}
// 等待题目渲染
for (let i = 0; i < 10; i++) {
const qs = getExamQuestions();
if (qs.length >= 1) break;
await new Promise(r => setTimeout(r, 1000));
}
const questions = getExamQuestions();
if (!questions.length) {
log('⚠️ 未解析到题目,可能是整页图片型试卷,跳过');
state.examStarted = false;
return;
}
log(`🔍 解析到 ${questions.length} 道题 (${questions[0].mode === 'A' ? '文本结构' : '图片结构'})`);
const summaryRows = [];
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
const answered = q.mode === 'B'
? (q.isJudge ? q.judges.some(j => j.el && /r_on|selected|checked/.test(j.el.className || '')) : q.opts.some(o => o.checked || (o.el && /r_on|selected|checked/.test(o.el.className || ''))))
: q.opts.some(o => o.checked);
if (answered) continue;
let res = await answerExamQuestion(q, i);
if (!res.ok) {
log(`❌ [${q.num}题] 获取失败: ${res.msg}`);
summaryRows.push({ ok: false, type: q.type, title: q.stemText || q.title, reason: res.msg });
continue;
}
let ok = fillExamAnswer(q, res.answer);
if (!ok) {
log(`⚠️ [${q.num}题] 题库答案未能填写,改走模型`);
const res2 = await answerExamQuestion(q, i, { skipBank: true });
if (res2.ok) {
res = res2;
ok = fillExamAnswer(q, res.answer);
}
}
summaryRows.push({ ok, src: res.src, type: q.type, title: q.stemText || q.title });
const ansShow = formatAnswerForLog(res.answer);
const hit = res.msg && /命中/.test(res.msg) ? ` ${res.msg}` : '';
log(`${ok ? '✅' : '⚠️'} [${q.num}题] ${ok ? '已填写' : '未能填写'} 答案=${ansShow || '?'}${hit}`);
await new Promise(r => setTimeout(r, 1500));
}
reportAnswerSummary(summaryRows);
state.examStarted = false;
log('🏁 考试自动答题完成');
if (store.tasks.examAutoSubmit) {
setTimeout(() => { trySubmitExam(); }, 2000);
}
}
function trySubmitExam() {
const links = document.querySelectorAll('a, button');
for (const el of links) {
const t = (el.textContent || '').trim();
if (t === '交卷') {
log('📤 自动点击交卷');
el.click();
setTimeout(() => {
document.querySelectorAll('button').forEach(b => {
const bt = (b.textContent || '').trim();
if (bt === '交卷') b.click();
});
}, 1000);
return;
}
}
}
function setupExamWatcher() {
setInterval(() => {
if (!store.tasks.exam) return;
if (isExamPage() && !state.examStarted) {
runExamAutoAnswer();
}
}, 3000);
}
/* ======================== 作业/测验自动答题 ======================== */
function extractWorkQuestions() {
// 通用提取:页面上可见的题目容器
const selectors = [
'[class*="question"]', '[class*="exam"]', '[class*="work"]', '[class*="quiz"]',
'.set-item', '[class*="stem"]'
];
const seen = new Set();
const questions = [];
document.querySelectorAll(selectors.join(',')).forEach(el => {
if (seen.has(el)) return;
seen.add(el);
const text = (el.textContent || '').trim();
if (text.length < 4) return;
// 选项
const opts = [];
el.querySelectorAll('input[type=radio], input[type=checkbox]').forEach(inp => {
const label = inp.closest('label');
const optText = label ? (label.textContent || '').replace(/\s+/g, ' ').trim() : '';
if (optText) opts.push({ text: optText, el: label || inp, input: inp });
});
questions.push({ el, text, opts });
});
return questions;
}
async function runWorkAutoAnswer() {
const questions = extractWorkQuestions();
if (!questions.length) return;
log(`📝 检测到作业/测验 ${questions.length} 个题目容器`);
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
const opts = q.opts;
if (!opts.length) continue;
const already = opts.some(o => o.input && o.input.checked);
if (already) continue;
const hasCheckbox = opts.some(o => o.input && o.input.type === 'checkbox');
log(`📝 作业第${i + 1}题 调后端...`);
const payload = { question: q.text.slice(0, 300), options: opts.map(o => o.text), type: hasCheckbox ? 'multiple' : 'single' };
const res = await callAnswer(payload, { u: store.account.username || '' });
if (!res.ok) { log(`❌ 作业第${i + 1}题失败: ${res.msg}`); continue; }
const raw = String(res.data.answer || '').trim();
let targets = [];
if (raw.startsWith('[')) {
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) targets = arr.map((x) => String(x).trim()).filter(Boolean);
} catch (_) {}
}
if (!targets.length) {
const letters = raw.match(/[A-Z]/gi) || [];
letters.forEach((l) => {
const u = l.toUpperCase();
if (!targets.includes(u)) targets.push(u);
});
}
let clicked = 0;
opts.forEach((o, idx) => {
const letter = String.fromCharCode(65 + idx);
const optText = (o.text || '').replace(/\s+/g, '');
const hit = targets.some((t) => {
const tt = String(t).replace(/\s+/g, '');
return t.toUpperCase() === letter || optText === tt || (tt.length >= 2 && (optText.includes(tt) || tt.includes(optText)));
});
if (hit && o.input && !o.input.checked) {
o.el.click();
clicked++;
}
});
log(`${clicked ? '✅' : '⚠️'} 作业第${i + 1}题 已处理 (${res.msg})`);
await new Promise(r => setTimeout(r, 1500));
}
}
function setupWorkWatcher() {
setInterval(() => {
if (!store.tasks.quiz) return;
if (/work|homework|quiz|test|作业|测验/.test(location.href)) {
runWorkAutoAnswer();
}
}, 5000);
}
/* ======================== 视频检测(含 Shadow DOM) ======================== */
function findVideosInShadow(root) {
const results = [];
try {
root.querySelectorAll('video').forEach(v => results.push(v));
root.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) {
el.shadowRoot.querySelectorAll('video').forEach(v => results.push(v));
findVideosInShadow(el.shadowRoot).forEach(v => results.push(v));
}
});
} catch (e) { /* ignore */ }
return results;
}
function scanForVideos() {
if (!store.tasks.video) return;
let videos = findVideosInShadow(document);
document.querySelectorAll('iframe').forEach((iframe) => {
try {
const iframeVideos = findVideosInShadow(iframe.contentDocument);
iframeVideos.forEach(v => { if (!videos.includes(v)) videos.push(v); });
} catch (e) { /* 跨域 */ }
});
if (videos.length > 0) {
videos.forEach(bindVideo);
if (!state.currentVideo || !document.contains(state.currentVideo)) {
state.currentVideo = videos[videos.length - 1];
}
updateUI();
}
}
/* ======================== SPA 路由监听 ======================== */
let lastUrl = location.href;
function watchRouteChange() {
const check = () => {
if (location.href !== lastUrl) {
lastUrl = location.href;
log('🔀 页面已切换,正在重新检测视频...');
state.currentVideo = null;
stopSpeedLock();
setTimeout(scanForVideos, 1500);
setTimeout(scanForVideos, 3000);
setTimeout(scanForVideos, 5000);
}
};
window.addEventListener('hashchange', check);
window.addEventListener('popstate', check);
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
setTimeout(check, 200);
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
setTimeout(check, 200);
};
}
/* ======================== MutationObserver ======================== */
function setupGlobalObserver() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.tagName === 'VIDEO') {
bindVideo(node);
} else if (node.querySelector) {
try {
const vids = node.querySelectorAll('video');
vids.forEach(v => { if (!v.__bound) bindVideo(v); });
} catch (e) { /* ignore */ }
}
}
}
});
const start = () => {
observer.observe(document.documentElement, { childList: true, subtree: true });
log('✅ 视频自动检测已开启,持续监测页面变化');
};
if (document.documentElement) start();
else document.addEventListener('DOMContentLoaded', start);
}
/* ======================== Page Visibility ======================== */
function setupVisibilityHandler() {
document.addEventListener('visibilitychange', () => {
if (!document.hidden && store.autoPlay && state.currentVideo) {
if (state.currentVideo.paused && !state.currentVideo.ended) {
log('🔄 回到页面了,恢复播放');
state.currentVideo.__userPaused = false;
state.currentVideo.muted = true;
state.currentVideo.play().catch(() => {});
}
}
});
}
/* ======================== 用户交互解锁 ======================== */
function setupUserInteractionUnlock() {
const unlock = () => {
if (store.autoPlay && state.currentVideo && state.currentVideo.paused) {
log('✅ 检测到你的操作,开始播放');
state.currentVideo.play().catch(() => {});
}
};
document.addEventListener('click', unlock, { once: false });
document.addEventListener('keydown', unlock, { once: false });
}
/* ======================== UI ======================== */
let panelEl = null;
let speedDisplayEl = null;
let statusEl = null;
let homeTitleEl = null;
let logListEl = null;
let recordListEl = null;
let activePage = 'home';
let loginStatusEl = null;
function switchPage(name) {
activePage = name;
document.querySelectorAll('.olvh-tab').forEach(t => t.classList.toggle('active', t.dataset.page === name));
document.querySelectorAll('.olvh-page').forEach(p => p.classList.toggle('active', p.id === 'olvh-page-' + name));
}
function renderLogPanel() {
if (!logListEl) return;
if (!logStore.list.length) {
logListEl.innerHTML = '暂无日志
';
return;
}
const html = logStore.list.slice().reverse().map(item => {
const t = item.time;
const ts = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}:${String(t.getSeconds()).padStart(2, '0')}`;
return `${ts}${safeText(item.msg)}
`;
}).join('');
logListEl.innerHTML = html;
logListEl.scrollTop = 0;
}
function renderRecordPanel() {
if (!recordListEl) return;
if (!records.length) {
recordListEl.innerHTML = '暂无学习记录
';
return;
}
const html = records.map(r => {
const badge = r.done
? '已完成'
: '学习中';
return `
${safeText(r.title)}
${badge} · 已学 ${formatDuration(r.watched)} · ${formatTime(r.lastSeen)}
`;
}).join('');
recordListEl.innerHTML = html;
}
function updateLoginStatus() {
if (!loginStatusEl) return;
if (store.account.scriptKey) {
loginStatusEl.innerHTML = `✅ 已登录:${safeText(store.account.username || '')}${formatQuotaText()}`;
} else if (store.account.token) {
loginStatusEl.innerHTML = `✅ 已登录(未同步密钥):${safeText(store.account.username || '')}`;
} else {
loginStatusEl.innerHTML = '❌ 未登录,请填写账号密码';
}
}
function createPanel() {
if (window.top !== window.self) return;
if (document.getElementById('olvh-panel')) return;
if (!document.body) {
document.addEventListener('DOMContentLoaded', createPanel);
return;
}
panelEl = document.createElement('div');
panelEl.id = 'olvh-panel';
panelEl.innerHTML = `
🔑 账号登录
登录后自动获取脚本密钥,用于云端AI答题。也支持用户名或邮箱登录。
📋 自动任务
进入考试页自动识别题目并填写答案
进入作业/测验页自动填写答案
自动播放、倍速、自动下一节
勾选的任务才会执行,未勾选则跳过。
视频倍数
${store.speed}x
${SPEED_PRESETS.map(s => `
${s}x
`).join('')}
`;
document.body.appendChild(panelEl);
speedDisplayEl = document.getElementById('olvh-speed-display');
statusEl = document.getElementById('olvh-status-text');
homeTitleEl = document.getElementById('olvh-home-title');
logListEl = document.getElementById('olvh-log-list');
recordListEl = document.getElementById('olvh-record-list');
loginStatusEl = document.getElementById('olvh-login-status');
bindUIEvents();
makeDraggable();
renderLogPanel();
renderRecordPanel();
updateLoginStatus();
}
function bindUIEvents() {
const toggleBtn = document.getElementById('olvh-toggle');
const body = document.getElementById('olvh-body');
toggleBtn.addEventListener('click', () => {
body.classList.toggle('collapsed');
toggleBtn.textContent = body.classList.contains('collapsed') ? '+' : '−';
});
document.querySelectorAll('.olvh-tab').forEach(tab => {
tab.addEventListener('click', () => switchPage(tab.dataset.page));
});
document.getElementById('olvh-speed-grid').addEventListener('click', (e) => {
const btn = e.target.closest('.olvh-speed-btn');
if (!btn) return;
setSpeed(parseFloat(btn.dataset.speed));
});
const customInput = document.getElementById('olvh-custom-input');
const customBtn = document.getElementById('olvh-custom-btn');
const applyCustom = () => {
const val = parseFloat(customInput.value);
if (val > 0 && val <= 16) setSpeed(val);
else { customInput.value = ''; customInput.placeholder = '0.1~16'; }
};
customBtn.addEventListener('click', applyCustom);
customInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') applyCustom(); });
// 登录
const loginBtn = document.getElementById('olvh-login-btn');
const userInput = document.getElementById('olvh-login-user');
const pwdInput = document.getElementById('olvh-login-pwd');
const doLogin = async () => {
const u = userInput.value.trim();
const p = pwdInput.value;
if (!u || !p) { loginStatusEl.textContent = '请填写用户名和密码'; return; }
loginBtn.disabled = true;
loginBtn.textContent = '登录中...';
const res = await backendLogin(u, p);
loginStatusEl.textContent = res.msg;
if (res.ok) {
userInput.value = '';
pwdInput.value = '';
document.getElementById('olvh-logout-btn').style.display = '';
updateLoginStatus();
log('🔑', res.msg);
refreshAccountBadge();
}
loginBtn.disabled = false;
loginBtn.textContent = '登录并同步密钥';
};
loginBtn.addEventListener('click', doLogin);
pwdInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
document.getElementById('olvh-usercenter-btn').addEventListener('click', () => {
window.open(CONFIG.backendBase + '/user', '_blank');
});
document.getElementById('olvh-logout-btn').addEventListener('click', () => {
store.account = { username: '', token: '', scriptKey: '' };
saveStore();
document.getElementById('olvh-logout-btn').style.display = 'none';
updateLoginStatus();
log('👋 已退出登录');
});
// 任务开关
const bindSwitch = (id, key, logText) => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('click', () => {
store.tasks[key] = !store.tasks[key];
el.classList.toggle('on', store.tasks[key]);
saveStore();
log(logText, store.tasks[key] ? '开启' : '关闭');
});
};
bindSwitch('olvh-task-exam', 'exam', '考试自动答题');
bindSwitch('olvh-task-quiz', 'quiz', '作业/测验自动答题');
bindSwitch('olvh-task-video', 'video', '视频自动处理');
document.getElementById('olvh-autoplay-switch').addEventListener('click', function () {
store.autoPlay = !store.autoPlay;
saveStore();
this.classList.toggle('on', store.autoPlay);
if (store.autoPlay && state.currentVideo) {
state.currentVideo.__userPaused = false;
autoPlayVideo(state.currentVideo);
}
log('自动播放:', store.autoPlay);
});
document.getElementById('olvh-mute-switch').addEventListener('click', function () {
store.autoMute = !store.autoMute;
saveStore();
this.classList.toggle('on', store.autoMute);
if (state.currentVideo) {
state.currentVideo.muted = store.autoMute;
state.currentVideo.volume = store.autoMute ? 0 : 1;
}
log('静音播放:', store.autoMute);
});
document.getElementById('olvh-autonext-switch').addEventListener('click', function () {
store.autoNext = !store.autoNext;
saveStore();
this.classList.toggle('on', store.autoNext);
log('自动下一节:', store.autoNext);
});
document.getElementById('olvh-autosubmit-switch').addEventListener('click', function () {
store.tasks.examAutoSubmit = !store.tasks.examAutoSubmit;
saveStore();
this.classList.toggle('on', store.tasks.examAutoSubmit);
log('考试自动交卷:', store.tasks.examAutoSubmit);
});
document.getElementById('olvh-log-clear').addEventListener('click', () => {
logStore.list = [];
renderLogPanel();
log('日志已清空');
});
document.getElementById('olvh-record-clear').addEventListener('click', () => {
records = [];
saveRecords();
renderRecordPanel();
log('学习记录已清空');
});
document.getElementById('olvh-force-play').addEventListener('click', () => {
log('手动触发强制播放');
if (state.currentVideo) {
const v = state.currentVideo;
v.__userPaused = false;
const wantSound = store.autoMute === false;
v.muted = !wantSound;
v.volume = wantSound ? 1 : 0;
const target = v.__vjsPlayer || v;
target.play().then(() => {
log('✅ 强制播放成功' + (wantSound ? '(带声)' : '(静音)'));
if (wantSound) setTimeout(() => { v.muted = false; v.volume = 1; }, 400);
}).catch(e => {
v.muted = true; v.volume = 0;
v.play().then(() => log('✅ 备用静音播放成功')).catch(err => log('强制播放失败:', err));
});
applySpeed(v, store.speed);
} else {
log('未检测到视频,重新扫描...');
scanForVideos();
}
});
}
function setSpeed(rate) {
store.speed = rate;
saveStore();
log('设置倍速:', rate);
if (state.currentVideo) {
applySpeed(state.currentVideo, rate);
}
updateUI();
}
function updateUI() {
if (!panelEl) return;
if (speedDisplayEl) speedDisplayEl.textContent = store.speed + 'x';
document.querySelectorAll('.olvh-speed-btn').forEach(btn => {
btn.classList.toggle('active', parseFloat(btn.dataset.speed) === store.speed);
});
if (statusEl) {
const dot = document.getElementById('olvh-status-dot');
if (state.currentVideo) {
const playing = !state.currentVideo.paused;
const rate = state.currentVideo.__realRate || store.speed;
statusEl.textContent = playing
? `播放中 · ${rate}x · ${Math.round(state.currentVideo.currentTime)}s`
: `已检测 · 暂停中 · ${rate}x`;
if (dot) dot.className = 'dot ' + (playing ? 'green' : 'red');
} else {
statusEl.textContent = '等待检测视频...';
if (dot) dot.className = 'dot gray';
}
}
if (homeTitleEl) {
if (state.currentVideo) {
homeTitleEl.textContent = `📺 ${document.title || '当前视频'}`;
} else {
homeTitleEl.textContent = '尚未检测到视频';
}
}
}
function startUIRefresh() {
setInterval(() => {
if (state.currentVideo) {
if (!state.currentVideo.paused && !state.currentVideo.ended && state.currentVideo.__record) {
state.currentVideo.__record.watched += 1;
state.currentVideo.__record.lastSeen = Date.now();
saveRecords();
}
updateUI();
}
}, 1000);
}
function makeDraggable() {
const header = document.getElementById('olvh-header');
const panel = document.getElementById('olvh-panel');
let isDragging = false, startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.id === 'olvh-toggle') return;
isDragging = true;
startX = e.clientX; startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left; startTop = rect.top;
panel.style.right = 'auto';
panel.style.left = startLeft + 'px';
panel.style.top = startTop + 'px';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panel.style.left = (startLeft + e.clientX - startX) + 'px';
panel.style.top = (startTop + e.clientY - startY) + 'px';
});
document.addEventListener('mouseup', () => { isDragging = false; });
}
/* ======================== 启动 ======================== */
function init() {
loadStore();
loadRecords();
log('景德镇艺术-学习助手 v4.1.0 启动');
setupExamWatcher();
setupWorkWatcher();
setupGlobalObserver();
const start = () => {
createPanel();
watchRouteChange();
setupVisibilityHandler();
setupUserInteractionUnlock();
startUIRefresh();
if (isExamPage()) {
log('📖 检测到考试页面');
if (store.tasks.exam) runExamAutoAnswer();
}
scanForVideos();
setTimeout(scanForVideos, 1500);
setTimeout(scanForVideos, 3000);
setTimeout(scanForVideos, 5000);
setTimeout(scanForVideos, 8000);
setInterval(scanForVideos, CONFIG.retryInterval);
};
if (document.body) start();
else document.addEventListener('DOMContentLoaded', start);
}
const __jdzInit = init;
init = function () {
try {
if (!store.account.token && !store.account.scriptKey) {
const raw = GM_getValue('localBackendLogin', '');
if (raw) {
const saved = JSON.parse(raw);
if (saved && saved.token) {
store.account.username = saved.username || '';
store.account.token = saved.token;
saveStore();
log('🔑 已同步学神助手登录态,可直接答题');
}
}
}
} catch (e) {}
__jdzInit();
};
init();
})();