# FinDataHub Mac App - DEVLOG ## 2026-09-13 — 提交后正确性检查:4 个关键缺陷修复 ### 变更概述 对未提交变更进行提交后正确性检查,发现 4 个高影响缺陷并实施定向修复。 ### 修复的缺陷 | # | 严重度 | 模块 | 缺陷 | 触发场景 | |---|--------|------|------|----------| | 1 | 高 | NewsFlashService.swift | `statusUnreachable` 自动恢复路径完全失效 | 采集器状态连续 3 轮查询失败(nil),`failCount` 在 `evaluate()` 调用前被重置为 0,`AutoRecoveryManager` 收到的 `failCount` 永远为 0,`statusUnreachable` 恢复条件 `failCount >= 3` 永不满足 | | 2 | 高 | pipeline.py + newsflash_collect.py | `UnifiedWriter` 每个采集周期重新实例化,L5 embedding 缓存失效 | `UnifiedWriter` 在 `fetch_and_insert()` / `insert_news()` 内部局部实例化,每 30 秒被垃圾回收,`_emb_cache`(跨批次语义去重环形缓存)和 `_seen_ids`(L1 精确去重集合)被重置,跨批次同义改写检测完全丧失 | | 3 | 中 | NewsFlashService.swift | `startCollector()` 失败路径泄漏 MySQL 引用计数 | `beginUse(reason: "collector")` 在方法入口执行,但 `process.run()` 抛异常或进程立即退出时提前 return 未调用 `endUse`,导致 `MySQLOnDemandManager.holders` 永久残留 "collector",MySQL 常驻不释放 | | 4 | 中 | PythonProcessManager.swift | `processNextQueued` 缺少 `beginUse("sync")` | 排队任务出队执行走 `processNextQueued → executeSync` 路径,不经过 `startSync`(仅 `startSync` 调用 `beginUse`),但 `endUse` 通过 `state.didSet` 在同步结束时无条件调用,导致排队同步期间 MySQL 引用为空可被误停止 | ### 修改文件 | 文件 | 变更 | |---|---| | `App/Sources/FinDataHub/Services/NewsFlashService.swift` | Bug 1: 在 `checkServiceHealth()` 第 2 阶段告警前保存 `collectorFailCount` / `backendFailCount` 快照,传递给 `recovery.evaluate()`;Bug 3: 在 `startCollector()` 两个失败 return 前补充 `MySQLOnDemandManager.shared.endUse(reason: "collector")` | | `App/Sources/FinDataHub/Services/PythonProcessManager.swift` | Bug 4: 在 `processNextQueued()` 中 `executeSync` 前补充 `MySQLOnDemandManager.shared.beginUse(reason: "sync")` | | `Services/kuaixun-service/extend/pipeline.py` | Bug 2: `Pipeline.__init__` 增加 `self._unified_writer = None`,`fetch_and_insert()` 中改为懒加载复用单例 | | `scripts/bin/newsflash_collect.py` | Bug 2: 新增模块级 `_get_unified_writer()` 懒加载单例函数,`insert_news()` 改为调用该函数复用 writer | ### 编译状态 - Swift 构建:**Build complete! (2.12s)**,零错误,2 个预存警告(actor-isolated call + unused result,非本次引入) - Python 语法检查:**OK**(py_compile 通过) --- ## 2026-09-13 — 自动化测试缺口分析:findatahub-core 数据预处理 + sync_bridge 线程安全 ### 变更概述 自动化测试缺口分析运行。审查近期合并代码后,识别出两个高优先级覆盖缺口: 1. `findatahub-core` 的 `DataImputer` 和 `OutlierDetector` 两个核心数据预处理组件**完全无测试覆盖**,且涉及数据验证、缺失值填充、异常检测等复杂逻辑 2. `sync_bridge.py` 的线程安全修复(4b62693:capture_output→Popen+线程转发)虽有部分测试,但缺少**线程创建验证**和**管道模式验证**两个关键缺口 **本次新增 63 个测试,全部通过。** ### 新增/修改文件 | 文件 | 变更 | |---|---| | `Services/findatahub-core/tests/test_imputer.py` | 新增 18 个测试(DataImputer 三种策略 + 边界条件 + 安全性) | | `Services/findatahub-core/tests/test_outlier.py` | 新增 19 个测试(OutlierDetector 三层检测 + 安全性 + summary) | | `Scripts/bin/tests/test_sync_bridge.py` | 修改:新增 2 个线程安全测试(`test_threads_started_per_fetch_type` + `test_popen_uses_pipes_not_capture_output`) | ### 测试覆盖行为 **test_imputer.py**(18 个测试,0 失败): | 测试类 | 覆盖的风险行为 | |---|---| | `TestImputeFfill` | 前向填充中间 NaN、首行 NaN 保持不变、标记行正确性、连续 NaN 全标记 | | `TestImputeLinear` | 线性插值中点计算、pandas 3.x 边界 NaN 行为兼容、标记插值行 | | `TestImputeMedian` | 中位数填充、全 NaN 列安全跳过、标记填充行 | | `TestImputeEdgeCases` | 未知策略抛 ValueError、无 NaN 不创建标记列、非数值列跳过、_is_ 前缀列排除、**输入不可变性**、多列并行处理、空 DataFrame、整数列处理 | **test_outlier.py**(19 个测试,0 失败): | 测试类 | 覆盖的风险行为 | |---|---| | `TestDetectLevel1` | 负价格标记、零价格标记、vol 不受绝对阈值约束 | | `TestDetectLevel2` | IQR 检测尖峰、MAD 检测尖峰、<10 数据点跳过统计检测、正常数据无误报、高阈值更严格 | | `TestDetectLevel3` | 涨跌停豁免(pct_chg > 9.5% 撤销 close 标记)、无 pct_chg 列时不触发豁免、小幅波动不被豁免 | | `TestDetectSafety` | **原始值不可变性**、输入 DataFrame 不可变、标记列正确添加 | | `TestDetectValidation` | 未知方法抛 ValueError、缺少价格列不崩溃 | | `TestSummary` | 按列统计异常值数、无异常返回空 dict、仅统计 _is_*_outlier 列 | **test_sync_bridge.py 新增测试**(2 个,0 失败): | 测试 | 覆盖的风险行为 | |---|---| | `test_threads_started_per_fetch_type` | 验证 Popen 转发线程确实被创建且为 daemon(防止输出静默丢弃) | | `test_popen_uses_pipes_not_capture_output` | 验证 Popen 使用 PIPE 而非 capture_output=True(防止缓冲满后死锁) | ### 验证结果 - `findatahub-core` pytest:**37 passed, 1 warning, 0 failures**(0.96s) - `sync_bridge` pytest:**26 passed, 0 failures**(0.05s) - 合计:**63 passed, 0 failures** ### 降低的回归风险 1. **数据预处理管线安全网**:`DataImputer` 和 `OutlierDetector` 是整个数据质量管线的前置组件,所有下游数据产品(报告、缓存、门控)都依赖它们的输出。此前任何对填充策略或检测逻辑的修改都无法被自动发现。现在 37 个测试覆盖了三种填充策略 × 三层检测逻辑 × 全部边界条件,确保核心数据校验逻辑的回归可立即被捕获。 2. **线程安全死锁防护**:`sync_bridge.py` 的 4b62693 修复将 `subprocess.run(capture_output=True)` 改为 `Popen + _pump_stream` 线程转发,解决了子进程 stderr 缓冲满后死锁的严重问题。新增的 2 个测试验证了线程确实被创建且管道模式正确,防止未来重构时无意回退到 capture_output 模式导致死锁重现。 --- ## 2026-09-01 — Swift 测试基础设施建立 + parseStatusJSON 防御性容错测试补齐 ### 变更概述 作为自动化测试缺口分析运行,审查近期(8 月)合并代码后发现: - Python 侧(Scripts/、Services/)核心修复已在 4b62693 中补充全面单元测试 - Swift 侧(App/)完全没有任何测试基础设施,d43a177 等关键 bug 修复裸奔 **本次建立 Swift 最小测试基础设施,并补齐最高优先级缺口。** ### 新增/修改文件 | 文件 | 变更 | |---|---| | `Package.swift` | 新增 `.testTarget(name: "FinDataHubTests", dependencies: ["FinDataHub"])` | | `App/Sources/FinDataHub/Services/NewsFlashService.swift` | `parseStatusJSON` 访问级别从 `private` → `internal`(供 @testable import 访问) | | `Tests/FinDataHubTests/NewsFlashServiceTests.swift` | 新增 12 个单元测试 | ### 测试覆盖行为 `NewsFlashServiceTests`(12 个测试,0 失败): | 测试 | 覆盖的风险 | |---|---| | `testParseStatusJSON_pureJSON` | 正常单行 JSON 完整字段解析 | | `testParseStatusJSON_minimalJSON` | 最小 JSON(仅 running)不崩溃 | | `testParseStatusJSON_mixedWithDebugLogs` | **d43a177 核心场景**:调试日志混入 stdout,只取最后一行 JSON | | `testParseStatusJSON_multipleDebugLinesBeforeJSON` | 多行调试前缀后紧跟 JSON | | `testParseStatusJSON_emptyString` | 空输出降级为 running=false | | `testParseStatusJSON_whitespaceOnly` | 纯空白行降级为 running=false | | `testParseStatusJSON_invalidJSON` | 无效 JSON 降级为 running=false | | `testParseStatusJSON_lastLineNotJSON` | JSON 在中间行、最后一行非 JSON → 降级为 false(防御不完整输出) | | `testParseStatusJSON_malformedJSONInLastLine` | 畸形 JSON 降级为 running=false | | `testParseStatusJSON_singleLineNoNewline` | 无换行符的单行 JSON | | `testParseStatusJSON_extraBlankLines` | 首尾多余空行不影响解析 | | `testParseStatusJSON_unicodeInDebugLine` | Unicode 调试日志(ℹ️ 等)不干扰 JSON 提取 | ### 验证结果 - `swift test --disable-sandbox --filter NewsFlashServiceTests`:**12 passed, 0 failures** - `Scripts/bin/tests` pytest:**195 passed** - `Services/akshare_fetcher/tests/unit` pytest:**181 passed, 4 skipped** - `Services/tushare_fetcher/tests/unit` pytest:**171 passed, 1 failed(既有 flaky 测试 `test_sleeps_when_within_interval`,时间边界断言 `sleeps[0] < 0.3` 偶发等于 0.3,非本次引入)** ### 降低的回归风险 - `parseStatusJSON` 是采集器/后端状态监控的唯一入口。d43a177 之前,Python 调试日志混入 stdout 会导致 Swift JSONDecoder 抛异常,整个状态监控误判为"停止"并触发反复重启。12 个测试覆盖了从正常路径到所有降级路径,确保任何对解析逻辑的修改都会立即被发现。 --- ## 2026-08-31 — 界面显示统一优化(不动功能与交互) ### 问题审计(全视图扫描结论) | 问题 | 范围 | |---|---| | 节标题两套风格并存 | 快讯中心 6 处用 `Image+subheadline`,其余页面均已用 `LineIcon+title3` | | 空态展示单薄 | 6+ 处仅一行灰色小字,与快讯页完整错误态质量不对等 | | 超小字号 | `size:7` ×3 处(低于可读下限);`size:8/9` 大量硬编码(不随用户字号设置缩放)| | emoji 与 SF Symbols 混用 | 快讯高热度区 🔥⚡⭐,违反「统一 SF Symbols 线性风格」| | 硬编码色 | 概览页数据源徽章 RGB 手调色、monthly 任务裸 `.purple` | ### 所做调整 1. **新增统一组件**(`Design/FinComponents.swift`,唯一 Component 层): - `SectionHeader`:卡片标题行(LineIcon + title3 + 右侧 ViewBuilder 补充位) - `EmptyStateView`:空态占位(图标 + 标题 + 可选说明,iconSize 可调) - `LoadingPlaceholder`:卡片内加载占位 2. **快讯中心**(NewsFlashView):6 处节标题换 `SectionHeader`(带语义色);空态升级(区分「暂无数据」与「筛选无结果」两种文案 + 引导说明);7pt→9pt ×2;emoji→`flame.fill/bolt.fill/star.fill` + 热度色阶;卡片内距 `padding(10)`→`FinDesign.Spacing.md` ×6(与其余页面对齐) 3. **空态统一**:InsightCard / AkShare / TuShare / Scheduler / DebugView 共 6 处换 `EmptyStateView`(带说明文案) 4. **加载态补齐**:AkShare / TuShare 数据概览在 loading 时此前显示空白,补 `LoadingPlaceholder` 5. **硬编码色修复**(DashboardView):数据源徽章 RGB→`FinDesign.statusBlue/statusGreen`;monthly `.purple`→`statusPurple` ### 验证与部署 - `swift build -c release` 通过(29s 增量编译;EnvChecker actor 隔离警告为既有,非本次引入) - 纯 Swift 改动、Resources 未变 → 走「只换二进制」路径:cp + codesign + 重启 - 部署后主进程/采集器/后端/MySQL 全部正常,后端 HTTP 200,采集中断约 30 秒 - ⚠ **codesign 坑**:连续两次签名(具名证书失败→ad-hoc 兜底)会触发 `.cstemp` 竞态, 封条引用已消失的临时文件 → `a sealed resource is missing or invalid`。 本地 ad-hoc 应用不受影响(无隔离标志不过 Gatekeeper),但 `codesign -v` 恒报错; 应用运行期 bundle 内 venv 生成 `__pycache__`(68 个目录)本就会破坏封条,属常态。 换完整产物请走 `build-app.sh`(在 .build 全新目录签名无此竞态)。 ### 补充修复与正式发版 v1.5.1(11:43) - **追加 2 处统一**:日志清空确认框、日历日期详情页的弹窗标题, `.subheadline + fontWeight(.medium)` → `FinDesign.FontStyle.headline`(随用户字号设置缩放)。 DatabaseSetupGuideView 的 `subheadline` 判定为引导流程专属视觉语言,刻意保留。 - **VERSION `1.5.0 → 1.5.1`**(UI 优化,patch 级),午休窗口走完整 `build-app.sh` 安装: 全新目录干净签名(无 cstemp 竞态,`codesign -v` 通过)、442M、 部署版本 1.5.1、服务全绿(采集器/后端自动拉起、HTTP 200、MySQL 全程未中断)。 - ⚠ **构建陷阱三连(本轮踩全了)**: 1. `swift build | grep ... | head -N` 会被 **SIGPIPE 中途杀掉编译**(head 取满即退出), 二进制停留在旧状态——构建输出**一律重定向到文件**再看; 2. SPM 增量判定用**内容指纹**,`touch` 改 mtime 不改内容不触发重编;要确定性重编就 清掉 `.build/arm64-apple-macosx`(mv 到 /tmp),全量重编仅 ~37s; 3. BSD grep 的 `\|` 交替在命令替换上下文中会失效(假 0 匹配引发误判), 一律用 `grep -E "A|B"`。 验收标准:`swift build` 后 `.build/release/FinDataHub` 的 **mtime 必须等于当前时间**。 ## 2026-08-31 — v1.5.0 发版(四项改动 + 三个真实 bug 修复) ### 版本变更 `v1.4.7 → v1.5.0`(语义化 minor:含用户可见新功能 + 运行环境变更,沿用 v1.4.0 同规模升 minor 的惯例)。 ### 本轮打包包含的内容 | # | 改动 | 验证 | |---|---|---| | 1 | SettingsView 拆分 2491→394 行 + 10 个子文件 | 编译通过 | | 2 | Python 统一 3.12(3.14→3.12) | 运行时确认 3.12.13 | | 3 | kuaixun venv 重建(jieba 等依赖完整) | HTTP 200 | | 3b | VenvManager 版本校验(可写 venv 优先级 > bundle 的 bug 修复) | 3.14 副本在原位时自动选 bundle 3.12 | | 4 | MySQL 按需启停(懒加载单例导致定时器不存在的 bug 修复) | 拉起 6s / 停止 5min 准时 | **三个真实 bug(全靠实测暴露,代码看着对、实际不生效)**: 1. 懒加载单例 → `MySQLOnDemandManager` 的 init 未执行、空闲定时器不存在 → 在 `FinDataHubApp.init` 补 `_ = MySQLOnDemandManager.shared` 2. 可写 venv 优先级高于 bundle venv → Python 版本统一形同虚设 → VenvManager 增加版本校验,优先选 bundle 内匹配版本 3. DebugView 定时器永不 invalidate → 主进程唤醒 503 次/秒 → 修复后降到个位数 ### 构建与部署要点 - **产物体积 653M → 442M(−32%)** - **`build-app.sh` 只产出 `.build/FinDataHub.app`,不自动安装到 `/Applications`** —— 这是本轮的关键坑:VERSION 改了、`.build` 里是 1.5.0,但部署位仍是 1.4.7,因为它从没被拷过去。安装需手动 `mv .build/FinDataHub.app /Applications/FinDataHub.app`。 - **本轮二进制与上一版字节数完全相同(11072016)**:因为只有 VERSION 文件变化,Swift 代码未动,功能代码早已在线上运行。重装仅更新版本号字符串。 - 安装方式:停 App + 停采集器/后端(保留 MySQL)→ `mv` 旧 app 到 /tmp 备份、`mv` 新 app 到 /Applications → `open -a FinDataHub` 自动拉起服务。 - 采集中断约 25 秒(远低于预估 1–2 分钟),MySQL 全程未中断。 ### 验证快照(发版后) - 部署版本 1.5.0,签名有效;主进程/采集器/后端均 running;后端 HTTP 200;MySQL 监听 3306。 ## 2026-08-28 — 恢复流程全自动化改造(AutoRecoveryManager + 双场景实战验收) ### 变更概述 将快讯服务(后端/采集器)恢复流程改造为全自动:检测 → 自动恢复 → 结果提示,全程零干预;保留手动恢复入口。 **新增 AutoRecoveryManager.swift**(独立状态机,阈值集中可配置): | 检测场景 | 判定 | 恢复动作 | |---|---|---| | 进程异常退出 | status running=false | 自动拉起 | | 服务状态不可达 | status 连续 3 轮失败 | 自动拉起 | | 采集器卡死 | stuck=true | 重启采集器 | | 数据停更 | last_write_time 超阈值(默认 15 分钟) | 重启采集器 | - 退避重试 30s→2min→10min;10 分钟窗口最多 3 次,超限熔断 30 分钟并通知手动恢复 - 手动停止保护期 30 分钟(尊重"停止"按钮语义,避免停了又被拉起) - 恢复后 20s 复查 status 确认,成功发通知("已自动恢复"+原因)并写同步历史 - 手动恢复按钮清空熔断与计数(备用方案) **NewsFlashService 改造**: - checkServiceHealth 接入 AutoRecoveryManager.evaluate(每 30s 一轮) - startServiceMonitor 幂等挂接恢复动作(修复:MenuBarExtra onAppear 晚于服务自启动,导致恢复闭包未注入、自动恢复静默失效) - stopCollector/stopBackend 记录 manualStopAt;"所有服务停止→关监控"在自动恢复开启时保持监控 - 设置页新增:自动恢复开关 + 数据停更阈值(5-60 分钟)+ 自动恢复结果通知开关 **build-app.sh 修复**:`swift build | tail` 管道吞掉退出码,编译失败时静默用旧二进制打包(本次部署旧二进制的元凶)→ 加 `set -o pipefail` + `--disable-sandbox`(本机 SPM manifest 沙箱不可用)。 ### 实战验收(真实杀进程) - 杀后端:18:01:07 检测"进程异常退出"→ 自动拉起 → 18:01:28 恢复成功 → health ok - 杀采集器:18:02:38 检测 → 18:03:00 恢复成功 → running=true - 恢复成功日志:`[INFO] [AutoRecovery] 快讯后端服务 自动恢复成功` ### 验证快照 - backend/collector 均 alive;/health ok - news_unified 表 300 条真实数据(含跨源合并),最新 18:02:08 - tushare 数据到 20260828(定时任务正常) ### 遗留 - App 启动时(LaunchServices 环境)collector 曾出现 `env: ~/Library/Application Support` 截断报错,手动/精简环境均无法复现,已用恢复机制兜底;若复现需排查 VenvManager 可写 venv 路径传递。 - VERSION 已于 2026-08-31 发版 bump 至 **v1.5.0**(见顶部发版记录)。 ## 2026-08-28 — 部署实例体检与修复:数据管道停摆恢复 + 统一表链路打通 ### 变更概述 对 /Applications/FinDataHub.app 部署实例做全面体检,发现并修复 4 处: 1. **快讯采集停更 2 天** — 采集器 offline,各源最后入库 08-25 23:15。已恢复实时入库。 2. **8100 后端跑旧代码** — 运行实例 36 条路由无 /api/v2、无 WebSocket。根因有二:(a) App bundle 内代码旧;(b) 入口脚本 newsflash_backend.py 漏挂 unified_router。已同步 bundle + 补挂路由(36→42 条)。 3. **news_unified 统一表从未被采集链路写入** — 采集脚本 insert_news 未接 UnifiedWriter(pipeline.py 中的接线只服务另一条流程)。已在 insert_news 内集成四层去重写入,跨源合并生效(日志见 [unified] 新增/合并统计)。 4. **行情数据停更** — launchd 任务实际已加载(launchctl list 在本环境显示异常为误判),手动 kickstart tushare/akshare 日更,tushare 已到 20260827。 ### 修复要点 - `Scripts/bin/newsflash_backend.py` — import + include unified_router,同步至 bundle bin/。 - `Scripts/bin/newsflash_collect.py` — insert_news 收集新增行并调用 UnifiedWriter.process_batch(非破坏性,失败仅打印警告),同步至 bundle bin/。 - bundle 热同步:kuaixun-service 全量(app/extend/src/common,排除 .venv/__pycache__/tests)。 - 服务重启:kill 旧 App(PID 1590)后 open -a FinDataHub,由 App 自有流程拉起后端+采集器(新 PID 44374)。 - 手动拉起进程与 App 单例 pkill 存在竞争(pkill -f newsflash_backend.py),结论:服务统一由 App 管理最稳。 ### 验证结果 - /health ok;/api/v2/news/live 返回真实跨源快讯(source_count=2 合并生效) - news_unified 表 4 条测试数据 → 实时增长(含跨源合并) - 采集器 running=true,120s/轮,AI 评分/打标管线运行 - tushare stock_daily MAX(trade_date)=20260827 ### 遗留事项 - plist 中 MYSQL_PASSWORD 明文(com.tushare.dailyupdate.plist),建议改 keychain/独立 env 文件。 - 手动 nohup 启动服务时必须带 MYSQL_PASSWORD,否则 v2 路由 500(App/launchd 链路已带)。 - launchctl list 在 WorkBuddy 运行环境返回空,判断 launchd 任务状态需用 launchctl print gui/501/