Skip to content

v4.8.0

Choose a tag to compare

@2026heshao 2026heshao released this 02 Oct 08:52
· 67 commits to main since this release

下载:FloatPulse-v4.8.0-win64.zip 解压即用(便携版,数据留在程序目录旁)。
插件按需下载 .fpplug,放进程序目录的 plugin_store\ 即可在「插件中心 → 插件商店」安装。
校验完整性:sha256sum -c SHA256SUMS.txt。


[v4.8.0] - 2026-10-02

本版把 v4.7.0 之后两天的打磨一次收口,主线是三件事:图标体系与高 DPI 显示彻底对齐(自绘图标全量替换 emoji、软件图标随卡片真正缩放)、设置页体验重整(番茄钟独立成页、新增窗口透明度 / 界面缩放 / 小卡片图标大小)、插件生态与数据安全补强(应用内插件市场、数据写入滚动备份与损坏自愈、安装版数据目录双轨)。

Added

  • 插件「密码保险箱」(v1.0.0,本版首次随发布包提供):只活在本机的密码保险箱——自定义字段收纳账号密码与任意机密,Ctrl+Alt+V 开主界面、Ctrl+Alt+B 热键快速取用,复制后 30 秒自动清剪贴板。加密是 DPAPI + 主密码(PBKDF2)双因子整形库加密:换机器或没主密码都打不开,不联网、不同步、不碰宿主数据。★能力声明恒为空(capabilities: [])——它拿不到宿主任何数据接口,也拿不到网络库;vault_core 刻意不 import PyQt6(纯逻辑可单测),搜索路径永不命中 secret 字段。上一版 Release 的附件区只有 5 个插件包,本版起为 6 个(vault.fpplug)。回归:tests/test_vault_core.py 115 项 + 离屏 tools/verify_vault.py 48/48;README 插件表与 marketplace/index.json(生成器 tools/build_marketplace.py,5 → 6 条)同步
  • 小卡片软件图标大小独立可调(用户可见变更,2026-10-02 用户报「图标大小调不动」):设置 → 外观新增「小卡片图标大小」步进器(app_mini_icon_size,24–56px、每档 4px,默认 36)。根因不是主窗口——主窗口软件导航页的图标一直是 card_size - 20 严格跟随(离屏实测 card 60/96/140 → 图标位图 40/76/120),用户看到「调不动」的是悬浮球旁那张小卡片:card_window._refresh_app_page 里 btn_size = 76 / icon_px = 36 是两处独立硬编码,与设置项完全脱钩。现改为读配置驱动,并抽出唯一换算点 constants.mini_btn_size()(按钮边长 = 图标 + 40;36 → 76,与历史值一致,老用户视觉零变化)。范围常量 MINI_ICON_{MIN,MAX,DEFAULT} / MINI_BTN_PAD 收进 src/constants.py(三处共用:card_window 渲染、settings_panel 步进器、config _CONFIG_RANGES),避免两处各写一个数字日后漂移;上限 56 的依据是内容区可用宽 ≈ 440−48−40 = 352px,按钮 96px 仍排得下 3 列。接线:CardWindow.mini_icon_size 懒读 property(set_config_manager 在构造之后注入,不能放 __init__;换源即作废缓存;越界钳制、脏值静默回退默认)+ 公开门面 apply_icon_size()(D3 契约,宿主不穿透私有成员;可见且停在 app 页才重建网格,不可见只更值、切页自然生效,同值不重复重建以免长按 ± 时反复重排);MainWindow.mini_icon_size_changed 信号 → knowledge_ball.main() 转发到 ball.card_window.apply_icon_size;「恢复默认设置」广播区补该键。回归:新增 tests/test_mini_icon_size.py 25 项(常量与换算单调/钳制、配置三件套、property 懒读/越界/脏值/换源、apply 门面幂等与可见性门控、源码级钉子「_refresh_app_page 里不得再出现 icon_px = 36 / btn_size = 76 字面量」、真实渲染三档 iconSize 与按钮边长、设置页落盘/广播/钳制/refresh 不误广播、MainWindow 信号 AST 断言);新增离屏 tools/verify_mini_icon_size.py A–G 七段全通过(三档实测 iconSize 24/36/56、按钮边长 64/76/96,deleteLater 结算后按钮数恒为 5 无残留;主窗口卡片链路回归 card=140 → 图标 120px);test_settings_nav 搬运完整性参数表补 _set_mini_icon_size;verify_settings_groups 8/8(外观组加行后分隔线动态校验通过,脚本零改动);全量 pytest 2035 passed / 3 skipped / 6 subtests;ruff 全绿;真截图目检 build/shots/mini_icon_{24,36,56}.png + mini_icon_settings.png(24px 一行 5 个 / 56px 自动换行 3+2,布局不溢出)。⚠本条的「主窗口图标一直正常」结论有误,已在下方 Fixed 段修正:当时只量了 pixmap 对象尺寸(card_size - 20),没量真实渲染像素;实机上是 QIcon 不放大导致图标恒卡在源帧尺寸(.lnk 更是恒定 40px),主窗口同样受影响
  • 番茄钟设置独立成页(用户可见变更,2026-10-02 用户拍板):设置页左侧分类导航新增「🍅 番茄钟」分类(原「⚡ 全局工具」卡内的 4 项整体搬迁:总开关 / 专注时长 / 休息时长 / 自动进入休息),工具卡此后只留快速捕捉与截图钉屏两组热键。纯 UI 重组,零行为变更——控件属性名、_on_pomodoro_* 回调、refresh() 同步与 pomodoro_changed 广播链一律不动,float_data/config.json 的 pomodoro_* 四键口径与老配置零迁移。设置分类 9 → 10(SETTINGS_CATEGORIES 新增 ("pomodoro", "🍅", "番茄钟"),插在 tools 与 system 之间);「全局工具」卡的末行标记随搬迁改挂到截图热键行(末行无悬空分隔线的不变量保持)。回归:test_settings_nav 分类数断言 9→10 + 搬运完整性参数表补「番茄钟」注释分组;verify_settings_groups 期望卡序同步(A/B 段 11 张卡逐卡校验通过,含「软件更新」子卡);全量 pytest 2035 passed / 3 skipped / 6 subtests;test_init 冒烟全 [OK];使用说明页「设置」章节的分类清单与「全局工具」描述同步改写;light/dark 双主题真截图目检(build/shots/settings_pomodoro_{light,dark}.png,临时截图脚本跑完即删)
  • 插件中心卡片降噪(用户可见变更,2026-10-01 用户拍板「降噪保留 + 动作保持逐行」):对照用户截图诊断——卡片信息全平铺(描述两三行 + 📖摘要 + 动作逐行 + 依赖行 + 能力行 + 4 按钮)导致单卡过高、一屏只能看两张。降噪三项:①描述截断:卡面最多 DESC_MAX=64 字(双列卡宽 ≈2 行,一眼能扫完),超出截断加省略号、全文进悬停提示(新增纯函数 clip_text);②依赖行消失:requires(PyQt6.QtCore 这类开发者信息)不再占卡面行,连同被截断的描述全文一起挂进卡片整体悬停提示(QToolTip 沿父链找最近有提示的控件,悬停卡面空白处可见)——有无能力行行为一致;③能力徽章化:原「能力: 🌐 网络访问、✍ 写入数据…」文字行改为一排胶囊徽章(pluginCapBadge,短文案 🌐 网络 / ✍ 写入 / 🛠 改删 / 🧠 AI,完整语义在徽章悬停提示 CAP_TIPS,从 _make_card 局部 dict 提升为模块级三卡共用),淡主色底 QSS 与 pluginStoreBadge 同族(light/dark token 双主题齐备)。附带:标题行「名称 v版本」拆成两个 QLabel(版本弱化为灰小字),商店卡 / 在线市场卡同步同款降噪(版本拆分 + 徽章行 + 描述截断,源包名/热键/体积保留文字行);「(无注册动作)」从主文字色(pluginCardDesc)降为弱化灰(pluginActionTag)不与描述抢层级。信息零删除——描述全文、依赖清单、能力语义全部可悬停查看;动作清单按用户选择保持逐行不动。回归:新增 tests/test_plugins_card_polish.py 15 项(clip_text 边界 / 长描述截断+悬停全文 / 依赖不占卡面且悬停可见(有无能力两态)/ 徽章数量短文案语义提示 / 标题拆分 / 商店卡在线卡同款 / 网格回归);既有 test_plugins_panel 42 项全绿(文本存在性断言不受排版影响);全量 pytest 1965 → 1987 passed;verify_plugins_panel 23/23 + verify_plugins_page 19/19;light/dark 双主题真截图(build/shots/plugins_card_{light,dark}.png,临时探针跑完即删);test_init 冒烟 RC=0。二次收敛(同日用户复看反馈「介绍重复太多,使用说明看 md 就行」):卡面 📖 使用说明摘要行整体移除(描述与摘自 md 的摘要两段都在介绍「这插件是干嘛的」,重复感是主要观感问题;详情点「查看使用说明」走程序内 md 查看器,信息零丢失)——usage_summary() 函数与 USAGE_SUMMARY_MAX 常量随之删除(无消费者不留死代码),theme.py 的 pluginCardUsage 死规则清理;描述再收短 DESC_MAX 64→48 字(约一行半,悬停全文保留)。测试同步:test_plugins_panel 的摘要函数三测删除、「摘要上卡」断言反转为「摘要不上卡 + 按钮保留」,verify_plugins_page C6 反转,test_plugins_card_polish DESC_MAX 常量断言对齐;复验:插件相关 54 项 + 全量 pytest(我方改动面全绿)+ verify_plugins_page 19/19 + verify_plugins_panel 23/23 + 双主题截图 build/shots/plugins_card2_*.png 目检(单卡高度再降约 1/3)+ 冒烟通过
  • AI 助手 1.12.0:新增「清空全部任务」动作 + 动作协议防谎报强化(用户可见变更,2026-10-01 用户实测反馈):用户实测「将任务清空,设置明天任务为…」时模型编造 delete_task #1/#2 编号(数据里根本不存在)被 id 护栏全部安全跳过,却在正文谎称「已清空了所有现有任务」——任务实际没清空。三处修复:①新增 clear_tasks{} 动作(无参数、manage 级 → 走既有确认卡「【清空】全部任务(当前 N 条)」),执行层现取现删(不用校验时旧快照,确认期间数据变化无碍)、逐条删除收集撤销令牌(清空 N 条 = N 个令牌,结果卡仍可「↩ 撤销删除」一键恢复;_execute_and_report 支持 list 令牌 extend)、部分失败如实报「成功 2/3 条」、空列表如实说「无需清空」;②协议规则补两条:清空/批量删除用 clear_tasks 不要逐条编造 delete_task;文字说明必须与动作一致——动作被跳过或失败时如实说明未完成,禁止声称已删除/已清空;③parse 校验豁免 clear_tasks 的 content 必填(无参动作)。契约注意:clear_tasks 是唯一无 id 的 manage 级动作(不进 _OP_TARGET),原「manage 级 ⟺ 需要 id」契约测试同步加例外。回归:test_ai_assistant_actions 60 → 67 项(parse 无参通过 / 描述带快照数量 / 执行层桩替身三场景——全成功返令牌列表、部分失败如实报、空列表诚实 no-op / _execute_and_report 聚合令牌 / 协议含防谎报条款);全量 pytest 1965 → 1972 passed;ruff 全绿;已重打包 plugin_store/ai-assistant.fpplug(1.12.0,37.5KB)
  • AI 总配置新增本地模型参数:思考模式 + 上下文长度(用户可见变更,2026-10-01 用户拍板:思考默认关 / 上下文默认 16K / 改参自动重启):设置 → AI 配置在本地模式下新增两行——「思考模式」(关=响应更快 / 开=输出思维链,ai_local_thinking)与「上下文长度」(8K/16K/32K/64K 档位,ai_local_ctx_size),仅对本地 llama-server 后端生效(纯启动参数,ctx.ai.params() 契约零变化,插件无感零迁移)。实现:config 三件套同步(bool 不进 RANGES;ai_local_ctx_size (2048,131072) + ai_server.sanitize_ctx_size 双层收敛);build_launch_args(gguf, port, ctx_size, thinking) 参数化;新增 AiServerManager.restart() 状态驱动平滑重启(运行中改参 → stop 后等旧实例真正退出(finished 信号)再以新参数拉起,替代固定延迟猜测——Windows 下 terminate 对无窗口控制台程序可能迟至 3s 强杀才生效;重复调用以后一次参数为准,期间被手动拉起则丢弃 pending 防双启);设置页 _ai_start_local 统一保存/手动启动入口(参数未变不打断、变化自动重启并提示);两行随本地/云端模式显隐同步(_ai_apply_mode_ui 补齐)。★参数选型实测存档:--reasoning-budget 0(预算立即耗尽)不被 Qwen3.5 模板遵循——同题对照实测 b10690:budget 0 与默认一样吐完整思维链(384 tokens 耗尽、content 为空、5.7s),--reasoning off 才是真开关(直答 0.3s/14 tokens,快约 20 倍),故「关」实现为 --reasoning off。回归:test_plugin_ai 扩至 29 项(新键类型/范围/bool 不进 RANGES、launch args ctx 收敛与 reasoning 旗标有无、restart 直启/覆盖/防双启守卫);verify_settings_groups 8/8(AI 卡加行后动态校验零改动通过);verify_ai_backend 26/26(B3/B4 补两新行显隐断言;顺带修正过期的 C1——weekly-report 自 v1.1.0 声明 ai 能力后属合法候选,反例换真零能力的 kb-search);全量 pytest 1965 passed / 3 skipped / 6 subtests;本地模式 AI 卡 light/dark 双主题真截图目检(build/shots/ai_local_card_*.png,临时脚本跑完即删)
  • 设置新增「窗口透明度」(用户可见变更,2026-10-01 用户提出):设置 → 外观与主题新增「窗口透明度」步进器(window_opacity,50–100%、每档 5%,默认 100=不透明)——主窗口整体半透明,方便「盯参考/对照材料」时让窗口退到背景(悬浮球、便签、卡片窗不受影响)。实现:config 三件套同步(int + _CONFIG_RANGES (50,100),下限 50 保文字可读——Qt windowOpacity=0 时窗口命中测试直接穿透不可点击);主窗新增 _target_window_opacity()(百分比→0.5~1.0 收敛,垃圾配置回 1.0 的防御纵深)与 _apply_window_opacity()(setWindowOpacity 单点应用,不走全量主题刷新);入场淡入终点改为读配置目标(_play_entrance_animation 原硬编码 setEndValue(1.0),预设半透明时启动动画直接到位,不闪「先不透明再变透明」);设置页 Stepper 改动即时持久化并直调主窗应用(getattr callable 守卫);「恢复默认设置」广播区补透明度回 100% + Stepper 同步刷新(refresh 走 blockSignals 护栏)。回归:新增 tests/test_window_opacity.py 17 项(config 三件套越界/错型拒写、垃圾加载回退、目标值收敛含防御纵深、真窗 show 后入场淡入终点=配置目标 70%≈0.7、Stepper 夹取/落盘/直调/同值省写、host 缺方法不崩、恢复默认回不透明且控件同步);全量 pytest 1948 → 1965 passed;verify_settings_groups 8/8(外观组加行后分组结构 B 段动态校验通过,脚本零改动);test_init 冒烟全 [OK]
  • 悬浮球右键菜单分组收纳(用户可见变更,2026-10-01 用户拍板):6 个插件动作此前全部平铺在一级菜单(站内搜索 / 周期任务 / 立即检查周期任务 / 密码保险箱 / 快速取用 / 生成日报周报草稿),装几个插件菜单就长一截。现参照 Win11 桌面右键「新建 >」的层级(用户提供的样图)把全部插件动作收进「🧩 插件功能」子菜单:一级菜单定型为「打开主窗口|开始专注|🧩 插件功能>|✂ 截图钉屏|退出程序」五段(分隔线分组不变,「截图钉屏」按用户选择留一级直达),二级保留各动作图标与标题、随启用/停用动态增删(refresh_plugin_menu 既有入口不变);子菜单显式套同一份 get_menu_qss(与托盘「📌 便签」子菜单同款做法),light/dark 跟随宿主主题(ball.apply_theme 正规换肤链覆盖)。回归:verify_plugin_loader 35/35(E1/E2 期望列表更新为子菜单形态 + 新增 E2b 子菜单收纳断言;F1/G2 计数同步);verify_weekly_report 65/65(D/E 段改走子菜单取 QAction);全量 pytest 1948 passed;test_init 冒烟全 OK;light/dark 四张真截图验证菜单结构与换肤(build/shots/ball_menu*,临时探针跑完即删)。★顺带对齐 verify_plugin_loader 三条过期断言(只改脚本不改产品):D1「.fpplug 自动解压后加载」→「残留 .fpplug 不再自动解压」(双目录模型 2026-09-27 移除自动解压后脚本一直没跟上,C4/F1/E2b 连锁红灯同源)
  • AI 助手思考动画自绘化(v1.11.0,用户可见变更):请求在途的「打字中」三点气泡从 ●○○ 字符帧轮换(180ms/帧,4 帧往返) 换成 自绘连续动画控件 ThinkingDots——①零字体依赖:●○ 几何符号在 msyh 下与 ▶▾▸ 同属 supportsCharacter=False 高危区(图标自绘铁律的既判场景),改为 paintEvent 画三个圆点后彻底摆脱字体;②观感:三点按正弦相位做垂直弹跳 + 透明度呼吸(33ms/帧 ≈30fps、周期 900ms、三点各差 1/3 相位,只取正半周弹跳落基线平滑),比 4 帧跳变顺滑;③主题跟随:颜色取宿主主题 $primary 并订阅 theme_changed(IconButton/PageTitle 同款 callable 守卫),换主题即时换色;④减弱动效联动:设置「减弱动效」开启时不启动计时器,静止显示三点(相位取 0.125 让透明度呈阶梯,不呆板);⑤气泡壳 chatBubbleThinking QSS 不变,同步删除已无消费者的 chatThinkingDots 文字规则(theme.py)。接线:页面级 _think_timer/_tick_thinking/_THINK_FRAMES 删除(计时器归控件自管,_hide_thinking 显式 stop() 双保险)。回归:新增 tests/test_ai_thinking.py 9 项(尺寸/启动即动画/stop 停/相位推进/离屏渲染非空/减弱动效静止但仍渲染/无宿主取色兜底/theme_changed 换色/页面挂气泡幂等+靠左+拆除计数回落+发送全链路在途挂起回复到达拆除);verify_ai_assistant 121/121(C20b/C20c 断言对齐新实现;顺带修正过期的 A7——weekly-report 自 v1.1.0 声明 network+ai,「未声明」反例换真零能力的 kb-search);全量 pytest 1820 passed
  • 五插件第二批增强(2026-10-01,用户可见变更):首批「推荐项」之外的五个可选批次一次做完,版本齐升 —— ①AI 助手 1.10.0:会话导出 Markdown(会话栏「💾」→ 选路径落盘 utf-8+LF,逐轮问答原样导出,快捷指令只导界面文案不含数据块,空会话拦截)+ 自定义快捷指令(快捷行「+」弹小对话框添加,上限 6 条随 config 落盘,点击直发不附加数据块,右键删除)。②AI 文本工坊 1.5.1:源文超 8000 字在「处理中…」状态行明示「已按上限截断发送」(此前悄悄砍内容)。③站内搜索 1.2.0:搜索历史(最近 10 条存插件私有目录,输入框为空时显示「最近」行,点击重搜并置前、右键删单条、可清空;读入消毒+原子写+坏文件降级)。④周期任务 1.2.0:周末顺延(规则可勾「周末触发日顺延到下周一」——周六/日触发改到下周一同时刻生成,双周末日顺延撞同一周一自动合并为一条;rule_fires_shifted 谓词换入 latest_fired_day/next_fire_at,老规则缺省行为逐日等价零变化)+ 规则导出/导入 JSON(import_rules 逐条 validate_rule、rule_id 撞车跳过现役优先、坏条目计数、MAX_RULES 照守)+ 规则卡片显示上次生成的任务标题与时刻(last_task 随 _fire 落档、编辑透传不丢)。⑤日报周报 1.2.0:段落开关(预览区「任务/碎片/笔记」三勾选即时重生成,build_report 新增 include 参数缺省 None=与旧版逐字节一致,任务勾选同时控制完成+未完成两段,专注统计恒保留)+ 导出历史(另存/写 vault 成功记入最近 10 条,exports.json 原子写,footer 新增「🕘 最近导出」菜单点击打开所在文件夹、可清空;既有五按钮文案未动,verify F6 保持全绿)。回归:全量 pytest 1743 → 1820 passed(新增 test_ai_assistant_export 22 / test_kb_search_history 15 / test_recurring_enhance 19 / test_weekly_v120 18 + workshop/kinds 各 +3);verify_weekly_report 65/65;5 个 .fpplug 已重打包;test_init 冒烟全 [OK]。★顺带修掉 test_kb_search_kinds 的 QApplication 裸调用雷(单跑 RC=127 硬崩,此前靠组合跑掩盖)——_APP 留存引用,项目铁律再现
  • 五插件优化增强批次(2026-10-01,用户可见变更):按「推荐项」范围对 5 个内置插件各做一轮增强,版本齐升 —— ①AI 助手 1.9.0:会话持久化 + 多会话(成功问答自动存 sessions.json,原子写、单会话 100 条/全局 15 个上限,重启恢复上次会话;页顶下拉切换历史会话、+新开、🗑删除(最后一条=清空内容);提示气泡与动作结果卡不落档,恢复只重现问答主线;坏档降级保留原文件)。②AI 文本工坊 1.5.0:结果历史(最近 20 条存 history.json,「🕘 历史」卡点击回填结果区可再落库,「提取待办」历史回填后转任务按钮照常可用)+ 自定义指令收藏(「★ 收藏指令」存成按钮、去重 10 条上限、点击填入右键删除)。③站内搜索 1.1.0:数据源范围过滤(搜索框下五源勾选默认全开,全开=与旧版逐条同路径;kb_search_core.search 的 kind 参数升级为可接受集合——分数在全库算好后按范围排除,多选排序与不过滤一致)+ 跳转自愈(碎片/笔记跳转前用同一份只读快照模拟目标面板的子串过滤(与宿主 _frag_search/_note_search 同口径),bigram 交叉召回必空时改跳空关键词只切页并 toast 说明,消灭「跳过去列表是空的」)。④周期任务 1.1.0:新增「每 N 周」规则类型(nweekly:间隔周数 1–52 + 星期几多选,锚点=创建当周的周一、周索引按周一对齐,编辑保留原锚点周期不漂移;老规则文件零迁移)。⑤日报周报 1.1.0:一键 AI 润色(「🧠 AI 润色」把当前草稿交 AI 总配置润色回填预览;手动编辑过的草稿先确认再覆盖;manifest 能力从只读升为 network+ai,只在点击时刻发请求、只送草稿文本)。回归:全量 pytest 1687 → 1743 passed(新增 test_ai_assistant_sessions 16 / test_workshop_history_ui 10 / test_kb_search_kinds 15 / test_weekly_polish 12 / recurring+workshop 纯逻辑扩充);verify 全绿——ai_assistant 121/121、text_workshop 34/34、kb_search 55/55、recurring_tasks 46/46、weekly_report 65/65、plugin_p0p1 37、plugins_panel 23/23、plugins_page 19/19;5 个 .fpplug 已 pack_plugin --force 重打包(plugin_store/)。⚠ verify_plugin_loader 出现 6 个红灯,经查为并行会话未提交的托盘/崩溃标记改动(knowledge_ball/tray 中间态)所致,与本批次无关(隔离探针:失败点全在其自有 fixture 与球菜单组合,不触及任何本批改动文件)

Fixed

  • 软件图标不随卡片缩放(用户第二次反馈,根因是 QIcon 从不放大):上一轮加了「小卡片图标大小」设置项后,用户截图指出主窗口软件导航页同样有问题——卡片框跟着设置放大了,里面的图标纹丝不动。自查发现上轮只量了 pixmap 对象尺寸(card_size - 20,看着是对的)而没量真实渲染像素,结论不成立。真机取证(真实 Windows 平台 / dpr=1.25)定位两层根因:①**QIcon.pixmap() 从不放大**——源帧小于请求尺寸时原样返回;②对**.lnk 快捷方式更极端:请求 40/76/108/120 一律只回 40×40(shell 给的 32px 图标 ×1.25),而 availableSizes() 谎报有 320 帧、actualSize(108) 也谎称能给 108 → 上游无从察觉。真实平台实测 card 60→140 图标可见宽恒定 39→39(跨度 0),与用户截图完全吻合。修复两条:①新增 src/win_icons.py——用 stdlib ctypes 调 shell jumbo 图像列表(SHGetImageList(SHIL_JUMBO) + IImageList::GetIcon,vtable 下标 10)拿 256×256 真高分图,经 GetIconInfo + GetDIBits 转 QImage(★GetDIBits 必须给 memory DC,传 NULL 直接失败——踩过;GetIconInfo 新建的两份位图由调用方 DeleteObject 回收,DestroyIcon 同理);这条路径连 .lnk 也能出大图**,顺带修掉「快捷方式显示成通用箭头图标」的观感问题。②新增 _fit_icon() 尺寸收口(extract_exe_icon 与 load_icon_pixmap 共用,口径唯一):把任意来源位图归到「逻辑边长 == 请求值」,dpr>1 的(QIcon 用 dpr 凑逻辑尺寸)原样保留不重采样,dpr==1 的按屏幕 dpr 补物理像素避免高 DPI 发虚。修复后真实平台实测 icon 38→116(跨度 78),逐档核对「卡片 −20 = 图标」。健壮性:win_icons 非 Windows / API 缺失 / 取图失败一律返回 None 回落 Qt 路径且永不抛异常,结果按规范化路径缓存(含失败结果,缓存命中 0.007ms,拖尺寸步进器不会每档重跑 shell)。回归:新增 tests/test_app_icon_scale.py 20 项(_fit_icon 放大/缩小/已正确不重采样/高 dpr 源保留/空位图/0 尺寸;extract_exe_icon Qt 兜底也收口、shell 优先且收口、缓存命中、源码级钉子「shell_icon_image + _fit_icon 不得被删」;win_icons 脏输入不抛/缺文件返 None/真实 exe ≥128px/缓存同对象);新增离屏 tools/verify_app_icon_scale.py A–E 全通过(端到端四档 visible bbox 38/72/103/114 单调增、跨度 76,修复前恒 0);全量 pytest 2035 → 2055 passed / 3 skipped / 6 subtests;ruff 全绿;真实 125% DPI 对照真截图 build/shots/icon_scale_{before,after}.png(前:四档恒定 40px 且带快捷方式箭头;后:40→76→108→120 且是软件真图标)
  • 细节优化与 bug 排查批次(2026-10-01 晚,全面排查):①侧栏拖宽重启即丢(用户可见)——side_bar_width 被 _save_side_width 读写落盘、却从未声明进 config 三件套,而 _load() 只认 DEFAULT_CONFIG 的键 → 每次重启宽度静默回 168;现补齐三件套(默认 168 / int / (140,280) 与 SIDE_BAR_MIN_W-MAX_W 对齐),test_restore_from_config 语义同步(越界值配置层拒绝内存保持合法值 + UI 层 _clamped_side_width 兜底夹取双层都钉死)。②AI 服务停止的 3s 强杀定时器会误杀新实例——stop() 后旧实例 3s 内退出、restart() 拉起新实例,过期定时器判 self._proc 把新实例 kill 掉(Windows terminate 快速生效时 restart 路径必触发);改为闭包捕获当时那个旧实例,只对它操作(test_stop_kill_timer_tracks_old_instance 三场景钉死)。③AI 服务启动失败状态机卡死——exe 缺失等 FailedToStart 错误进程不会发 finished,_proc 永不清空 → 之后 start() 全被「已在启动/运行中」守卫拒绝,只能重启程序恢复;现 _on_error_occurred 就地清 _proc + 停探活复位状态机(Crashed 等错误仍等 finished 清理,语义不变;test_error_occurred_failed_to_start_resets_state_machine 钉死)。④第二实例唤醒静默失效——唤醒后台线程(无 Qt 事件循环)里 QTimer.singleShot(0, cb) 永不触发(offscreen 探针实测 FIRED=False),第二实例拉起主窗功能一直没生效;改主线程 QObject 信号桥(跨线程 emit 自动排队回主线程,探针验证 FIRED=True IN_MAIN_THREAD=True)。⑤页后台预热异常会崩整个进程——_warm_one_page 是 QTimer 槽,PyQt6 对槽内未捕获异常默认 qFatal 中止;预热属锦上添花,现单页失败仅 warning 留痕(切页时会再走一次构建)。⑥托盘右键菜单悬空隐患——setContextMenu 不接管所有权,QMenu 靠子菜单闭包引用环碰巧续命;显式 self._tray_menu 持有。⑦五处静默吞异常补日志:AI 探活输出订阅者回调、剪贴板碎片写入(文本/路径)、全局热键回调(此前表现为「热键按了没反应」无从排查)、config 写盘失败;顺带修正 ai_local_thinking 过时注释(--reasoning-budget 0 → --reasoning off)。⑧ai_server _on_finished 的 del was → was.deleteLater()(finished 信号发射途中丢最后引用,防 wrapped-object-deleted)。⑨信号桥落地漏写信号声明(⑧修复的真机回归,用户截图反馈)——_WakeupBridge 类体漏了 woke_up = pyqtSignal() 类属性,第二实例唤醒路径运行期 AttributeError;补回声明并新增 tests/test_wakeup_bridge.py 2 项护栏(AST 静态断言信号声明+槽方法+QObject 基类缺一不可、按真实源码 exec 出类实例化 connect→emit 全链路;反向验证:内存构造漏信号的坏源码必触发断言失败)。回归:全量 pytest 1988 passed / 3 skipped;ruff 全绿
  • 小卡片 Tab 快速连点「键与页错位」(移除页间滑动转场,用户可见变更):小卡片左侧图标栏快速连点时,选中键与显示页可能永久错位(如截图:task 键亮着、页面停在临时笔记)。根因是页间 260ms 滑动转场的结构性缺陷——转场期间真实 QStackedWidget 停在源页索引且被隐藏(动画结束才 setCurrentIndex),_switch_mode 每次读 stack.currentIndex() 当转场起点,连点即读到过期索引:第二击点回转场源页时走「同页直切」分支、旧转场 finished 稍后仍把 stack 设成旧目标页 → 键亮 A 页显示 B,且不再自愈;即使不错位,第二段转场也从过期源页起跳产生视觉跳变。该快照合成转场已有三代实现、两代翻车史(淡入穿帮/双层玻璃发白,docstring 有存档),修补成本高于收益,按用户授权整体移除:删 _animate_page_transition / _page_content_snapshot / _finalize_page_transition / _TransitionCanvas(约 200 行),_switch_mode 收敛为「选中态 + 指示器滑动 + 即时 setCurrentIndex」——页面即切、指示器仍带 260ms 滑动余韵,错位从构造上不可能发生;驱动旧转场的回归脚本 tools/check_transition.py 一并删除。回归:新增离屏护栏 tools/check_tab_rapid_switch.py 5/5(沿真实按钮信号路径复现旧错位序列 fragment→note→task→fragment 中途不泵事件,断言 stack/checked/_last_mode/可见性四方一致 + 全页轮巡两圈逐点一致 + 同页重复点击幂等);全量 pytest 1687 passed / 3 skipped 不变,test_init 冒烟全 [OK]
  • 碎片「钉为便签」不再自动收录笔记(用户可见变更):碎片页右键「📌 钉为便签」此前复用转存公共路径——先 add_note() 建一条笔记再钉它,每次钉便签笔记管理就多一条「💾 xxx」记录,污染笔记库(用户拍板取消)。现便签新增 kind="fragment" 直接锚定碎片:内容写回 fragments.json、不经过笔记;碎片无标题字段 → 标题取内容压平前 20 字、右键无「标题重命名」、删除项为「🗑 删除碎片并关闭」;孤儿清理照常(碎片被删 → 便签自动关闭、几何记录清掉)。笔记「钉到桌面」/任务「钉为便签」不受影响(本就锚定已有数据)。接线:StickyNoteManager/StickyStore 增加 fragment_manager/fragment_ids 注入(None = 碎片便签不可用,旧构造点零改动),宿主构造点补传;_to_note_id 公共转存路径保留给「💾 存为笔记」。回归:verify_sticky_notes 新增 H 段 7 项 → 29/29(H2 ★ 直钉后笔记数不变 / H5 编辑写回碎片 / H6 标题随内容刷新 / H7 删碎片孤儿关窗);全量 pytest 1782 passed(全量中两处瞬时红灯 test_kb_search_history/test_ruff_clean 单跑与手动复跑全绿 = 并行会话写文件竞态,非本轮改动面);test_init 冒烟全 OK

Added

  • 首启三步引导(3.4):新增 first_run_done 与 src/onboarding.py——首启 800ms 后弹出三步向导(① 热键速查:Ctrl+Alt+K 快捕 / Ctrl+Alt+S 截图钉屏 / Ctrl+K 站内搜索需装 kb-search 插件 / Esc 关卡不退出;② 悬浮球玩法:贴边吸附、悬停弹卡、拖文件进碎片池、拖 exe 建启动器、关窗收托盘;③ AI 可选说明),文案口径逐条对照 README 与实际注册热键,Esc/跳过即完成不再骚扰;设置页「🔄 重看引导」可随时重看
  • 界面缩放(3.5):设置外观新增「界面缩放」85–150% 档位(ui_scale),全局字号按 round(BASE_FONT_PT × 比例) 即时重设并走主题广播链,不重启生效;刻意只缩字号不缩 px 布局(低风险取舍,85–130% 观感最佳),QSS 内 75 处写死 px 字号不随缩放(已列清单为遗留)
  • 大列表窗口化渲染(3.6):新增 src/list_windowing.py 分块建行助手(首屏 120 行 + 滚到底续载),接入碎片工作台与日程任务的 refresh(≤200 行全量直建与旧路径逐字节等价);500 条实测 refresh 3.3–3.7ms(全量直建对照 7.3–7.8ms),筛选/搜索/选中恢复语义不变
  • 数据安全三件套(成熟化批次):① config.json 损坏不再静默丢设置——先备份 .corrupt.bak 再回退默认,启动时托盘告知(此前 except: pass 无痕迹回退,热键/AI key/主题全丢);② 全部数据 JSON 写前滚动备份(float_data/backups/ 每日一份保留 7 份,失败不阻断保存)+ 顶层 data_version 版本号与迁移层(json_store.STORE_VERSIONS/MIGRATIONS,存量文件零迁移);③ 6 个数据 Manager(fragments/notes/tasks/groups/stickies/assets)统一走 save_records 骨架(写前备份+版本号+原子写,fragments 保持紧凑格式)
  • 插件网络桥 SSRF 防护闸(src/net_guard.py):协议白名单(仅 http/https)+ 本地主机名黑名单 + getaddrinfo 解析全量私网判定(回环/私网/链路本地/保留/组播/未指定,IPv4-mapped IPv6 先还原再判)+ 解析失败 fail-closed;拒绝结果与既有 _deny 同构并进审计日志。本地 AI 不被误伤:宿主经 ai_server.sync_loopback_allowlist 把 llama-server 端口与回环 base_url(Ollama)登记进回环白名单(启动 + 设置页保存时各一次),未登记的回环/内网一律照拒;set_allow_private_network 留作后续高级开关
  • 安装版数据目录双轨(2.2):zip 便携版(包内含 portable.marker)数据随 exe 走、行为与历史一致;安装版(frozen 无 marker)数据走 %APPDATA%\FloatPulse(APPDATA→LOCALAPPDATA→exe 目录三级兜底),首启检测旧目录有数据时弹一次询问迁移(整树复制+数量校验,旧目录改名 float_data.migrated-<时间戳> 留备份,绝不删除);源码运行模式行为不变(app_paths.resolve_data_root 纯函数 + 模式矩阵测试);logger 与 TempAssetManager 在安装版跟随数据根,不再写进安装目录
  • 更新被动提示(2.3):启动 15s 后静默检查(默认每 7 天,设置页「自动检查更新」可关;只访问 GitHub Releases API,不自动下载、失败静默),发现新版托盘气泡 + 关于页版本旁「(有新版本 vX.Y.Z)」
  • 主题「跟随系统」(3.1):theme 新增 follow 值,经 QStyleHints.colorScheme 解析并监听 colorSchemeChanged 即时全链路重刷(主窗/球/卡片/便签/快捕条/截图层);设置页外观三态(浅色/深色/跟随系统);老版本 PyQt 缺 API 时安全回落深色
  • 崩溃可感知(3.2,后续按用户反馈瘦身):日志新增成对的会话标记([会话] 启动 vX.Y.Z / [会话] 正常退出)圈出会话区间,供人工排障定位会话边界;设置页「关于」新增「📂 打开日志」直达数据目录(启动时的「上次可能异常退出」气泡已移除,见 Removed)
  • 首次关窗收进托盘提示一次(3.3):主窗口 closeEvent 收进托盘时托盘气泡说明「程序仍在后台运行」(tray_hint_shown 防重复,信号分层不改分层纪律)
  • 发布工程三件套(2.1/2.4/2.5):ISCC 进 CI(choco 安装 Inno Setup,FP_ISCC_PATH 注入,找不到时软跳过不崩)→ Release 自动产出 setup.exe;发布前「三源对齐」自检(tag ↔ app_version.py ↔ CHANGELOG 标题,不符即终止发布,tools/check_release_consistency.py 可本地复用);产物附 SHA256SUMS.txt(zip/setup.exe/全部 fpplug,GNU 格式可 sha256sum -c);新增 GUI 夜检 workflow(workflow_dispatch 手动档,run_gui_check.py --only 跑 7 个精选离屏脚本)
  • CI 加 ruff 零告警闸门 + pip-audit 信号位(4.1):ruff==0.16.9 写死进 tests.yml,规则只查正确性(E4/E7/E9/F/W605,不开风格类);全仓一次性清零 114 处机械违规(F401 未用 import 90 处为主,全部零行为变化,pytest 全绿背书);pip-audit 非阻断任务先给信号(优化清单 3.5 落地)
  • 应用内插件市场「🌐 检查在线市场」(按需联网,与更新检查器同一立场):插件商店弹窗新增「🌐 检查在线市场」按钮——点击那一刻向 GitHub API 发两次 GET(仓库 marketplace/index.json 索引经 contents 接口取 raw + /releases/latest 解析附件 asset id),列出「远端有而本地未装」的插件卡片(名称/版本/描述/能力/热键/体积),点「⬇ 下载安装」即经 octet-stream 通道下载 .fpplug → sha256 校验(索引带哈希,不符即丢弃并提示,绝不把坏包放进商店)→ 原子落盘 plugin_store/(临时文件 + os.replace,同名旧版本源包被替换但不碰已安装目录)→ 委托既有 _on_install 链路(rescan / 菜单重建 / 商店刷新 / 结果反馈全覆盖)。失败分级文案:403 → API 限流提示、404/超时 → 对应可重试提示、其他 → 「无法连接 GitHub,离线不影响任何本地功能」;不做静默升级——已装插件不出在线卡,本地版本低于远端时只提示「可更新」(先卸载再装),绝不覆盖用户已装目录;busy 期间重复点击被忽略。基础设施:src/plugin_market.py 纯逻辑模块(刻意不 import PyQt6:索引解析逐条消毒——id 过 is_safe_plugin_id、file 强制纯文件名防路径穿越、sha256 格式校验、size 正整数;已装状态分流复用 parse_version 宁漏报不误报;http_get_bytes 二进制下载与 plugin_net 同契约永不抛异常 + SSRF 闸);plugin_net.py 新增二进制 GET 通道(http_get_bytes + make_async_bytes_getter,与文本 GET 同一条 relay 管道)——二进制不能走 utf-8 replace 解码,单独实现。索引生成器 tools/build_marketplace.py(扫 plugin_store/*.fpplug 出 marketplace/index.json,manifest 缺 description 退回使用说明首段;--check 模式可进 CI)——索引只存文件名不存 asset id(每次发版 id 都变),运行时按文件名解析,插件增改时重跑一次即可,发版不用动。回归钉子 tests/test_plugin_market.py(27 项:索引解析逐条坏只丢那条 / 已装分流 / 附件解析 / 本地 HTTPServer 实测下载 200/404/超限/SSRF 拦截 / sha256 / 落盘原子替换与穿越拒绝)+ 离屏端到端 tools/verify_plugin_market.py(10/10:本地伪造「索引+Release+附件」三件套走真实点击路径——出卡、下载安装全链路、sha 拒收、缺附件、403/404 分级、已装过滤、busy 防重,light/dark 双主题真截图)

Removed

  • 启动时「上次可能异常退出」托盘气泡(3.2 的提示半边,用户可见变更):按用户要求移除。根因不是误报——日志取证(2026-10-01)显示判定口径正确([会话] 启动 后无 [会话] 正常退出 即真),但当天 6 次启动前的退出全部是强杀(开发/调试场景 IDE 停止 / Ctrl+C / 杀进程是常态,历史 61 次 aboutToQuit 正常触发均为日常使用日),导致判定恒真、每次启动必弹、沦为纯噪音。保留:成对 [会话] 标记仍写入日志供人工定位会话边界;设置页「📂 打开日志」入口不动;config 损坏重置告知气泡不动。改动面:tray.notify_startup_once() 去参(只保留 config 损坏告知)、logger.previous_session_abnormal() 与 16KB 尾窗扫描删除、启动装配处(knowledge_ball)不再判定;test_crash_marker.py 收敛为纯标记写盘/收尾排序契约(3 项),test_tray_controller.py 启动告知测试同步(corrupt 单气泡 / 正常零气泡)

Changed

  • 启动闪屏玻璃化 + 悬浮球品牌图形(用户可见变更):启动第一眼的闪屏此前是「实底白卡 + 细边框 + 干巴巴的旋转弧」,与全应用玻璃拟态语言割裂(便签玻璃化同因)。现整体重绘——①玻璃化本体:glass_fill 半透明填充 + primary_border 描边 + 顶部 1px 内高光,light 白玻璃 / dark 深蓝黑玻璃随启动主题;token 的函数式 rgba() 串 QColor 不认 → 内置 _css_color 小解析器(第 4 参 >1 按 0-255、≤1 按比例,失败回退兜底色绝不画黑;与便签同因同款);②品牌图形 = 悬浮球:替代裸旋转弧——主题色径向渐变球体(高光偏左上 30%)+ 白色高光点,球体随帧计时器正弦上下浮动 2.5px(周期 1.5s),底部影子随升降缩放淡出(悬浮感的关键参照物),外围保留旋转弧(_angle 帧驱动口径不变);③入场淡入 160ms(OutCubic;与 300ms 淡出共用 windowOpacity,finish 时先停入场动画防打架);④主窗出场动画调优:上移 10px→16px、260ms→320ms(OutCubic 不变),「浮起来」更明显且与品牌契合。闪屏仍是 300×170、show 不抢焦点、12 阶段文案照常。回归:verify_splash 11/11(D1 比色口径随玻璃化同步:取色点下移避开标题文字、底色断言 card_bg_solid→glass_fill、alpha ≥190 容两主题;A/B/C/E 契约零改动);test_init 冒烟全 [OK];全量 pytest 我方改动面全绿(2 红灯 test_ruff_clean/test_plugin_ai 经 mtime 取证为并行会话对 settings_panel/ai_server 的实时编辑中间态,与本轮无关);双主题真截图 build/shots/splash_{light,dark}.png
  • 主窗口启动懒加载 + 后台预热(用户可见变更,启动 2.47s → 1.06s):日志 [启动] 分步打点取证显示总启动 2.47s 中「构建主窗口界面」占 2079–2250ms(≈90%,其余各段毫秒级;「加载插件」仅 31ms 不是瓶颈)——根因是 10 个页面在主窗构造期全部同步构建(设置页/知识库/软件导航等重页即使用户从不点开也照建)。现改懒加载 + 预热:首页(0)仍同步构建保证首屏即现,页 1–9 用空占位 QWidget(objectName lazyPagePlaceholder{idx})占槽;10 个页属性全部 @Property 化(首次访问经 _ensure_page_built 幂等构建并以「insertWidget 到同槽位 + removeWidget 占位」替换,物理索引不变 → 插件页 10+ 起始契约与全部外部引用零迁移),_switch_page 入口先 ensure、refresh_page 只按需访问可见页属性;主窗 show 后 QTimer 40ms 逐拍后台构建一页(预热),用户点开任何页时页面已就绪、换页无感。apply_theme 只刷已构建页(未构建页构建时自会用当前主题)。三个必踩坑全踩全修:①builder 表漏填 key 0 → stack.addWidget(None) 直接 0xC0000005 段错误(run_gui_check 子进程崩溃丢 stdout → 裸跑探针取证);②refresh_page 的 panel 字典字面量一次性求值全部页 property → 构造期强推全页构建 → TasksPanel 构造内再触发 ensure 无限递归(CPython 把递归深度耗尽转译成误导性的 AttributeError: no attribute '_page_tasks')——修复=字典改按需 if/elif + _ensure_page_built 加 _lazy_building 重入守卫(重入返回 None,调用方已判空容忍);③QStackedWidget 没有 replaceWidget(那是 QLayout 的 API)→ 等价「先 insert 再 remove」并守住 currentIndex 不被替换动作扰动(预热期间用户看到的页面纹丝不动)。效果(test_init 真实启动口径):构建主窗口段 2079–2250ms → 515ms,总启动 2.47s → 1.06s(-57%)。回归:全量 pytest 1943 passed / 3 skipped / 6 subtests 全绿(较上轮 1782 多出的 161 项为并行会话同日新增,非本轮改动面);verify_nav_groups / verify_settings_groups 8 项 / verify_empty_state 16 项(页属性重依赖护栏)全过;test_init 冒烟 RC=0 全 [OK](含 0–5 页切换与双主题切换)
  • 动效 token 与时长口径下沉(UI 强化方案 A1 / 阶段 1):新增 src/motion.py(刻意不 import PyQt6,与 nav_layout/md_export/app_version 同款约束 → pytest 可直跑,无需 offscreen):MOTION 五个语义档位(instant 0 / fast 120 / base 180 / slow 280 / stagger 18,后三个与既有 NAV_DROP_MS/NAV_GROUP_EXPAND_MS/NAV_GROUP_STAGGER_MS 同值)+ EASE 三个缓动名(字符串键而非 QEasingCurve.Type 枚举,调用侧自行 getattr,以保持模块零 Qt 依赖)+ duration(ms, speed) / eased_ms(kind, speed)。收敛三处重复实现:card_window、tasks_panel、main_window._nav_anim_ms 原先各写一份 ms / speed 缩放式(main_window 的注释还专门写着「与任务面板保持一致」,实际一个用 round 一个用 int),现全部改调 motion.duration——唯一口径 max(0, int(ms / speed)),档位非法/非正回退中性档 1.0(杜绝「档位 0 → 时长爆炸」)、MIN_SPEED=0.01 兜底防除零;区间夹取仍留在配置层,动效层不再夹一次(避免掩盖配置错误)。行为零变化(仅非整数档位下与旧 round 差 ≤1ms,测试与肉眼均不可见),全量 pytest 1511 → 1553 passed / 3 skipped(新增 tests/test_motion.py 42 项:token 键完整性与取值序、缩放口径与 verify_nav_drag 硬断言 110/220/440 严格对齐、八类脏档位回退、非正时长归零、缓动名必须是 Qt 真实成员名、AST 断言模块零 Qt 依赖且仅用标准库、三处接线点回归钉子)。顺带修掉三处已过期的口径注释(constants.py / tasks_panel 模块 docstring / main_window.anim_speed 属性)——它们是 A2 图标与 A3 空态的前置地基,reduce_motion 开关未来只需改 motion.py 一处
  • 键盘焦点态统一(UI 强化方案 A4 / 阶段 2,用户可见变更):此前全应用键盘 Tab 走查基本无反馈——输入框只有 $primary 细边、按钮/列表/复选框对焦后毫无视觉变化,键盘用户在「现在焦点在哪」这件事上是瞎的。本轮补齐全应用焦点可见性:①新增主题 token focus_ring(浅色 #27787A / 深色 #6FFFE9)——刻意不复用 $primary,因为实测浅色主题下主色对面板底对比度不足 3:1(WCAG 非文本 3:1 是硬线),focus_ring 与 $secondary_text 同值同因(该类文字色本就按「浅底上要看得清」选过),并把它钉成 test_theme_contrast.py 里可执行的证据。②主窗 QSS 输入类(QLineEdit/QPlainTextEdit/stepValue):focus 环色由 $primary 改 $focus_ring 并补 outline: none;末尾追加统一焦点块覆盖按钮四族(#primaryBtn 用 $on_primary、#secondaryBtn/#iconBtn/#navBtn/#settingsNavBtn/#tableOpenBtn 用 $focus_ring、#dangerBtn 用 $danger)+ QListWidget + 复选框。③卡片窗 QSS 同步追加焦点块(sideTabIconBtn/modeBtn/navSiteCard/fragCopyBtn/appLaunchBtn → $focus_ring;modeBtn:checked:focus/nextBtn/taskAddBtn → $on_primary;cardCloseBtn/fragDelBtn → $danger;undoUndoBtn → $primary_lite;taskDate/taskList → $focus_ring)。④盒模型不变性铁律:焦点态只允许改 border-color,不得动 padding/margin/尺寸——否则对焦瞬间触发重排,破坏大量既有几何断言;两处原本 border: none 的实底按钮(#nextBtn 9px 24px / #taskAddBtn 7px 15px)改为 1px solid transparent 时同步把 padding 各减 1px 补偿(→ 8px 23px / 6px 14px),盒尺寸逐像素不变。⑤ToggleSwitch 自带 focusInEvent/focusOutEvent + paintEvent 内缩焦点环(控件固定 44×24,环必须画在内部,不能外扩占位)。★顺带挖出一个真 bug:QCheckBox:focus::indicator 被 Qt 无条件应用——四态探针实测「从未聚焦」的复选框也恒有环,等于把焦点态变成了常态(写法看着像「子控件对焦态」,Qt 实际解析成「对焦后【及其子控件】」)。同测变体对照(A 未聚焦 / B 聚焦中 的环像素):QCheckBox:focus::indicator 210/210(无条件,坏)、QCheckBox::indicator:focus 0/128(条件性 OK)、QCheckBox:focus {color} 0/20、QCheckBox:focus {border} 0/57 → 定稿改用 QCheckBox::indicator:focus,并加护栏 test_no_unconditional_subcontrol_focus_form 禁止该写法复发。回归钉子 v4/tests/test_keyboard_focus.py(34 项:焦点规则禁 padding/margin/尺寸、边框宽恒 1px、非 indicator 规则必须有 outline: none、覆盖清单齐备、NoFocus 控件(stepBtn/navGroupHeader)不得有死规则、对比度 ≥3:1、「为什么不用 $primary」的可执行证据、实底按钮 padding 补偿、离屏渲染三例——secondaryBtn 只扫顶边 6px 带(其文字色本就是 $secondary_text,与 $focus_ring 同值,整块扫会假阳性)、QCheckBox 两态都断言(「未聚焦必须无环」+「聚焦有环」,只断言后者是恒真陷阱)、ToggleSwitch 焦点环)+ 离屏走查 v4/tools/verify_focus_ring.py(56/56:真实 MainWindow 焦点环 + Tab 走查 + 控件台,主窗/卡片窗两套 QSS,light/dark 双主题真截图 build/shots/focus-*.png)。顺带修掉 test_theme_contrast.py 一处随本轮暴露的脆弱点:它用整份 QSS 当 parametrize id,而本轮焦点块把 QSS 推过 Windows 环境变量 32767 字符上限(pytest 会把完整 nodeid 写进 PYTEST_CURRENT_TEST),测试 body 全过、teardown 才炸、表象极易误判成 4 个测试失败 → 改用显式短 id("light-main_window" 等)。全量 pytest 1553 → 1581 passed / 3 skipped / 0 error
  • 自绘图标体系(UI 强化方案 A2 / 阶段 3,用户可见变更):侧栏与页面标题此前全靠 emoji 字符当图标,而 emoji 是字体依赖的:实测 msyh(微软雅黑)下 ▶ ▾ ▸ ✂ ⚙ 等符号 QRawFont.supportsCharacter 直接为 False、离屏环境更是 QFontDatabase.families() == 0,渲染出来是一排 □ 豆腐块(阶段 2 的双主题截图里菜单/按钮图标肉眼可见仍是方块,正是本项的需求佐证);更麻烦的是打包发到用户机上,同一个 emoji 在不同系统/字体版本下外观能差出十万八千里。现改为纯自绘矢量图标:①技术路线是被打包现实逼出来的——QtSvg 不可用(打包产物 _internal/PyQt6/Qt6/bin/ 实测没有 Qt6Svg.dll,为几个图标引入新运行时依赖不划算),而 QPainterPath 不解析 SVG path d 字符串(Qt 没有 fromString),解析 SVG 路径本来也得靠 QtSvg → 于是自带一个 d 子集解析器,零依赖、零打包风险。②**src/icons.py(纯数据层,零 Qt 依赖):11 个图标路径(fragments 三层叠片 / tasks 圆角方框+对勾 / notes 文档+三行字 / knowledge 摊开书 / assets 相框+太阳+山 / nav 地球 / apps 2×2 宫格 / plugins 插头 / settings 三根滑杆 / help 圆圈+问号 / plugin 包裹——插件页占位),几何助手 circle / rrect / poly / dot(圆与圆角矩形一律用三次贝塞尔逼近,_BEZIER_K = 0.5522847498307936;dot 用零长度线段 + RoundCap 得实心圆点),全模块只用 re(AST 断言钉死)。解析器支持 M/L/H/V/C/Q/Z 大小写、M 后的裸坐标组按 L、科学计数法(1e-5 里的 e 不算命令字母);刻意不支持 S/T/A——S/T 只是书写便利(可展开成 C/Q),A 的端点参数化→圆心参数化换算冗长易错,而圆和圆角矩形我们本来就是自己生成的。最阴的失败模式是「静默跳过」:不认识的字母被忽略会让图标悄悄画成完全另一个形状,所以不支持字母 / 非法字符 / 参数不足 / 命令前裸数字 / 结尾半组一律 ValueError,绝不猜。③src/icon_render.py(Qt 渲染层):icon_path / icon_pixmap / icon / paint_icon / current_dpr / clear_cache;刻意用 QGuiApplication 而非 QApplication(只依赖 QtGui,不把 QtWidgets 拖进依赖);QPixmap 缓存键必须含 (name, size, color, dpr, width) —— 少任何一维都会在换主题/换缩放时拿到脏位图,_color_key 统一把 #fff/QColor 各种写法归一到 HexArgb 再入键;高分屏按 width × dpr 画进 size × dpr 位图后 setDevicePixelRatio。④controls.IconLabel / controls.PageTitle:PageTitle 唯一的设计约束是文字仍是 QLabel#pageTitle——图标是独立子控件、不参与文字度量**,因此 theme.py 里 QLabel#pageTitle 的 QSS 与全项目既有的几何/样式断言口径零变化(把标题做成「一个自绘整体」会同时改掉文字度量与 QSS 命中,是没必要的风险)。换主题订阅宿主 theme_changed,且订阅的是 PageTitle 自身的绑定方法而不是 lambda——绑定方法让 Qt 在部件析构时自动断开;lambda 没有接收者上下文,宿主活得比面板久时会在已析构对象上回调(段错误的经典成因)。⑤接线(P0 范围:侧栏 10 个内置页 + 各面板页标题 + 插件页占位,共 11 个位点):主窗侧栏按钮新增 iconName 属性 + _apply_button_icon / _apply_nav_icons,QIcon.State.On 承载选中态 → 选中项图标自动变主色、零信号接线(离屏实测 QIcon.State.On 对 checkable QPushButton 生效);NAV_PAGE_TITLES 去 emoji 并新增 NAV_PAGE_TITLES_FIXED(设置/说明是固定非拖拽项);register_plugin_page 开头 strip_leading_emoji(title) —— 插件 manifest 里带的 emoji 一律剥离(整串都是图标时原样保留,避免把纯图标标题清空),图标由宿主统一给;10 处面板页标题(碎片/日程/笔记/知识库/素材/网址导航/插件中心/软件导航/设置/使用说明)统一换 PageTitle。⑥顺带修掉一个真 bug:PageTitle 订阅宿主信号时只判 is not None 不够——全量 pytest 冒出 3 个 error(test_ui_scale.py:'types.SimpleNamespace' object has no attribute 'connect'),因为该文件的宿主替身是 SimpleNamespace(theme_changed=SimpleNamespace(emit=...)):属性在、connect 不在。改判 callable(getattr(signal, "connect", None))(连 connect=None 的形态也一并挡住),与 settings_panel._on_ui_scale_changed 既有的 callable 守卫同口径。⑦★本轮最值钱的一条经验是测试脚手架硬崩:pytest tests/test_icons.py 收集 62 项、打完 51 个点、在第一个创建 QApplication 的测试上静默死掉——没有 traceback、没有失败摘要、退出码 127,而前 51 项全是纯逻辑不碰 Qt,崩点恰好落在「第一个 Qt 测试」上,看上去非常像渲染层的问题。逐语句无缓冲打点 + 减法二分后坐实:_app() 辅助函数写成 return QApplication.instance() or QApplication([]),而调用点裸调用、丢弃返回值 —— PyQt6 下 QApplication([]) 的 Python 包装对象持有 C++ 实例的所有权,函数一返回引用计数归零 → C++ 侧 QApplication 被销毁 → 紧接着 QPixmap 构造触发 Qt 的 qFatal("QPixmap: Must construct a QGuiApplication before a QPixmap") → 进程 abort,连 pytest 摘要都来不及打。对照组 test_keyboard_focus.py 用同一个 helper 却一直没事,差别只在它写的是 app = _app()(引用留在调用方)——这也是为什么第一轮排查方向(「QApplication 获取方式不一致」)反而对了一半:真正不一致的是引用有没有被接住。修法:helper 内部把 app 存进模块级 _APP 再返回。PyQt6 里 QApplication / QPixmap 这类带所有权的对象,绝不能在辅助函数里返回后就被丢弃。回归钉子 tests/test_icons.py(63 项:数据健全性 6——非空 str dict / 名称唯一 / 路径可解析且命令在 M/L/H/V/C/Q/Z 内 / 坐标全部落在 0..24 视框内 / _ARGC 键全集;解析器健壮性——5 类 malformed + 6 个不支持字母 + 非法字符 + 科学计数法可接受 + 相对/隐式命令 + Q/Z;几何助手 5 项;零 Qt 依赖 AST 双断言(顶层 import 必须 == {"re"},且源码里不得出现任何 PyQt6 import/符号——初版用字符串搜索会命中模块头注释里反复出现的 "PyQt6" 字样,与 test_kb_search_core.py 已记录的同类陷阱一模一样);前缀剥离 20 项(12 个带 ASCII id 的 parametrize + 纯图标不剥 + 非 str 透传 + 中间 emoji 不剥);覆盖清单 4 项(NAV_ICON 与 NAV_FIXED_ITEM_GROUP ∪ _BASE_GROUP_KEYS 一致、三张表与 NAV_ICON 键一致);渲染层 Qt 6 项;控件接线 6 项)+ 离屏端到端 tools/verify_icons_render.py(34/34:A 11 图标 × 4 档尺寸(16/24/32/48)不透明像素数全 > 0(实测 82..170 / 136..320 / 190..420 / 272..640);B 外接框占比 ≥ 25%(防「画是画了但缩成一个小点」);C 双主题 × 两个 token(text/text_secondary)× 11 图标 渲出的像素色 == 传入色;D 缓存语义 5 项(同参同图 / 异色不共用 / 异尺寸不共用 / 异写法归一 / clear_cache 生效);E 真窗口 ×2 主题 8 项(全导航按钮图标已登记 / 文案无 emoji 残留 / 插件页剥离 + 占位图标 / Off 态 == text_secondary / On 态 == primary / 用两个不同按钮扫 QRectF(20,0,16,h) 图标带取证);G 换主题跟随(判据是「均值色离哪个参考色更近」——两主题的 text_secondary 只差 ~13,用数像素或宽容差都会假阳性);H 面板标题 4 项(10 个页面各恰 1 个 PageTitle + 图标名正确 + objectName()=="pageTitle" / 全窗 ≥10 / 取色 == 主题 text / 透明底渲染笔画均值色更接近对应主题 text),附 light/dark 双主题真截图 build/shots/icons-*.png)。验证方法论两处坑记档:①QWidget.grab() 会用调色板窗口底色把整张位图填满,底噪直接把笔画色淹掉(H3 一度得到 (177,183,188) 既不像浅色也不像深色的荒谬均值)→ 新增 render_transparent(),走 render(pm, QPoint, QRegion(), QWidget.RenderFlag.DrawChildren) + 透明底,并给 IconLabel 加 color 属性做直接断言;②QRegion 在 QtGui 不在 QtCore(QPoint 才在 QtCore);③test_strip_leading_emoji 的 parametrize 必须给 ASCII ids=[...],否则非 ASCII id 会在 GBK 控制台把 pytest 输出打断。全量 pytest 1581 → 1650 passed / 3 skipped / 6 subtests passed / 0 error;离屏回归全绿——verify_focus_ring 56/56、verify_nav_groups、verify_nav_live_letway 9/9、verify_plugins_page 19/19、verify_plugins_panel 23/23、verify_theme_live_switch 32/32、verify_app_context_menu 10/10、verify_recurring_tasks 46/46、verify_text_workshop 34/34、verify_plugin_store 40/40、verify_kb_search 55/55、verify_card_nav_grid、verify_settings_groups 8 项
  • 动作按钮图标化(UI 强化方案 A2 / P1,用户可见变更):界面残留的最后一批 emoji 按钮字形全部换成自绘矢量图标——此前标题栏的 🌙☀️ ⛶ ❐ ×、便签 ✕、以及各工具栏「🔄 刷新 / 🗑 删除 / ✏️ 编辑 / 💾 保存 / 🔍 检查更新」的 emoji 前缀,在离屏环境、精简系统、缺字形机器上渲染成 □ 豆腐块,且外观随系统字体漂移。①**icons.py 新增 14 个动作图标**(pin 图钉 / trash 垃圾桶 / refresh 弧线+三角箭头 / edit 铅笔 / save 软盘 / search 放大镜 / close / minimize / maximize / restore / moon / sun / plus / minus)与 arc() 助手((4/3)·tan(Δ/4)·r 控制柄是 _BEZIER_K 在任意圆心角上的一般化;月牙=外圆长弧+裁剪圆短弧的两圆求交数值解);顶层 import 放行 math(AST 守卫同步改为 {re, math},仍零 Qt 依赖)。②**controls.IconButton:纯图标(固定方尺寸)与「图标+文字」两用;★各站点 objectName 原样保留**(#iconBtn/#dangerBtn/#stepBtn/#sideTabIconBtn/#secondaryBtn/#cardCloseBtn 等 QSS 契约不破,本类只负责 QSS color: 管不到的 QIcon 位图);常态/悬停/选中/禁用四态取色(缺省 text_secondary/primary/primary/text_disabled,可传 token 名或 #RRGGBB 字面量;主色底按钮传 on_primary 让图标跟文字同色);悬停换色走 enterEvent/leaveEvent 重设 Off 态位图(pixmap 缓存按颜色分键,进出各只付一次生成成本);主题跟随三通道=宿主 theme_changed 订阅(PageTitle 同款 callable 守卫)+ 父链 _theme 探测兜底(插件页运行期新建的按钮不用接宿主管线也能立即取对色)+ main_window._apply_theme 全窗 findChildren(IconButton) fan-out(卡片窗/玻璃弹窗/便签各自在自家 apply_theme 里循环)。③位点替换 43 处:标题栏 4 钮(🌙/☀️、⛶/❐ 一钮两态由 setText 改 set_icon_name)、玻璃弹窗/小卡片/便签关闭钮、小卡片侧 Tab ×7(复用现成导航图标,_TABS 的 emoji 仅留作注释)、Stepper ±(原先挑 U+2212/ASCII+ 的字体权衡作废,自绘后与字体无关)、AI 助手规则行 🗑/+添加规则/💾保存规则、kb-search 🔄重建索引、周报 💾另存为;工具栏/页脚混排按钮 ~26 处剥 emoji 前缀 + setIcon(碎片 ×3 / 素材 ×2 / 设置 ×3 / 任务 ×2 / 笔记 ×2 / 知识库 ×5 / 软件导航 ×3 / 插件中心 ×3 / 其余页脚;GlassDialog.add_footer 新增可选第 4/5 元素=图标名+常态色,老调用点零变化)。④刻意不做:右键菜单/托盘菜单/确认弹窗的 emoji 文案(真实系统菜单字体渲染正常且文本断言密集),📋 复制 / 👁 预览 / 📥 加入碎片池等非本轮六类 emoji 留待后续批次。⑤验证:全量 pytest 1650 → 1660 passed(test_icons.py +10:P1 图标登记契约 / objectName 与方尺寸透传 / 三态位图 / 悬停换位图并还原 / set_icon_name 换形 / 主题订阅与 SimpleNamespace 替身容忍 / 父链探测含脏值防误判 / icon() Disabled 位图);verify_icons_render.py 34 → 44/44(B 段对一维字形 minimize/minus 改按「跨度≥50%」判——面积判据对线条字形恒假;新增 H 段「真窗口动作按钮」:数量≥18 / 三态位图齐全 / 文案无 emoji 残留 / 一钮两态 / Stepper 契约保持 / 换主题取色跟随 + 双主题整窗截图 build/shots/icons-actions-*.png);其余离屏回归全绿(focus_ring 56/56、theme_live_switch 32/32、card_nav_grid、card_close 9/9、settings_groups 8 项、plugins_panel 23/23、kb_search 55/55、sticky_notes 22/22、card_click_feedback 6/6);受影响断言面为零——「⬇ 安装」「✂ 截图钉屏」等 emoji 文本断言全部不在迁移范围(两个本已过期的 verify 脚本 _ai_assistant/_weekly_report 走独立对齐任务,前者一条 =="保存并测试连接" 反而因本次剥 emoji 开始转绿)
  • 空状态通用化 + 补齐 7 页(UI 强化方案 A3 / 阶段 4,用户可见变更):此前只有碎片页是结构化空态(图标+标题+提示+动作钮),笔记/素材/网址导航/插件中心是一行干巴巴的占位文字,任务/知识库/软件导航三页干脆什么都没有(只有"共 0"计数,空白区毫无引导)。现把碎片页的 _EmptyState 提升为 controls.EmptyState(自绘图标 + sectionLabel 标题 + hintLabel 提示 + 可选动作钮),8 个页面全部结构化:①fragments 改引用组件(set_mode 两态改 set_state,_action 点击语义与 QSS 契约逐字保留,emoji 图标 🧩/🔍 换自绘 fragments/search);②notes:QLabel 占位 → EmptyState 覆盖层,搜索无命中给「没有匹配「关键词」的笔记」态;③assets:_stack 空态页换成 EmptyState;④nav:_NavList 占位 QLabel → EmptyState(空态高度 72→150);⑤plugins:外层空态换 EmptyState(objectName 另起 pluginEmptyState——QSS 的 QLabel#pluginEmptyHint 是商店弹窗的契约不能动,verify_kb_search 断言其存在);⑥tasks / knowledge / 软件导航三页从无到有:任务页「还没有任务:在上方输入待办回车添加,到期会在悬浮球提醒你」、知识库「知识库还是空的:点上方「新增知识」…」(只有整库为空才显示,搜索无命中仍走列表内占位条目——两种"没有"语义不同不混用)、软件导航「还没有常用软件:点上方「新增」…」。实现要点:attach_to 覆盖层模式——EmptyState 叠成列表控件的子件、随 resize 贴合(自带 eventFilter),列表本体一行不动,itemAt(0)/count() 定值断言与动态重建(软件导航 _clear_layout 会清空整张网格)全部避开;无动作钮的空态 WA_TransparentForMouseEvents 鼠标全穿透(右键/滚轮照常落到列表);图标取色走主题 text_placeholder 并在宿主 apply_theme 链里跟随。★护栏已知雷全部绕开(先侦察后动手):plugins 卡片网格有精确占位断言(EmptyState 绝不进 _cards_layout)、每页恰好一个 PageTitle 的 H 段断言(EmptyState 内禁止 pageTitle 字样)、计数标签口径不动。回归:tests/test_empty_state.py 9 项新增(构造契约/QSS objectName 老契约/动作接线/无 pageTitle 护栏/图标登记/两态切换/主题取色/attach_to 几何/鼠标穿透)+ 新增离屏端到端 tools/verify_empty_state.py(16/16:真 MainWindow 空数据走查 7 页 ×2 主题空态可见 + plugins 页用 loader=None 最小宿主单独渲染,16 张双主题截图 build/shots/empty-*.png);受影响 verify 全绿(fragment_category 14/14、plugins_panel 23/23、kb_search 55/55、list_perf 12/12、md_export 34/34、icons_render 44/44);全量 pytest 1660 → 1669 passed;★记档:隐藏的顶层控件 resize 事件在 show 前不发(attach_to 的测试须先 show 宿主再 resize);页面索引用 NAV_PAGE_INDEX 查表(栈里 6=设置、9=plugins,写死数字必踩坑)
  • 减弱动效开关(#14 / UI 强化方案收尾,用户可见变更):设置 → 外观与主题新增「减弱动效」开关——打开后导航展开/掉落、任务勾选、卡片窗口的所有过渡动画一律瞬显,适合低性能机器与晕动效用户(悬浮球自身的动画不在此口径内,仍按「动画速度」倍速走)。实现是 A1 地基的红利兑现:动效时长早已收敛进 motion.duration() 单点,本轮只加了一个模块级总闸——motion.set_reduce_motion(),开启时 duration()(含 eased_ms、含脏输入)一律返回 0;语义刻意选「瞬显」而不是「按比例缩短」——部分缩短会让动画停在中间帧,观感更差。配置三件套同步(reduce_motion 默认 False;bool 不进 _CONFIG_RANGES——区间夹取只属数值档位,既定口径);主窗启动即按配置置位,设置页 ToggleSwitch 翻转即时持久化 + 总闸同步,「恢复默认设置」路径同步回读。回归:test_motion.py +6(默认关闭口径不变 / 开启后全输入归零 / 开关可逆 / config 三件套与 bool 归一 / 不在 _CONFIG_RANGES / 设置页行端到端翻转落盘并同步总闸),全量 pytest 1669 → 1675 passed;verify_settings_groups 8 项(卡片数不变)、verify_nav_live_letway 9/9、verify_nav_groups 全过(motion 消费方零回归)
  • 侧栏分组展开/折叠丝滑度调优(用户可见变更):侧栏分组收放此前有三个可感瑕疵,全部经离屏探针取证后修掉——①连点重定向跳变:快速连点同一组头时,旧实现先 _stop_nav_group_anims() 落终态再按新目标重放,折叠中途再展开会先「啪」地弹回满高再收(探针 C:中断后首拍 39px 满高、~13ms 后才回落);②stagger 延迟期满高僵住:逐条错峰期间尚未起播的条目以未约束满高亮出(探针 B:末条 0.9ms 即达 39px,假「瞬间到位」,last_movement 307ms);③急起—长尾漂移:OutQuint 对 39px 行高 ~105ms 就走完 97%,余下 175ms 只挪 1px。修复三件套:重定向代替打断——新增 _stop_nav_group_anims_keep_pose(group) 只停本组在飞动画且不落终态(动画对象以动态属性 _nav_group 标记组归属,别组动画不受影响;拖拽路径仍走落终态的 _stop_nav_group_anims()——槽位几何正确性依赖它),条目起播值改取当前视觉高度(隐藏=0 / 未约束=行高 / 在飞=当前 maximumHeight),连续点击从半途丝滑续走;先压高再显形——setMaximumHeight(start_v) 必须先于 setVisible(True),stagger 延迟期与重定向首帧都以起点姿态亮出(根因:QSequentialAnimationGroup 的 addPause 期间条目保持未约束态);缓动/时长收紧——OutQuint→OutCubic,280/210/18→220/170/12(motion.MOTION 全局档位数值不动,注释解耦:侧栏本地常量只服务侧栏)。效果:末条 reach 0.9→178.7ms、last_movement 307→243ms、中断后满高帧 True→False(首拍 29px 半途姿态续走)。回归:护栏全绿 verify_nav_groups 49 项(E/F 中间高度采样证明过渡真实、H 段拖拽落终态契约未破坏)/ verify_nav_live_letway 9/9 + 全量 pytest 1687 passed / 3 skipped / 6 subtests 精确对账基线 + test_init 冒烟全 OK + 双主题真截图(light/dark 稳定态 + 中间帧半高过渡,build/shots/nav_smooth_*.png)
  • 便签浮窗玻璃化(用户可见变更):桌面便签此前是「teal 实底标题栏 + 直角实底窗」的早期朴素画风,与全应用玻璃拟态割裂。现对齐主窗语言:WA_TranslucentBackground + paintEvent 手绘圆角面板(RADIUS=10,glass_fill 填充 + primary_border 描边),标题栏/正文透明融入本体、仅以 1px primary_border 分隔线分层,标题文字/关闭钮/缩放抓手全部改走主题 token(text / text_secondary,关闭钮悬停 danger)——light 白玻璃、dark 深蓝黑玻璃,随主题广播实时换肤;交互契约零改动(26px 标题热区、右下抓手、600ms 几何防抖、透明度三档)。两处非显而易见的实现决策:①本体刻意手绘而非窗口 QSS background——translucent 顶层窗的本体 QSS 背景在渲染管线里不可靠(offscreen grab 实测整窗透明、连 WA_StyledBackground 都救不回,被 E1 像素护栏当场抓住),CardWindow 为画阴影走 paintEvent 是本项目先例;②QColor 不认函数式 rgba() 串——theme token 的 glass_fill="rgba(255,255,255,224)"(alpha 0-255)与 primary_border="rgba(91,192,190,0.30)"(alpha 比例)只能被 QSS 解析器消费,本体手绘走 _css_color 小解析器(3~4 个数值分量,第 4 参 >1 按 0-255、≤1 按比例,解析失败回退兜底色绝不画黑)。回归:verify_sticky_notes 22/22(E1 取样同步改为整窗 grab——标题栏透明后抓本体只会得到透明底、两主题无差恒假);全量 pytest 1743 passed / 3 skipped(并行会话同日新增 67 项一并全绿);双主题真截图 build/shots/sticky_{note,task}_{light,dark}.png
  • 窗口材质(UI 阶跃 J1,已代码级回退):【2026-10-01 实机回退】深色 Mica 实机观感发闷(用户看截图拍板),代码已全部移除——src/dwm_material.py、theme.material_colors()、get_main_window_qss(material=) 参数、主窗全部接线(_material_wanted/_apply_backdrop/apply_window_material/showEvent 挂载/_apply_theme 分流)、设置行「窗口材质」、config 键 window_material、tests/test_dwm_material.py、tools/demo_backdrop.py 全部删除,主窗观感与 09-30 收官版逐字节一致(今早的图标体系 P1 与 #12/#13/#14 完好未动)。以下为存档技术结论(未来重试材质时的现成地基,备份见 .workbuddy/backups/1240_material/):期 0 demo 实测 Qt6 的 WA_TranslucentBackground 窗口不是 WS_EX_LAYERED(EXSTYLE=0x100)→ DWM backdrop 与现有无边框+半透明体系不互斥,真主窗可挂 Mica 且浅→深主题实时切换保持;两处必踩坑已验证——初始化期提前取 winId 会抢在半透明属性之前建原生窗(须 WA_WState_Created 守卫)、import verify_icons_render 会把 QT_QPA_PLATFORM 打回 offscreen 致真桌面预览从未上屏。回归:全量 pytest 1675 passed(材质 11 项测试随代码一同移除,数目精确对账)Win11 22H2(build 22621+)上经 DWMWA_SYSTEMBACKDROP_TYPE 给主窗挂系统级 Mica 背景模糊,同时主窗玻璃填充切换到低实度变体(light:glass_fill 88%→63%、panel_fill 67%→54%;dark:82%→59%)——此前"玻璃"实为挡壁纸所迫的高实度磨砂(theme.py 旧注释「67% 背景会打架」),真模糊交给系统合成器后自绘填充降为薄纱;关闭 = 经典配方逐字节不变;低于 22621 / offscreen / 任何 DWM 失败一律静默回经典(回滚 = 关开关)。实现要点:①期 0 demo(tools/demo_backdrop.py,独立脚本不合入产品链路)实测回答方案 6 问的关键三条:Qt6 的 WA_TranslucentBackground 窗口不是 WS_EX_LAYERED(EXSTYLE=0x100)→ backdrop 与现有无边框+半透明+自绘软阴影体系不互斥;属性在真主窗读回 Mica 且浅→深主题实时切换保持;②src/dwm_material.py 纯 ctypes 零 Qt(能力门控 / 取值编码 / 注入 setter 全部离线可测,AST 禁 PyQt6——打包 DLL 清单零新增);③theme.material_colors() 纯函数变体只覆盖 glass_fill/panel_fill、无全局态——卡片窗/便签/玻璃弹窗永远经典配方(get_card_window_qss 无 material 参数作防污染钉子);get_main_window_qss(material=) 老调用点默认输出逐字节不变;④主窗 _apply_backdrop 挂 WA_WState_Created 守卫——初始化期提前取 winId 会抢在 WA_TranslucentBackground 之前建原生窗毁掉半透明(预览脚本实踩);backdrop 暗色位跟主题实时切换;⑤验证方法论:import verify_icons_render 会经模块顶部 setdefault 把 QT_QPA_PLATFORM 打回 offscreen——真桌面预览必须在建 QApplication 前显式覆盖回 windows,否则窗口从未上屏、读回恒 0。回归:tests/test_dwm_material.py 11 项,全量 pytest 1675 → 1686 passed;填充百分比是首档手感,实机不满意直接调 theme.material_colors 里的四个 rgba
  • 悬浮球悬停轮询空闲降频(3.6):HoverPollPolicy 状态机——鼠标静止 1s 后轮询 50ms→200ms,任何活动立即恢复 50ms;吸边隐藏/拖拽逻辑零改动(代价:静止后快速甩离的收卡判定最坏 ~400ms,原 ~100ms,可接受)
  • 巨型入口文件第一批拆分(4.3 部分):托盘创建/右键菜单/气泡整体抽到 src/tray.py(TrayController,依赖显式注入),快捕条/截图/插件三组全局热键注册样板收敛为 src/hotkey_binding.py 绑定表驱动;knowledge_ball.py 3613→3558 行,test_host_attribute_contract 扩展 145 行钉住新公开 API;wiring.py 大拆与剩余私有穿透点留待下轮(/ 命令面板开工前必须完成)
  • 全局 Esc 不再退出程序(成熟化批次 1.3):删除「任何窗口按 Esc 都杀进程」的全局快捷键(与桌面软件惯例相反、最易误触且后果最重);Esc 逐表面收口——卡片/便签关闭、快捕条收起、截图框选取消(后两者本就支持),主窗口 Esc 无动作;退出只走托盘菜单/悬浮球右键/主窗菜单三处
  • 退出显式收尾(1.4):aboutToQuit 与 _safe_quit 统一收口到幂等的 _shutdown_once——注销全部 3 组全局热键(兑现 global_hotkey.py 自家注释的约定)、主动 AI_SERVER.stop()(本地 llama-server 不再只靠 JobObject 内核兜底)、停剪贴板监听;单步失败只告警不阻断退出
  • 发布包默认内置空白知识库模板(用户可见变更):主程序 zip 此前不带 float_data/知识库.docx(防个人内容夹带),但下载用户首次打开知识库页是「共 0 段」且写不进去(新增段落 / 碎片加入知识库都因文件不存在而失败,点「🔄 重新加载」还会弹「找不到文件」),知识库功能等于摆设。现 tools/build_release.py 在不带 --with-knowledge 时现场生成一份空白模板补进包:纯标准库手造 OOXML 最小三件套([Content_Types].xml + _rels/.rels + word/document.xml),内含一段可编辑可删除的欢迎示例,实测 python-docx 对它「打开 / add_paragraph / save」全通过(Word / WPS 亦可正常打开),不依赖 python-docx、CI 零负担;--with-knowledge(打真实文件)时行为不变。事后校验新增断言「包内必须有 float_data/知识库.docx」,真实文件与模板都没有就删输出报错、不留坏包。隐私口径不变:模板内容是写死在脚本里的欢迎语,绝不读本机真实知识库

Changed

  • 临时素材加载丝滑化(异步缩略图管线,用户可见变更):素材页此前在 paint() 里同步解码缩略图——首次打开/滚动到新行时,QImageReader 在 UI 线程逐张解码,实测单张 2400×1600 噪声 PNG 约 43ms、12 张混合素材合计约 161ms(= 首帧冻结时长,多张大图时肉眼可见卡顿)。现改为占位即显 + 后台解码 + 淡入三段式:①paint() 未命中缓存只画类型占位图并落 _PENDING 哨兵(绘制路径永不解码),由面板的 QThreadPool(2 线程,不抢满核)后台做「解码期缩放 → Smooth → 居中裁切」(与旧产物逐像素同规格),经 Queued 信号回主线程 QPixmap.fromImage 入缓存;②到货 140ms OutCubic 淡入(时长走 motion.duration 总闸,reduce_motion 开=瞬显),并只重绘该格(visualItemRect 局部 update,不惊动整页);③代际防串:apply_thumb_size 变尺寸时 _epoch+1,旧尺寸的在途结果按代作废,不会出现旧图回填;在途去重(_pending 集合)、素材删除后的迟到结果按 _valid_ids 丢弃、读失败仍落 False 哨兵防反复重读,缓存清理口径与旧版一致。④非图片/文件失效仍走原占位分支;loader=None 的独立使用场景保持旧同步路径(兼容直用 delegate 的既有脚本)。度量:打开素材页列表即刻就绪(12 条目无解码阻塞),加载期 400ms 内事件循环跑 152 拍(主线程持续响应),5s 内全部到货;变尺寸 96px 后按新宽度全部重灌。回归:新增 tests/test_asset_thumbs.py 12 项(解码纯函数 / 占位+派发恰一次 / 非图片哨兵 / 同步回退 / 到货入缓存 / 换代丢弃旧结果 / 删除素材迟到结果丢弃 / 缓存与淡出剪枝 / 同值短路 / 淡入完成摘除 / reduce_motion 瞬显);新增离屏端到端 tools/verify_asset_loading.py(6/6:真窗大图全链路);verify_asset_thumb_setting.py 两处同步假设改等待式(小图 150ms 内实际也能到货,加固防慢机抖动)并 5/5 通过;全量 pytest 1675 → 1687 passed

Changed

  • 插件中心观感优化(纯视觉,功能与交互零变化):针对卡片"信息全同级灰、像一堵文字墙"的三层重排——①层次:插件描述从次要灰升为主文字色(它才是用户要读的内容),依赖/能力/id 元信息行降为 11px 次级灰,使用说明摘要同步降级——标题亮、描述可读、元信息退后的三档 hierarchy;②降噪:依赖: PyQt6.QtCore… 这类开发者信息不再独占一行,并入能力行的悬停 tooltip(仅当插件只声明依赖、无能力时才保留独立行,信息不丢);③破坏性操作分级:「卸载」从普通次按钮改为危险色(#dangerBtn QSS + 图标同色,悬停实底红白字),与「停用/打开目录/查看使用说明」拉开视觉权重;④状态徽章(已启用/已停用/包不合法)加粗 + 加大内距圆角,更"徽章";⑤卡片内距与行距各 +1~2px 呼吸感。所有 objectName 契约(pluginCard/卡片网格精确占位断言/各文案断言)逐字未动。回归:全量 pytest 1687 passed 不变;插件族 verify 全绿(plugins_panel 23/23、plugins_page 19/19、plugin_store 40/40、plugin_p0p1、plugin_market 10/10);深浅双主题截图 build/shots/plugins-polish-{dark,light}.png

Fixed

  • 两个过期 verify 脚本对齐现行架构(只改脚本、不改产品代码):①tools/verify_ai_assistant.py 此前停在第 15 项就硬崩(B4 起连环红)——2026-09-30「AI 后端收归设置页总配置」重构后,脚本仍按 09-28 的页面级后端口径断言:build_request 用旧键 backend_mode/cloud_*(新键为 mode/base_url/api_key/model/local_*)、引用已删除的页面设置卡与本地服务状态机(_settings_card/LOCAL_SERVER/_mode_*_btn/_switch_mode/_cloud_active 闸门)。对齐后 114→121/121 全绿:假 ctx 注入 ai_providers(params/add_listener/remove_listener/stop_local/is_attached)模拟设置页;新增「构造即订阅 / 销毁即退订 / ready 广播→快捷停止钮出现 / 停止走 ctx.ai.stop_local / 参数实时直读(改快照下一发即生效)/ 本地未就绪拦截」六项跟随断言替代旧模式切换断言;B7b 改判「旧后端键被白名单合并忽略(零残留)」;C19 改判「插件配置零后端键」;C23-C25/C31(断开闸门、_collect_settings)随功能移除一并删除(设置页侧由 verify_ai_backend.py 覆盖);★踩坑记档:_clear_chat 的 deleteLater 是延迟删除,布局项即时消失但行容器 QWidget 仍挂在 _stream_host 下,findChildren 会数到"幽灵卡"——清场后须显式 sendPostedEvents(None, DeferredDelete) 再跑按 findChildren 计数的断言(C20 计数污染、C28 的 120px 下限按新首卡「对话已清空。」改 80px 防塌缩地板)。②tools/verify_weekly_report.py 两处:A3「热键注册表只允许 weekly-report 一个」改为包含判定(2026-10 起 5 个插件各注册热键);F6 另存为按钮文案随 P1 图标化剥除 emoji 前缀。回归:verify_ai_assistant 121/121、verify_weekly_report 65/65、全量 pytest 1660 passed 不变
  • 离屏脚本路径自举统一(开发工具修复,5 个文件):验证脚本此前有两类路径硬编码,换机器 / 改目录名 / 换用户目录即整体失效。①v4/tools/verify_card_nav_grid.py 写死 sys.path.insert(0, <项目根>/v3),而 v3 目录早已废弃删除 → 该脚本自 v4 独立后一直跑不起来(ModuleNotFoundError: No module named 'src');②另有 4 个文件把绝对路径写死成 r"D:\桌面\AI Port\FloatPulse\v4"——tools/verify_card_click_feedback.py、tools/verify_plugins_panel.py、tools/verify_plugin_store.py、tests/test_plugin_market.py(后者还写死 REPO_ROOT 用于读 plugin_store/kb-search.fpplug)。现全部改为按 __file__ 推导(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),与同目录其余 15 个脚本同一写法),REPO_ROOT 再多上溯一层。回归验证:verify_card_nav_grid.py 5/5 段 PASS(域名提取 / 卡片统一 46×173 / 右边缘 351 ≤ 内容区 352 不溢出 / 双列 4 行×2 / 标题截断+域名副标题+tooltip)、verify_card_click_feedback.py 6/6、verify_plugins_panel.py 23/23、verify_plugin_store.py 40/40、pytest tests/test_plugin_market.py 27 passed、全量 pytest 1511 passed / 3 skipped;全仓再扫一遍已无 D:\桌面 类项目路径残留
  • 未完成任务不弹托盘提醒(用户报障,2026-09-30 修复):设置里「任务提醒」是开的、也没有任何拦截,但启动与每日 9:00 两处通道都不出声。根因是提醒的判定口径漏了一整类任务——_check_task_reminders 只收 STATE_TODAY(今日到期)与 STATE_OVERDUE(已逾期),而用户手头 5 条未完成任务截止日全是空串(随手记下来的待办,还没来得及安排日期),task_state("") 归为 STATE_NONE → 三处都不匹配 → 末尾 if not due and not overdue: return 静默返回。真实数据复算:修复前 0 项、修复后 5 项。修复把「未完成任务的提醒口径」下沉成纯函数 task_manager.bucket_unfinished(tasks, today)(无 Qt 依赖,pytest 可直接跑),返回三桶 (overdue, today_due, undated)——新增「未安排日期」桶,先跳过 done=True,再按 task_state 分流;knowledge_ball._check_task_reminders 改为按三桶拼气泡(每组标题 + 最多 5 条标题 + 「…另有 N 项」,6000ms 气泡,总数进标题),判定与渲染彻底分离,以后新增桶只改一处。刻意不动 refresh_badge():徽标数字仍只算逾期 + 今日到期(徽标是「今天要看什么」的紧迫度信号,把「没日期的待办」也堆上去会让数字长期虚高、失去提示意义),气泡才是「你可以处理一下这些」的完整清单——两者口径有意不同且在设置页行描述里写明。文案同步四处(设置页行描述 / 帮助页「任务提醒」/ config.py 注释与默认值行注释)。回归钉子 v4/tests/test_task_reminder.py(7 项:空 deadline 未完成必被收 / 六种脏值 ""·" "·None·"2026/09/30"·"2026-13-40"·"今天" 全归未安排 / 三桶互斥且保序 / 已完成永不收 / 空输入 / 复刻用户场景 5 条全空 → 提醒数 = 5 / STATE_NONE 常量接线)
  • 「小卡片保持显示」关着但卡片常驻不收回(用户报障,2026-09-30 修复):设置里该开关是关的(config.json 实测 card_always_show=false,截图里也是灰的),悬浮球的小卡片却一直挂在屏幕上不自动收回——看起来像「设置没保存」,实际是拖动锁卡死。根因:CardWindow.is_locked() 直接 return self._dragging,而 _dragging 只在 mouseReleaseEvent 里清零;一旦这次 release 丢失(拖动中途切窗 / 弹模态 / 抬起点落到别处),标志位就永久为 True,而宿主的两条收回路径——悬停检测 _check_hover_state 与看门狗 _card_watchdog_tick——双双以 if card.is_locked(): return 早退,连兜底的看门狗都救不回来,卡片就此滞留(用户感知为「开关失效」)。修复给拖动锁加物理按键交叉校验:is_locked() 在 _dragging 为真时再问一次 QApplication.mouseButtons() & LeftButton,左键实际已松开就就地自愈(清标志 + 补发 card_drag_finished 让宿主正常收尾)并返回 False;mouseMoveEvent 同样补一道(收到不带左键的移动事件说明按键已抬)。正常拖动路径零变化(拖动中左键必然按着,判定恒过),只把「release 丢失」这一异常分支兜住。回归钉子 v4/tools/verify_card_close.py(9/9 离屏端到端,make_ball(always_show) 造真 ConfigManager + 隔离指针把「鼠标既不在球上也不在卡片上」钉死):A1/A2 关闭态自动收回、B1 常驻态保持、C1 运行中改开关即时收回、D1/D2/D3 僵尸拖动锁被 is_locked() 自愈 + 补发 card_drag_finished、F1 自愈后卡片能正常收回、E1 基线恒 False;顺带修掉并行改动留下的过期替身(verify_card_click_feedback.py 的 FakeCardWindow 补 current_mode 特性与 switch_mode 方法,6/6 恢复通过)
  • verify_settings_groups 适配分类导航版式:2026-09-29 设置页改「左导航 + 分类 QStackedWidget」后该脚本一直按旧单页世界写死(跨页 y 排序无意义、期望 8 卡实际 10 卡)——分组卡收集改为按栈页序展开,期望表对齐 9 分类卡 + 软件更新子卡,分隔线校验升级为逐卡动态(行数 - 1,以后新增分组/行不再使脚本过期);顺带清理上批重构遗留的调试探针文件
  • 测试盲区补齐(4.4):clipboard_monitor 与 screenshot_pin 各补逻辑层回归(前者:去重/自写抑制/开关启停/文本-路径-文件分发/容量裁剪信号/应用过滤/图片捕获闸门,不碰真实剪贴板;后者:框选归一化与 Esc 取消、dpr 换算、光标锚定缩放与边界不抖动、抓手等比 resize 钳制、批注撤销栈像素级断言);顺带修掉框选真 bug——QRect(p1,p2) 右下角含端点语义使 _selection() 的归一化结果随拖拽方向差 1px(同两个角点反向拖得到不同裁剪),现取两角包围盒保证方向不变;另修正 2 处剪贴板测试预期与实现不一致(trim 信号按次通报、FragmentManager 自带 12 条窗口去重)
  • 换主题后碎片列表文字不跟随(用户报障,2026-09-30 修复):点右上角主题按钮换风格时,碎片工作台的列表文字停留在旧主题配色——浅色主题下仍是深色主题的字色(#E4E8EE 落在白底上几乎不可见),必须切一次页面才恢复。根因是列表有两处颜色不经 QSS:①绘制代理(_MatchHighlightDelegate)的字色 / 时间色 / 内容命中高亮色 / 类别色条;②条目前景色(日期分组行=主色、路径行=link 色)。两者都只在 refresh() 里赋值,而 refresh 仅由「切到该页」的 host.refresh_page() 触发,故换主题时列表仍是旧配色。修复:新增 FragmentsPanel.apply_theme()——按主题 token 就地重取色 + 重绘,不重建列表(换主题与数据无关,重建会白跑一遍筛选/搜索并多发一次 data_changed),由 MainWindow._apply_theme() 显式调用;同时把日期行/路径行颜色从写死的 theme == "light" ? A : B 三元表达式改为按主题词取色(新增 COLOR_TOKEN_ROLE 存 token,让取色点唯一),杜绝下次再出现「一个主题漏改」。顺带修掉同类残留:知识库页「⚠️ 检测到外部修改」的警告文字是 inline stylesheet 着色,QSS 换主题同样刷不到,现抽出 _apply_modify_label_color() 并由 KnowledgePanel.apply_theme() 在警告仍显示时重新着色。回归钉子 v4/tools/verify_theme_live_switch.py(32/32,四段式):A 面板级逐项断言代理 / 条目 / 类别色条 / 高亮色双向跟随;B 真实 MainWindow 端到端——点右上角按钮换主题且不切页面,直接抓列表 viewport 数像素(旧主题字色像素 5335 → 0、新主题字色像素 0 → 5335,再切回同样对称);像素判据是必需的,纯逻辑断言会被「切页顺带 refresh」掩盖,只有像素计数能钉死「不切页面也生效」;C 知识库警告色;D apply_external_theme(设置页路径)与按钮路径行为一致。截图留档 build/shots/theme-switch-{before-dark,after-light,after-dark}.png

Changed

  • README 对外叙事校准(文档变更,2026-10-01):首屏定位从「碎片收集 → 分类归档 → 转任务/笔记 → 截图钉屏」改为「剪贴板自动留存 → 素材临时中转 → 截图钉屏对照」,并新增「它不适合下面这些期待」筛选表。依据=真实使用数据复核 + 代码级核实:①🔴**「碎片转任务」是假功能**——碎片右键菜单实测只有 查看详情/编辑/复制/存为笔记/加入知识库/添加至网址导航/钉为便签/导出 Obsidian/归类为/删除,fragments_panel.py 全文零 add_task 引用(add_task 只在 tasks_panel.py:375 与 card_window.py:1543 被调用),而原 README 在定位句 / 竞争力表 / 功能描述三处都写了「转任务」→ 竞争力表拆成「碎片转笔记 / 入知识库 ✅」与「碎片转任务 ❌ 手动无入口(AI 文本工坊可自动提取待办)」两行,功能描述同步改口径;②真实使用画像(43 天,211 条碎片)显示 97% 来自剪贴板、内容中位数仅 32 字符(IP / 文件名 / 路径 / 临时链接,属中转态而非资产)、任务 10 / 笔记 3 / 知识库自 08-12 未动 → 原「第二大脑」叙事与真实行为错位,改为明说「它不打算做你的第二大脑」并把剪贴板提到第一位;③体积口径误导:「打包后约 75 MB」实为解压后体积,实际下载 zip 仅 31 MB,读者会误以为要下 75 MB → 改为「下载包 31 MB(解压后 75 MB)」;④顺带修正三处事实数字——数据层 JSON 7 → 8 个、UI 层「九个 Panel」→「十个页面(索引 0–9)」、竞争力表「快捕条 + 剪贴板」→「剪贴板自动收(为主)+ 快捕条 + 拖拽」(实测剪贴板 205 条 vs 快捕条 3 条)。英文段同步并新增 Not for 一句。★刻意保留不动:GitHub Stars 0 与「自用打磨 / 无外部用户验证」的诚实说明——这是 README 里最有说服力的部分