Skip to content

ltool v1.5.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 12:22
· 4 commits to main since this release

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

🎉 欢迎使用 ltool 1.5.0!本次共 12 笔改动,重点是 商店顶部那两行、「为什么时不时有大量磁盘 IO 和 CPU」这轮全部拿数回答、广告弹窗那页加了一颗「从正在跑的进程里挑一个」放行(ads…。下面每一节是一笔提交,原因和实测数字都在里面。


🌟 本次改动

🧩 1. 商店顶部那两行:短话上页面,全文进悬停说明和日志(他一句「不要放到插件详情页面」,选的是 A 档)

  • 他给了一张截图:那两行是「网络拉取未成功:Operation timed out(HTTP 0,走的是 https://…v1.5.0/plugins-index.json)—— 当前展示本地缓存清单(共 19 条)」和「自动更新:10-23 11:02 · 没有要换的(网络没成(Error transferring … 404 …)),拿本地缓存那 19 条清单试)」。三个落点摆给他(页上留短话 / 整行挪去「关于」/ 页面一行都不留),他选「页上留一句短话,全文进日志」。
  • 一条规矩只留一个出口:ToolsPage::setStoreLine(短话, 全文) 接管那两颗标签的全部 11 处写入(拉取中 / 失败有缓存 / 失败无缓存 / 成功那条 / 一键那五句 / 建墙那两句),setAutoNote() 管自动更新那一行。那五句一键状态原先各写各的 m_storeStatus->setText(...),一并改走这颗 —— 不是顺手:「上一句的悬停说明要收掉」这条规矩只有在一个出口的前提下才成立。页面那四句变成「正在拉清单…」「网络没通,先看本地缓存那 19 条」「清单没拉到,这一面退回本机装着的那几个,它们照常能用」「清单 20 条 · 这一面 20 张卡 · 能装或可更新 0 · 拒收 0 · 清单外 0 —— …」;URL、走的哪一档、试了几次、拒收的是谁都在悬停说明里,日志那三份本来就带原文,一处没减。
  • 两份唯一实现(不这么写就会漂):
  • 自动更新那句里"哪条链拿到的"从 note 搬进新键 store/autoUpdateWhy,note 只留「清单 19 条」这种短话;autoUpdateSummary 那颗纯函数没动(判据一直喂它短 src)。
  • PluginStore::autoBrief() = "配置里存的那条 note → 页面上那句短话"那颗谓词,读的时候也要过一刀:1.5.0 之前 note 存的就是整条原文,只改写入点的话他屏幕上那行要一直难看到明天早上跑完下一次自动更新。切法:带 http 的从第一个「(」切掉;切完还带 http(老格式里没括号那句)就退成「上次那一趟没成(原文在悬停说明里)」—— 宁可退这一句也不把原文推上页面。原文一个字不丢:没写 why 时悬停说明拿回那条老 note。
  • 数字:--storetest 109 条 0 红 / 9,661 ms(新 5 条:带 http 的切短、不带 http 的原样过不许多切一刀、没括号的退固定短话、老 note 进悬停说明、写了 why 之后两份各管各的;另有一条改名 —— 原来问"清单从哪条拿到的要带出来",现在带出来的是短话、链在 why 那份),--plugintest 396 → 399 条 0 红 / 8,125 ms(windows 平台,新 3 条:建墙时短话带悬停说明、短话接管后收掉、页面上那句不许有 http),离线九面旗全套 859 条 0 红(--rectest 在 offscreen 必红 7 条是"那颗要真窗口才录得到",单独在 windows 平台量到 0 红,不是回归),闸六道全 0。
  • 我自己这一笔带出来的两条真红,先说清再改:① 把 check() 第三个位置当"报告文本"用了,它是 want ⇒ 那条自己判自己红(got=[yes] want=[没有要换的]);② 在"没跑过时那一行说今天还没跑"之前先把 note 写进了配置,它读到上一句留下的值 —— 记忆里那条"判据读到上一查询留下的状态"这次是红给我看的,改法是那条判据前先 remove 两个键。
  • 反向验两条各咬住一处:autoBrief 改恒等 ⇒ storetest 红 3 条;"清悬停说明"改成只在非空时才设 ⇒ plugintest 红 1 条;改回来重编(grep -c MUTATION = 0 才继续)两边各 0 红。
  • 顺带把 --storenet 那栏的等待认了新短句:它以前只等「清单…」开头的状态行,而失败有缓存那句现在改成「网络没通…」开头 —— 不认它,这一栏在未发版那次只会白等 20 秒,而"页面上不许有 URL"那条判据就永远只在一条跑不到的成功路径上量(装饰性的绿)。现在正则读 文本 + 悬停说明 合起来那份,"不许有 http"读短的那份。
  • 版本:不升号。core 仍在未发布的 1.5.0(这一档吸收同批功能),插件一颗没动(plugin_version_gate 认),API 仍 4;多了一个 store/autoUpdateWhy 键但老配置读得动(没这个键就走"悬停说明拿回老 note"),不算不兼容。这轮没装机 —— 装着的还是 11:54 那份(1900d67),它照旧显示整条原文,装完才会变短。

📦 2. 「为什么时不时有大量磁盘 IO 和 CPU」这轮全部拿数回答,没动产品代码一行

  • 先给结论的三条依据:
  • 静置的常驻那颗(pid 23336)连采 12 个窗口 \Process(*)\IO Read/Write Bytes/sec = 读 0.0 / 写 0.0 KB/s,整榜排不进前 10;20 秒 TotalProcessorTime 增量 16 ms = 单核 0.1%;工作集 152→152 MB。⇒ 5 秒的窗口扫描、2 秒的剪贴板轮询都不是来源。
  • 同窗整机排名:qoder cn 1,221/1,217 KB/s(就是我这个会话)、chrome 967/970、qqpctray 54/6、wechatappex 13/13。他看见的盘灯此刻主要不是 ltool 给的。
  • 一次完整扫描(沙箱数据目录里跑产品同一条 startScan()):墙钟 2,952 ms、CPU 1.11 核·秒、索引 9,409 条目,硬缺页只有 0–28 次/秒(累计约 9 页)⇒ 目录枚举读的是 NTFS 元数据,一在全系统文件缓存里,二记在 System/缓存管理器头上、不记在 ltool 自己的进程 IO 计数里 —— "进程读 0"和"盘灯亮"两件事不矛盾。同一份探针里 PhysicalDisk 那栏采样器没匹配上实例名,是空的,所以这一格我拿硬缺页那列说事,不拿它。
  • 节奏和形状(他日志原时间戳):12:06 / 12:27 / 12:45 / 13:16 / 13:58 / 14:44 各一次 scan finished: 350 apps ⇒ 3.5 小时 6 次,约每 20–45 分钟;点火的是挂在开始菜单 116 个目录上的 watcher,节流只保证 ≥60 秒,另有一颗和 index/refreshHours=7 同周期的。要点是重扫不只重扫快捷方式:startScan() 尾巴上挂着 reindexAsync(),桌面 4 层 / 文档·下载 3 层 / 图片·音乐·视频 2 层 / 主目录 1 层每次跟着整棵再走一遍 —— 这就是"时不时一下":一次约 1 核·秒的 CPU 脉冲,盘上很轻。
  • 排除掉的一条:deep index / MFT / volume 在他这份日志(10-21 16:12 起)里 0 次命中,配置 index/deep=false ⇒ 那条每卷直读 $MFT 的路根本没跑;deepindex.dat 那 335 MB 是 10-03 留下的死文件,占地方不产生 IO。
  • 两条我自己的账,写进文档而不是只在这儿说:
  • 15:00:26–15:02:09 那串 v1.5.0/plugins-index.json 三镜像 404 + 直连两次 20 秒超时是我造成的:--smoke、--mfttest 不在 src/main.cpp:622-632 那份沙箱名单里,直接落进他真实的数据目录,把"一天一次拉清单"点着了,还往他的缓存里写了东西。
  • 顺着这条查出一个真问题:src/main.cpp:227 让非提权的 --smoke 把 index/deep 写成 false(本意只该管 CI 那份配置)。全项目只有两处写这个键(另一处是配置页那颗勾),而他现在是 false ⇒ 如果他自己没关过,深索引就是我某次不带沙箱的 --smoke 给他关掉的。把两面旗挪进沙箱名单 + 要不要替他把深索引打开,等他拍。
  • 探针留在仓库:build/verify/io_probe2.ps1(沙箱跑一趟同时采 CPU/硬缺页)、build/verify/io_top.ps1(整机按进程排名 IO)。两张都走 $env:TEMP,加进 git 后重跑 tracked_leak_check:580 张扫,TRACKED_LEAK_HITS=0。
  • 版本:没动产品代码 ⇒ 不升号,core 仍 1.5.0、插件接口仍 API 4。

✨ 3. 广告弹窗那页加了一颗「从正在跑的进程里挑一个」放行(ads 1.0.1 → 1.0.2):手打进程名这件事不用他记

  • 他一句话:"广告弹窗的忽略进程可用点击一个按钮选择进程列表的一个进程进行忽略"。落在那页「编辑规则…」里 那一栏底下,不开新页:按下弹一张此刻在跑的进程,双击一行或选中按「加进去」就写进「忽略的进程」那一栏,可以连着挑几颗,最后照样按「保存」才生效 —— 和旁边那四栏同一个草稿/保存模型,不搞第二套。挑中一颗本来就在名单里的,当场点名「被「wechat」盖着」,不加第二遍;列不出进程的机器上那句话照实说,不摆一张空表当"没进程在跑"。筛是藏行不重建(重建会把选中项和滚动位置一起丢掉,软件页那条老规矩)。
  • 两份唯一实现:① 映像名 → 名单那一行的规整法(小写、去 .exe)原来只长在"右键放行这一颗"那一句里,现在两条路都要用,抽成 AdBlocker::allowProcFor(),两处各写一遍迟早会不一样;② "这颗是不是已经被盖住"抽成 coveredByAllowProc(),方向照那一侧的真匹配来(进程名包含片段),不许反过来。
  • 列进程加在平台缝第五道上:Platform::runningImageNames() —— Windows 用 CreateToolhelp32Snapshot,按映像名去重(这台机 260 个进程压成 89 行:浏览器、IM、svchost 都是一个 exe 跑好几份)+ 不区分大小写排序;POSIX 那颗如实返回空,注释写明"是没写、不是做不到"(Linux 上读 /proc//exe 可行,谁要接就接,别让它假装成功)。
  • 数字:--plugintest 389 → 396 条 0 红(这一栏 8 条),offscreen 和 windows 两个平台各跑一遍都 0 红,整趟实测 7650 ms;离线全套 1058 条 0 红 / 3 条点名 SKIP;闸全绿(theme 0、proxy 0、plugin_version NEEDS_BUMP=0、core_abi 0 ⇒ API 仍 4、tracked_leak 0、notes 两查 0);插件号 ads 1.0.1 → 1.0.2。
  • 反向验抓出了我自己写的一条假绿:那条写的是"名单里那行空的不能盖住一切",我把 continue 改成不跳过空片段,函数回的还是空串、调用方照样当"没盖住" ⇒ 396 条全绿,判据一动不动。两个原因叠在一起:那个输入根本到不了这儿(存进名单前 linesToPatterns 就把空行丢了),而"空串"这一个值同时表示"没盖住"和"被一条空的盖住了" —— 两个意思挤在一个返回值里,谁也不能把它判红。换成一条打得动的:名单里那条比这颗更长 ⇒ 不算盖住(包含方向不能反),再把 one.contains(p) 翻成 p.contains(one) 重编 ⇒ 那条当场红。这是今天第二次撞到"判据在 0 个对象上空转"那一族(第一次是 has(..., true) 那种永远真不了)。
  • 没做的:那颗按钮和那张表本身没有判据 —— plugins/ads 没有自带判据文件(--gridtest / --randtest 那种),要补得先给它加一颗 --adstest 并接进 CI 那栏;纯函数那两颗钉住了,界面这半现在是"我本机没点过、CI 也没数"。
  • 版本:1.5.0 不动(未发版,同批功能吸收);插件 API 仍 4(新增的都是 core 侧函数,ProcInfo.h 不在 SDK 那 26 份头的闭包里)。没装机。

🎨 4. 「一键下载全部」那颗按钮的预留宽度改成量 sizeHint(),不按字数估

  • 本机 windows 平台下这条判据红了三次才认账:两版标签都塞得进预留的那块 got=[0/1]。上一版留宽度是按「几个字 × 一个字宽」估的,而且只按「确认这 12 颗」那一版留 —— 可基线那句「一键下载全部」是六个纯汉字,比夹了数字和空格的「确认这 12 颗」还宽,装不下 ⇒ 点第一下换字时按钮变宽,把页眉旁边那两颗挤走,正是他否过两轮的"一点就重新排"。
  • 改成拿产品自己那颗 sizeHint() 量两版标签取大的(QSS 的内边距和边框它都算,我算不准)。判据也跟着换成量 sizeHint(),不再自己数字符;顺带补一条"探针借道试宽标签之后要把那句还回去"—— 试宽度这件事本身不许留下状态。
  • CI 看不见这条:它跑 offscreen,字体度量不一样,上一版在那儿是绿的。这是今天第二条"只在本机才红"的判据(前一条是 grid 那颗角标读一个像素)。
  • 数字:--plugintest 389 条 0 红,两个平台各两趟(offscreen / windows)都 0 红;离线全套 1051 条 0 红 / 3 条点名 SKIP;theme / proxy / plugin_version / core_abi / tracked_leak 全 0。版本 1.5.0 不动(未发版,同批吸收),没装机。

🧩 5. 剩下几条边界接上同一层 SEH 壳:全仓库一份 __try,五种"跨进插件"的调用都从它过

  • 他拍的"继续做剩下的边界保护"。没按"每条边界套一次 __try"写 —— 收敛成一份壳:一个 POD 的 Cross(要调谁、参数、结果口子、哪一类)+ 一个 crossCall() 的 switch + 一个 crossSeh()。五种调用全从这颗过:createPage / feedText / invoke / 那个 extern "C" 的激活函数 / apiVersion / actions。
  • 为什么非得是一份:__try 那个函数里不能有需要析构的局部对象(C2712),四份就是四次同样的指针舞蹈,漏改一份就有一条边界没人守 —— #94 那次「--storenet 四条红没人看见」是同一个形状。
  • 产品那四个入口现在各自只剩"查一下,交出去":page() → buildPage()、feed() → feedInto()、invoke() → actionsInto() + invokeInto()、startBackground() → activateInto()、plugin() 装载那一问 → apiVersionInto()。判据直接调的也是这同一批函数,不是另写一个近似的外壳。
  • 契约写死在头文件注释里:返回值 = 这一趟回没回来,插件自己答"是/否"走 accepted / didIt —— 两种"否"在界面上是两句话。三条具体的:① 撞了不许被算成"插件答了「是」"(判据拿 ans 初值 true 去量,回来必须是 0);② why 进来先 clear()(上一笔那条教训的推广:不清就会把上一趟的失败念成这一趟的);③ 各条撞了之后的产品行为不一样 —— 装载那一问撞了 = 这颗 dll 直接拒掉并写进 g_loadErrors(10-22 那批"名字对得上、一用就崩"就死在这一脚);路由时 actions()/invoke() 撞了 = 跳过这一家但照样把 who 给出去(不然"按了 F1 什么都没发生"又回来);后台激活撞了 = 不算进 started 那个数并点名(悄悄少一个后台功能是最难查的)。
  • 数字:--plugintest 377 → 388 条 0 红(五条边界共 20 条:建页 9 + 另四条 11),整趟实测 7706 ms(两趟 7706/7660);离线全套 1050 条 0 红 / 3 条点名 SKIP;闸全绿 —— theme 0、proxy 0、plugin_version NEEDS_BUMP=0、core_abi 0(PluginHost.h 不在 SDK 那 26 份头里,加函数不动插件布局,API 仍 4)、tracked_leak 0、notes 两查 0。
  • A/B 做在「递文本」这条上,真插件、真走产品入口 feed(),只改一个变量:把 plugins/json 的 feedText 开头改成写坏内存 ——
  • 有壳:rc=1、386 条落地、红只有 2 条且都点名 json 递文本(「把一段 JSON 递给 json 工具,插件接住了」/「那段文本真的进了工具」),其余 19 颗照常,stdout 37916 字节,沙箱日志里那句实话出现 6 次:plugin: json 递文本时撞在异常 0xC0000005(指令地址 7ff8d9ee4d44)· 这一趟没干成,程序没跟着死。
  • 把 feed() 换回直调(绕过壳):rc=139、只有 181 条落地、stdout 16566 字节,后面连同尾巴被吃。
  • 两处都原样改回,plugins/json 一字未动(提交里也没有它)。
  • 两条没做 A/B,如实说:invoke() 与 startBackground() 的接线只靠"产品入口各调一次 + 同一份壳 + 建页/递文本两次已证的机制"。真路由一个动作得走 --fileinfo 那面旗,而那颗真插件会把对话框弹到他屏幕上(自测不许碰他的桌面);后台那颗要在自测里真 activate 就会把 linuxdo 的轮询定时器起起来,判据又变成看网络脸色。要补得先给自测造一个不弹 UI、不联网的动作口子。
  • 还有一条判据自己写错被当场抓的:QStringLiteral("%1/%2").arg(一个 bool) 少填第二个洞,那条报 got=[1/%2] —— 格式串的 %N 和 .arg() 个数这轮又欠了一次。
  • 版本:1.5.0 不动(未发版,同批功能吸收;这轮动的只有 core 的一份壳 + 判据 + 文档,一颗插件产物都没改)。没装机。

🧩 6. 建页那一条边界包了一层 SEH 外壳:插件里读坏内存(0xc0000005),程序不再跟着它一起死

  • 他问"有没有办法插件有报错、但程序不崩溃"。三档里拍的是第二档(包边界),第三档(插件进子进程)要重写整个插件模型 —— 插件交回来的是 QWidget 页,跨进程就得把控件树和事件走 IPC,不划算。这一轮按他说的只铺建页那一条:历史上真崩过的就这一条。
  • 落点只有一颗:PluginHost::buildPage() 是全仓库唯一一处跨进插件代码造页的地方(原来那句 p->createPage(parent) 长在 page() 里)。page() 现在只做"查缓存 → 找接口 → 交给这一颗 → 成了才缓存"。外壳的形状是被编译器逼出来的:__try/__except 只能待在没有需要析构的局部对象的函数里(C2712),所以那层是一个纯指针进出的小壳,QString 全留在外面拼;异常码和指令地址先落进 POD 的 SehHit,出壳之后才写日志 —— 异常展开里再分配大对象,第二次崩会把线索一起吃掉(和 SetUnhandledExceptionFilter 那条同一个理由)。
  • 界面那一行走的是早就有的那句实话: 建页时撞在异常 0xC0000005(指令地址 7ff8…)· 这一趟没给你页,程序没跟着死 —— ToolsPage 开不成页时本来就把原因写在页眉,不是只进日志。
  • 买到什么、没买到什么写在头文件里:买到窗口不消失 + 证据留下来 + 半建那张不当好页缓存;没买到"这个 bug 被关住了" —— 那一趟已经碰过坏内存,后面是带着未知损伤继续跑,所以 WER 转储和 pe_rva 那条取证路一律保留。一律接住也意味着栈溢出(0xC00000FD)和 C++ 异常(0xE06D7402)同样被接住并如实写码。非 Windows 那边这层如实不装(#else 就是直调 + 一句注释:接信号要 sigsetjmp,从信号里继续跑更不可靠,真要做得走进程隔离),那几条判据也跟着 #ifdef Q_OS_WIN —— 在 Linux 上拿故意崩的接口去喂它不是"红",是假红。
  • 决定性的一条是 A/B,只改一个变量:把 plugins/json 的 createPage 改成开头就写坏内存(真插件、真装载、真走 page()),编两颗对着跑 —— 有外壳:rc=1、359 条落地、13 条红全点在 json 那颗、其余 19 颗照常、日志 36120 字节 384 行尾巴完整;把 page() 换回直调(外壳绕过):rc=139、只有 55 条落地、日志 4152 字节,后面连同尾巴被吃掉。这一对才是"接线了"的证据 —— 合成接口那 9 条自己量不到(判据直接调 buildPage,产品绕过它也不会红)。两处都原样改回,plugins/json 一字未动。
  • 数字:--plugintest 368 → 377 条 0 红(SEH 这一栏 9 条),整趟实测 8702 ms(三趟 8801/8634/8702);离线全套(CI 那 19 面旗、offscreen)1039 条 0 红 / 3 条点名 SKIP;闸全绿 —— core_abi_gate 查的是 SDK 那 26 份头,PluginHost.h 不在里面 ⇒ 加一颗函数不动插件布局、API 仍 4;plugin_version NEEDS_BUMP=0(这轮一颗插件源码都没动);theme/proxy/tracked_leak/notes 两查都 0。
  • 探针自己犯的错是判据撞出来的,不是我看出来的:第一版为了"外壳别挡好人"直接拿真插件走 buildPage(plugin("json"), …),那张页没进 g_pages 缓存、parent 又是空 ⇒ 那颗壳里的 m_page 被我这张孤儿页顶掉,跑在后面的"整扇窗关掉后再拿"当场从 refused/usable 变成 fed/usable(19/20)。判据自己弄坏被测对象,和上午那条"读到上一轮留下的状态"是一族。改成三颗自造的接口(撞的 / 正常给一张挂在我自己那棵树上的 / 返回空的),用完 delete host 收走。
  • 顺手补了一条产品规矩:buildPage() 进来先 err->clear()。契约是"给出页了就没有原因"—— 不清的话,调用方复用同一个 QString 时会把上一趟的失败念成这一趟的(那条判据现在钉着:正常那一支 page/quiet)。
  • 还没铺的三条(他拍的顺序,先只建页):feed() 递文本、invoke() 热键/右键转发、startBackground() 后台那颗 —— 同一层壳套三次就行,但每条要先有一条会红的判据(同这次这个 A/B 形状),不然就是包了个没人数的守卫。
  • 版本:1.5.0 不动(还没发版,同批功能吸收进那个号;这轮改的是 core 的一层壳 + 判据,不动插件产物、不动接口布局)。没装机。

🧩 7. 插件商店页眉那颗「一键下载全部」:两段就地确认,排的还是那条「一次只飞一颗」的队列

  • 他一句话点的功能,两处读法他拍的:范围 = 整个商店里没装的那几颗(不跟着上午那行过滤走 —— 跟着走"全部"就名不副实),确认 = 就地两段(不是模态框:模态框挂在失焦就自藏的启动器里,他看不见也答不了,功能看着永久失灵)。
  • 第二下走的是同一颗 installEntry():卡片上那颗按钮、他一张一张点、一键这一批,三条都进同一个队列。被过滤挡在墙外那几张拿不到卡片控件就传 nullptr —— 那条队列本来就允许"没地方写字、照样下",不另开一条后台下载路(平行实现早晚说法不一)。上一批还在飞时再按它不插队,那一行说"等它空了再按"。
  • 排谁只认一颗:ToolsPage::bulkTargets() 只收 State::Missing。接口对不上的那条在 compare() 里就已经换成 ApiMismatch 了,这里不再自己比一遍 Api(比第二处就会和那里说法不一);"另有 N 颗用不了"数的是 ApiMismatch + Blocked。落没落地也只认一颗:bulkMissed() 问数据层现在的说法,不是"我数了几次回调"—— 回调次数会把"下成了但哈希不对被扔掉"那一支算成成功。
  • 确认态的寿命钉在 rebuildGrid() 第一句:那个 N 是上一秒的商店,中间他点了「刷新清单」或哪颗装完,数字就不作准,得重新按第一下看新的。两版标签的宽度按最宽那版预留过,点下去只换字、不撑页眉那一排("一点就重新排"他否过两轮)。
  • 数字:--plugintest 339 → 368 条 0 红(一键这一栏 29 条),整趟实测 7691 ms(连着四趟 7949/7709/7691/7130 全 rc=0);离线全套按 CI 那 19 面旗、offscreen = 1030 条 0 红 / 3 条点名 SKIP。闸全绿:theme 0 漏、proxy 0、plugin_version NEEDS_BUMP=0、core_abi 0(ToolsPage 不在 SDK 那 26 份头的闭包里 ⇒ API 仍是 4)、tracked_leak 0、notes 两查 0。
  • 反向验三处,各改一版各编一遍(不是一起编,红了才知道各是谁的红):① 绕过队列直接 runInstall(一次全飞出去)⇒ 红 2 条,两条都是 got=[2/2] want=[1/2] —— 就是他连点九颗那次崩的那一类;② 拆墙时不收回确认 ⇒ 红 3 条;③ 确认那一档改成"永不停留"和"永不放行"两个方向 ⇒ 分别红 5 条和 3 条,红的正是"第一下一颗都不动"和"第二下真排队"那两组。三处都改回来 0 红。
  • 两条先红了才想明白的,都记在 notes 里:
  • 我原本让那两条假下载真失败之后再量"收尾那一行"。本机同一条 4200 ms 和 12000 ms(撞上限)都出现过;换成 127.0.0.1 上没人听的端口更糟 —— 这台机每次连接要磨 2 秒,两条 × 两个候选 × 两趟就是十几秒。一面闸不该看 DNS 的脸色 ⇒ 把"收尾说成了几颗"抽成纯函数 bulkMissed(),界面那侧只留确定性的三条(一颗在飞 + 其余排着 / 在飞途中拆一次墙 / 在飞途中把这扇窗关掉)。顺带删了一条"目录里该留下两个 .part"的判据:第二颗要等第一颗失败才开始,数文件同样是看脸色。
  • 「在飞途中关窗:没崩」我一开始写成 has(..., true) —— 一条永远真不了的判据就是装饰性绿。改成一行 INFO + 证据是整趟 rc=0(真崩就死在那两句之间,缓冲的 stdout 一起没)。
  • 假清单那一扇是这么造的:往沙箱数据目录写 plugins_cache.json(两颗 api=4 的假条目 + 一颗 api=3 的),再把 store/lastFetchDate 写成今天 —— 不写这颗日期,构造函数会自己起那 100 ms 的自动拉清单定时器,中途把这一面重排掉,判据就成了看网络脸色的(--storeexit 那条注释里早钉过同一件事)。地址全用 .invalid(RFC 2606 保留域,本机实测 31 ms 解析失败,一条字节都不出这台机器)。那两条留下的 ltool-fake*.part 由判据自己收:路径拿 PluginHost::pluginsDir() 现算成绝对路径、只删这一个前缀的名字,最后一句量的是"删完再数 = 0"。
  • 版本:1.5.0 不动(还没发版,同批功能吸收进那个号)。这轮没装机 —— 上午那句「打包安装」给的是上午那两笔,授权不延续。

🧩 8. 插件商店那面墙顶上常驻一行过滤:名字 + 全部/已装/未装(两维是「与」,各自只有一份实现)

  • 他要的第一句是"在插件商店加一个搜索,搜索插件名称",第二句补成"增加过滤,安装状态,全部、已装、未装"。落法是常驻一行而不是弹层:一颗名称框(带 ✕ 清空)+ 三档单选,底下再留一行常驻的话 —— 点一下当场换那一面墙,那一行同时说清「筛出几张 / 共几张 / 被哪一维挡的」。0 命中也不留一面白墙让人猜(那句会改口说「上面没有可列的卡」,并点名怎么回去)。
  • 两维各只有一份实现,判据和界面读同一颗:
  • 名字那一维只有 ToolsPage::nameMatches() 一颗,只比卡片上那个显示名 —— 关键词和 id 一概不算(想按关键词找走主搜索框)。这条边界钉了两条会红的判据:「宫格」筛不出「图片排版」、「b64」筛不出「Base64 编解码」。哪天要放宽,那两条必须明着改。
  • 状态那一维只有 PluginStore::countsAsInstalled() 一颗:已装/可更新/与商店不同/清单外/已禁用 = 「已装」(盘上有它就得说在),能装/接口对不上/条目不能用 = 「未装」,排进待删而文件还在盘上的那条 = 「已装」。卡片墙只管筛,一颗都不重算。
  • 正看着一张插件页时改过滤不许把人拽回卡片墙:重排欠着(m_filterDirty),按「返回商店」那一步才补,而那一行当场如实说「返回商店时才重排这一面」。判"现在是不是正看着一张页"只准问 isHidden() —— 离屏那扇窗从没真 show 过,isVisible() 对它的每个孩子恒 false,第一版就是这么判成"没在看页"、跳了墙。换档走的是同一条 applyFilter(不另开一份"要不要打断"的规矩)。
  • 数字:--plugintest 312 → 339 条 0 红(过滤这一栏 27 条),整趟实测 7764 ms(连着三趟 7764 / 7753 / 7793,都 rc=0);离线全套按 CI 那 19 面旗、offscreen 跑 = 1001 条 0 红 / 3 条点名 SKIP(没提权读不了 $MFT、offscreen 的剪贴板是内存桩、--pathcfg 一条)。闸全绿:theme_gate 36 处挂样式表 0 漏(新加那两处是 token-only)、proxy 0、plugin_version NEEDS_BUMP=0、core_abi 0、tracked_leak 0、notes 两查 0。
  • 反向验两处,各红各的,改回来都 0 红:
  • passesShelf() 一上来就 return true(三档全废)⇒ 红 3 条:「点未装:两档相加」got=40 want=20、「两维是与」got=1 want=0、0 命中那一行改口。
  • passedRows() 里把名字当空串(名称框全废)⇒ 红 5 条:名字筛「随机」got=20、不存在的串 got=20、活得过拆墙 got=20、重排后那一行 got=no、返回商店补上 got=20。
  • 这台机量不到的一栏如实说:本机 20 颗全在盘上 ⇒「未装」那一档 0 颗。所以只坏状态那一维时,「点已装 = 卡片数 = 20」那一条是白过的(真红的是"两档相加"和"与"那两条);换一颗真没装的机器上它才开始出力。
  • 上一轮那条 rc=139 收了,不是产品崩:那一笔编译是红着退出的(src/main.cpp(1496) C2001),跑的其实是旧 exe —— 旧那版过滤框没有 accessibleName ⇒ 判据先 FAIL,探针接着拿 null 指针去 setText ⇒ 段错误,还把后面 30 条和整段尾巴(缓冲的 stdout)一起吃掉。教训是"编红了不许说跑过了";探针在"那颗控件没找到"之后还继续 deref 这个形状记在 notes 里,本轮没动那 60 行缩进。
  • 版本:1.5.0 不动 —— 还没发版,同批功能吸收进那个号;ToolsPage 在 core 里、不在 SDK 那 26 份头的闭包里,加成员不动插件 ABI ⇒ API 号还是 4。发版时商店清单会是 20 条。

🐛 9. 「图片排版」那条角标判据本机红、offscreen 绿:它赌的是一个像素,而那个点正落在白色 ✕ 的字身上

  • 那条是探针自己的体检(grab 万一画出来是空图,旁边两条像素判据会"一起绿"骗人)。它只读 (130,23) 一个点,而实测角标矩形是 (119,12 22x22) —— 那个点就是 ✕ 的正中心。角标是"深色半透明底 + 白色那个 ✕",windows 平台用雅黑、offscreen 用默认字体,字形一换那一笔就盖住采样点:读到白(G=255)就判成"没画"。dpr 两次都是 1,不是缩放的事。
  • 先分是不是今天带进来的:拿装在软件目录里那颗 1.5.0(= 昨天 641ccef 那一次构建的产物)跑同一条旗、同一个平台,也红同一条 —— 与商店过滤那两笔无关,是这条判据自己一直脆。
  • 改法是数整块矩形里有几个比图暗的点,并且不许探针自己重算矩形拼法:把产品自己那颗 GridBoard::closeRect() 从 private 提到 public,画角标、命中 ✕、探针数的从此是同一块(在判据里再抄一遍拼法就是平行实现,一漂就各自绿)。grab 的取色也改成按 devicePixelRatio() 换算再读 —— 本机是 1 所以旁边两条没动,换缩放就不漂。
  • 门槛拿实测数拍:256 个点里暗 228、亮 28(亮的就是那一笔白字),所以拍在 40 —— 比实测低一个数量级、空图给 0,两头都够远。反向验只跳过 cell 0 那颗角标的绘制 ⇒ 暗点 0、亮点 0、那条红、rc=1;改回来 64 条 0 红,两个平台都绿。
  • 号:grid 1.0.3 → 1.0.4(plugin_version_gate 那条纪律:plugins// 里除 plugin.json 之外改了源码,那颗自己 +1)。产品行为一字未变,这轮动的只有探针和一颗内部 helper 的可见性。

🐛 10. 把三面插件自带的判据接进 CI 那栏:grid 64 条 / linuxdo 44 条 / random 52 条。以前这个点名跑的名单里没有它们 —— 等于"只有写它的人本机跑过一遍",之后谁改那张页都没人数(#94 那次 --storenet 四条红没人看见,是同一个形状)。三条都离线、都不 show 窗口,本机实测 0.9s / 0.2s / 0.1s,全 0 红;本地过 js-yaml:jobs=1 steps=25。

  • (这一笔没写正文,标题就是全部。)

🧩 11. postrelease_check 那条颗数是我刚自己引入的"量错对象":拿 v1.4.1 那次发出去的清单(19 条)比今天的工作树(20 颗)报了一条 PROBLEM —— 过去的产物和未来一棵树比,本来就没得比。改成从那个 tag 数(git ls-tree 那一版的 plugins/*/plugin.json),并多打一行"那次几颗 / 工作树现在几颗"。tag 不在本地时退回现算。v1.4.1 公开面三查因此是真的过了:说明 0 处家目录、清单 HTTP 200 且 19 条对得上那一次的 19 颗、sha256.txt 那一行和资产 API 的 digest 逐字对上(4feb2231c1b64296…)⇒ POSTRELEASE_BAD=0。

  • (这一笔没写正文,标题就是全部。)

🐛 12. 第 20 颗插件:随机数生成(区间取数·不重复抽取·随机字符串·UUID),core 1.4.1 → 1.5.0。生成那一层是 Rand::generate() 一颗纯函数,页面和 --randtest 读同一份(字符集池子同理只有一份 poolOf)—— 判据不许重抄算法,两份一漂就各自绿。三条"故意不做"钉住了:区间不够就静默少给(红)、小数档被问不重复就偷偷实现(说不)、参数弄坏后留着上一轮那五行(清空+改口说 ✘)。窄/宽区间两条不重复分支分开量。qint64 的 span 靠无符号回绕算差值,整条 2^64 那一档单独挡掉,不然 span 加回 0 会当成正常结果。判据自己抓到我写错的一处:changeEvent 里拿"已经展开过的样式表"再 expand 一遍 = 读上一轮留下的状态,切完白天还挂着夜里那支绿;改成把最后一句原文和 ok/not-ok 各存一份。两处坏法一起编 → --randtest 红 3 条,改回来 52 条 0 红 / 136 ms。第一次编译红 5 处全是新代码(bounded() 不是 const、QChar 没有 ++、QSet::insert 在 Qt6 返回 iterator、generateDouble() 是实例方法)—— 主仓库有 PCH 兜着,这类要到插件仓库独立编那次才暴露。数字:--plugintest 300→312 / 6104 ms(三趟按颗铺开的闪退判据自动多铺一颗,新插件从第一天就用 QPointer),离线全套 760 条 0 红,--smoke 0(日志里 20 颗全"看到",那条 CRASH 是昨天 20:58 已根治的)。顺手把还活着的"19 颗"计数清掉:CI 步骤名、deliver.ps1 文案、四份工具改成"每一颗"或现算(postrelease_check 现在从 plugins/*/plugin.json 数,版本号也从 git describe 问,不再钉 v1.4.0 —— 那是"绿了但量错对象")。插件仓库和打包安装没动,等你一句话。

  • (这一笔没写正文,标题就是全部。)

📦 下载与安装

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

文件 大小 sha256 前 8 位 说明
ltool-1.5.0-setup.exe 21.58 MB d43f263e 就用这个:双击安装/升级(约 22 MB,开箱零插件,插件由商店按需拉)
ltool-sdk-1.5.0.zip 273 KB eaff5a38 插件 SDK 快照:头文件闭包 + 导入库,给独立编插件用
plugins-index.json 11 KB 9028f592 插件商店清单:这一版 app 读的就是它(地址按版本号钉死,见下面那条)
ltool-ads.dll 104 KB bf4e1922 商店里的插件 ads(v1.0.2,插件接口 4)
ltool-base64.dll 68 KB 576fbb7f 商店里的插件 base64(v1.0.2,插件接口 4)
ltool-capture.dll 361 KB 0d5e4183 商店里的插件 capture(v1.0.7,插件接口 4)
ltool-clipboard.dll 135 KB f4e6e90e 商店里的插件 clipboard(v1.1.3,插件接口 4)
ltool-disk.dll 180 KB f91fdfab 商店里的插件 disk(v1.0.2,插件接口 4)
ltool-downloads.dll 66 KB 925305eb 商店里的插件 downloads(v1.0.2,插件接口 4)
ltool-grid.dll 155 KB 82f5fc36 商店里的插件 grid(v1.0.4,插件接口 4)
ltool-json.dll 79 KB 89453812 商店里的插件 json(v1.0.3,插件接口 4)
ltool-language.dll 174 KB 9db1f5dd 商店里的插件 language(v1.0.2,插件接口 4)
ltool-linuxdo.dll 178 KB df6c67cb 商店里的插件 linuxdo(v1.0.2,插件接口 4)
ltool-network.dll 100 KB f87e14e5 商店里的插件 network(v1.0.1,插件接口 4)
ltool-random.dll 136 KB d3c29c30 商店里的插件 random(v1.0.0,插件接口 4)
ltool-record.dll 276 KB 37d11120 商店里的插件 record(v1.0.2,插件接口 4)
ltool-registry.dll 94 KB 53992d90 商店里的插件 registry(v1.0.2,插件接口 4)
ltool-repair.dll 88 KB d972cb52 商店里的插件 repair(v1.0.2,插件接口 4)
ltool-shell.dll 135 KB 04380a6a 商店里的插件 shell(v1.0.2,插件接口 4)
ltool-software.dll 237 KB 62a47bcc 商店里的插件 software(v1.0.2,插件接口 4)
ltool-startup.dll 150 KB 54fa2753 商店里的插件 startup(v1.1.0,插件接口 4)
ltool-tasks.dll 96 KB d30a8464 商店里的插件 tasks(v1.0.1,插件接口 4)
ltool-user.dll 140 KB 266cce64 商店里的插件 user(v1.0.3,插件接口 4)
sha256.txt 2 KB — 上面这些文件的 SHA-256 全表(Get-FileHash -Algorithm SHA256 逐行对得上)

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

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


Full Changelog: v1.4.1...v1.5.0