Skip to content

ltool v1.4.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 17:21
· 27 commits to main since this release

🚀 ltool v1.4.0 发布说明 / Release Notes

🎉 欢迎使用 ltool 1.4.0!本次共 19 笔改动,重点是 闸自己踩红过一次、推 v1.4.0 之前先给发版说明加一道脱敏、把「runner 上每条旗标量到几个」数出来并钉住。下面每一节是一笔提交,原因和实测数字都在里面。


🌟 本次改动

🐛 1. 闸自己踩红过一次:判据的图案要和脱敏的图案是同一颗

  • 刚加的那道「发版说明脱敏闸」在 main 上就红了(run 39641227629 第 6 步):
  • NOTES_CHECK 6 条全过,而 leak check 报 带家目录的行=1,残留 C:\Users\<用户名>\AppData\Roaming\ltool。
  • 红的原因不是漏了真用户名,是我自己这两颗图案不一致:
  • scrub() 认的是"家目录 + 一段 [A-Za-z0-9._-]+ 的用户名";
  • leak check 里另写了一条更宽的(C:\Users\ 后面接任意非空白)。
  • 于是 e6128e8 那笔提交正文里为了说明这件事而写的占位符 C:\Users\<用户名>\...
  • 脱敏不动它(本来就不该动),却被闸当命中拦下 —— 一条合法的说明文字把每推都跑的闸踩红。
  • 改法:leak check 直接 import make_release_notes.USER_HOME_RE,判据和脱敏共用一颗图案
  • (这仓库的规矩:同一个判断不许有第二份答案)。闸判的应该是"有一个真用户名活着出来了",
  • 不是"出现了 C:\Users 这几个字"。
  • 两边都验过:
  • 真区间 v1.3.3..HEAD 180 行 → 命中 0(占位符那条不再红);
  • 拿一份临时文件塞三行(合法路径 / 占位符 / 真用户名 zzrealname)→ 只点名为 1 条、退出码 1。

📝 2. 推 v1.4.0 之前先给发版说明加一道脱敏:家目录 + 用户名不进公开 Release 页

  • CI 的发布步骤用 tools/make_release_notes.py 把 v1.3.3..v1.4.0 每笔提交的正文原样拼进 Release 页,
  • 而真历史里就有一句带用户机的路径(9313288「数据目录搬到软件目录下」的正文:
  • 「旧目录 C:\Users<用户名>\AppData\Roaming\ltool 还是 378 MB 原样在那儿」)。
  • 同一串字符在 git log 里是一行历史,印在发版封面上是另一回事 —— 这一区间数过就 1 处。
  • scrub() 只吃"家目录 + 用户名"那一段(反斜杠、正斜杠两种写法都认,后者是 Qt 归一化后的形状), 换成 %USERPROFILE%;D:\software\ltool、%LOCALAPPDATA%\Programs\ltool 这类文档要的路径一个字不动。 接在 commits() 的标题和 body_lines() 的每一项上。
  • 两道闸放在每推都跑那一栏,不是只在发版那一次(只在推 tag 才量的话,漏出去的那版已经发出去了): make_release_notes.py --check(6 条)+ tools/notes_history_leak_check.py(真区间 17 笔 / 171 行 → 命中 0; 发布步骤再拿将要发出去的那份 release-notes.md 量一遍)。
  • 反向验过:scrub 改成原样返回 → --check 当场红 4 条;往临时文件塞一句 %USERPROFILE%\AppData → leak check 退出码 1 并点名那一行。判据脚本里不写死任何用户名,只按形状判(那文件自己要进公开仓库)。
  • 顺手留 build/verify/yaml_check.js:这台机的 msys python 没有 pip,yml 用 $TEMP/jsy 那份 js-yaml 校; 现在 1 个 job / 24 步,新那一步排在配色闸后面。

📦 3. 把「runner 上每条旗标量到几个」数出来并钉住

  • run 39636325299(= bbd6265,终态 success)按旗标分节数出来的:
  • --pathcfg 40 / 本机 37 过 + 1 跳 —— 差的正是冷启动子进程 + 交接那三条:本机有常驻实例,
  • 那种子进程会把启动器叫到屏幕上所以点名跳过;runner 上它们真跑了,
  • 凭据是日志里那行 INFO 冷启动子进程 verdict=alive ms=6005。
  • --pinyin 本机 68 / runner 60:少的 8 条是按环境点名跳过(这台机器的 webs.json 没配 gg、
  • 索引里没有 Antigravity / QQ / 中文名应用 / ChatGPT 那颗),而这一节的"跳过"是 INFO 开头
  • 不是 SKIP 开头 —— 只 grep ^SKIP 就会把"少 8 条"读成"莫名少了"。
  • 顺手留了工具 build/verify/ci_log_pathcfg.py:从 Actions 的日志 zip 里按旗标分节数
  • PASS / FAIL / SKIP / INFO,认 --cfgcheck 的 ok 行首(不认就会报 PASS=0,看着像一面空旗标),
  • 也认 INFO 跳过 那种点名。--smoke 那 0 条判据是本来如此(它是诊断,CI 那道闸判"有输出且退出码 0")。

📦 4. 迁移脚本把「补过来的图有没有被历史引用」自己说出来:那 4 个是孤儿,不是没搬干净

  • dry 复测同一台机器:扫 992 / 缺 4 / 都有 988;那 4 个(两张图+缩略图)现在这份 clipboard.json 一条都不引用,程序按天清理又把它们收走了(日志两条 swept 4 orphan,正好在 -Apply 之后)。不说清就会让人以为迁移没做完。脚本的复制语义没动(老目录里有而新目录里缺的一律补,字节不同的一个字不碰)。

📦 5. 把"往还没搬的目录里播默认值"那一手连根拔掉 + 一次性迁移脚本

  • 上一条修的是后果,这条找的是"谁会写"。答案在代码里不是推测:
  • 沙箱名单里没有 --plugintest,而 ensureDefaultWebs() 对所有路径都要跑 ——
  • deliver 的 installed 那一栏跑的正是 装好的 exe --plugintest,那趟把 2 条的默认
  • webs.json 播进了还没搬的数据目录,于是"同名不覆盖"把你那份 24 条挡在老目录里。
  • --plugintest 进沙箱名单(先实测:数据目录指到临时目录时它仍然 250 过 / 0 红, 那 19 颗 dll 从 exe 目录\ltool-plugins 装,跟数据目录不是一条线)。
  • ensureDefaultWebs() / DevEnv::ensureDefaultPlugins() 先问 Utils::legacyMigrationPending(): 还没接住真数据的目录一个字节默认值都不写。判断只有一份,拆成纯函数 migrationPendingFor(legacy, now) 让判据能拿两个临时目录直接问(--pathcfg 5 条), 另加一条钉"沙箱设着 LTOOL_DATA_DIR 时 pending 必须是 false"——少它任何离屏夹具 都会被自己的守卫挡住、默认表播不进去。
  • 搬过之后那趟补一次 backfillFromLegacy:只把现在这份 clipboard.json 引用着、而本地缺的 剪贴板图从老目录补回来(按文件名认,不按记着的那串路径)。有一条本来要加、最后决定不加: "目标等于内置默认就拿老目录那份换掉"也搬进这一趟 —— 依据是本机 plugins\java.json 那两份 (老 538 / 新 379)逐字节 diff 之后差的只有程序自带默认文字自己的改版措辞, "等于这一版的默认"推不出"老目录那份是用户数据",它可能是上一版的默认,换过去是把新文字退回旧的。 1.4.0 还没发 ⇒ 没有需要救的现场,要救的已经在搬家之前堵住。
  • 一次性脚本 build/verify/migrate_ltool_data.ps1:老目录有而新目录缺的按 sha256 补进去, 两边都有但字节不同的一个字都不碰、只列出来;默认 dry run,-Apply 才写盘; 落点从 Inno 卸载键 InstallLocation 推。纯 ASCII(.ps1 放中文又没 BOM 这仓库炸过)。 本机 dry 跑:扫 992 / 缺 4 / 都有 988(同 981、不同 7);-Apply 补齐那 4 个、失败 0。
  • 判据 --pathcfg 26 → 37 过 + 1 跳。反向验过:摘掉补图那步的调用处,红的正是最后 4 条 (got=[0] want=[2]、got=[n,n]),前面 33 条全绿 —— 又是"helper 绿了而漏调用处"那个形状。 如实点名一处量不到:那两句守卫本身没判据(离屏全设着 LTOOL_DATA_DIR = pending 必然 false, 不带它的进程会往 build\Release\data 落文件被安装包收走),兜住它的是另外三层。
  • 顺手(上一条留下的):deliver 的 relaunch 这一栏这次第一次读对了文件(\data\log.txt)。
  • 全套本机离屏 0 红(plugintest 250 / cfgcheck 28 / cliptest 88+1 / startuptest 55 / pathcfg 37+1),
  • 四道闸绿(core_abi_gate 动布局的头 0 个 ⇒ 接口号 4 不升)。
  • 交付 install MATCH 9F2B7ACB7854、19 颗对齐、装好的那份 --plugintest 250/0、relaunch pid=25060。
  • 版本:core 不升号(仍未发布的 1.4.0 这批,定性是"把搬家做对"不是新功能);插件号不动。

📦 6. 搬家那条"同名不覆盖"被程序自己写的默认值满足了:网站关键字 24 条被默认 2 条挡在老目录

  • 他报「网站关键字直达只有这么点数据了」。量出来:新目录 webs.json 345 B / 2 条,
  • 老目录 4286 B / 24 条 —— 不是没搬,是搬的时候目标里已经有同名文件(deliver 的 installed
  • 那一栏先拿装好的 exe 跑 --plugintest,那一趟在还没搬的目录里写下了默认表),
  • "已有同名不覆盖"就把它跳过了;而 .migrated-from-appdata 照写,第二次启动也不再搬 = 永久性。
  • 改:搬家复制之前,先把目标里那些"内容逐字节等于内置默认"的文件请走
  • (Utils::clearStockDefaultsForMigration,判"什么算默认"只有一份答案 = 再算一遍 stockWebsJson())。
  • 判据 --pathcfg 加 8 条:3 条问 helper、5 条拿两个临时目录跑整条链
  • (新拆的 Utils::migrateBetween —— 漏调用处才是这次的形状,反向验过:删掉那一步红的正是链那 3 条,
  • helper 那 3 条全绿)。中途我先写过一条"按源码顺序闸住"的判据,用的 SRC_ROOT 这个宏仓库里根本没有,
  • 作废换成真的那条。
  • 复测同一份真数据时挖出同家的第二半:clipboard.json 记的是绝对路径,搬家后 39 条图条目里
  • 37 条还指着 %APPDATA%\ltool\clipimg,而 load() 那句"图不在了就不留空条目"会把它们连条目一起丢
  • (日志里 sweep 已经两次把新目录里那 78 个副本当孤儿收走)。改:resolveImagePath 先按文件名在
  • 现在这个 clipimg 认一次(文件名本来就是内容的 sha1),认不到才原样返回;--cliptest 加 6 条,
  • 其中 2 条真改写 clipboard.json 再 load() 一遍(反向验过:换回原样抄路径,红的正是那 2 条)。
  • 他的数据也修好了:78 个副本按 sha256 补回新目录,装上新版重启后 39 条 path 全变成新目录。
  • 顺手:deliver 的 relaunch 那一栏一直读 %APPDATA% 那份老日志(1.4.0 之后日志在 \data),
  • 报的是上一版留下的那条 hide —— 改成先读现在这份,并把读的是哪一份打进那一栏。
  • 全套判据本机 0 红(cliptest 88+1 跳、pathcfg 26+1 跳、plugintest 250),四道闸绿
  • (新加的都是自由/静态纯函数,不动布局 ⇒ 接口号 4 不升)。core 不升号:1.4.0 还没发,
  • 这是这批里的一处数据丢失修复;clipboard 插件号不动(只改了它的判据文件)。

✅ 7. CI 那圈补上 --pathcfg:这次那三条"冷启动/交接"判据本来一次都没被执行过

  • 拿 run 39607595058 的日志复核 --startuptest 时发现的:那一圈里没有 --pathcfg。
  • 于是新加的三条(无旗标子进程不许崩 + 只连不写的连接不许叫出窗口 + 真写 show 的要照常叫)
  • 本机因为有常驻实例全部点名 SKIP、CI 又根本不跑 —— 等于没执行过就进仓库。
  • 加进去之后那一步是 16 面旗标;yml 拿 js-yaml 过了一遍(1 个 job,不是 0 个 job 的那种"没过校验")。
  • 同一份日志里读到的:--startuptest 在 runner 上 54 过 / 0 红 / 1 跳,跳的是 hklm_refusal_reason
  • (runner 提权,HKLM 真写成功了,判据按设计删掉那个键并说明"正常权限下才有得测");扫描 51 行 / 577 ms。

🐛 8. 剪贴板:点一张卡就复制(clipboard 1.1.3,1.4.0 同批)

  • 原来是双击才复制,他说"点击剪贴板卡片就是复制",所以接 itemClicked,并把 itemDoubleClicked 那根
  • 同时摘掉 —— 不是留着兼容:单击已经是复制,双击的第一下就完成了,再挂一条等于同一件事两条通道,
  • 还会让一次操作写两遍剪贴板、多两行日志。下面那行说明跟着改成「点一张卡 = 放回剪贴板」。
  • 判据这次换了量法:以前是自己调 cs.restore(...) 再比剪贴板,那量的只是 core 那颗函数,
  • 页面把信号接回双击照样绿。现在送一对真鼠标按下 + 抬到那张卡的矩形上(发给列表的 viewport),
  • 走的才是它自己那根 itemClicked。配两条护栏:点之前先把剪贴板清成别的值(不然上一段留下的同一个
  • 字符串会让这条白绿 —— 反向验时红出来的正是"占位还在"),以及一条"那块卡片矩形量得到"的前置判据。
  • 再加一条连点两张不同的卡:第一下之后整墙会重建,重建后信号接不回来就只复制得动第一次。
  • 反向验:把 itemClicked 改回 itemDoubleClicked 重编 → 单击那两条当场红,其余全绿。
  • 真剪贴板那一档顺手复测了"自我那次写不把历史顺序搅动"(2026-10-16 那次闪退的形状),仍然绿。
  • 数字:--cliptest 离屏 82 过 + 1 点名跳过(offscreen 的剪贴板是内存桩),QT_QPA_PLATFORM=windows 87 过 0 红。
  • 装的那份复验:--plugintest 250/0、--cliptest 0 红、19 颗里只换了 clipboard 这一颗。
  • 版本:clipboard 1.1.2 → 1.1.3;core 不升号(还是未发布的 1.4.0 这批)。

🐛 9. 开机启动分析能关掉应用自启 + 修掉那个"双击图标必崩"(startup 1.1.0,1.4.0 同批)

  • 两件事,一个是他要的功能,一个是我上一轮带进去的崩溃。
    1. 开机启动分析这一页从"只读"变成能动一件:把某一条应用启动项开/关。 走的就是任务管理器 · 启动 那同一个存储(StartupApproved{Run|StartupFolder} 下的 REG_BINARY), 不动 Run 值本身,所以随时反得回来;服务 / 计划任务 / 加载项一律拒绝并说清为什么。 顺带修了一个方向反了的读法:原注释写"0x03=启用"、代码照它写,界面于是把开着的说成关掉的。 本机拿真注册表对回来(ltool/utools = 02 00…、Steam/NeatDM/Cisco/Edge = 03 + FILETIME): 首字节 0x02=开着、0x03=被关掉、没有这个值=从没碰过=开着。写回去只改首字节、长度照原来那份。 系统服务默认不列(他点名),但藏着多少条必须在汇总、分组标题和那颗开关三处说实话。 确认走"就地两段",不弹模态框。StartupItem 那两颗新字段放在结构体最后,老字段偏移一个没动。 新判据 --startuptest 55 条(真写走自造的临时键 HKCU\Software\ltool-startuptest-,跑完删掉; 页面那半只点第一下,并用三个 StartupApproved 键的整棵快照逐字节证明第一下一个字节都没写)。 反向验:把 02/03 认反 → 当场红 9 条。
    1. 崩溃:双击图标 / 开机自启那次启动必崩(c0000005,Qt6Core.dll+0x1489c9),而所有离线判据全绿。 根因是我 16:17 那笔搬家守卫:std::none_of(app.arguments().cbegin() + 1, app.arguments().cend(), ...)。 QCoreApplication::arguments() 在 Qt 6.10 按值返回 QStringList,那两个迭代器来自两个临时容器, 区间头尾不在同一份数据上;没有旗标时头指针正好越过末尾,于是解引用别人家的堆。 带任意 -- 旗标反而不炸(predicate 第一个元素就命中)—— 而每一条自测都带旗标,所以 250 条绿灯照绿。 修:拿一份局部 QStringList 再用它的首尾。 补判据 3 条(--pathcfg 末尾):起一个"一个参数都不给"的子进程 6 秒内不许崩; 只连上不写字节的连接不许被当成"把启动窗口叫出来";真写了 show 的还得照常叫。 反向验:改回坏形状 → 冷启动那条当场红 crashed-0xc0000005(26 ms)。 同一族里我自己造过一次打扰:那三条第一版用 QLocalSocket 连管道问"有没有实例", 而服务端把"只连不写"也当成交接 ⇒ 21:15 把他那份启动器叫出来了(日志留着那行)。 已换成 WaitNamedPipeW(不建立连接)+ 服务端忽略空消息;改完复跑,他日志里不再多一行。

✨ 10. JSON 格式化那页改成「左边输入、右边看效果」(json 1.0.2)

  • 他这句是「json 格式化可以直接在左边的区域输入内容,右边是看效果」。
  • 以前 QSplitter 里是树在左、文本框在右 —— 要粘 JSON 得先把光标挪到右半边,
  • 而左边那块先被解析结果占住。改成文本框在左、树在右,两边 1:1。
  • 实测(900 宽的页):输入框 x=10 宽=467 · 结果树 x=481 宽=409。
  • 判据在 --plugintest 加 3 条,量的是真实控件的几何(不是"splitter 第 0 个是谁"那种自己造的断言): 输入框 x 小于树 / 输入框 !isReadOnly() / 两边各拿到不少于页宽 1/4 的宽度。
  • 两个自己踩的坑:① 隐藏着的控件不跑布局,第一次量到"输入框 48px、树 48px"——那是 minimumSizeHint 不是布局,判据必须先 show() + resize() + 跑一轮事件循环再量;② 阈值别写死像素, 离屏那个虚拟屏比真机窄,写死 >=300 会被环境先夹掉,改成按 页宽/4 算。
  • 版本:json 插件 1.0.1→1.0.2(插件源码改了,plugin_version_gate.sh 那道闸要它自己递增, 现在 NEEDS_BUMP=0);core 不升号 —— 跟着还没发的 1.4.0 同一批。 deliver 九栏全绿(plugins 那一栏:18 颗本来就是这份哈希、换成新的 1 颗 = 只有 json)、 装好的 exe 跑 --plugintest 250 过 0 红。

📦 11. 数据目录搬到软件目录下:日志/配置/索引/剪贴板图/OCR 模型都不再压 C 盘(1.4.0 同批)

  • 他这句是「所有的日志和数据目录都放在软件目录下,不要放在 C 盘」。改的是一颗函数:
  • Utils::dataDir()(88 处调用点都问它),三档优先抽成纯函数 resolveDataDir:
  • ① LTOOL_DATA_DIR(自测沙箱 / 便携模式)→ ② <exe>\data → ③ 只读位置才退回 %APPDATA%\ltool 并留一条日志。
  • 搬家一次性:老目录还在 + 新目录里没有真配置 → 整棵树复制,每个文件按 sha256 对过, 对不上的删半成品;旧目录一个字都不动,搬完留一颗 .migrated-from-appdata 标记。
  • 本机实测:983 个文件 / 360 MB,旧目录 %USERPROFILE%\AppData\Roaming\ltool 还是 378 MB 原样在那儿。 耗时约 9 秒(16:12:49→16:12:58),排在托盘和窗口之前 —— 升级后第一次启动会慢这 9 秒,只有一次。
  • 判据:--pathcfg 11 → 18 条(三档优先 + "这一趟 dataDir() 真等于沙箱" + 搬家五条含"目标已有同名文件不覆盖")。
  • 踩到并修掉的三个(都是判据/守卫的坑,不是业务代码): ① 守卫第一版写"新目录非空就别搬" —— dataDir() 一被问到就 mkpath,Utils::log() 立刻写下 log.txt, 而 deliver 的 installed 那一栏先拿装好的 exe 跑 --plugintest ⇒ 新目录已有日志 ⇒ relaunch 那次判定"搬过了",378 MB 一个字节没动,界面和日志都没说。改成只认真信号, 并且每个提前返回都把"为什么没搬"写进 note(data: 没搬:…)。 ② "不覆盖"那条第一版白绿:夹具里源目录根本没有 log.txt,跳过分支一次都没走到。 ③ 另一条红是我连着 new 了两个 QFile,第二个没 open 就 readAll。
  • 自测沙箱从挪 APPDATA 改成挪 LTOOL_DATA_DIR(数据默认已在软件目录下,挪 APPDATA 管不着了); ltool.iss 的 Excludes 加 data(否则构建目录里那份 data\ 会跟安装包出去 —— 和当年 captures\ 同一族)。
  • 版本:不升号 —— 跟着还没发的 1.4.0 同一批。deliver 九栏全绿、装好的 exe 247 过 0 红。

📦 12. 搜索结果改序:没命中直达时应用第一、网站第二(1.4.0 同批)

  • 他这句是「搜索框把应用放在第一个,网站放在第二个候选」。动手前先问清一件事:工具直达要不要让位 ——
  • 他选"命中直达时直达仍第一"(回车还是它,那是 10-19 定的)。所以新规矩一句判据:
  • toolFirst = 命中工具直达 && 网站关键字没赢 → 直达第 0 行;否则应用节最前、网站组第二。
  • 发射顺序从「电源 → 工具(精确+松散) → 网站命中 → 应用 → 松散工具 → 文件 → 引擎兜底」 改成「电源 → [工具] → 应用 → 网站(命中 / 网址直开 / 引擎兜底) → [松散工具] → 文件」。 引擎兜底那条从列表最末尾搬进网站这一组 —— 它本来就是"网站"性质,甩在最后等于网站排到最底。
  • 顺手补一颗表头:网站那一组以前没有表头,那几行直接跟在上一节表头底下(搜 b64 就挂在"工具"下面), 看着像工具的结果。现在叫「── 网站 ──」。
  • 结构动了一处:工具命中改成只算不发(toolExact),应用匹配(ms)搬到发射点之前算 —— "谁排第一"要同时知道直达命中没有和应用命中没有,在发射点之后算就来不及了。 命中直达但网站关键字赢时那几条精确工具依旧整节不发(原有行为,没顺手改)。
  • 判据:--plugintest 242 → 247,数的是行号不是"在不在"(两条都在但顺序反了,正是这次要改的东西)。 反向验:toolFirst 硬写 false → 当场红 5 条(含两条老判据)。 实测行号:renwu 应用 0 / 网站 3;b64 工具 0(本机没有名字含 b64 的应用 ⇒ 应用节 -1 行, 所以那条钉的是"直达在第 0 行"而不是"直达在应用之前",后者在 0 个应用上会空转); windows 工具 0 / 应用 2 / 网站 11 —— 这一条同时钉住"直达拿第一行"和"不许铲平应用"。
  • 常用面板(空查询)没动,它一直是 应用 → 网站 → 工具 → 文件 → 系统;电源直达保留在最前 (shutdown 让位给应用的话,回车可能变成启动一个名字像 shutdown 的应用)。
  • 版本:不升号 —— 1.4.0 那一批里的一处排序改动,1.4.0 本身还没发。 deliver 九栏全绿、装好的 exe 247 过 0 红。

📝 13. 发布说明那句"本次重点":标题以版本号开头时要往后挪一段

  • 本机 dry-run v1.0.3 第一次就撞上:那笔标题写的是「1.0.3:检查更新不再打 api.github.com…」,
  • 按冒号切第一段拿到的就是 1.0.3 —— 概述成了"重点是 1.0.3",等于什么都没说。
  • 改成取第一个"不短于 6 个字符、且不是纯版本号"的短句(太短、[\dvx.]+ 全数字的都跳过),
  • 全跳过了就退回整条标题。现在那句是「重点是 检查更新不再打 api.github.com(匿名 60 …」。

📝 14. Release 说明改成自己生成(照 mateo-ki/vpn-parser v1.8.9 那份的形状)

  • 他给了那条 Release 当样板。以前 CI 用 gh release create --generate-notes,GitHub 拼出来的是
  • 裸提交标题列表 —— 而这个仓库的提交信息本来就写着原因和实测数字(一笔动辄几十行),全被丢了。
  • 新增 tools/make_release_notes.py(读 git log + installer/ + build 产物,一次网都不上): 概述 → 一笔提交一节(图标按标题关键词挑:🐛 修 / 🧩 插件商店 / 🎨 界面 / ⚡ 性能 / ✅ 判据 / 📝 文档 / ✨ 新增) → 资产表(文件名 / 大小 / sha256 前 8 位 / 这颗是干什么的,19 颗 dll 逐颗带自己的版本号) → Full Changelog 的 compare 链接。本机对 v1.0.3 跑出来 23 个资产 · 3276 字符(样板那份 3285 字符)。
  • 顺带产出 installer/sha256.txt:说明里写了"下 sha256.txt 对整批",那它必须真在资产里 —— CI 收尾多核对一步:sha256.txt 和 plugins-index.json 这两个名字确实在 Release 资产里,不在就红。
  • 三个自己踩的坑写进注释:① "上一个标签"要用 git describe <标签>^ 问,不是拿 tag --sort=-creatordate 取第一个不等于自己的(后者给 v1.0.3 找出 v1.3.3,区间反了 ⇒ 0 笔提交, 脚本因此非 0 退出、不发空说明);② 资产表要按这一版的版本号过滤文件名,本机 installer\ 里躺着 十几个历史 setup,全收就成"46 个资产",而 CI 那棵目录是干净的 ⇒ 这类错只会在本机复跑时露出来; ③ 提交正文里的缩行续行要并回上一条 bullet,否则一行被拆成两条。
  • workflow YAML 拿 js-yaml 校过(jobs=windows · 23 步)。版本不升号 —— 只动 CI 与工具,不进任何产物字节。

🐛 15. 搜索框隐藏时那一下卡顿:宽限期把"真点走了"当成"激活竞争"吞了(1.4.0 同批)

  • 现场证据先拿到再说修法:log.txt 里 show: 失焦落在宽限里,复查后收起 今天 35 次,
  • 最近两次 14:07:23 / 14:07:26 就夹在他两次按热键中间(14:07:22 呼出 → 14:07:23 才收)。
  • 那条日志打的是复查定时器触发那一刻,所以等待是 600ms 起步;加上"呼出后 500ms 内的失焦一律忽略",
  • 最长能挂 ~1.1 秒 —— 他看到的就是"我点开了别的东西,它还挂在那儿"。
  • 根因是判据只问了一件事:「离上次呼出够不够 500ms」。它想保的是 #40 那一支 (SetForegroundWindow 抢来的前台被 Windows 撤回去,那一下失焦不是用户点走了, 当场 hide 就变成"按了没出来")—— 但它把两种失焦当成了一种: ① 激活被撤回(前台回到这次呼出之前那个窗口)② 用户点了别的东西(前台去了别的窗口)。
  • 拆成纯函数 SearchWindow::blurAction(拿到过前台, 回到原主, 距呼出ms, 菜单开着, 截图进行中), 四支各有名字:立刻收 / 当竞争稍后复查 / 等截图结束 / 菜单开着不动。 两颗新输入都是本来就有的状态:m_hadForeground(WindowActivate 置真、每次呼出复位)、 m_prevFg(呼出前的前台窗口,Ctrl+Enter 粘贴早就在用它)。 宽限期内只有「没拿到过前台」或「前台回到原主」才继续等,其余当场收。
  • 判据:--toggle 6 → 13 条 PASS。离屏造不出真失焦那两支(那个平台抢不到前台, "在前台时再按=收起"一直是 SKIP),所以这七条问的是谓词本身;其中一条专门钉 "这两支必须给不同答案,否则『拿到过前台』这个输入等于没读"。
  • 每次失焦现在写一行 hide: 失焦 → 哪一支(拿到过前台=? 距呼出=?ms); 下次还说慢就 grep 'hide: 失焦' log.txt,能分清是没收到事件还是走了复查那一支。
  • 版本:不升号 —— 修的是可感知的迟钝,跟着还没发的 1.4.0 同一批。 deliver 九栏全绿 MATCH 6115896F65CE、装好的 exe 242/0;--plugintest/--cfgcheck/--about/--rowmenu 全 0 红。

📦 16. 「搜索框为什么会莫名其妙出现」:三条路能把弹它出来,以前只有一条留日志

  • 他问的是"为什么",先数入口再回答:grep showLauncher 数出来五处调用、三条真实路径 ——
  • 热键、托盘图标左键单击(含右键菜单那颗)、以及第二个实例把 "show" 转交给常驻实例。
  • 其中只有热键那条留日志,另外两条一个字都不写 —— 所以这件事在现场本来就查不到。
  • showLauncher(const char* why):五个调用点各自报上自己那一句,收口打 show: 呼出(要求者=…)。下次它自己出来,grep 要求者 log.txt 一眼就知道是谁。
  • 日常两个嫌疑按这条日志能分开:① 托盘单击就是呼出(设计如此,手滑一次=弹一次); ② 任何不带 -- 参数再启动一次 ltool.exe(双击 exe、快捷方式、任务栏那颗、开机自启重复拉起)。 右键那几项不走这条:注册表里实测是 "...ltool.exe" --compress-7z "%1",带杠就绕开单实例守卫。
  • 排除了预热那一支:它 move(-32000,-32000) + 透明度 0 映射一遍再收起,可那条日志排在 move(saved) 之后才读 x()/y() —— 打的是恢复完的落点,本机一直显示 (0,0 660x100), 看着像"Qt 把它夹回屏幕里"。改成量"停在预热位置那一刻"的矩形并判它压不压在显示器上, 实测 (-32000,-32000 660x100) · 在所有显示器之外 ⇒ 不是它。那条日志自己就是"读上一步留下的状态"。
  • 顺手扶正 --toggle:确实造出了「最小化」这个状态 在 offscreen 上必然红(没有真 HWND, SW_MINIMIZE 空操作)。拿装好的那份 A/B 过 —— HEAD 上就红,不是这轮带的;而 --toggle 不在 CI 那 14 面里所以没人看见(同 #94 那一族)。改成点名 SKIP:现在 失败 0 项 + 2 条 SKIP。
  • 版本:不升号 —— 加的是日志与判据形状,行为没变(showLauncher 多一个参数是内部签名, 不在插件公开面上;core_abi_gate GATE_HITS=0)。跟着还没发的 1.4.0 同一批。 deliver 九栏全绿、装好的 exe 242/0;--plugintest/--storetest/--cfgcheck/--about/--rowmenu/--smoke 全 0 红。

🐛 17. 他问「测了吗」,去测内存和 Win10:量出来一个会被永久卡死的闸(1.4.0 同批)

  • 补测不是走个过场 —— 第一节就把上一轮搬 installEntry 时埋的洞量出来了。
  • 新判据(--storetest 99 → 104):① 起一趟、当场看「在飞」标记真挂上(量被测代码真造的那趟); ② 把 parent 删掉(= 商店页在下载中途被拆)之后标记必须跟着放掉;③ 连跑 200 趟真下载(空地址、一次网都不上): 每趟回话、跑完无残留标记、工作集涨幅有上限。
  • ② 第一次跑就是红的:标记只在 done 里放,而 fetch 的连接全挂在 parent 上 —— 页没了 done 永不回。 搬进 core 之前那个计数是页面成员(跟着页一起没,不出事),搬出来之后是进程级的, 于是那颗 dll 从此永远卡在「已经有一趟在飞」,他每次点都是同一句话。 修法:标记的生命周期挂下载本身(new QObject(parent) + destroyed 里放),放的时候对号 (QHash<文件,趟号>)—— 不对号的话"上一趟收尾"能把"下一趟刚挂上的标记"错放掉,那道闸就白装。
  • ③ 的量法顺带被 ② 救了:修之前那 200 趟全在闸口被拒(「回话 200 次」是 200 次拒绝), 数字漂亮其实什么都没跑。修完才是 200 趟走完 fetch+QFile+chain:工作集 25 → 26 MB(约 5 KB/趟,分配器留页,不是线性涨),整面 9.8 秒。
  • Win10:这轮新增 81 行里 windows.h/HWND/DwmGet*/winrt 0 命中(只有 QSettings/QFile/QNetwork/QObject)。 拿 node 解 PE 导入表(54 个依赖去重):有 UCRT、api-ms-win-core-winrt-*、mfplat+mfreadwrite、d3d11、dwmapi, 没有 Win11 独有的导入 ⇒ 不是只能在 Win11 跑。但依赖面里有三档: WGC 录屏要 Win10 1809+;Win10 N/KN 没装 Media Feature Pack 时录屏不可用;DWMWA_USE_IMMERSIVE_DARK_MODE 同样 1809+(早的版本只是标题栏不变暗)。
  • 安装包以前没写 MinVersion:Qt 6.10 只支持 Win10+,而 UCRT 在 Win7/8.1 还得单独装 —— 那两种系统上的表现是"装是装上了,双击没反应"(Qt6Core 起不来),最难查的一种坏。现在补 MinVersion=10.0, ISCC 收了、包照打(22.22 MB)。Win7/8.1 上真会拒装这一条没证 —— 这台是 Win11,别当已验。
  • 版本:不升号,跟着 1.4.0 同一批走(1.4.0 还没发,这是它自己那一版里的修 bug + 打包声明; core_abi_gate GATE_HITS=0、--plugintest 242/0、deliver 九栏全绿 MATCH 0D9BD7506D4E)。

🧩 18. 插件商店每天自动更新一次:只换已装的,启动后 15 秒后台跑(1.4.0)

  • 他这句是「插件商店增加一个自动更新功能,一天自动更新一次,如果有新插件」。
  • 两句问答拍定边界:只换已装的那几颗(清单里新出现的插件仍要点「安装」,
  • 不往机器上塞他没选过的 dll)、程序启动后后台跑(不进商店也跑)。默认开 ——
  • 这句是"自动做 X",不是"给我个按钮我按了才做"。
  • 先对现状才动手:store/lastFetchDate 那套"每天拉一次清单"早就有,缺的只是拉回来之后 自动装那一步 —— 这一轮不是新造一条链,是把最后一段接上。
  • 不平行实现:「下到 .part → 验哈希和字节 → 落地」原来缠在 ToolsPage::runInstall 的按钮/状态行上。 搬成 PluginStore::installEntry(entry, parent, onProgress, done),商店页只剩"把进度写成字"; 自动那趟问的是同一颗函数。顺带一道 installInFlight(file) 闸:手动和自动能同时点中同一颗, 两趟写同一个 .part 必坏,而第二趟哈希一定对不上 —— 那会被界面说成"仓库被人动过"。
  • 挑谁 = 纯函数 autoUpdateTargets(rows):只认 State::Update。Missing(新插件)故意不要、 Different(哈希不同但清单不比本机新)不要、Disabled 不要(他亲手禁的,不许顺手启用回来)、 entryUsable=false(Api 对不上)即使有新版也不换。反向验:把 Missing 放进谓词 → 当场红 2 条。
  • 那句结果 = 纯函数 autoUpdateSummary。第一版三种形状都带"已落盘,下次启动用新的", 真跑第一次就撞上:404 → 用缓存 → 挑出 0 颗,却说"已落盘" —— 那是假话。改成只有真换了东西才说。
  • 商店那一页多一行常驻说明(ToolsPage::autoUpdateLine(),static 为了判据不造整扇页就能问它): 关着说"没开"并指路配置页、开着没跑过说"今天这一趟还没跑"、跑过带出结果和时间。留一行空白会被读成功能坏了。
  • 一天一次的闸先记日期再动手(跑挂了不至于每次启动刷网络,那的表现是"开机就卡"); 延迟 15 秒才跑(冷启动在扫应用 + 预热窗口,别让慢网络把"呼出很卡"带上); 带 -- 旗标的自测进程一律不跑(复用现成的 inLoopSelftest),否则就是边测边往插件目录写 dll。
  • 新增 --autoupdatenow:把那句"今天跑过"推开、立刻跑同一颗 runDailyUpdate,打完结果就退出。 本机实测:没有要换的(网络没成(v1.4.0/plugins-index.json 404),拿本地缓存那 19 条清单试) · 8169 ms, 第二次 24195 ms。404 是版本钉死的必然:清单地址是 releases/download/v<我这版>/, 1.4.0 没发之前那一版就没有清单 ⇒ 装好这一版的机器上商店暂时拉不到,发版即恢复。
  • 判据:--storetest 84 → 99(默认开 / 关掉跟着关 / 今天跑过不再跑 / 日期过去该跑 / 从没跑过该跑 / 只挑 Update 那一档 / 满墙新插件不自动装 / Api 对不上的不换 / 那句话三种形状 / 没动文件不许说已落盘 / 三种说法都带"清单从哪条拿到的" / 商店那一行三种情况);--storenet 17 → 18(拿真清单 + 真目录量"会挑谁", 只挑不装;挑 0 颗时那条自己写明"在 0 个对象上,不算量到")。
  • 版本:1.3.4 → 1.4.0(可感知新功能 = minor;1.3.4 没发布过,被这一版吸收 —— 号跟着发布走不跟着 commit 走)。 这轮只动 src/,19 颗插件一行没动 ⇒ 没有插件要 bump;core_abi_gate 在工作树上 GATE_HITS=0。 deliver 九栏全绿(MATCH 2674C6DA524C、19 颗哈希对齐、装好的 exe 242 过 0 红)。

🐛 19. 剪贴板那张卡在日间看不清:不是它抄错颜色,是它读不到表里那两颗 rgba()(1.3.4)

  • 他这句"日间模式剪贴板每个卡片很难看,那个背景完全看不清"的病因不在剪贴板:
  • card/cardLine 在 Theme 表里写的是 CSS 的 rgba(...),QColor 解析不了,
  • 自绘的 delegate 拿不到,只能在 dll 里各抄一份深色真值 —— 切到日间就是白卡上画白字白线。
  • 那两颗角色改成 #AARRGGBB(QColor 和 QSS 都吃,rowSel/hairline 一族早就是这么办的); 夜间值按 alpha 等价换算 0.08→0x14、0.12→0x1f,换算不是改外观:夜里三个数一字没动。
  • 剪贴板改成只问表:ClipboardPage::cardInks(bool) 一颗函数当 delegate 唯一取色口, 再接 QEvent::ThemeChange → viewport()->update()(不然切了模式要等鼠标划过才换色)。
  • Theme 加 relativeLuminance/contrast/over(WCAG 那套只留一份;--cfgcheck 里那颗 局部 lambda 搬进来)。半透明必须先 over() 合成到它压着的那层底再比 —— 拿 rgba 的原始 rgb 直接算,8% 的白卡能算出 20:1 的假高对比。
  • 实测(正文/小字/描边):夜间 12.01/4.63/1.47 · 日间 16.83/5.07/3.47。 第一版量到的日间描边是 1.52:那颗照抄了 iOS separator #d1d1d6,白卡压白页等于没有线, 换成启动器那圈同一支 #868a90 才到 3.47(非文本 ≥3:1)。
  • --cliptest 加 4 条钉决策的输入(日/夜正文 ≥4.5、日描边 ≥3、夜描边 ≥1.4: 夜里那条他一直用着,拿 3:1 去动默认外观得他点头),两个模式各渲一张 PNG 供肉眼复核。
  • 连带读到这两颗角色的地方:商店卡片墙 border: 1px solid @cardLine 的日间描边一起变深 (同族问题一次改到位),启动器 hover 底前后同值;--cfgcheck 那 11 条一字没动。
  • 事故为什么能发生:theme_gate.sh 只扫 src/,plugins/** 明确写在闸外 —— 插件侧"漏一处" 连数的人都没有。这次只把剪贴板一颗接进表,README 那行按实际改掉(录屏/置顶窗/宫图仍在名单里)。
  • CI 那圈离线判据原来不含 --cliptest/--storetest,这 4 条只有我本机跑过 → 补进同一循环(12 面→14 面)。
  • SDK 闭包 25→26 个头上多了 Theme.h(剪贴板第一个 include 它),插件仓库下次同步要重生成。
  • 版本:core 1.3.3→1.3.4(可感知显示 bug=patch,接口/数据格式没动,API 保持 4,core_abi_gate GATE_HITS=0)、
  • 剪贴板插件 1.1.1→1.1.2。deliver 九栏全绿(abi 缺 0 符号 / 布局陈旧 0 颗 / 19 颗哈希对齐 / 装好的那份 242 过 0 红)。

📦 下载与安装

Windows 10 及以上(安装包已声明 MinVersion=10.0)。按用户安装,不需要管理员,默认装到 %LOCALAPPDATA%\Programs\ltool;双击安装向导走完即可,托盘常驻,Alt+Space 呼出。

文件 大小 sha256 前 8 位 说明
ltool-1.4.0-setup.exe 21.53 MB 1895560f 就用这个:双击安装/升级(约 22 MB,开箱零插件,插件由商店按需拉)
ltool-sdk-1.4.0.zip 270 KB 9f0603fc 插件 SDK 快照:头文件闭包 + 导入库,给独立编插件用
plugins-index.json 11 KB a9c820e5 插件商店清单:这一版 app 读的就是它(地址按版本号钉死,见下面那条)
ltool-ads.dll 93 KB 867fddbd 商店里的插件 ads(v1.0.1,插件接口 4)
ltool-base64.dll 68 KB 0cda7bde 商店里的插件 base64(v1.0.1,插件接口 4)
ltool-capture.dll 361 KB 5422db5f 商店里的插件 capture(v1.0.7,插件接口 4)
ltool-clipboard.dll 135 KB 837754ff 商店里的插件 clipboard(v1.1.3,插件接口 4)
ltool-disk.dll 180 KB 2812e8e8 商店里的插件 disk(v1.0.2,插件接口 4)
ltool-downloads.dll 66 KB c5441579 商店里的插件 downloads(v1.0.2,插件接口 4)
ltool-grid.dll 154 KB 0232bbe8 商店里的插件 grid(v1.0.2,插件接口 4)
ltool-json.dll 79 KB 51d2c023 商店里的插件 json(v1.0.2,插件接口 4)
ltool-language.dll 174 KB 646f5849 商店里的插件 language(v1.0.2,插件接口 4)
ltool-linuxdo.dll 178 KB 3d0c18cc 商店里的插件 linuxdo(v1.0.2,插件接口 4)
ltool-network.dll 100 KB adcc505e 商店里的插件 network(v1.0.1,插件接口 4)
ltool-record.dll 276 KB a3c29615 商店里的插件 record(v1.0.2,插件接口 4)
ltool-registry.dll 94 KB 433ae697 商店里的插件 registry(v1.0.2,插件接口 4)
ltool-repair.dll 88 KB 8f583c85 商店里的插件 repair(v1.0.2,插件接口 4)
ltool-shell.dll 135 KB d3a96f38 商店里的插件 shell(v1.0.2,插件接口 4)
ltool-software.dll 237 KB 67742e43 商店里的插件 software(v1.0.2,插件接口 4)
ltool-startup.dll 150 KB e1be36d5 商店里的插件 startup(v1.1.0,插件接口 4)
ltool-tasks.dll 96 KB 4e47f691 商店里的插件 tasks(v1.0.1,插件接口 4)
ltool-user.dll 140 KB 5e779beb 商店里的插件 user(v1.0.3,插件接口 4)
sha256.txt 2 KB — 上面这些文件的 SHA-256 全表(Get-FileHash -Algorithm SHA256 逐行对得上)

一共 23 个文件。校验:Get-FileHash <文件> -Algorithm SHA256,或下 sha256.txt 对整批。

插件商店读的是 releases/download/v1.4.0/plugins-index.json —— 清单和这 19 颗 dll 都出自编出这一版 core 的那同一次构建,所以"商店里那颗和这台机器的 core 对不上"这件事从源头没了(对不上也只会是被拒绝并说清,不是闪退)。


Full Changelog: v1.3.3...v1.4.0