ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Chrome扩展实战:Managebac成绩页自动计算IB加权总分

Chrome扩展实战:Managebac成绩页自动计算IB加权总分 简介这是一款面向 Managebac 用户的 Chrome 扩展程序可帮助学生在 opengate.managebac.com 上即时计算课程成绩免去手动估算的麻烦。压缩包共 14 个文件以 7 个 JavaScript 脚本为核心包含 content script、popup 逻辑及成绩计算模块同时附带 HTML 弹窗界面、3 个图标资源、2 个 JSON 配置文件manifest 与本地化和说明文档整体仅 56KB结构紧凑。对于学习 Chrome 扩展开发的读者源码清晰展示了 manifest 配置、页面内容脚本注入、jQuery 操作 DOM 以及跨文件通信等典型实现也可以直接加载已解压的扩展使用。已有 1451 人浏览/下载适合需要快速了解 Managebac 成绩计算方式或借鉴扩展开发思路的开发者。1. ManagebacGradeCalculator 是什么在成绩页上直接算加权的 IB 总分说实话第一次点开 Managebac 上的成绩单时我是有点懵的每个任务都写着得分和满分可就是看不到自己目前的 IB 预估分到底是多少。ManagebacGradeCalculator代号 abacus就是为这个痛点做的 Chrome 扩展程序。它在你打开 Managebac 成绩页时自动读取各考核任务的原始分按课程大纲里的评估权重实时换算成 1-7 分制 IB 成绩并把结果画回页面侧边。适合三类人天天刷总分的学生、需要快速模拟 IA 分数的老师以及想减少手工算分的家长。后面我会把评分结构、扩展最小实现、参数边界和常见踩坑逐一说清楚。2. 这个扩展原理是什么Managebac 成绩页上的一层“加权换算层”2.1 为什么 Managebac 的原生成绩页让总分变成黑匣子Managebac 的课程页通常会列出若干考核项例如 Paper 1、Paper 2、IA、单元测验每一项都有“得分/满分”和一个百分比。问题在于IB 学科最终给的是 1-7 分的等级分而等级分来自多个评估组件的加权合成不是所有任务分数的简单平均。你盯着页面上 87%、73%、91% 三列数字很难心算出一个可靠的 IB 预估值如果拿计算器把所有百分比加起来除以三又完全忽略了评估权重结果和老师手里的总分经常差一个大档。很多学生第一次自己估算总分时都会翻车原因就在这里Managebac 只呈现“任务级数据”不呈现“学科级评分模型”。比如有的学科 IA 占 20%Paper 1 占 30%Paper 2 占 30%Paper 3 占 20%这些信息躺在课程大纲 PDF 里不会自动映射到页面上的每个任务。扩展要做的事就是把这层隐藏的权重关系变成可执行的换算逻辑把散落的原始分收敛成一个可读的总分和换算等级。另外还要注意Managebac 页面是典型的动态渲染页面成绩区域经常是异步加载的。直接刷新后立刻抓 DOM很可能拿到一个空白容器。这也是为什么许多初版脚本会在“抓不到数据”上失败而不是在计算逻辑上失败。2.2 为什么做成 Chrome 扩展程序而不是控制台脚本、用户脚本或爬虫如果说只是算分打开浏览器开发者工具在 Console 里粘贴一段 JS 也能做到。但这种做法有三个问题每次打开成绩页都要重新粘贴、重新执行跨页面刷新没有任何持久化配置普通学生拿到一段代码也不知道往哪贴。用户脚本方案需要先装 Tampermonkey对非技术背景的使用者来说多一跳就多一次流失。独立爬虫方案就更重了。Managebac 一般走学校统一身份认证页面里很多数据要带登录态请求才能拿到为了算一个分数去维护 Cookie、会话和请求头属于典型的过度工程。Chrome 扩展里的 content script 跑在页面上下文同一进程能直接读渲染后的 DOM也天然继承了浏览器登录态。它不需要额外请求接口不会触发跨域限制也不需要用户理解“脚本注入”这个概念。还有一个现实原因是发布与更新成本。如果你只是给自己或同班同学用把扩展目录打个包让其他人按“开发者模式加载”即可。需要承认的是这种加载方式会出现浏览器的安全提示例如“该扩展程序未列在 chrome 应用商店中并可能是在您不知情的情况下添加的”。这个提示本身不是错误只是 Chrome 在提醒你确认来源具体处理我会在避坑章节展开。2.3 MV3 架构content script、storage 与页面渲染的分工Manifest V3 下一个最小可用的成绩计算扩展通常只有三个组成部分。content script 负责在 Managebac 页面里读取任务列表、执行加权计算、把结果渲染到页面上chrome.storage 负责保存权重模板、阈值配置和各学科的开关状态可选的 action 或 options 页负责让用户修改权重而不去改代码。这里有一个容易被新手带偏的架构判断计算逻辑放在 content script 里而不是放在 background service worker 里。原因在于DOM 读取和结果渲染都发生在页面上下文如果让 background 去计算交互过程会多一层 chrome.tabs.sendMessage 来回传递数据调试时还会遇到消息序列化问题。content script 可以直接拿到document.querySelectorAll的结果直接操作chrome.storage链路最短。要放在 background 里的只有那些需要跨页面保留的全局状态而成绩计算这类功能不需要跨页面状态。值得一提的是content script 虽然和页面共享 DOM但并不共享页面的 JavaScript 全局对象。你不能在 content script 里调用 Managebac 自己定义的某个内部函数最稳的交互边界就是“只读 DOM只写 DOM”不做任何页面内部 API 的假设。理解了这一点后面写抓取逻辑时就会自然避开很多玄学错误。3. 从零搭一个可变的 Abacus 扩展manifest、注入脚本与权重计算流程3.1 最小 manifest.json权限声明与 MV3 注意项先做一个能跑通的最小版本命名随意目录里放一个 manifest.json 和一个 content.js。填写内容如下{ manifest_version: 3, name: ManagebacGradeCalculator::abacus, version: 0.1.0, description: 在 Managebac 成绩页上按评估权重折算 IB 总分。, permissions: [storage], host_permissions: [https://*.managebac.com/*], content_scripts: [ { matches: [https://*.managebac.com/*], js: [content.js], run_at: document_idle } ], action: { default_title: Abacus } }这段配置只申请了storage权限没有申请tabs、scripting或activeTab目的是把权限面缩到最小。host_permissions里的https://*.managebac.com/*不是每个学校都一样有些学校使用自建域名或区域镜像你需要让学生打开成绩页看一下地址栏域名再改匹配规则。run_at: document_idle表示页面基本渲染完成后注入比document_start更稳妥。需要特别提醒MV3 的 manifest 是严格的 JSON 格式不允许注释多余逗号会直接导致加载失败。很多人说“Chrome 无法安装扩展程序”排查时先看chrome://extensions页面里有没有红色报错最常见的原因就是这里的 JSON 语法问题而不是权限或签名问题。3.2 抓取成绩数据用通用规则定位任务与原始分Managebac 的页面结构会随学校配置变化不同学科页的 DOM 也不一致。如果直接写死某个div.class-name换一个班级就可能失效。我更推荐的做法是不依赖单一选择器而是在页面里寻找所有“得分/满分”文本模式然后向上找到容器过滤出考核任务行。// content.js 片段从当前页面收集评估任务行 function collectAssessmentRows() { const result []; const seen new Set(); const scorePattern /(\d(?:\.\d)?)\s*\/\s*(\d(?:\.\d)?)/; // 覆盖表格单元格、分数卡片和带 score 标识的容器 const nodes document.querySelectorAll( td, div[class*score], span[class*grade], [data-testid*score] ); for (const node of nodes) { const text node.textContent || ; const match text.match(scorePattern); if (!match) continue; // 用父级文本作为任务标题但去掉“得分/满分”那一段 const parentText (node.parentElement node.parentElement.textContent) || ; const title parentText.replace(match[0], ).trim(); if (title.length 0) continue; // 向上找容器同一任务只保留一次 const container node.closest(tr, [class*task], [class*assessment]); if (!container || seen.has(container)) continue; seen.add(container); result.push({ title: title.slice(0, 80), raw: parseFloat(match[1]), max: parseFloat(match[2]), percent: parseFloat(match[1]) / parseFloat(match[2]) * 100, el: container }); } return result; }这段代码是典型的“宁可多匹配、后续再过滤”策略。先收集所有含“数字/数字”模式的文本节点再通过closest找到任务容器用Set去重避免同一个任务被单元格级别的多个节点重复收集。后期如果发现误匹配了日期或页码可以再按title里的关键词做一次白名单过滤比如只保留包含paper、exam、ia、test、quiz的行。这个选择器的好处是不依赖某个具体课程的 DOM 类名坏处是提取出来的title可能包含一些导航文案。我的习惯是把title.slice(0, 80)作为后续组件分类的输入分类时用正则匹配关键词而不是表白字符串这样即使文案是繁体中文或英文也能映射到正确的权重组件。3.3 计算加权成绩组件聚合、缺失权重归一化与页面渲染抓到的原始行需要归类到评估组件再按权重汇总。下面的mapTitleToComponent是分类函数calculateGrade是核心计算逻辑// 把任务标题映射到评估组件 function mapTitleToComponent(title, weights) { const t title.toLowerCase(); if (t.includes(paper 3) || t.includes(paper3)) return Paper3; if (t.includes(paper 2) || t.includes(paper2)) return Paper2; if (t.includes(paper 1) || t.includes(paper1)) return Paper1; if (t.includes(exam) !t.includes(paper)) return Paper1; if (t.includes(ia) || t.includes(internal) || t.includes(exploration)) return IA; return null; } // 按权重计算最终百分比并对缺失组件做归一化 function calculateGrade(rows, weights, cutoffs) { const bucket {}; for (const key of Object.keys(weights)) { bucket[key] { got: 0, possible: 0, count: 0 }; } for (const row of rows) { const component mapTitleToComponent(row.title, weights); if (!component || !bucket[component]) continue; bucket[component].got row.raw; bucket[component].possible row.max; bucket[component].count 1; } let weightedSum 0; let missingWeight 0; for (const key of Object.keys(weights)) { const b bucket[key]; if (b.count 0) { missingWeight weights[key]; continue; } const componentPercent (b.got / b.possible) * 100; weightedSum componentPercent * weights[key]; } // 归一化比如 IA 还没评分时剩余组件权重之和为 0.8就按 0.8 算修正百分 const usableWeight 1 - missingWeight; const adjustedPercent usableWeight 0 ? weightedSum / usableWeight : 0; return { percent: Math.round(adjustedPercent * 100) / 100, ibScore: scoreFromThreshold(adjustedPercent, cutoffs), missingWeight: Math.round(missingWeight * 100) / 100, detail: bucket }; }参数里weights是各组件权重例如{ IA: 0.2, Paper1: 0.3, Paper2: 0.3, Paper3: 0.2 }cutoffs是一个数组表示不同百分比对应的 IB 等级。计算时先把每个组件的原始分加总并折算成百分比再乘权重如果有组件完全没数据先记录missingWeight最后把加权结果除以1 - missingWeight这样不会因为还没评分就把总分严重低估。这里有一个容易被忽略的细节组件内部的多个任务应该做“加总原始分后算百分比”而不是“先算每个任务百分比再平均”。比如 Paper 1 有两次测验第一次 20/25第二次 30/40正确做法是(2030)/(2540)76.9%而不是(80%75%)/277.5%。两者的差异在分量值不对等时会被放大。渲染结果时我不会去改动 Managebac 原有的页面内容而是把计算结果插到成绩列表附近function ensureSummaryBox(anchor, data) { const existing document.getElementById(abacus-summary); if (existing) existing.remove(); const box document.createElement(div); box.id abacus-summary; box.style.cssText [ margin:12px 0, padding:10px 14px, background:#f5f7fa, border:1px solid #d9e0e7, border-radius:6px, font:14px/1.6 system-ui, color:#1f2937 ].join(;); box.textContent Abacus 估算${data.percent}% IB ${data.ibScore} 未计入权重 ${data.missingWeight}%; anchor.insertAdjacentElement(afterend, box); }用insertAdjacentElement锚定在成绩表后面而不是直接改表格能最大限度保留原始行为。这里有一个血泪经验扩展样式不要试图复用页面的 CSS 变量因为 Managebac 的样式表升级很快直接用内联style.cssText反而最可靠。3.4 持久化配置把权重模板和 IB 阈值写入 storage权重不要写死在 content script 里应该放进chrome.storage.sync让用户可以修改也方便跨设备同步。下面的loadConfig会先从 storage 读取读不到时回退到默认值const DEFAULT_WEIGHTS { IA: 0.2, Paper1: 0.3, Paper2: 0.3, Paper3: 0.2 }; const DEFAULT_CUTOFFS [ { score: 1, min: 0 }, { score: 2, min: 40 }, { score: 3, min: 52 }, { score: 4, min: 63 }, { score: 5, min: 74 }, { score: 6, min: 83 }, { score: 7, min: 91 } ]; async function loadConfig() { const defaults { weights: DEFAULT_WEIGHTS, cutoffs: DEFAULT_CUTOFFS, enabled: true }; try { const cfg await chrome.storage.sync.get(defaults); return cfg; } catch (err) { // storage 不可用时回退默认值不让扩展白屏 return defaults; } }chrome.storage.sync.get的用法要注意传入一个对象作为默认值时它只会对还没存储的键填充默认值已经存过的键会保留用户设置。这个特性正好满足我们的需求用户改过一个学科的权重后其他学科仍然走默认值。另外sync存储有容量限制单个配置键大约几千字节权重和阈值加起来很小不会超限如果你还要存很多日志就改用chrome.storage.local因为它会大一个数量级。默认的DEFAULT_CUTOFFS只是一个可改的初始参照不是官方标准。不同学校的评分细则不同扩展应该提供一个 options 页面或 popup 让用户修改这些数字。下面第 4 章会专门讲这些参数怎么设以及边界情况怎么处理。4. 参数怎么设权重、阈值与动态评分场景4.1 权重模板从 IB 常见评分模型到扩展里的默认值权重表是整个扩展的灵魂。它决定了一个 90 分的 Paper 2 和一个 70 分的 IA 对最终成绩的影响比例。我们在默认配置里采用了一种常见但不是所有学校都适用的 IB 学科模型评估组件默认权重说明IA / Internal Assessment20%通常包含实验报告、探索性任务Paper 130%无计算器的试题卷Paper 230%扩展题试卷Paper 320%针对 HL 课程的选项模块注意这个权重表对 SL 课程可能不成立因为 SL 往往没有 Paper 3有些学校的 IA 占 25%Paper 1 占 35%。所以扩展里不能把默认权重做成不可改的死数据。我会设计成 options 页提供“学科模板”选择用户按自己课程大纲调整后保存到chrome.storage.sync下一次自动读取。权重之外另一个常见问题是“任务没有先显示在页面上就开始了计算”。比如学期初 IA 还没发分bucket里 IA 的count为 0此时missingWeight会变大。如果目标是想看“当前已发任务下的预估分”归一化处理是对的如果目标是看“全部完成后的大概率区间”就要在 UI 上显示“未计入权重 20%”避免学生误以为当前百分比就是最终百分比。关于默认阈值我给出的是一个渐进区间40% 对应 IB 2 分52% 对应 3 分63% 对应 4 分以此类推。这个区间要结合学校 rubric 调整因为有些学校 7 分的门槛是 90%有些是 85%。如果扩展算出的 IB 等级和 Managebac 上老师的预估分长期对不上先别怀疑代码逻辑优先怀疑阈值配置。4.2 四舍五入与浮点误差别让显示值误导判断JavaScript 的浮点运算有个老问题0.1 0.2的结果不是0.3。在加权计算里这种误差会被放大最后显示成76.99999999999999%虽然对学生影响不大但看着就像 bug。我的处理约定是所有中间百分比保留到 2 位最终展示前再做一次归一化舍入。// 保留两位小数同时处理浮点尾差 function round2(value) { return Math.round((value Number.EPSILON) * 100) / 100; } // 在加权求和后使用 const finalPercent round2(weightedSum / usableWeight);为什么要加Number.EPSILON因为Math.round对于76.995这种值在某些浮点表示下会舍入到7699而不是7700加一个极小量可以把边界值拉回预期。另一个容易踩的坑是不要把round2用在单次任务百分比上再去加权应该先加权、后舍入否则每一步的舍入损失会叠加。这个壳在最终展示里也统一使用ibScore直接取整数percent用round2missingWeight也用round2。这种约定能保证 UI 上所有数字加起来和手算结果一致给用户一种“扩展可信”的感觉。不要小看这种观感问题很多用户会因为一个显示尾差就放弃整个工具。4.3 动态刷新与未评分任务MutationObserver 与防抖兜底Managebac 页面有两个让静态脚本失效的场景教师改分后页面无刷新更新、列表分页切换后 DOM 被替换。解决方式不是定时轮询而是MutationObserver监听成绩区域的变更并做防抖。let timer; function scheduleRecalc() { clearTimeout(timer); timer setTimeout(() { const cfg await loadConfig(); const rows collectAssessmentRows(); const data calculateGrade(rows, cfg.weights, cfg.cutoffs); const anchor document.querySelector(.score-table, [class*task-list], body); if (anchor) ensureSummaryBox(anchor, data); }, 300); } new MutationObserver(scheduleRecalc).observe(document.body, { childList: true, subtree: true, characterData: true });300ms的防抖足够应对异步渲染也不会在滚动时触发太多次重算。这里要注意content script里不能直接在顶层使用await以外的语法到回调里省略异步标记上面的setTimeout回调需要声明为async或在内部调用异步函数否则loadConfig的返回会变成 Promise后续计算拿不到配置。具体写法取决于你的模块封装原则是保证计算流程拿到的是解析后的配置对象。对于未评分任务除了missingWeight归一化我还建议在renderSummary里显示“包含未评分组件”的标记。因为当 IA 还没发分时一个学生可能只看到“当前折算 83%”却不知道这是基于已评分的 80% 权重算出来的。这个标记能避免很多不必要的咨询你只需在 UI 上多输出一行文本就把“为什么我的 IB 等级变化这么大”的疑问挡掉了。5. 避坑指南扩展装不上、抓不到数据、结果对不上的排查入口5.1 “该扩展程序未列在 Chrome 应用商店中”这条提示到底在说什么现象用户加载扩展后新标签页顶部出现“该扩展程序未列在 Chrome 应用商店中并可能是在您不知情的情况下添加的”的黄色提示部分人会误以为浏览器被劫持了。原因这条提示只表示扩展是通过“加载已解压的扩展程序”方式安装的不是从官方商店分发渠道安装的Chrome 对这一类扩展统一显示提示并不代表它有恶意行为。解决进入chrome://extensions查看扩展卡片右侧的“来源”字段如果确认是自己或同事发的在小范围内发放时接受这条提示即可如果担心被植入先移除扩展然后用本机安装包重新加载一遍。这里有一个习惯开发版扩展记得在name字段带(dev)前缀避免用户分不清哪个是自己装的。5.2 内容脚本不执行刷新页面也没反应现象扩展加载成功但 Managebac 页面没有任何新增渲染Console 里看 content script 没有报错似乎根本没执行。原因常见情况有两种一是matches没有命中当前登录域名二是页面是单页应用document_idle触发时成绩区域还没渲染而脚本只执行了一次。解决先在chrome://extensions里对扩展卡片打开“查看使用情况”或直接console.log(abacus loaded)确认注入如果确认注入但没抓到数据就引入MutationObserver监听整个document.body而不是依赖一次性执行。这条也解释了我前面为什么反复强调动态渲染初次调试时最容易觉得“扩展根本没加载”其实就是时机问题。5.3 计算结果和老师给的 IB 分对不上现象同一门学科扩展算出 85% 对应 IB 6 分Managebac 上的预估分是 5 分。原因大概率不是权重算错了而是两个地方用了不同的阈值口径。Managebac 上老师的预估分往往来自 IB 总体 rubric比如听力、口语、论文各一级指标先转 A-E再合成总分而扩展默认的cutoffs是全卷面百分比到 1-7 分的线性映射两者不可能处处相等。解决不要试图用扩展替代老师估算而是把扩展定位成“当前已获得任务分数的加权百分比快照”要核对时打开课程大纲 PDF把阈值表改成官方的分段值再比较一次。如果仍然对不上就打开detail字段查看每类组件的原始分和百分比重点检查mapTitleToComponent有没有把某次 Paper 1 测验错误归类到 IA。5.4 提示“清单无效”或“无法读取 manifest”JSON 语法与版本号问题现象把扩展目录拖入chrome://extensions页面直接出现红色错误“Manifest file is missing or unreadable”或者“无法安装此扩展程序因为它的 manifest 文件无效”。原因最常见的是 manifest.json 里写了注释或者最后一行的host_permissions末尾多了逗号其次是直接把 MV2 的browser_action搬到 MV3导致整个清单校验失败。解决用任意 JSON 校验工具过一遍文件内容确认没有//和多余逗号MV3 里按钮配置使用action不是browser_action文件保存成 UTF-8不要带 BOM。还有一个容易被忽略的点matches里的*通配符不能出现在路径部分所以https://*.managebac.com/*是合法的而https://managebac.com/*/*里第二个星号是多余的。5.5 同一页面多个学科区块互相串权重现象页面同时展示“数学 AA HL”和“数学 AI SL”两个区块扩展把 AI 的 IA 任务也算进了 AA 的bucket。原因collectAssessmentRows抓的是整个页面的文本节点没有限定到当前学科卡片两个课程区块同时渲染时所有任务行就被混在一个数组里了。解决在收集阶段定位“当前学科容器”做法是找到当前激活的 tab 或最近的一个包含“IB grade”的标题容器然后用这个容器作为querySelector的根节点。如果 Managebac 的结构不足以精确定位就在配置里增加一个“仅计算当前可见卡片”的开关用element.closest([class*course])的文本内容匹配学科名称兜住多区块场景。5.6 扩展在多人使用时配置被覆盖现象两个同班同学同时修改权重互相把对方的设置覆盖了。原因chrome.storage.sync是按 Chrome 账号同步的同一个账号下所有设备共享同一份配置多人共用账号时后写入的配置会覆盖先写入的。解决给配置加一个版本字段和更新时间戳写入前比较updatedAt只接受时间戳更新的配置或者干脆把“保存”按钮放在显眼位置并提示当前配置的修改时间。对个人使用来说这个坑不容易遇到但一旦遇到会因为“配置玄学消失”而非常折腾。6. 进阶用“快照 假设”把成绩计算器变成决策工具6.1 给扩展增加“如果这次 IA 换成另一档分数”的场景模拟基础版的扩展只能展示当前状态但真正对学生有用的功能是“假设”如果 IA 从 18/24 变成 22/24总分会提高多少如果 Paper 3 没考好能不能靠 Paper 2 拉回来这类问题靠心算很难但扩展做起来很简单只要在拿到原始行后先做一个快照再覆盖指定任务的raw重新走一遍计算。async function runHypothesis(overrides) { const cfg await loadConfig(); const rows collectAssessmentRows(); // 快照保留原始分仅替换用户指定的任务 const hypothesis rows.map(row { const changed overrides[row.title]; return { ...row, raw: changed ! undefined ? changed.raw : row.raw, max: changed ! undefined ? changed.max : row.max }; }); const data calculateGrade(hypothesis, cfg.weights, cfg.cutoffs); const diff data.percent - calculateGrade(rows, cfg.weights, cfg.cutoffs).percent; return { data, diff: round2(diff) }; }这段代码的价值不在于计算本身而在于它把“决策”前置到了界面。学生可以在 popup 里输入“IA 预计可得的原始分”扩展直接显示当前分和假设分的差老师也可以批量输入“全班同学 Paper 3 都按模拟分计算”快速判断最终 A 率会怎么变。实现时注意overrides的 key 用row.title并做去重因为同一个标题可能出现在多个任务里键冲突时优先保留用户最后输入的那个。6.2 用 Managebac 的导出数据做交叉核对而不是只信屏显我自己的习惯是扩展写好第一周不做任何推广先把扩展算出的加权百分比和 Managebac 导出的成绩单逐行比对。如果学校科目开启了 Gradebook 导出功能你会拿到一份包含每项任务得分、满分和百分比的表格把这些数据放进扩展的detail输出与导出文件逐项对照偏差超过 0.5 个百分点就说明选择器抓到重复行或漏了某个任务。具体做法是在控制台执行JSON.stringify(collectAssessmentRows())把结果与导出表格的行数、任务标题列表比对行数和标题一致再看计算字段。这个核对习惯能挡住绝大多数“看起来能用、实际算错”的发布事故。我做过太多工具最深的体会是计算逻辑本身很少出错出错的往往是数据口径不一致——比如导出文件里某个任务被老师临时改成“不计分”而扩展仍然把它按满分加权或者同一任务在页面上出现两次扩展去重逻辑没挡住。所以最后建议你保留一个 debug 开关按住右 Shift 双击扩展图标时在页面底部输出collectAssessmentRows的原始 JSON方便任何使用者主动核对。这是一个朴实但长期有用的设计方案。祝你的 Abacus 扩展能顺利跑起来希望帮到你。本文还有配套的精品资源点击获取
返回列表