本项目的 AI 工作卡。任何 AI 会话接手本项目,先读这里,再动代码。 规范见
C:\AI Document\AGENTS.md。
Windows 桌面工具。三件事:
- 输入答案 —— 左边一个大输入框写答案,切到学习通(超星)窗口后按一个热键, 程序用模拟键盘输入(SendInput)把答案逐字打进答题框,绕过学习通禁止复制粘贴的限制。
- 搜答案(v1.1.0 新增)—— 把题目贴进来,三路答案来源并发检索: 本地题库(离线)/ 网络检索(搜狗 → 360 → 必应)/ AI 解答(OpenAI 兼容接口), 结果按相关度排序,一键填进大框,再走上面那条输入链路。
- 富文本输入(v1.4.0 新增)—— 答案里用
$…$标数学公式、用 ``` 标代码块, 公式会被转成 Unicode 数学符号(√ ≤ π a² ⁻³ …)后逐字输入; 开editor档还会去点学习通自带的「公式」「代码」按钮,走它自己的排版能力。 没被标记的内容一个字都不动。
| 项 | 值 |
|---|---|
| 中文名 / 产品名 | 学习通答题助手 |
| 目录名 | projects/StudyAnswerHelper(与 GitHub 仓库名逐字一致) |
| 状态 | 可用 — v1.4.0 源码已推送(本轮未重打安装包,见 §8) |
| 最后更新 | 2026-10-08 |
| 项 | 值 |
|---|---|
| 运行时 | Electron 32.3.3 |
| 原生调用 | koffi ^3.3.1(SendInput / 全局热键 / 读前台窗口) |
| 打包 | electron-builder ^26.15.3 |
| 持久化 | 自研 JSON Store(src/main/lib/store.js)+ 自研题库(src/main/lib/answer-bank.js),零第三方依赖 |
| 联网 | 自研 HTTP 客户端(src/main/lib/http.js),基于 Node 内置 http/https/zlib |
| HTML 解析 | 自研正则 + 实体解码(src/main/lib/html.js),不引 DOM 库 |
| 界面 | 原生 HTML / CSS / JS,无框架、无 CDN、完全离线 |
| 运行时依赖总数 | 2 个(electron + koffi),其余全是自研 |
cd C:\AI Document\projects\StudyAnswerHelper
npm start # 开发态启动
npm start:safe # 受限环境启动(渲染进程被杀时用这个)
npm run icons # 重新生成图标
node tools/make-question-image.js # 生成示例题目图(assets/sample-question.png)
npm run build # 打包(输出目录见 package.json 的 build.directories.output)
npm run build:dir # 只生成 win-unpacked,不生成安装包校准学习通的公式/代码按钮名(v1.4.0 新增):
node tools/probe-uia.js --list # 列出当前可见的顶层窗口,找到学习通那个
node tools/probe-uia.js --match 学习通 # 按标题关键字直接定位并导出元素清单
node tools/probe-uia.js --hwnd <句柄> --json report.json # 导出完整清单到文件跑之前请把学习通答题页切到前台(浏览器里的学习通可以,桌面客户端不行,见 §6.8)。 把输出里「疑似公式/代码相关的元素」那些名字填进设置页「公式与代码输入 → 按钮名」即可。
打包时显式指定一个新的输出目录,否则会被本环境的删除钩子拦下:
node node_modules/electron-builder/out/cli/cli.js --win --x64 --config.directories.output=dist-installer-v5本机打包必须带 NODE_TLS_REJECT_UNAUTHORIZED=0(踩坑记录见 6.4-7):
env -u ELECTRON_RUN_AS_NODE NODE_TLS_REJECT_UNAUTHORIZED=0 \
./node_modules/.bin/electron-builder --win只跑 --dir 时不需要 winCodeSign,所以历史上 dist-installer-v1~v4 里只有 win-unpacked、没有安装包。
要真正产出 Setup/Portable,第一次必须让 winCodeSign 下载成功(约 2.5 MB,落到
%LOCALAPPDATA%\electron-builder\Cache\winCodeSign),之后离线也能打。
| 项 | 值 |
|---|---|
| GitHub 仓库 | https://github.com/1494948/StudyAnswerHelper |
| 分支 | main |
| 当前版本 | v1.3.0 |
| appId | com.xu.studyanswerhelper |
| 安装包命名 | StudyAnswerHelper-Setup-<版本>.exe(NSIS,中文安装界面) |
| 便携版命名 | StudyAnswerHelper-Portable-<版本>.exe |
| 归档位置 | C:\AI Document\releases\StudyAnswerHelper\v<版本>\ |
| 发布说明 | 归档目录下的 发布说明-复制到GitHub.txt,内容可直接贴到 GitHub Release 页面 |
- 产物约 75 MB/个。GitHub 单文件 >100 MB 会被拒绝,禁止提交进仓库。
.gitignore已排除node_modules/、dist-installer*/、release/、_*.txt。- 需要发布 Release 时:tag 用
git push推(走 git 协议,认证是通的), 附件由用户在网页拖拽上传,不要把账号凭据交给脚本。
StudyAnswerHelper/
├── src/
│ ├── main/
│ │ ├── main.js # 窗口 / 托盘 / IPC / 生命周期 / 搜答案 + 识别编排
│ │ ├── preload.js # contextBridge 白名单 API
│ │ ├── preload-capture.js # 截图遮罩窗口专用 preload
│ │ ├── engine/input-engine.js # 键盘注入引擎(被 fork,必须 asarUnpack)
│ │ └── lib/
│ │ ├── store.js # JSON 持久化(草稿/队列/历史/设置)
│ │ ├── http.js # 零依赖 HTTP(重定向/超时/gzip/大小上限)
│ │ ├── html.js # HTML 实体解码 / 去标签 / 属性提取
│ │ ├── textsim.js # 题目归一化 / 相似度 / 检索式构造 / 答案抽取
│ │ ├── answer-bank.js # 本地题库(匹配 / 增删 / 批量导入导出)
│ │ ├── answer-search.js # 三源检索编排 + 搜狗/360/必应解析 + AI 客户端
│ │ ├── ocr.js # 识别编排:AI 视觉(主备双模型)+ 系统 OCR 兜底
│ │ ├── ocr-win.ps1 # WinRT Windows.Media.Ocr 的桥(asarUnpack)
│ │ ├── subject.js # 学科识别 + 选择题解析(纯词法,零依赖、离线)
│ │ ├── option-click.js # 选择题点选编排(逐个字母、失败即停、如实报告)
│ │ ├── option-click-win.ps1 # UI Automation 查找并选中选项(asarUnpack)
│ │ ├── richinput.js # v1.4.0 富文本输入:分词 + LaTeX→Unicode(纯函数)
│ │ ├── rich-insert.js # v1.4.0 富输入编排:驱动自带公式/代码按钮 + 回退
│ │ ├── uia-tool.js # v1.4.0 通用 UIA:按名字找元素 / 枚举元素(校准用)
│ │ └── uia-tool-win.ps1 # v1.4.0 上者的 PowerShell 实现(asarUnpack)
│ └── renderer/
│ ├── index.html # 4 个标签页 + 题库弹窗
│ ├── capture.html # 全屏框选截图遮罩
│ ├── css/app.css
│ ├── css/capture.css
│ └── js/
│ ├── app.js
│ └── capture.js
├── tools/
│ ├── make-icons.js # 纯 Node 生成 PNG/ICO
│ ├── make-question-image.js # 纯 Node 生成"像照片"的示例题目图(离线测识别)
│ ├── click-target-win.ps1 # 带真实单选按钮的 WinForms 目标窗口(SP_CLICKTEST 用)
│ ├── rich-target-win.ps1 # 伪装成学习通答题框的窗口(SP_RICHTEST 用)
│ ├── probe-uia.js # v1.4.0 探测工具:导出某窗口的无障碍元素清单(校准按钮名)
│ ├── probe-koffi.js # koffi / FFI 假设验证探针
│ └── selftest-script.js # 注入渲染进程的自检脚本(SP_SELFTEST,50 项)
├── assets/ # icon.ico / tray.ico / png / sample-question.png
├── preview/ # 界面截图(含 dark/ 深色版)— 进仓库
├── release/ # 交付目录 — gitignore
└── dist-installer-vN/ # electron-builder 输出 — gitignore,可整目录删
界面结构:左侧主区(前台窗口条 + 答案大框 + 工具栏 + 输入按钮 + 自动模式开关),
右侧侧栏 400px,4 个标签页:答案队列 / 搜答案 / 历史 / 设置;
题库管理是独立弹窗(#bankModal),不在标签页里。
v1.2.0 起「搜答案」页顶部是识别条:截图选题 主按钮 + 重新识别 / 修正题目,
下面是只读题目框(内容只能来自识别,不再支持手动键入)。
截图遮罩是独立 BrowserWindow(无边框、setAlwaysOnTop(true,'screen-saver')),
不是主窗口里的一个层——所以它有自己的一套 html/css/js/preload,四个文件都别漏。
ELECTRON_RUN_AS_NODE必须清除。 本机 shell 预设了该变量,不清除时 electron.exe 会以纯 Node 模式启动,报Cannot read properties of undefined (reading 'requestSingleInstanceLock')。- 受限环境要加沙箱参数。 否则渲染进程被杀(
render-process-gone {"reason":"killed"})。 用npm run start:safe,参数--no-sandbox --disable-gpu --disable-software-rasterizer。 EBUSY: app.asar 正由另一进程使用。 构建前先结束残留进程:taskkill //F //IM electron.exe //T。- asarUnpack 三件套。 原生模块(
koffi、@koromix/**)、被fork的src/main/engine/**、assets/**都必须解包,否则打包版启动即崩。取路径用unpackAware()。src/main/lib/**不需要解包 —— 它们只被require,不原生、不 fork,require能直接读 asar。 - koffi 返回的句柄是 bigint。 直接丢进
process.send会因JSON.stringify(BigInt)抛异常; 若发送处写了空catch,消息会静默消失。读取处统一Number(),发送处做 JSON 安全兜底。 - 托盘常驻会抢走单实例锁。 测试模式必须在
app.requestSingleInstanceLock()之前 把userData切到临时目录。 - 常驻托盘要关后台节流。
webPreferences: { backgroundThrottling: false }, 否则窗口被遮挡时定时器降频到约 1 秒。自检里"等落盘"的断言一律用轮询等待。 - 管理员窗口收不到按键(UIPI)。 Windows 安全模型,不是 bug。
a+b=3和a-b=3会被文本相似度判成同一道题。 实测 bigram Dice = 0.867、数字序列完全一致,加权怎么调都压不到阈值以下(约 0.68)。 解法见textsim.js里的criticalMinorDiff():归一化后长度相同、差异仅 1~2 字符、 且差异落在运算符/数字上 → 直接判为不同题(分数压到 0.3)。 改动相似度逻辑后,务必回归tools/之外的那组用例(见第 8 节)。- 归一化不能删标点。
+ - = . / ( ) ² √ π在数学题里都是有效信息, 按普通文本预处理删掉标点会把a+b=3与a-b=3压成同一个串。 - 网页摘要不能整段比对。 题干十几字、摘要三四百字,整段比会被摊薄到 0.2 左右。
webScore()分别对"标题""摘要前 140 字""整段"各算一次取最大值。 - 搜狗会弹人机验证页且 Cookie 预热无效。 实测:取首页 Cookie 再检索,仍然拿到
antispider.min.js页面。判定方式是页面里出现antispider/verify.css/g-recaptcha等特征(looksBlocked())。对策是三引擎依次降级 + 该引擎冷却 3 分钟, 而不是重试。搜狗最快被限流,360 最抗压(且结果页带data-mdurl真实地址,不必解跳转), 必应对中文数学题基本无效(只留作最后兜底)。这两个引擎都不能删。 - 两个引擎的 HTML 结构差异大且会变。 搜狗:
<div class="vrwrap">分块,标题在h3, 摘要取.text-layout/.fz-mid,真实地址在尾部data-url。360:<li class="res-list">分块, 标题在h3.res-title,真实地址在a[data-mdurl],摘要取p.res-desc或span.res-list-summary。解析失败一律安静降级成"没有解析到结果",不要让异常冒到界面。 - 360 结果流里夹着"其他人还搜了"这类相关搜索块,没有真实链接。
parse360()里要求必须有 URL 才收,并显式过滤标题以"其他人还搜/相关搜索/大家还在搜"开头的块。 - 从网页摘要抽答案会抽出文档碎片(实测抽出过"向左平移个单位长度 4.(2009年高考辽宁卷改编)…")。
plausibleTextAnswer()做结构校验:长度 ≤ 30、不含句号分号、不含4.(2009类题号、 不含年份、不含半截括号。另外kind = 'answer'还要求相关度 ≥ 0.25(WEB_ANSWER_MIN_SCORE), 否则降级成线索 —— 不给它"填入大框"按钮。 - 搜题热键触发时主窗口必须在前面。 热键回调里先
showMain(),再setTimeout(180ms)去读剪贴板并检索,否则界面还在后台、用户看不到结果。 - AI Key 不能推给渲染层。
publicSettings()删掉aiApiKey,只暴露aiKeySet与aiKeyHint(尾号)。自检里有一条专门断言守着(往状态里塞一个明文 Key,断言JSON.stringify(state)里不含它)。改publicState()时别把这条破坏掉。 - AI 接口一律走 OpenAI 兼容
/chat/completions。 未配置 Key、地址非法、主机不存在、 密钥被拒,四种情况都要返回友好文案而不是抛异常(实测四种都覆盖了)。
- 搬动项目目录用
robocopy /MIR,不要用shutil.move(本环境删除钩子会拦下删除环节)。 - 不要对同一个文件并行发多个 Edit —— 本环境会出现"每条都报成功、但改动互相覆盖丢失", 批量改同一文件要串行或整体重写,改完落盘核对。
- 截图模式不要用真实数据。 早期
SP_SHOT会复制真实answer-data.json来生成preview/截图,会把用户的答案内容截进要提交进仓库的 PNG 里。现在改成seedShotData()现造演示数据, 所有测试模式启动时都清空两个数据文件。
- 系统自带 OCR 认不出数学公式,只能当兜底。 拿
assets/sample-question.png实测, WinRT OCR 把a²认成a2、把3²认成32、把减号−认成汉字一, 还整行丢掉了A.3 B.4那一行。用这种文本去搜题必然搜不到。 所以ocrEngine默认ai,界面在系统 OCR 结果上必须挂ocrQualityWarn()的黄色警告, 否则用户会以为"识别成功了"。 - PowerShell 5.1 不能对 WinRT 的
IAsyncOperation用.GetAwaiter(), 报无法对 System.__ComObject 调用方法。必须用AsTask反射桥接。 另外IAsyncOperation\1里的反引号在**双引号字符串**里是转义符,会被吃掉, 要用单引号字符串。这两点都写在ocr-win.ps1` 的注释里了。 - WinRT 的文件 API 不认正斜杠。
StorageFile.GetFileFromPathAsync传C:/a/b.png报"指定的路径无效",必须$Image.Replace('/','\')。 $res.Lines直接读.Count会得到空值(PowerShell 对 WinRT 集合不展开), 要写@($res.Lines).Count。RecognizedLanguage也可能为空,得留$usedTag兜底。- 遮罩窗口会被"工作区"限制住。 无边框窗口默认高度是屏幕减去任务栏(实测 1920×1040,
而屏幕是 1920×1080)。这会让底图被 CSS 拉伸,于是用户框的位置和实际截到的内容错位
(早期实测:框住题目,识别出的是任务栏的日期)。
两道保险:① 底图
#bg按屏幕真实像素 1:1 摆,绝不用百分比/CSS 拉伸; ②show()之后再setBounds()强行铺满整屏边界。 desktopCapturer的缩略图尺寸要在thumbnailSize里显式给屏幕尺寸, 否则拿到的可能不是 1:1;裁剪时按kx = thumb.width / display.bounds.width换算, 不要假设恒等于 1(多屏/缩放下会变)。- 裁剪内容要比对像素,但只能在"不放大"的前提下比。
ocrUpscale > 1时用的是高质量插值,放大图每个像素都是邻域混合值, 逐字节比对必然差几个色阶(实测[64,55,54]vs[70,60,59])。SP_CAPTURETEST里把ocrUpscale置 1 再比 16×16,才是"框哪裁哪"的硬证明。 captureCtx在识别收尾时会被置null。 自检里如果在那之后再读captureCtx.full,会抛Cannot read properties of null,被空catch吞掉后 表现为"校验字段莫名消失"(实测cropPixelMatch: undefined)。进入流程前先快照引用。document.activeElement在窗口没有系统焦点时不可靠。 状态推送回来时它可能是body,于是"用户正在输入的关键词"被判成"没在编辑",直接被覆盖清空。 统一改用syncTextInput(el, value, norm)+el.dataset.dirty脏标记。$是getElementById,不是querySelector。 回调里传'#ocrModelInp'会静默返回null,然后在.addEventListener上抛Cannot read properties of null。onTextChange()现在自己兼容两种写法。- 本地 electron 会拒绝对
--disable-gpu这类开关参数做文件名。tools/make-question-image.js早期用argv[argv.length-1]当输出路径, 结果生成了一个名叫--disable-gpu的 28 KB PNG。取参数要过滤掉-开头的开关。
- 本机直连 GitHub 会 SSL 失败(
unable to verify the first certificate)。 本环境有 HTTPS 代理(https_proxy=http://127.0.0.1:14068)做了 TLS 中间人。curl加-k能正常下载,说明内容完好、只是证书链不被信任。 所以electron-builder下载 winCodeSign 时必须带NODE_TLS_REJECT_UNAUTHORIZED=0。 --dir不会下载 winCodeSign,--win才会。 所以dist-installer-v1~v4里 只有win-unpacked,从没真正产出过安装包。首次真正打包需要联网下载 winCodeSign(约 2.5 MB),落到%LOCALAPPDATA%\electron-builder\Cache\winCodeSign后可离线复用。
- PowerShell 里
/* */不是注释。 我按 C/JS 习惯写了三行块注释,脚本直接报 「无法将"/*"项识别为 cmdlet…」。PS 5.1 只有#行注释。更麻烦的是:脚本一抛异常, 结果 JSON 就不生成,JS 侧只能报出无意义的no-result。所以脚本里加了全脚本trap, 把异常消息和行号写进结果文件 —— 否则只能靠一遍遍重新复现来定位。 (单个 Hashtable)[0]是按键取值,不是按下标。$byName | Sort-Object {...}在只有 1 个命中时返回的是 Hashtable 而不是数组,[0]会去找 key 为 0 的项 → 返回$null。 现象极具迷惑性:Test-LetterName明明返回了True(诊断字段里能看到), 但$pick永远是空,最终报"没找到该选项"。一律写@(...)[0]强制成数组。 同类:New-Object System.Drawing.Point(20, $y + 6)会被解析成 3 个参数, 必须写(20, ($y + 6))。- Chromium 系窗口的无障碍树是"按需构建"的,第一次查询可能什么都读不到。
实测本机沙箱下 Electron 窗口(
--no-sandbox --disable-gpu)对 UIA 只暴露 18 个名为Chrome Legacy Window的 Pane 桩节点,加--force-renderer-accessibility也一样。而真实 Edge 窗口第一次查询就返回 1138 个元素(含页面内容与窗口按钮)。 所以脚本里主动给窗口发WM_GETOBJECT(0x3D) 且lParam = UiaRootObjectId(-25)(用SendMessageTimeoutW+SMTO_ABORTIFHUNG,避免被卡住的目标阻塞), 再配合"重试 + 全量TrueCondition遍历"。这条要记住的是:目标程序读不到界面结构时, 要如实告诉用户"这个窗口没有暴露界面结构",而不是含糊地说"没找到选项"。 - 把子窗口也当作查找根。 内容树可能挂在子 HWND 上,只扫顶层 frame 会漏。
Get-Roots会枚举到 2 层子窗口,逐个尝试。 - 自检里的"重复内容"不能靠模糊匹配定位。 判定签名
sig一开始只含isChoice + 选项 + 答案字母,结果"两道都是 ABCD、答案都是 B"的题签名相同, 界面会把上一题的失败提示挂到这一题上。签名里必须带上题干指纹(textHash)。 - 大框内容走的是
app:setDraft,不是setDraftText。 前者原来直接写S.draft, 绕过了refreshChoice,于是"手动输入答案后,点选按钮仍显示没有可点选的答案"。 任何修改S.draft的入口都必须同步重算选择题判定。 - 被 spawn 的 .ps1 必须把 asar 路径改写成 asar.unpacked 路径。(本版最严重的一个坑)
lib/option-click.js一开始照着path.join(__dirname,'xxx.ps1')拼路径,前面的fs.existsSync检查能通过(Electron 的 fs 读得懂 asar 虚拟路径), 但把这个路径当-File参数交给脚本宿主时,宿主看到的是虚拟路径,直接报 「-File形式参数的实际参数 …app.asar\src\main\lib\option-click-win.ps1 不存在」。 结果是开发态一切正常、打包版点选完全不可用。lib/ocr.js里的psScriptPath()早就做了这个替换,新写的模块漏了。 规矩:凡是要交给外部进程执行的文件(.ps1 / .exe / .cmd),路径一律先做replace(/app\.asar([\\/])/,'app.asar.unpacked$1')并确认文件真的存在。 另外:tools/里的.ps1确实会进 asar 也会被asarUnpack解包(我一度以为没有), 判断这件事不要靠strings app.asar | grep,要用实测 —— 直接跑一次那个测试模式。v1.4.0 复查:新增
src/main/lib/uia-tool-win.ps1时又差点重演 —— JS 侧的scriptPath()做了 asar 改写,但package.json的asarUnpack名单 必须同步加一行,否则改写到app.asar.unpacked\...之后文件并不存在。 这是"改写 + 解包"两件事必须成对出现,漏掉任何一件都是打包版才炸。 - 断言不要写成"不是某个坏值就算过"。 自检里最初写的是
assert(code !== 'spawn-failed'),结果上层没有透传code,code是undefined, 断言恒真、什么都没验证。改成"必须是脚本自己产生的错误码白名单之一"之后, 立刻暴露了上面那个 asar 路径缺陷。凡是"排除法"断言,都要再问一句: 如果这个字段根本不存在,它会通过吗? - 演示预览要被动的状态推送覆盖。
previewAutoInput只是自检/截图用的演示, 但任何一次pushState都会按真实状态把它关掉,断言就会随机地"浮层没有显示"。 现在预览有 8 秒保护期,只有真正的autoinput事件能覆盖它。
- PowerShell 变量名不区分大小写 —— 一个局部
$req直接把参数-Req清空了。uia-tool-win.ps1里写了$req = $null(想当局部变量用),而参数就叫-Req, 于是脚本一进 try 就把参数置空,ReadAllText($Req)报「空路径名是非法的」, 外层只看到bad-request、完全没有指向性。 规矩:PowerShell 脚本里局部变量名不要和参数同名(哪怕大小写不同)。 ShowDialog()的窗口接受输入、正常绘制,但跨进程 UIA 查询拿到的后代数是 0。 这是本次最费时间的一个坑,也是经过受控实验确认的因果结论:把tools/rich-target-win.ps1在Show()+Application::Run()与ShowDialog()之间 来回切换、其余代码一字不改 —— 前者scanned=5且能点中按钮,后者恒为scanned=0。 现象极具迷惑性:窗口可见、手打字符能进去、AutomationElement.FromHandle也成功, 只有FindAll返回空,且不抛异常。 → 需要被 UIA 读取的测试窗口,一律用Show()+Application::Run()。FindAll(Descendants, TrueCondition)不是可靠路径。 本次把它换成了 option-click-win.ps1 早就验证过的"按 ControlType 逐个 PropertyCondition 查询", 并把 TrueCondition 降级成最后的兜底。打包时保留两者,是因为"按类型"是快路径而非保证 (见 6.6 第 37 条的同类经验)。- 学习通桌面客户端不暴露页面无障碍树,浏览器才暴露。 实测:客户端窗口
(
Chrome_WidgetWin_1,标题「学习通」)只能读到 3 个名为Chrome Legacy Window的空 Pane;同一个 Edge 窗口能读到 681 个控件、130 个有名字,连页面上的Copy code to clipboard这类按钮都在。 → 「自动点学习通自带公式/代码按钮」这条路只对浏览器里的学习通可行。 默认档位因此定为unicode(不碰学习通界面),editor档是可选增强。 - PS 5.1 里嵌套在 Hashtable 中的空数组会序列化成
{}而不是[]。patterns = @()经ConvertTo-Json出来是{},JS 侧.join()直接抛TypeError: join is not a function。改成返回斜杠连接的字符串,彻底避开这个歧义。 System.Windows.Automation.LegacyIAccessiblePattern在本机 PS 5.1 里不可解析。 引用它就报「找不到类型」,而脚本的trap会把整次扫描变成一个笼统的script-error。现在不再探测这个 pattern —— 它对我们的用途没有增量。- 代码围栏后面那枚换行不能被吃掉。 分词器最初把关闭围栏后的
\n一起跳过, 于是"代码块下面那一行"会和代码粘成一行(x=1完毕)。现在围栏只吃到关闭行的末尾, 换行留给后续文本片段。 - 本机桌面会被其他程序周期性抢焦点,UI 测试因此天然带随机性。
实测测试期间 WorkBuddy / msedge 反复激活自己,而焦点门控遇到"中途失焦"会
按设计停手,于是同一条链路有时通过、有时停在半路。
→
SP_RICHTEST的正常组允许最多 5 次重试;若用尽重试后所有失败都是focus-lost/focus-timeout,就报成 skip(跳过)并写明原因,而不是 "通过"、也不是"失败" —— 没验证到的东西不能给人虚假的覆盖感,但纯属桌面噪声的 东西也不该记成受测逻辑的缺陷。失败里只要出现别的错误码(例如not-found), 就一律按真实失败上报。实际尝试次数与环境噪声判定都会写进结果 JSON。
- 默认档位是
unicode,不是editor。 前者只改被标记过的内容、不碰学习通界面、 离线可验证;后者要依赖对方窗口暴露无障碍接口,失败模式更多。 - 任何一步失败都回退成纯文本,并且如实记账。 一份答案里可能有 5 个公式, 其中 1 个因工具栏被滚动出视口而点不到 —— 把整份答案丢掉,比"1 个降级、其余照常"糟糕得多。
- 绝不"自作主张"转换未标记的内容。
a^2在没被$…$包住时必须原样输入,1/2绝不能被偷改成½。这是产品红线。
更完整的坑清单与自动化验证方案见技能 electron-desktop-app。
set SP_SELFTEST=1 && npm start # 交互级自检 74 项(离线可复现,含富输入纯函数断言)
set SP_SMOKE=1 && npm start # 真实键盘注入冒烟
set SP_SHOT=1 && npm start # 视觉走查截图(7 个页面 × 2 套主题)
set SP_SEARCHTEST=1 && npm start # 真实联网检索链路(需要网络,结果只写日志)
set SP_CAPTURETEST=1 && npm start # 真实「框选截图 → 裁剪 → 识别」链路(本机会真开遮罩窗口)
set SP_CLICKTEST=1 && npm start # 真实「选择题自动点选」链路(会开一个带单选按钮的窗口)
set SP_FOCUSTEST=1 && npm start # v1.4.0 焦点门控:对照组必须复现"开头丢字",修复组必须一个字不丢
set SP_RICHTEST=1 && npm start # v1.4.0 富输入:对着模拟答题框真点「公式/代码/确定」按钮
node tools/make-question-image.js # 先造一张示例题目图,给 ocr:test 用SP_SEARCHTEST 支持环境变量覆盖:SP_SEARCH_Q / SP_SEARCH_ENGINE / SP_AI_KEY /
SP_AI_BASE / SP_AI_MODEL。
SP_CAPTURETEST 加 SP_CAPTURE_KEEP=<路径>.png 会把"遮罩画面"和"裁剪结果"各存一份,
用来人工核对"框的位置 vs 裁到的内容"。
注意:SP_SELFTEST 必须离线可复现,不要在自检里加任何联网断言;
也不要在自检里点 截图选题(会弹出全屏遮罩把后续断言全毁掉),
自动输入浮层只能通过 App.previewAutoInput() 走演示路径。
打包后也要对 win-unpacked\学习通答题助手.exe 和便携版各跑一遍
SP_SELFTEST / SP_SMOKE / SP_SEARCHTEST,确认 asar 封装没破坏相对路径
(识别链路特别依赖 ocr-win.ps1 能按相对路径读到,已进 asarUnpack)。
v1.1.0 验证结果(开发态与两个打包产物各跑一遍,结果一致):
SP_SELFTEST 32 项通过 / 0 项失败 / 0 个渲染层 JS 错误
SP_SMOKE 5 项通过 / 0 项失败,键盘注入 21 字逐字一致(450 ms)
SP_SEARCHTEST 本地题库命中 1.000 并自动填入大框;搜狗 9 条线索,头名相关度 0.905
全程 900 ms(开发态 747 ms);状态里无 API Key 泄漏
v1.2.0 验证结果(开发态):
SP_SELFTEST 39 项通过 / 0 项失败 / 0 个渲染层 JS 错误
SP_SMOKE 5 项通过 / 0 项失败,键盘注入 21 字逐字一致(459 ms)
SP_SHOT 6 个页面 × 2 套主题,含新增 main-autoinput.png
SP_CAPTURETEST 遮罩窗口 1920×1080(与屏幕一致)、底图 1:1 未被拉伸、
裁剪尺寸 960×324 == 选区换算值、
16×16 逐字节比对完全一致(cropPixelDiffAt = -1)、
识别出 132 字并落盘(questionPersisted = true)、
遮罩关闭、主窗口恢复
v1.3.0 验证结果:
开发态 / 打包版 win-unpacked / 便携版:SP_SELFTEST 均为 50 项通过 / 0 项失败 / 0 个 JS 错误
SP_SELFTEST 50 项通过 / 0 项失败 / 0 个渲染层 JS 错误
(新增 10 项:学科识别正确性/手动覆盖/非法值拒绝、
选择题选项与答案字母解析、多选 AC、纯数字答案禁用、
非选择题如实说明、无窗口时失败且不卡住按钮、两个开关落盘)
SP_SMOKE 5 项通过 / 0 项失败
SP_SHOT 7 个页面 × 2 套主题,含新增 main-choice-picked.png
SP_CLICKTEST 6 项通过 / 0 项失败 —— 这是本版最有价值的一条:
对着一个带真实单选按钮的 WinForms 窗口,
「按选项文字定位」→ 真的点中,并从目标窗口读回确认选中了 C;
「名称里没有选项字母」→ 拒绝点选,目标窗口确认没有被误选;
「选项数与实际可选控件数不符」→ 拒绝点选;
「无效句柄」→ 如实失败并给出可操作提示
v1.4.0 验证结果(开发态;本轮未重打安装包):
SP_SELFTEST 74 项通过 / 0 项失败 / 0 个渲染层 JS 错误
(新增 24 项:LaTeX→Unicode 的上标/下标/希腊字母/根号/求根公式/定积分/角度、
"货币 $ 不误判"、"\$ 转义"、代码围栏识别、
off/unicode/editor 三档行为差异、
"未标记文本一字不改"、按钮名解析、非法档位回落、
以及"读不到控件"的提示必须区分于"没找到按钮")
SP_SMOKE 5 项通过 / 0 项失败(键盘注入 21 字逐字一致,456ms)
SP_FOCUSTEST 4 项通过 / 0 项失败 —— 对照组复现"开头丢失"(目标 0/37、诱饵 37),
修复组目标完整拿到 37 字、诱饵 0 字;中途被抢焦点时在第 6 字停手且不漏字
SP_RICHTEST 9 项通过 / 0 项失败 / 0 项跳过 —— 这是本版最有价值的一条:
对一个模拟答题框窗口真点「公式」→ 弹窗输入 LaTeX → 点「确定」→ 落进答题框;
代码块同理;顺序保持"前缀 → 公式 → 中缀 → 代码 → 后缀"一个不丢;
把按钮名改成不存在的名字时必须**回退成纯文本**(写出 1/2)且不伪装成公式;
目标句柄无效时如实失败、不假报成功
(正常组第 4 次尝试才通过:本机桌面被其他程序反复抢焦点,属环境噪声,
见 §6.7 第 52 条;报告里会把尝试次数与"是否纯环境噪声"一并写出来)
SP_RICHTEST 的输出示例(模拟窗口回读的答题框内容,可见顺序与完整性):
开头这几个字要在 [FORMULA:\frac{1}{2}] 与
[CODE:
print("hi")
]
之间不能丢
SP_RICHTEST 的覆盖率说明(务必知情):它验证的是机制——按名字匹配控件、
调用 UIA 接口、焦点交接、顺序、回退。真实学习通是浏览器里的 UEditor 页面,
它的工具栏按钮无法在模拟窗口里 1:1 复现;"真实按钮叫什么名字"必须靠
tools/probe-uia.js 在真实环境里校准(本轮尝试校准但用户的学习通在桌面客户端里,
客户端不暴露页面结构,故默认档位保持 unicode,见 §6.7 第 48 条)。
打包版验证(--dir 产物,dist-installer-v9\win-unpacked):
SP_SELFTEST 74 项通过 / 0 项失败 / 0 个渲染层 JS 错误
SP_RICHTEST 8 项通过 / 0 项失败 / 0 项跳过(第 1 次尝试即通过)
另核对:resources/app.asar.unpacked/src/main/lib/uia-tool-win.ps1
resources/app.asar.unpacked/tools/rich-target-win.ps1
两份被 spawn 的脚本都确实解包到了,且打包版能按改写后的路径读到 ——
这是"脚本路径 asar 改写 + asarUnpack 名单必须成对出现"的实测证据(见 §6.6 第 42 条)。
本轮只做
--dir(不产出 Setup/Portable),目的是验证打包路径而不是发版; 产物目录dist-installer-v9属构建中间产物,按规范不长期保留。
SP_CLICKTEST 的适用范围(务必知情):它验证的是匹配与调用逻辑(找元素 → 选中 →
回读确认)。本机无法用它验证"真实浏览器页面里的选项" —— 原因见 6.6 第 37 条:
沙箱下的 Electron 窗口不暴露无障碍树,而代码里那条"序号兜底"路径需要真正的
RadioButton 类型控件才能触发,本机 WinForms 窗口的单选按钮被系统桥接成 Pane,
所以那条兜底路径在本机未被覆盖。它的作用是"当名称匹配失败、且可选控件数量恰好
等于选项数时,按序号点第 n 个",且数量不符时一律拒绝 —— 设计上偏向"宁可不点"。
坐标映射的自检要点(这是本版最容易悄悄坏掉的地方):
SP_CAPTURETEST 会在"关掉放大"的前提下,把裁剪图左上角 16×16 与整屏截图对应位置的
16×16 做逐字节比对。只要 DPI 换算、遮罩窗口尺寸、底图 CSS 尺寸任何一处错了,
这个断言立刻红。改截图相关代码后必须重跑它。
相似度回归用例(改 textsim.js 后必须手工跑一遍)——这些是离线纯函数,
可以直接写临时脚本 require 验证,不用起 Electron:
| 题目 A | 题目 B | 期望 |
|---|---|---|
已知a+b=3,ab=2,则a²+b²的值为( ) |
已知a+b=3,ab=2,则a²+b²的值为() |
= 1.0(全半角/标点差异视为同题) |
已知a+b=3,ab=2,则a²+b²的值为 |
已知a-b=3,ab=2,则a²+b²的值为 |
< 0.55(只差一个符号必须不同题) |
函数f(x)=x²-2x+3在区间[0,3]上的最大值是 |
同题但区间 [1,4] |
< 0.55 |
求圆的面积,半径r=5,π取3.14 |
完全不同的题目:求三角形周长 |
< 0.3 |
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-09-22 | 项目从 2026-09-21-23-21-04\StudyAnswerHelper\ 迁移到 projects\StudyAnswerHelper\ |
纳入 AI Document 目录规范。迁移后逐字节校验一致(8186 文件 / 955,452,999 B),git status 干净,HEAD 仍为 3348149,远端配置未变。未改动任何源码 |
| 2026-09-22 | 建立本文件 PROJECT.md | 补齐 AI 工作卡,记录本机环境坑 |
| 2026-09-22 | 分发包归档至 releases\StudyAnswerHelper\v1.0.0\ |
安装包 + 便携版 + 使用说明 + 发布说明 |
| 2026-09-23 | 删除项目内重复的 4 个 exe(release\ 与 dist-installer-v3\) |
执行规范「分发包只保留 releases/ 一份」。删除前已逐一比对 MD5,三处副本完全一致;保留的 releases\StudyAnswerHelper\v1.0.0\ 归档完整。释放 301.0 MB,工作台 804.8 MB → 503.7 MB |
| 2026-09-24 | v1.1.0:新增「搜答案」功能 | 新增 5 个主进程库(http / html / textsim / answer-bank / answer-search,全零依赖)+ 侧栏第 4 个标签页 + 题库管理弹窗 + 设置页两个分组;新增搜题热键 Ctrl+Alt+F(剪贴板取题)。三路答案来源并发:本地题库(离线模糊匹配)/ 网络检索(搜狗→360→必应,自动降级与冷却)/ AI 解答(OpenAI 兼容)。AI 密钥只留主进程。侧栏 344→400px,窗口 1000×700→1100×720 |
| 2026-09-24 | 修正相似度算法的危险误判 | 只差一个运算符的题目(a+b=3 vs a-b=3)原被判为 0.92 高相似,会串题。引入运算符特征 + criticalMinorDiff() 硬否决规则,实测降到 0.3。加了 8 条回归用例 |
| 2026-09-24 | 修正 SP_SHOT 会把用户真实答案截进公开截图 |
改为 seedShotData() 现造演示数据;所有测试模式启动时清空数据文件,保证自检可复现 |
| 2026-09-24 | 打包并归档 v1.1.0 | 输出目录 dist-installer-v4,构建退出码 0(未触发删除钩子)。产物归档到 releases\StudyAnswerHelper\v1.1.0\(MD5 与构建目录逐一比对一致)。开发态与两个打包产物各自跑过 SP_SELFTEST 32/0、SP_SMOKE 5/0、SP_SEARCHTEST 通过 |
| 2026-09-24 | 清理垃圾文件(经用户确认,工作台 1.06 GB → 666.0 MB) | 释放 418 MB:① cleanup.py --clean --aggressive 清掉 Claw/ 与 v1~v4 的旧构建输出,释放 266.8 MB;② 手工删除 dist-installer-v4 下与 v1.1.0 归档 MD5 完全一致的 2 个 exe 副本 + 构建元数据,以及项目内 release/ 下与 v1.0.0 归档 MD5 一致的 2 个旧 txt,释放 150.7 MB。残留 4 个 dist-installer-v*/win-unpacked 空壳(含被进程占用的 app.asar,错误码 32)删不掉,重启后重跑 cleanup.py 即可 |
| 2026-09-24 | 推送 v1.1.0 到 GitHub | 先 ls-remote 探明远端 main 停在 3348149(本地领先 3 个提交,可快进,未强推)。推送 main(3348149..3ca2260)+ 注解标签 v1.1.0,实测无认证弹窗。推后核验:远端 refs/heads/main == 本地 HEAD == 3ca2260,refs/tags/v1.1.0^{} == 3ca2260,git status -sb 显示正常 upstream。仓库体积 527 KiB / 37 个跟踪文件(exe 未进仓库,走 Releases 附件) |
| 2026-09-24 | v1.2.0:新增图片识别题目 + 框选截图 + 识别后自动输入 | 新增 src/main/lib/ocr.js(AI 视觉主备双模型 + 系统 OCR 兜底)与 ocr-win.ps1(WinRT Windows.Media.Ocr 的 PowerShell 桥,走 JSON 文件回传以绕开 GBK 控制台乱码);新增全屏框选遮罩窗口(capture.html / capture.css / capture.js / preload-capture.js,独立 BrowserWindow);新增截图热键 Ctrl+Alt+X、识别后 5 秒自动切窗输入(可调 3/5/8/12 秒、可整组关闭);tools/make-question-image.js 生成离线测识别用的示例题目图 |
| 2026-09-24 | v1.2.0:取消手动输入题目,题目框改为只读 | 题目框内容只由识别产生,默认 readonly;点「修正题目」解锁、改完「保存修正」。同时删掉 #qPasteBtn/#qClearBtn/#qUseDraftBtn 与相关逻辑 |
| 2026-09-24 | 修掉截图坐标错位(本版最危险的一个 bug) | 无边框遮罩窗口默认被"工作区"限制成 1920×1040(屏幕是 1920×1080),底图被 CSS 拉伸 → 用户框住题目却截到任务栏。改为底图按屏幕真实像素 1:1 摆 + show() 后 setBounds() 强制铺满。新增 SP_CAPTURETEST 做 16×16 逐字节像素比对守住这条(实测 cropPixelDiffAt = -1) |
| 2026-09-24 | 修掉状态推送会清空用户正在输入的识别配置 | 根因是 document.activeElement 在窗口没有系统焦点时不可靠,被判成"没在编辑"后被覆盖。统一改用 syncTextInput() + dataset.dirty 脏标记(与 v1.1.0 同类问题,这次抽成了工具函数) |
| 2026-09-24 | 修掉退出时最后一次改动静默丢失 | store 是 160ms 合并延迟写盘,app.on('before-quit') 里没有强制 flush,导致"刚识别完就关掉"丢题目。已在退出钩子里补 store.flush() + 题库 flush |
| 2026-09-24 | 打包并归档 v1.2.0 | 首次真正产出安装包:历史上 --dir 不下载 winCodeSign,所以 dist-installer-v1~v4 里只有 win-unpacked。本机代理做 TLS 中间人导致 unable to verify the first certificate,改用 curl -k 预取 winCodeSign-2.6.0.7z(sha256 与官方 cdaec715…16743a4 完全一致)放入缓存后,构建 1m40s 通过,退出码 0。产物 dist-installer-v5(Setup 79,057,649 B / Portable 78,922,985 B,均 MZ 头 + Nullsoft 特征),归档到 releases\StudyAnswerHelper\v1.2.0\(MD5 逐一比对一致)。开发态 + win-unpacked + 便携版三处各跑过 SP_SELFTEST 39/0、SP_SMOKE 5/0,win-unpacked 另跑通 SP_CAPTURETEST 全绿 |
| 2026-09-24 | 删除误生成的垃圾文件 --disable-gpu |
另一个模型在用 make-question-image.js 时把 electron 的开关参数当成了输出文件名,生成了一个 28,812 B 的 PNG。经用户确认删除;同时修掉 make-question-image.js 的 argv 解析(过滤 - 开头的开关) |
| 2026-09-25 | v1.3.0:学科识别 + 选择题自动点选 | 新增 lib/subject.js(10 个学科的纯词法打分识别 + 选择题/答案字母解析,离线零依赖)与 lib/option-click.js + option-click-win.ps1(走 Windows UI Automation 在学习通窗口里找并选中正确选项);AI 提示词由"你是数学老师"改为"全科答疑老师"+ 按学科追加作答规范;新增 SP_CLICKTEST 端到端测试与 tools/click-target-win.ps1 目标窗口 |
| 2026-09-25 | v1.3.0:界面新增学科条与选择题点选条 | 「搜答案」页在识别状态下方显示学科标签(含置信度与判断依据)+ 快捷切学科下拉;题目框下方新增「点选答案」按钮 + 选项字母高亮(正确答案高亮、其余淡化)+ 结果说明。设置页新增「学科与选择题点选」分组(学科覆盖、自动点选、点选后是否仍输入文本) |
| 2026-09-25 | 修掉 4 个会让新功能"看起来没生效"的缺陷 | ① app:setDraft 绕过 refreshChoice,手动输答案后仍显示"没有可点选的答案";② 题目变更时未重算选择题判定,会拿上一题的选项去点;③ 判定签名不含题干指纹,两道同选项同答案的题会串提示;④ 点选提前返回时不发 choice 事件,渲染层按钮永久卡在"点选中…" |
| 2026-09-25 | 点选实现方式的关键取舍(有实测依据) | ① 不用"记住选项坐标再点"——坐标随分辨率/缩放/滚动必然失效,UIA 拿的是语义元素;② 不用键盘发字母——多数答题页没把字母键绑成选中,盲发只会把字母打进输入框;③ Chromium 系窗口无障碍树按需构建,脚本主动发 WM_GETOBJECT+UiaRootObjectId 唤醒并重试;④ 选项数与实际可选控件数不符时拒绝点选,宁可点不中也不能点错 |
| 2026-09-25 | 修掉"打包版点选完全不可用"(本版最严重的缺陷) | lib/option-click.js 把 asar 内的路径直接当 -File 参数交给了脚本宿主。因为 Electron 的 fs 读得懂 asar 虚拟路径,前面的存在性检查能通过,所以开发态一切正常、打包版直接报「参数不存在」。加了与 lib/ocr.js 同样的 psScriptPath() 改写。这个缺陷是对打包版跑 SP_CLICKTEST 才抓出来的 —— 若只跑开发态,它会一路带进发布版 |
| 2026-09-25 | 修掉一条"恒真"的自检断言 | 原来写的是 assert(code !== 'spawn-failed'),但上层 clickAnswer/fail() 都没有透传 code,code 是 undefined,断言恒真。改成"必须是脚本产生的错误码白名单之一"后立刻暴露了上面那个 asar 路径缺陷。教训写进 6.6 第 43 条 |
| 2026-09-25 | 打包并归档 v1.3.0 | 输出目录 dist-installer-v8(本机安全删除钩子不允许复用已填充的输出目录,改用一个全新目录名;构建用 --config.electronDownload.isVerifyChecksum=false 绕过代理 502 导致的校验文件拉取失败)。产物 Setup 79,083,092 B / Portable 78,948,431 B,归档到 releases\StudyAnswerHelper\v1.3.0\(MD5 逐一比对一致)。三处产物各跑:SP_SELFTEST 50/0、SP_SMOKE 5/0,win-unpacked 另跑通 SP_CLICKTEST 6/0 |
| 2026-09-25 | 清理 v1.3.0 产生的垃圾(工作台 2.43 GB → 约 1.10 GB) | ① cleanup.py --clean --aggressive -y 清掉 v6/v7 的 win-unpacked 与 v1~v5 旧壳;② ctypes 删掉 v8 里与归档 MD5 一致的 2 个 exe + blockmap + latest.yml + builder-debug.yml,释放 150.8 MB;③ 清空 playground/v13(本次验证产物)与 2 个散落脚本,释放 0.21 MB |
| 2026-10-08 | v1.4.0(一):修掉"还没点学习通就在后台自动输入、前面答案没输进去" | 用户报的原文就是这个。根因是自动输入的老写法 engineFocus(目标窗口) + 固定 delay(420ms) + 立即逐字注入:Windows 的前台切换是异步的、还可能被前台锁定拒绝,420ms 不够时开头若干字符就落进了仍持有焦点的那个窗口。改为焦点门控:把期望窗口交给引擎,由引擎等它真的到前台再开打(FOCUSTEST 实测切窗+激活需 100~600ms),等不到就一个字都不发并如实提示;打字途中被抢焦点也停手,且失焦期间一个字都不发(早期"失焦累计 250ms 才判定"的写法实测会漏 2 个字到别的窗口)。新增 SP_FOCUSTEST,用两个真实窗口把"对照组必须复现缺陷 / 修复组必须一字不丢 / 句柄无效必须一字不发"钉死 |
| 2026-10-08 | v1.4.0(二):新增富文本输入(公式 / 特殊符号 / 代码) | 新增 lib/richinput.js(分词 + LaTeX→Unicode,纯函数、离线可测)、lib/rich-insert.js(编排:点学习通自带按钮 → 弹窗输入 → 确认,失败即回退并记账)、lib/uia-tool.js + lib/uia-tool-win.ps1(通用 UIA:按名字找元素 / 枚举元素)、tools/probe-uia.js(校准工具)、tools/rich-target-win.ps1(模拟答题框的目标窗口)。答案里用 $…$ 标公式、``` 标代码块;默认 unicode 档只改被标记的内容,`editor` 档才去点学习通自己的按钮。界面新增「插入公式 / 插入代码 / 看实际输入」与一行"其实会打进去的字"预览,设置页新增「公式与代码输入」分组(含可编辑的按钮名) |
| 2026-10-08 | v1.4.0:三条不许被"简化"掉的产品红线 | ① 默认档位是 unicode 而非 editor;② 任何一步失败都回退成纯文本并如实记账(一份答案里 1 个公式点不到,不该毁掉整份答案);③ 绝不转换未标记的内容(a^2 不许变成 a²、1/2 不许变成 ½) |
| 2026-10-08 | v1.4.0:修掉 9 个新踩的坑 | 见 §6.7。其中最有价值的一条:ShowDialog() 的窗口能被手打输入、能正常绘制,但跨进程 UIA 查询恒返回 0 个后代且不抛异常 —— 用受控实验(只切换 Show()/ShowDialog()、其余不动)确认为因果,scanned=5 → scanned=0 |
| 2026-10-08 | v1.4.0:尝试用真实学习通校准按钮名,结论是"客户端这条路不通" | 实测学习通桌面客户端窗口只暴露 3 个名为 Chrome Legacy Window 的空 Pane;同一台机器上的 Edge 窗口暴露 681 个控件、130 个有名字(连页面上的 Copy code to clipboard 都在)。所以 editor 档只对浏览器里的学习通可行,默认档位保持 unicode;校准办法(node tools/probe-uia.js --list → --hwnd)已写进 §3 与设置页提示 |
| 2026-10-08 | v1.4.0:源码已推送 GitHub(未重打安装包) | 本轮只推代码与标签,不产出 Setup/Portable(用户要求是"更新到 GitHub")。但为了验证"打包后 uia-tool-win.ps1 能被 spawn"这件只在打包版才炸的事,额外做了一次 --dir 构建(dist-installer-v9),并核对解包文件齐全、对打包版 exe 跑通 SP_SELFTEST 74/0 与 SP_RICHTEST 8/0。推送结果:refs/heads/main 与 refs/tags/v1.4.0^{} 均 == 本地 HEAD == 2dbe898(可快进,未强推) |
| 2026-10-08 | v1.4.0:清理本次构建产物(经用户确认,项目 614.9 MB → 347.6 MB) | python cleanup.py --clean --aggressive -y 删掉本次为验证打包路径而建的 dist-installer-v9\win-unpacked(154 个文件 / 267.2 MB),顺带清掉 dist-installer-v8 空壳与 Claw/。工作台 7.50 GB → 7.24 GB。残留 dist-installer-v9 一个 973 B 的目录壳(本环境常态,不反复重试) |
| 2026-09-26 | 推送 v1.3.0 到 GitHub —— 已完成 | 前一天失败是三层原因叠加,逐个解决:① TLS:schannel 报 CRYPT_E_NO_REVOCATION_CHECK(代理做中间人,吊销列表取不到)。http.schannelCheckRevoke=false 无效,换 http.sslBackend=openssl 报 unable to get local issuer certificate;真正管用的是 GIT_SSL_NO_VERIFY=true。② 凭据:报 could not read Username … terminal prompts disabled,但 git credential fill 明明能取到用户名与口令 —— 问题出在默认的 credential.helper=helper-selector 在 push 时不返回凭据,加上 -c credential.helper=manager 就通了。③ 邮箱隐私:远端拒绝 GH007: Your push would publish a private email address(3504821363@qq.com 被账号设为私密;之前的提交是在该设置生效前推上去的)。解法是不动用户的隐私设置,改用 GitHub 的 noreply 地址:把本仓库(不动全局)的 user.email 设为 66010812+1494948@users.noreply.github.com(id+login 形式,由 api.github.com/users/1494948 取到),用 git rebase origin/main --exec "git commit --amend --no-edit --reset-author" 重写这 2 个尚未推送的提交,重建标签后推送。远端核对:refs/heads/main 与 refs/tags/v1.3.0 的哈希与本地一致 |
| 2026-09-26 | 创建 GitHub Release v1.3.0 并上传附件 | 本机没装 gh,改用凭据管理器里的凭据直调 API:git credential fill 取出凭据写入 curl --config 的头部文件(全程不打印任何密钥值),再调 api.github.com 建 Release(id 397017263)与 uploads.github.com 传附件。发布正文从 releases/StudyAnswerHelper/v1.3.0/发布说明-复制到GitHub.txt 里两条分隔线之间提取,不用手抄 |
| 2026-09-24 | 推送 v1.2.0 到 GitHub | ls-remote 探明远端 main 停在 89bedf1 == 本地 HEAD,可快进,未强推。提交 9c83bfd(29 文件 / +2977 / −245),推送 main(89bedf1..9c83bfd)+ 注解标签 v1.2.0,无认证弹窗。推后核验:远端 refs/heads/main == refs/tags/v1.2.0^{} == 本地 HEAD == 9c83bfd。仓库 47 个跟踪文件 / .git 2.9 MB(exe 未进仓库,走 Releases 附件) |
| 2026-09-24 | 清理 v1.2.0 产生的垃圾(工作台 1.21 GB → 817.2 MB) | 释放约 418 MB:① cleanup.py --clean --aggressive -y 清掉 v1~v4 旧构建壳与 v5 的 win-unpacked,v5 从 417.9 MB 降到 327 KB;② 用 ctypes DeleteFileW 删掉 v5 里与 releases\v1.2.0\ MD5 完全一致的 2 个 exe + blockmap + latest.yml + builder-debug.yml,释放 150.7 MB;③ 清空 playground/ocr-capture(本次验证日志/截图/脚本)与 playground/search-answer-feature(v1.1.0 遗留),释放 4.68 MB。残留 908.1 KB 是 5 个旧 win-unpacked 空壳(含被进程占用的 app.asar,错误码 32),按规范不反复重试,重启后重跑 cleanup.py 即可 |