FinDataHub
|
9c9af451e2
|
chore(security): 添加 gitleaks 豁免配置
- .gitleaksignore:4 条 generic-api-key 误报(tushare 表名 top10_holders 等)
- .gitleaks.toml:继承内置规则,仅豁免上述两个审计文件自身(防指纹 hash 被误判)
|
2026-11-03 20:48:16 +08:00 |
|
FinDataHub
|
e2310bb60f
|
fix(security): 移除源码硬编码凭据,改为环境变量注入
- cloud_sync.py: RDS 主机/账号/密码改为 RDS_HOST/RDS_USER/RDS_PASSWORD,无源码默认值
- ths/__init__.py: AIMIAI AppID/AppKey 去掉硬编码兜底值,缺失时明确报错
凭据已迁至 ~/.config/findatahub/env(权限 600,不入库)。
推送前由 gitleaks pre-push 钩子检出,此提交为清理动作。
|
2026-11-03 20:46:58 +08:00 |
|
FinDataHub
|
56cb97979b
|
docs: L3 标记为已完成,补沉淀两条部署硬约束
v1.6.1 已装机,L3(部署位更新)不再是搁置项。补记执行结果与两条过程中
踩出来、值得留档的约束:
1. 替换 app 前必须停掉全部 Python 服务(漏后端 → 旧进程存活、新 app 跳过启动,
旧代码继续服务且不报错)
2. 停应用用 pgrep -x FinDataHub,不用 pgrep -f "<bundle路径>"——后者会匹配
Bash 工具自身命令行,把自己杀掉(exit 143)
|
2026-10-27 23:26:13 +08:00 |
|
FinDataHub
|
b777f4e241
|
docs: 收口 L2 / T5-2 / T8,版本 1.6.0 → 1.6.1
## 待办清单
- L2 标记完成,并记录「为什么不在脚本里写 try/except 双路径,而是给 paths.py 补函数」
- T5 标题更新为「842 → 800 行,第 1–2 步已完成」,补第 2 步完整记录
(含 SyncSession 的取舍、证伪结果、以及第 3 步为何仍不做)
- 新增 T8:生产入口生成随机 API_KEY 导致受保护端点恒 401,
含根因对照表、修复、部署位实测、防回退锁
## DEVLOG
追加本轮三段记录。其中两个坑值得单独留档:
1. `pgrep -f "FinDataHub.app/Contents/MacOS"` 会匹配到 Bash 工具自身命令行 →
kill 自己(exit 143),链式命令中断。部署片段已全部改为 `pgrep -x FinDataHub`。
这是本项目第二次踩同类问题(第一次是 pkill -f)。
2. Swift 编译报 `failed to produce diagnostic for expression` 时,错误位置会退化到
文件**最后一行**,真正原因藏在中间的 #Preview 固件里。
## MEMORY.md
同步三处约束:API_KEY 必须走共享加载函数(第二处双入口分叉)、
paths.service_dir 用于服务资源定位、SyncSession 已是状态唯一来源 +
`replaceSession` 的用途与其编译陷阱。
## VERSION
1.6.0 → 1.6.1(patch):本轮为内部重构 + 缺陷修复,无新增用户可见功能。
|
2026-10-27 23:23:16 +08:00 |
|
FinDataHub
|
cb11a3d445
|
fix(backend): API_KEY 统一从持久化文件加载,修掉受保护端点恒 401
## 现象
装机后测受保护端点,文件 key → 401、乱码 key → 401、无 key → 401。
全 401 说明不是「没鉴权」,而是**鉴权用的 key 与客户端手里的不是同一个**。
## 根因(与 T6 同族,又一处双入口分叉)
app/main.py → _load_or_create_api_key() 读持久化文件
Scripts/bin/newsflash_backend.py → secrets.token_urlsafe(32) 现生成随机值
生产走后者,每次启动换一把钥匙。客户端读 ~/Library/Application Support/
findatahub/.api_key 去请求,永远对不上;两边都不报错,只表现为「点了没反应」。
## 修复
把加载逻辑收进 app/dependencies.py::load_or_create_api_key(),两个入口共用:
- 文件已存在 → 复用,**不覆盖**(覆盖会让客户端手里的旧 key 立即失效)
- 文件存在但内容为空 → 补生成(空 key 会让 compare_digest("", "") 恒真)
路径用 platformdirs.user_config_dir("findatahub"),macOS 上即
~/Library/Application Support/findatahub,与 Swift 端读取位置一致。
## 部署位实测
文件 key → HTTP 200 (修复前 401)
乱码 key → HTTP 401
无 key → HTTP 401
## 防回退锁
test_app_factory.py::TestApiKeySource 3 条:共享加载函数存在、复用时不被覆盖/
空内容补生成、两个入口源码都必须出现 load_or_create_api_key 且都不得出现
secrets.token_urlsafe。
backend 443 → 446。
## 这一族的教训
T6 收敛了「装配」,但 lifespan 里还藏着同类分叉。凡是「两个入口都该有且必须一致」
的逻辑,都该像 key 加载这样收成单一函数,而不是指望两边同步。
|
2026-10-27 23:23:06 +08:00 |
|
FinDataHub
|
f112e9d277
|
feat(paths): 服务资源定位支持 bundle 与源码树两种布局(关闭 L2)
## 问题
backfill_stock_daily.py 把部署路径写死在 sys.path.insert 里:
sys.path.insert(0, "/Applications/FinDataHub.app/Contents/Resources/akshare_fetcher/src")
macOS 现网能跑,换平台或换安装位置即 ImportError。这是待办清单里长期挂着的 L2。
## 为什么不在脚本里写 try/except 双路径
那样只解决 bundle 形态,源码树运行时仍走 hardcode。正确做法是让「资源在哪」
由 paths.py 统一回答——它已经是路径的唯一事实来源。
## 新增三个函数
- resources_root():bundle → <App>/Contents/Resources;源码树 → 仓库根
- service_dir(name):<root>/<name> 或 <root>/Services/<name>
- service_src_dir(name):service_dir(name)/"src",可直接塞进 sys.path
**判据是内容而非层级**:仓库根下有 Services/,bundle 的资源目录下没有
(build-app.sh 把各服务平铺拷进 Resources/)。首版按「两级往上」硬算,
源码树里解析成 <repo>/Scripts——被 test_real_repo_layout_resolves 当场抓出。
## 验证(两形态实跑)
源码树:<repo>/Services/akshare_fetcher/src 存在 ✓
bundle:/Applications/…/Resources/akshare_fetcher/src 存在 ✓
import akshare_fetcher 成功 ✓
bundle 侧解析结果与旧硬编码**逐字一致**,故行为零漂移。
## 回归
TestServiceLayout 6 条:bundle / 源码树 / 两者共存时优先 bundle /
都不存在时返回 bundle 形态(错误信息才能指向期望位置)/ src 后缀 / 真实仓库解析。
scripts 257 → 263。
|
2026-10-27 23:23:06 +08:00 |
|
FinDataHub
|
54626a69fb
|
refactor(client): 同步状态收敛为 SyncSession 值类型,修掉 reset 漏字段
## 问题
PythonProcessManager 上 20 个各自独立的 @Published 散落着,带来两个实际问题:
1. 重置靠逐个赋值。旧 reset() 实际漏了 6 个字段——syncRate / successCount /
failCount / currentTableProgress / currentTableStocksDone /
currentTableStocksTotal。症状是「第二轮同步一开始就显示上一轮的速率与成功率」,
不报错、不崩溃,只能靠肉眼在界面上发现。
2. 每个字段各自触发一次 objectWillChange,一次状态推进可能连发数次刷新。
## 改动
新增 Models/SyncSession.swift(值类型,20 字段 + 默认值)。manager 上改为:
@Published private(set) var session = SyncSession()
private(set) var progress: Double { get { session.progress } set { … } }
… 共 20 个同名转发属性
- View 层零改动:对外仍是同名属性,90+ 处读取照旧
- reset() 改为一条 `session = SyncSession()`,新增字段自动被重置
- setter 收为 private(set):外部只能读,写入口全收在类内(拆文件的必要前提)
- 未纳入 session 的两个字段:state(带 didSet 副作用,语义是状态机)、
consecutiveFailureWarning(UI 横幅状态)
## 必须记住的坑
编译报 `failed to produce diagnostic for expression`,位置在 DashboardView /
SyncProgressView 的**最后一行**。真正原因是 #Preview 固件里在直接
`pm.progress = 0.45`,而 setter 已收为 private(set)——Swift 对大 body 的类型检查
失败会把错误位置退化到文件末行,看不出真正的出错点。
解法不是放开 setter,而是给预览/测试一个显式缝 replaceSession(_:),
注释写明「业务代码不要用:它绕过节流刷新、日志缓冲与状态机既有路径」。
另外 reset() 从 private 放宽到 internal,仅为了让回归测试能直接调用。
## 证伪
把 reset() 改回逐字段赋值(漏 6 个字段)后,testResetClearsAllSessionFields
精确报出 6 条失败,形如:
XCTAssertEqual failed: ("3999") is not equal to ("0") - successCount 未被重置
恢复后 47 tests 全绿,grep 证伪实验 无残留。
## 回归
SyncSessionTests 5 条(默认值中性 / 值语义 / replaceSession 往返 /
重置覆盖全字段 / 保留 queuedTasks)。Swift 42 → 47。
|
2026-10-27 23:22:57 +08:00 |
|
FinDataHub
|
88a2ba0675
|
docs: T6 收尾 + 拆出运维基线 + 记忆瘦身 43%
## 待办清单:T6 标记为已根治
补记第二阶段(装配收敛)与第一阶段(两处都改)的区别,新增三张表:
应统一 / 不应统一 / 防回退锁,以及验收结果与遗留说明
(两入口仍是独立进程,彻底合并需先统一 SQLAlchemy 与 db_pool 两套 DB 层,独立工程)。
## 新增 Docs/FinDataHub-运维基线.md
这些是**引用时需带口径**的参考数据,不属于「改代码前必读」,从 MEMORY.md 拆出:
性能基线(交易/非交易时段对比与测量方式)、生产实际运行链路(launchd + 为何
pipeline_runner 未在生产跑)、MySQL 版本升级链与依赖坑、体积口径(26GB vs 41GB)、
排查技巧(日志路径、jieba 重启信号、BSD grep 漏匹配)。
## MEMORY.md 瘦身
26,422 → **14,980 字节(压 43%)**,400 → 225 行,重构为 11 节分层:
速查 / 过时纠正 / 版本 / 构建部署 / 后端 / 数据层 / 测试 / 跨平台 / 客户端 /
操作禁忌 / 运维指针。
同时清掉已失效内容:`db.replace()` 早已加熔断、部署位已更新到 v1.6.0、
「akshare 是 Windows 硬阻塞」已推翻、`collector.py` 已重构。
24 项关键约束逐条 grep 验证零保留丢失。
动因:原文件已超注入上限被截断(实测截断点约 14.2KB),而截断是从**尾部**发生,
会丢掉最新的第 17–20 条(生产入口、命名冲突、部署流程、build-app 通配)——
恰恰是最该被读到的部分。
|
2026-10-27 13:30:45 +08:00 |
|
FinDataHub
|
3de1d78863
|
refactor(backend): 后端装配收敛到 create_app,根治双入口漂移
## 问题
仓库有两条 FastAPI 启动路径——app/main.py(uvicorn,手册/开发)与
Scripts/bin/newsflash_backend.py(**生产实际入口**),两者各自 new FastAPI 并各写一遍
中间件与路由注册。只改 app/main.py 的装配时,生产**完全空转且不报错**;而
app/database.py 因被 import 反而生效——一半生效一半不生效,最难察觉。
2026-10-27 装机实测:CORS 预检 405、/health 只有 {status, time} 两个字段,
而源码里明明是完整实现。
早先的止血只做到「两处都改」(给 backend 补中间件、health 载荷共用),
漂移风险仍在。本次根治。
## 改动
新增 Services/kuaixun-service/app/factory.py::create_app(),把**必须逐字一致**的
两件事收敛到一处:
1. 中间件顺序(由内到外)exception_catch → inner_middlewares → security_stack → cors
Starlette 的 add_middleware 是 insert(0),先添加的位于更内层。CORS 必须最外,
否则异常响应拿不到跨源头、浏览器只报 "blocked by CORS policy"。
2. 基础路由清单(7 个 router)
两个入口现在都只调 create_app(),self-build 的 FastAPI 实例全部删除。
差异只保留「本就该不同」的部分:
| 差异项 | 处理 |
|---|---|
| lifespan | 各自实现(main 等建表 / backend 走 db_pool) |
| backend 独占端点(analysis / source-test / shutdown / SPA catch-all) | 留在 backend |
| ResponseCache / RequestLog | 经 inner_middlewares 传入,**层序仍由 factory 决定** |
app/main.py 顺带清掉变为无用的导入(WebSocket / Request / JSONResponse /
HTTPException / 7 个 router 名)。
## 必须保留的实现细节
_security_module() 先试常规导入、失败再按 __file__ + importlib 加载。原因:
Scripts/bin/common.py(模块)与 kuaixun-service/common/(包)同名,bin 在 sys.path
前部且已被缓存,生产入口下 from common.security import ... 会报
'common' is not a package。实测该场景确实走 fallback,加载到正确的 security.py。
## 顺序验证(实测,非推理)
app.user_middleware 由外到内:
app.main : CORSMiddleware → SecurityHeaders → ExceptionCatch
backend : CORSMiddleware → SecurityHeaders → RequestLog → ResponseCache → ExceptionCatch
与原 backend 的 exception_catch → ResponseCache → RequestLog → security → cors
逐层一致。新版 FastAPI 用 _IncludedRouter 惰性挂载(path 为 None),
故测试断言挂载数量而非 path 集合。
## 防回退锁 + 证伪
tests/test_app_factory.py 8 条,核心是「两个入口源码里不得出现 FastAPI( 与
include_router(」(注释行豁免)。
证伪:把 backend 改回 app = FastAPI(...) 后,
test_backend_entry_does_not_build_app_itself 立即失败:
E AssertionError: 生产入口必须经 create_app 装配
恢复后 443 passed,grep FastAPI( 无残留。
## 部署位实测(只改 Python 资源,免全量重建)
/health → {"status":"ok","db":{"ready":false,"connected":true,
"timebase_drift_seconds":0.537},...}
OPTIONS 预检 → 200, access-control-allow-origin: http://tauri.localhost
安全头 → x-content-type-options / x-frame-options / referrer-policy 齐全
analysis/* → overview / trend / hourly 全 200(后端专属端点未被破坏)
news/live,stats→ 200
RequestLog → stderr 有 "POST /api/v1/news/notify-internal → 200 (14ms)"
回归:后端 435 → 443 passed。全量 8 套件 PASS(含真实库集成),40.6s。
|
2026-10-27 13:30:37 +08:00 |
|
FinDataHub
|
08f7c4155e
|
docs: 记录部署验证发现的三个新问题与部署流程
## 待办清单新增第四节
- **T6 双后端入口**(已修,架构隐患仍在):记录两条启动路径、为何"改错地方"
不报错、修复方式,以及长期收敛建议(让 backend 只做启动器)
- **T7 health 的 ready 语义不一致**(已修):backend 走 db_pool 不建表,
ready 恒 false 会让可用服务报 degraded
- **部署踩坑三条**:必须停全部 Python 服务(漏后端会静默用旧代码)、
build-app.sh 白名单会漏文件、只改 Python 资源可免全量重建
## 记忆新增 17–20 条
- 17. 生产后端入口是 newsflash_backend.py,不是 app/main.py(本项目最易踩的坑)
- 18. common 命名冲突与路径推导不能用 KXS_HOME
- 19. 部署 app 的正确流程与三条验证要点
- 20. build-app.sh 已改通配
|
2026-10-27 00:32:24 +08:00 |
|
FinDataHub
|
9a5fa23228
|
build: 脚本通配复制 + 产物完整性自检;版本 1.5.2 → 1.6.0
## build-app.sh 第 3 步改为通配复制
原先是 ~18 行 `cp -f xxx.py` 的显式白名单。代价是每新增一个运行时依赖文件
都必须记得回来加一行——漏了只在**部署位**暴露为 ImportError(本地跑源码完全正常)。
本轮就撞上了:paths.py / process_utils.py 未入产物,导致部署位的
newsflash_collect.py 静默走 ImportError 降级到硬编码路径,跨平台抽象层等于没装。
改为 for 通配复制 *.py / *.sh:tests/ 与 __pycache__ 都是子目录不会被匹配;
带入的开发脚本每个仅数十 KB,且部署位可用来做诊断。
## 新增构建期完整性自检
在打包完成后断言最小运行时必需集(paths.py / process_utils.py / common.py /
db_pool.py / sync_bridge.py / newsflash_collect.py)都在产物里,缺任一即 exit 1。
原则:**打包漏文件必须在构建阶段失败,不能等上线后才发现**。
## VERSION 1.5.2 → 1.6.0
minor 升级依据:本轮新增跨平台抽象层(paths/process_utils)、前端 runtime.js
端点解析、CORS 白名单(对外接口行为变化)、SyncOutputParser。
不是纯修补,故走 minor。同版本号无法区分新旧,部署验证会失去锚点。
|
2026-10-27 00:32:23 +08:00 |
|
FinDataHub
|
e48b2e0ce6
|
fix(backend): 生产入口补齐 CORS 与 health,两路入口收敛到共享实现
## 部署验证发现的严重问题
本轮装机后实测发现:加在 app/main.py 的修复**在生产完全不生效**——
部署位 /health 只返回 {status, time}、CORS 预检返回 405,而源码明明是新版。
根因:仓库有**两条后端启动路径**——
Services/kuaixun-service/app/main.py uvicorn app.main:app(开发/手册)
Scripts/bin/newsflash_backend.py **生产实际入口**,自建 FastAPI(...),
从不 import app.main
后者自己 app = FastAPI(...)、只 import 各 router,并自己注册了 /health。
因此 apply_cors / apply_exception_catch / apply_security_stack / 增强 health /
init_db 重试全部空转。而 app/database.py 的改动(now() 时间基准、双检锁、
连接超时)因为被 import 而生效——一半生效一半不生效,最难察觉。
## 修复
1. newsflash_backend.py 装三层中间件,顺序(由内到外):
exception_catch → ResponseCache → RequestLog → security_stack → cors
(后者必须最外,否则 500 响应拿不到跨源响应头)
2. health 载荷抽到 app/health.py::build_health_payload(),两个入口共用,
避免再次分叉
3. app/main.py 的 health 只做路由注册,转调共享实现
## 途中踩到并绕开的两个坑
- **命名冲突**:Scripts/bin/common.py(模块)与 kuaixun-service/common/(包)同名,
且 common 已被 `from common import ...` 缓存,故
`from common.security import ...` 报 "ModuleNotFoundError: 'common' is not a package"。
改为按 __file__ 推导路径 + importlib 显式加载。
- **路径推导**:不能用 common.KXS_HOME,它在 bundle 下解析为 Contents/Services/...
(缺 Resources 层级),实测 FileNotFoundError。改用
os.path.dirname(os.path.dirname(abspath(__file__))) 得到 Contents/Resources。
## 顺带修正 health 的 ready 语义
ready 表示「SQLAlchemy 建表完成」。生产入口走 db_pool(pymysql 池),不负责建表,
ready 恒为 False → 健康检查对完全可用的服务持续报 degraded,反向误导运维。
改为 ok = connected and drift_ok and not(init_failed),
即连通性与时区基准是硬判据,ready 只作诊断信息。
## 部署位实测(修复后)
GET /health → {"status":"ok",
"db":{"ready":false,"connected":true,"error":null,
"timebase_drift_seconds":0.274},
"ws_connections":0,"time":"2026-10-27T00:31:13"}
OPTIONS /api/v1/news/live (Origin: http://tauri.localhost) → 200
access-control-allow-origin: http://tauri.localhost
access-control-allow-headers: ... X-API-Key ...
access-control-allow-credentials: true
业务接口 GET /api/v1/news/live?limit=1 → 返回真实快讯数据
测试:test_timebase.py 的 health 用例改为 patch app.database 模块属性
(health 内部按模块属性取值),435 passed。
|
2026-10-27 00:32:16 +08:00 |
|
FinDataHub
|
c7f26ea3a0
|
docs: 更新 T5 进展,记录解析器抽取与 NSRange 缺陷
- 待办清单 T5 从「842 行待拆」更新为「842 → 729 行,分步推进中」,
记录第一步已完成、剩余两步及其判据
- DEVLOG 追加本轮记录(含为何放弃 SyncSession 优先路线)
- 记忆新增第 16 条:解析规则只改 SyncOutputParser、NSRange 必须用
NSString.length、tqdm 表名取尾词的既有行为勿"修正"
|
2026-10-27 00:05:18 +08:00 |
|
FinDataHub
|
fa23425cbe
|
refactor(client): 抽取 SyncOutputParser,解析逻辑首次可测
## 为什么选这个切入方式
原计划先引入 SyncSession 值类型再拆文件。但实测 View 层对 @Published 状态的
直接访问约 90+ 处(15 × state、9 × progress、7 × startTime …),那条路要改
20 个属性的读写路径并全量验证。
改为更小的第一步:**抽取无状态解析**。关键区别是这一步**不需要放宽任何
private 访问级别**——Swift 的 private 是文件级作用域,一旦为拆文件而放开内部
状态,封装性就实质下降;而解析器接收「一行字符串」、返回「动作描述」,
天然不依赖 manager 状态。
## 改动
- 新增 SyncOutputParser.swift(260 行):5 个预编译正则 + stdout/stderr/tqdm
解析 + 缓冲切分,全 static 无状态
- manager 的 4 个方法改为「调用 parser + switch 应用动作」
- 删除 manager 内 5 个正则声明(避免同一模式两处维护)
- 主文件 842 → 729 行
## 关键设计:返回动作序列而非直接改状态
原 parseStdoutLine 用 matched 标记而非提前 return,因此**一行可命中多个模式**
(如 `✅ [x] 完成 成功: 3` 同时产出两条日志)。改成 guard-else-return 会静默
改变行为。故 parser 返回 [StdoutAction],manager 逐条应用。
用例 testMultiplePatternsInOneLine 锁定该语义。
## 顺带修掉一个真实缺陷
原实现部分正则用 NSRange(location: 0, length: trimmed.count) 构造范围。
NSRange 按 **UTF-16 单元**计数,String.count 按**字素簇**计数:
含 emoji 的行(⚠️ 占 2 个 UTF-16 单元)长度偏小 → 行尾的「成功: N」「速率: X」
落在范围外被**静默漏匹配**。
统一改为 (s as NSString).length,并写回归用例
testEmojiLineStillMatchesTrailingRegex(先断言该行确实存在长度差,
否则用例本身没有意义)。
## 还原一处既有行为
`daily sync: 45%` 的表名取到 "sync" 而非 "daily"——原实现取冒号前最后一个
非数字片段。测试首次失败暴露这点,是测试期望写错、不是代码错。
已在用例中锁定并注明,避免后人"顺手修正"改变既有显示。
## 测试
Swift 18 → 42 tests(新增 24 条:模式识别、tqdm 提取、stderr 分级、
emoji/NSRange 回归、缓冲切分边界)。全量 8 套件 PASS。
|
2026-10-27 00:05:18 +08:00 |
|
FinDataHub
|
21293dd982
|
docs: 建立遗留待办清单,归档三项搁置与外置条件
此前「已知但本轮不处理」的事项散落在 DEVLOG 与工作记忆里,半年后无人知道
能不能动。新建 Docs/FinDataHub-遗留待办清单.md 集中管理,分三类:
搁置项(等外部条件)、技术待办(可推进)、已知不在范围。
每项必须写明「为什么留着」+「触发条件」+「验收标准」。
## 三项搁置(等外部条件)
- **L1 未推送**:仓库无 remote,GitHub 连接器 auth-required
- **L2 backfill_stock_daily.py:15 硬编码 /Applications 路径**:部署环境专用脚本,
改 paths.py 需先补 bundle 资源路径函数且未验证,故原样入库
- **L3 部署位未更新**:/Applications/FinDataHub.app 仍是旧构建,
需 build-app.sh + 手动 mv(该脚本只产出 .build/,不自动安装)
## 技术待办本轮进展
- **T1 已完成**:site.addsitedir 失败留痕
- **T2 已评估,不做迁移**:published_at 在 NewsFlash(BigInteger)与
NewsUnified(TIMESTAMP)类型不同,但逐条核对后判定为「设计不统一而非缺陷」——
跨表 UNION 的都是同构分表,两个模型从未出现在同一条 SQL 里,
无 JOIN、无比较、无跨表对齐。为它做 36 万行迁移属无需求驱动的风险支出。
- **T3 假设被推翻**:akshare 在 Windows + py3.12 可解析(mini-racer 0.14.1
与本机一致),已在 CI 加实测步骤。附「包名 mini-racer / 模块名 py_mini_racer」
的命名陷阱。
- **T5 成本重新量化**:除 private 是文件级作用域(约 37 个成员需放宽)外,
实测 View 层对 @Published 状态的直接访问约 **90+ 处**。因此建议的执行顺序
改为「先引入 SyncSession 但对外仍以计算属性暴露 → View 零改动即可交付」,
而不是直接拆文件。
|
2026-10-26 21:08:10 +08:00 |
|
FinDataHub
|
21606708d0
|
ci(windows): 实测 akshare 可行性,推翻「硬阻塞点」假设
## 假设被推翻
迁移计划曾把 akshare 的 py_mini_racer(62MB V8 预编译)列为「唯一硬阻塞点」,
理由是 Windows + cp312 是否有 wheel 未验证。
2026-10-26 用 uv 做离线解析(无需 Windows 机器):
uv pip install --python-platform windows --python-version 3.12 --dry-run akshare
→ 解析成功,mini-racer==0.14.1,与本机 macOS 上的版本完全一致
→ pandas/numpy 仅小版本差异(3.1.0/2.5.4 vs 本机 3.0.3/2.4.6)
## 但「能装」不等于「能跑」
V8 原生库在 win32 上能否真正加载并执行 JS 必须实测。故选在观察期的
windows job 里加三步实测(各自 continue-on-error,避免装不上时中断后续步骤):
1. 安装 akshare
2. 验证 py_mini_racer 可执行 JS(ctx.eval('1+1') == 2)
3. 跑 Services/akshare_fetcher/tests
## 一个必须写死的命名陷阱
PyPI 包名是 mini-racer,但**顶层模块名仍是 py_mini_racer**
(akshare 源码里全是 import py_mini_racer)。写成 import mini_racer 会
ModuleNotFoundError,从而误判为「Windows 装不上」。
本机已验证正确写法:import py_mini_racer → ctx.eval('1+1') == 2,
akshare 1.18.64 及其依赖 mini-racer 的模块均可正常导入。
首次 CI 反馈后:若三步全绿,可去掉 continue-on-error,并考虑把 tushare
一并纳入 Windows 范围。
|
2026-10-26 21:07:59 +08:00 |
|
FinDataHub
|
649085ab3b
|
fix(scripts): site 目录加载失败留痕
common.py 的 site.addsitedir() 原为 except Exception: pass。
此处静默的代价是:某个 site 目录没加载成功时,「依赖明明装了却 import 不到」
完全无从判断——而排障时最需要知道的恰恰是「哪个目录失败、为什么」。
改为 stderr 留痕(沿用本文件的 ⚠️ 风格)。单个目录失败仍不阻断启动。
说明:其余回填/分析脚本中的 except: pass 保持原样。
全项目 40 处已分类审计(见审查方案 §3.4),多数属合理防御——
错误处理路径内再失败、崩溃处理器自身失败——无差别改成 logger.exception
会制造噪音甚至改变行为。
|
2026-10-26 21:07:58 +08:00 |
|
FinDataHub
|
5901055e16
|
chore(repo): 补入两份历史遗留产物
此前长期处于未跟踪状态的资产,属 P0-7「版本控制失效」的同类隐患——
一次 git clean -fd 即不可恢复。
1. Scripts/bin/backfill_stock_daily.py(88 行)
个股日线历史回填,按 OVERRIDE_DATE 逐日补 akshare_data.stock_daily。
文档说明了两个关键判据:只跑 stock_daily 不做板块快照(后者的接口只返回
实时数据,用于历史回填等于用今天快照伪造历史行情,会污染题材分析);
以及运行前需清除注入的 http_proxy(否则国内源 ProxyError)。
⚠️ 已知问题(未在本轮修改):第 15 行硬编码
/Applications/FinDataHub.app/Contents/Resources/akshare_fetcher/src
与跨平台方向冲突,应改为经 paths.py 解析。因其为部署环境专用脚本,
本轮不做无验证的改动,留作待办。
2. FinDataHub-应用功能设计与模块结构方案-2026-10-17.html(37K)
10-17 的功能与模块结构设计文档。
|
2026-10-26 20:56:28 +08:00 |
|
FinDataHub
|
cb905cda01
|
docs: 新增架构说明与全面审查方案,补入工作记忆
## Docs/ARCHITECTURE.md(新增)
此前缺一份「四层是什么、边界在哪、数据怎么流」的单一入口——新人与跨端协作
只能翻 5000 行 DEVLOG。本文控制在 300 行内,只写当前稳定结构:
- 全景图与四层边界(采集/数据/服务/展示,含"不该做什么")
- 数据流(日更行情、快讯、读取三条链)
- 跨平台约束表(该用 paths/process_utils 的什么,禁止直接写什么)
- 已知边界:明确标注「七步 Pipeline / 四级门控不在生产路径」等
- 测试约定、代码组织约定
## Docs/FinDataHub-全面审查与改进方案-2026-10-25.md(新增)
审查结论 + 11 项修复 + 待办。核心判断:
架构无结构性问题,真正风险集中在「失败时静默」——三处最危险的缺陷同一个模式:
出错不报警、不中断、只悄悄留下错误状态。这类比崩溃难查,因为崩溃有日志。
含两处对初版判断的自我更正:
- datetime.utcnow 并非"已导致数据偏移"(实测主链路用 datetime.now,
36 万行数据无偏移),真实缺陷是潜在的基准混存
- extend/fetchers/collector.py 已不存在(采集器已重构)
## DEVLOG.md
追加 6 轮记录,含每轮的证据、判据与被推翻的判断。
## .workbuddy/memory
按 P2-23 既定策略入库。MEMORY.md 新增 14 条硬约束,含两处记忆纠正:
collector.py 路径作废、PythonProcessManager 拆分的决策依据与执行顺序。
|
2026-10-26 20:56:10 +08:00 |
|
FinDataHub
|
ad3dbdb9dc
|
ci: 新增 Windows job 与 MySQL service
## Windows 跨平台回归(观察期,不阻塞)
目的:验证 paths.py / process_utils.py 在 win32 上真的可用,而不是"写完就算"。
这两个模块已接线 4 个脚本,一旦 Windows 分支写错,表现是采集器静默不启动
或 PID 检测失效——本地 macOS 上完全看不出来。
**范围有意收窄:不装 akshare**。它传递依赖 py_mini_racer(62MB V8 预编译),
Windows 上是否有 3.12 wheel 未验证,属迁移计划列明的唯一硬阻塞点。
在验证它之前塞进 CI 只会让 job 长期飘红、失去信号价值。
覆盖:check_python_version --strict → compileall → test_paths_process_utils
(32 条,核心)→ Scripts/bin/tests(252 条,此前只在 POSIX 跑过)→ core + client。
continue-on-error: true + 注释写明转正条件(连续两次全绿后删掉)。
首次接跨平台 CI 若直接阻塞,一次环境差异就把主线挡死,反而逼人绕过它。
已提前修掉一处确定会失败的用例:test_newsflash_collect 的 SIGTERM 留痕测试。
Windows 的 send_signal(SIGTERM) 实际走 TerminateProcess,不触发子进程里的
Python handler,断言必然失败。已加 skipif 并注明该能力本身是 POSIX 专属。
## MySQL service(集成测试转阻塞)
python job 增加 services: mysql(mysql:8.0),集成测试成为阻塞性步骤。
三个必须处理的差异:
1. 认证插件:MySQL 8 默认 caching_sha2_password,首次连接走 full path
需 RSA → 装 cryptography,另加 --default-authentication-plugin 双保险
(注意 8.4 起该参数已移除,升镜像需同步改)。
⚠️ 本机某些 venv 没装 cryptography 也能连——那是认证缓存命中 fast path,
不代表 CI 也需要。只看本地结果会误判依赖冗余。
2. 版本:CI 8.0 vs 生产 26.7。事务/回滚语义一致(InnoDB),套件只用
CREATE TABLE + UNIQUE KEY,无版本敏感语法。
3. YAML 的 >- 折叠块内不能写注释:'#' 会被当成内容传给 docker,容器启动即失败。
注释必须置于 options: 之前(已用 assert '#' not in opts 验证)。
两级排除避免重复跑:python job 单元测试步骤与 windows job 均加
--ignore=.../integration。
|
2026-10-26 20:56:01 +08:00 |
|
FinDataHub
|
cd076018ba
|
test: 统一测试入口 + 真实 MySQL 集成测试
## 统一入口 run_all_tests.py
跑一次全量测试要记住四件事,任何一条忘了结论就是错的:
1. 每套件用哪个 venv(findatahub_core 只装在部分 venv 里)
2. 必须注入 PYTHONPATH,否则验的是已安装的旧版本
3. Scripts/bin/tests 需要 httpx,选错 venv 在收集阶段直接报错
4. **不能并行**——两个 pytest 会互踩系统临时目录,报 pytest-of-root
EEXIST 假失败(表现为几十个 errors,看着像大回归)
摩擦本身就是回归风险的来源,固化成脚本。四个设计点:
- 解释器按能力选:每套件声明 requires,扫描 Services/*/.venv 挑第一个
满足的;挑不到 exit 2 并列出缺什么,绝不用错的 venv 硬跑出假结果
- 强制注入 PYTHONPATH,源码目录排最前
- **每套件独立 --basetemp**(.pytest-cache/<suite>/):从根上消除 tmpdir
争用,不依赖"记得串行",也不去删系统临时目录
- 摘要提取要求「数字+状态词」相邻且 stdout 优先,避免把
(Background on this error at: sqlalche.me/...) 当成统计行
实测 6 套件 14.9s;加 --include-swift --include-frontend 共 8 套件 37s。
**重要发现**:scripts 从 205 → 252——多的 47 条是 test_rss_fetcher.py,
此前因 akshare venv 缺 httpx 一直被 --ignore 跳过。这批用例一直存在且可用,
只是从没在本机跑过。
## 真实 MySQL 集成测试
findatahub-core/tests/integration/,8 条用例。
为什么 fake 不够:test_db_replace_guard.py 用 Fake 游标验了代码路径,但
Fake 验不了三件事——事务是否真的回滚(Fake 的 rollback 只是置标志位)、
跨 chunk 失败是否留下半成品(Fake 不会真提交,这个场景永远复现不出来)、
熔断时表是否真的没动(需真实读行数)。
隔离:每次 fixture 新建 fdh_it_replace_<随机8位> 空库,用完 DROP;
库名带前缀且 DROP 前再断言。不读现有配置、不碰现有库。
无 MYSQL_PASSWORD 时整组 skip,CI 无 MySQL 不会变红。
**证伪验证**:把 replace() 临时改回「每 chunk 提交」旧行为,该套件报
「回滚不完整:表内行数既不是旧值 1000,说明留下了半成品数据」→ 测试有牙。
恢复后复跑 8 passed,生产库行数前后一致(news_flash 368346 /
stock_daily 8158172),无 fdh_it_% 残留。
--with-integration 开关默认关闭(要真实库且会建临时库)。
|
2026-10-26 20:55:52 +08:00 |
|
FinDataHub
|
73b45d7c0c
|
fix(client): debug.log 加轮转并缓存时间戳格式化
三个问题都在 DebugLogger:
1. 无轮转:minLevel 默认 .debug 且应用常驻,实测 debug.log 已达 1.5 MB
且只在手动清理时下降,长期运行会到 GB 级。
修:5MB × 3 份轮转(debug.log → .1 → .2 → .3),磁盘上限约 20 MB。
2. timestampStr 是计算属性,每次读取新建 DateFormatter(构造约 100µs),
而它被日志列表逐条渲染读取——刷新一次列表就新建 N 个。
修:创建条目时格式化一次并存储。
3. 每条日志 open/close 一次 FileHandle(3 次系统调用)。
修:复用句柄。
实现约束(已写入注释):
- 共享 DateFormatter 非线程安全,故标注仅主线程调用
(DebugLogger 是 @MainActor,条目只在其方法内创建)
- nonisolated(unsafe) 的两个状态只在 logQueue 串行队列上访问
- 阈值常量放文件作用域:@MainActor 类型的 static let 会被 actor 隔离,
nonisolated 方法引用会触发 Swift 6 错误
测试:DebugLogEntryTests.swift 6 条(格式、稳定性、级别顺序与配色唯一性)。
Swift 测试 12 → 18,全部通过。
|
2026-10-26 20:55:42 +08:00 |
|
FinDataHub
|
f7df96333b
|
fix(frontend): 去外链字体阻塞、端点解析多端化、补齐来源映射
1. 移除 Google Fonts 外链
<link rel="stylesheet"> 指向 fonts.googleapis.com 是 render-blocking,
墙内通常连接超时——浏览器会一直阻塞首屏渲染,用户看到白屏而非降级。
改为系统字体栈(--font-sans / --font-mono),零网络请求。
验证:vite build 产物已无 fonts.googleapis / fonts.gstatic 引用。
2. 端点解析收进 runtime.js(两处同源假设阻断桌面壳)
- api/index.js: baseURL '/api/v1'
- useWebSocket.js: location.host
两者在 Tauri 下分别解析到 tauri://localhost/api/v1 与 tauri.localhost:80,
**都不报错、只是数据全空**——界面能开、列表全空,是最难排查的一类故障。
收敛为 runtime.js 单一入口,优先级:VITE_API_BASE > 桌面壳本机 > 浏览器同源。
axios 拦截器同步补 X-API-Key。
3. 来源映射缺 3 个源
缺 tencent_news(后端有独立分表 news_tencent)、xueqiu、yicai(RSS 侧)。
这是既有测试先写、实现没跟上的典型:tests/sources.test.js 3 条用例长期失败。
顺带修正匹配顺序:'东方' 会被泛化规则抢先命中,改为 '东方财富'。
4. 多端适配补强
- isMobile() 触屏判定、safe-area 安全区、overflow-wrap
- @media (hover: none) 触摸设备用 active 代替 hover(触屏上 hover 会粘住)
- @media (max-width: 380px) 极窄屏、prefers-reduced-motion 无障碍
- 触屏点击目标不小于 44×44
测试:前端从 28 passed / 3 failed → 44 passed / 0 failed
(新增 runtime.test.js 13 条覆盖桌面壳端点解析)。
|
2026-10-26 20:55:41 +08:00 |
|
FinDataHub
|
fb76b10033
|
feat(cross-platform): 收敛路径与进程语义到 paths / process_utils
Windows 迁移 M0。纯新增两个模块 + 4 个脚本接线,**macOS 行为零漂移**。
背景:仓库此前把 /tmp/findatahub-*.pid、~/Library/Logs、
~/Library/Preferences/...plist 直接写死在十来个文件里;采集管线散落着
start_new_session=True、os.kill(pid,0)、signal.SIGHUP 等 POSIX 专属写法。
这些在 Windows 上分别是:路径不存在、ValueError、探测即杀进程、AttributeError。
- paths.py:temp_dir / pid_file / heartbeat_file / log_root / log_dir /
app_support_dir / backups_dir / config_dir / user_defaults_plist / env_file
- process_utils.py:detach_kwargs / is_running / terminate_pid /
available_signals / register_signal_handler / restore_default_and_die /
utf8_subprocess_env
两个必须记住的坑(已写成测试断言):
1. tempfile.gettempdir() 在 macOS 返回 $TMPDIR(/var/folders/.../T),
与现网 PID 文件所在的 /tmp **不是同一目录**。paths.temp_dir() 返回
/tmp 是刻意的,混用会导致新旧进程互相看不见。
2. Windows 上 os.kill(pid, 0) 等价于 TerminateProcess,不是「探测」,
必须用 OpenProcess。
接线:newsflash_collect(PID/心跳/日志/plist)、daily_summary(PID)、
migrate_date_format(备份目录)、install_collector_launchd(plist + 平台守卫)。
同时修 newsflash_collect.write_heartbeat 的静默失败:Swift 端以心跳
300s 未更新判定卡死并重启,而此处原为 except OSError: pass——重启理由
在日志里毫无痕迹。改为状态翻转才记录(心跳 60s 一次,无脑打日志会刷屏)。
验证:现网采集器 status 正常,5 个路径与旧硬编码逐字一致。
回归:test_paths_process_utils.py 32 条 + 心跳可观测性 5 条。
|
2026-10-26 20:55:31 +08:00 |
|
FinDataHub
|
0cf9fde232
|
fix(backend): 补 CORS 与异常捕获链、DB 就绪状态、统一时间基准
四类此前「失败时静默」的缺陷,共同点是出错不报警、只留错误状态。
1. 缺 CORS(变量定义了却从未接线)
app/dependencies.py 有 CORS_ORIGINS,但全仓无 add_middleware(CORSMiddleware)。
Tauri 2 生产态从 tauri://localhost 加载页面,与本机 8100 不同源 → 全部 API 失败。
这是 Windows 迁移真正的阻塞点,比 GUI 选型更靠前。
修:显式白名单(含三种 Tauri 来源与 X-API-Key 预检头),不用 "*"
(allow_credentials=True 与 "*" 组合会被浏览器直接拒绝)。
2. 仅加 CORS 不够:Starlette 的 ServerErrorMiddleware 恒在最外层,
未捕获异常绕过 CORS 中间件 → 500 响应无跨源头 → 浏览器只报
"blocked by CORS policy",看不到真实错误。
修:新增 apply_exception_catch() 放在 CORS 之内,就地捕获转 JSON。
中间件顺序固定(由内到外):exception_catch → security_stack → cors。
3. DB 初始化失败仍报 healthy
threading.Thread(target=init_db, daemon=True) 使异常消失在子线程,
/health 恒 ok、业务全 500、运维零信号。
修:_db_state 记录就绪/错误;/health 返回真实状态 + 实时 SELECT 1;
create_engine 补 connect_args={"connect_timeout": 5}
(MySQL 卡住时健康检查不再一起僵死);限时等待默认 5s。
4. 时间基准混存(潜在)
ORM 默认值用 datetime.utcnow(UTC),而主链路 unified_writer 用
datetime.now()(本地),MySQL session.time_zone = SYSTEM = UTC+8。
dal.batch_insert 不显式填 fetched_at → 新行会比同列其他行早 8 小时。
与已修的 trade_date 混存两格式同构,但不报错、不影响查询,更隐蔽。
修:统一到 app/database.py::now()(本地 naive),9 处 ORM 默认值 +
/health 时间全部改指向它;并加 check_timebase_drift() 把
「Python 与 MySQL 同时区」这个隐含前提变成 /health 可观测指标。
另修:get_engine() 双检锁(并发首请求会建两个引擎,连接池泄漏)。
回归:test_cors.py 11 条、test_timebase.py 14 条(含防回归锁——遍历全部
TIMESTAMP 列断言 default 解析到 now,写回 utcnow 立即失败)。
端到端实测 /health:timebase_drift_seconds = 0.809,time 已是本地时间。
|
2026-10-26 20:55:23 +08:00 |
|
FinDataHub
|
8a2c4548d2
|
fix(data): replace() 改为单事务并加收缩熔断
旧实现 DELETE 与 INSERT 之间每 1 万行 commit 一次,finally 只有 close()
没有 rollback——中途失败会留下「旧数据已删、新数据写了一半」的中间态。
且不做行数比对:上游取数不完整(部分代码失败、接口限流返回空、字段改名
解析出空列)时不完整批次直接替换完整历史,且不可逆。
这正是 tushare_data.dividend 由数万行跌至 19 行的同一模式。上次只修了
调用方(改走 upsert),方法本身仍是地雷——全仓仍有 6 处调用 replace()。
改动:
- 单事务:DELETE 与全部 INSERT 同处一个事务,异常统一 rollback
- 收缩熔断:删除前比对行数,新批次不足原表 50% 抛 ReplaceShrinkError
且表保持原样;原表 <100 行不做比例校验(小表波动正常)
- 空批次不清表(「本次没取到数」不等于「表应该为空」)
- allow_shrink=True 供显式承认的预期收缩
兼容性:正常运行路径不变,既有数据层测试零改动通过。
回归:test_db_replace_guard.py 11 条(熔断触发/边界/旁路、单事务提交、
失败回滚、空批次不清表);两处 Fake 游标补 fetchone()——真实 pymysql
游标必有该方法,夹具不完整。
|
2026-10-26 20:55:15 +08:00 |
|
FinDataHub
|
e48fc5da88
|
docs: 新增 Windows 迁移整体执行计划(M0-M6 里程碑 + 任务 + 验收 + 风险 + 双轨切换)
- 7 里程碑约 4 周;数据策略定 mysqldump 全量迁移
- 双轨并行:M0-M5 macOS 照常、Windows 只读验证;M6 切换单写
- 每里程碑含复选框任务、验收、风险登记册 R1-R6
|
2026-10-07 00:54:11 +08:00 |
|
FinDataHub
|
91eea2894e
|
docs: Windows GUI 选型定案——性能优先,选 Tauri 2
- 选型表加性能三列(冷启动/常驻内存/渲染层),结论写死 Tauri 2
- 理由重构两层:壳层 Rust 最优 + 渲染层 ECharts 走 WebView2 最划算
- 明确唯一让纯原生 WinUI 反超的场景(脱离 WebView2 + 无重图表)
|
2026-10-07 00:51:34 +08:00 |
|
FinDataHub
|
9138904cad
|
docs: Windows 迁移计划修订——SwiftUI 留作 Mac 分支,Windows 侧用 Tauri 2 做 GUI
- 结论先行与依赖清单:GUI 行改为「SwiftUI(App/)=Mac 分支保留;新建 App.Win/(Tauri 2 + 内嵌 Vue3)」
- 阶段 4 改为「Windows GUI 分支」:三方案选型(推荐 Tauri 2)、Mac/Win 代码组织、
Windows 特性与 macOS(SwiftUI) 的等价物对照表
- 速查表与落地形态同步更新
|
2026-10-07 00:49:14 +08:00 |
|
FinDataHub
|
73875acc99
|
docs: 新增 Windows 迁移实施计划(盘点+风险+分阶段步骤)
- 结论:SwiftUI App 无法迁移,策略为角色重划(数据管线+后端+Web前端上 Windows,
界面用已有 Vue3 前端;SwiftUI App 冻结/降级/移除)
- 盘点:20 脚本 / 31 plist / 258 py / 77 swift / 1804 前端;macOS 专属点集中约 8 文件
- 兼容风险:路径、命令、权限、编码、信号、进程、py_mini_racer 二进制、launchd 调度
- 分 6 阶段(0 预研 → 5 收敛),含每阶段风险与验证
|
2026-10-07 00:37:43 +08:00 |
|
FinDataHub
|
83f1546074
|
docs: 记录 stuck 误报根因与本批(3/4/5 项)交付汇总
- 清单:新增 P0 章节(stuck 误报机制 + 6 条用例 + 撞锁守卫),
并补本批交付汇总表(含 5c 凭据轮换仍须外部操作)
- 记忆:stuck 误判 → 自动重启的完整机制与修法;E2E 与采集器撞锁
|
2026-09-19 23:10:38 +08:00 |
|
FinDataHub
|
ecae6767f9
|
fix(health): 修「节能模式下 stuck 误报 → 触发自动重启」——历史上百次误判的机制
缺陷与危害
----------
`get_status()` 用「数据库最后写入距今 > 300s」判定 `stuck`。
但非交易时段采集器进入节能模式、采集间隔由 30s 退避到 **3600s**
(日志 `[eco] 非交易时段,采集间隔 30s → 3600s`),**期间刻意不写数据** ——
于是每个非交易小时里约 55 分钟被误判为卡死。
而 `AutoRecoveryManager.swift:166` 把 `stuck == true` 当作 `collectorStuck` 故障并
**自动重启采集器** → 这正是历史上「一天内被判定异常停止 193 次」的机制。
(今天的 debug.log 时间线也印证:20:27 / 20:46 / 21:05 / 21:24 / 21:49 / 22:11
呈规律性停止,间隔与健康检查周期一致。)
修法
----
`stuck` 改由**心跳**判定:心跳文件由独立线程每 60s 刷新,长休眠时也保持活跃
(代码注释本就写明"即使采集循环处于长休眠(深夜 3600s),心跳也保持活跃"),
Swift 侧的免 spawn 探测一直是读心跳的 —— 本次把 CLI 路径与之统一。
- 心跳新鲜(≤3×60s)→ 不卡死
- 心跳过期/缺失 → 回退判定,且阈值取 **4200s**(>节能上限 3600s + 余量)
- **无写入时间可比时(空库 / DB 读取异常)保守判为不卡死** ——
stuck 会触发自动重启,误判代价高于漏判
- `last_write_time` 语义不变,仍如实反映数据写入
顺带修一处解析脆弱性:DB 返回 `datetime` 对象时 `str()` 带微秒,
原 `strptime("%Y-%m-%d %H:%M:%S")` 抛 `unconverted data remains: .219352`;
新增 `_parse_ts()` 容错(支持带/不带微秒与 ISO 格式)。
验证
----
新增 6 条用例:
- **核心回归**:节能空闲 1 小时但心跳鲜活 → 不得判 stuck(旧逻辑必判 true)
- `last_write_time` 仍如实上报
- 心跳过期 + 写入 <4200s → 不判卡死;>4200s → 判卡死
- 无心跳文件时回退阈值须 >3600s(否则长休眠仍误判)
- 无写入时间时保守不判卡死
已有 5 条契约用例同步更新(它们断言的是旧的"按写入判 stuck"),
并新增 autouse fixture 让它们走回退路径。
部署位实测:`{"stuck": false}`,而 `last_write_time` 仍为 26 分钟前 —— 语义正确。
Scripts/bin 214 → **220 passed**。
附带:修 kuaixun E2E 用例与采集器撞锁
--------------------------------------
`test_unified_writer_e2e` 会真实写 `news_unified`,与正在运行的采集器撞锁:
pymysql.err.OperationalError: (1205, 'Lock wait timeout exceeded')
(它此前"通过"只因采集器当时停摆 13 小时 —— 修好采集后反而暴露。)
已加守卫:采集器心跳活跃时 `pytest.skip` 并说明原因与停止方法,
而不是让它随机失败。该用例在 CI 里本就属非阻塞组(CI 无 MySQL)。
kuaixun 410 passed / 1 skipped,耗时 54s → **3.9s**。
回归:Scripts/bin 220、core 122、client 38、tushare 174、akshare 183、kuaixun 410
—— 合计 **1147 passed**。
|
2026-09-19 23:10:21 +08:00 |
|
FinDataHub
|
ff2dcc10f4
|
docs: 记录 venv 自包含 / client SQL 注入 / 搜索提速否决 / Swift 拆分
- 清单:总表更新 P1-11(提速已否决)、P2-19(首批完成)、P2-20(已修);
新增三章详细说明(含 9 查询等价性对照表、注入复现、拆分与可见性调整)
- 记忆:venv 自包含两条关键坑(shebang 必须指向 venv python;
codesign --deep 覆盖不了内联 stdlib)、采集器代理事故、
性能优化须先证等价性、Swift private 是文件作用域、长构建需后台
|
2026-09-19 22:57:40 +08:00 |
|
FinDataHub
|
f17b3fb8dd
|
refactor(ui): 按仓库约定拆分 NewsFlashService.swift(1054 → 821 行 + 3 个 extension 文件)(P2-19)
做法:遵循既有仓库约定「超大文件用 extension 拆到多个文件,不拆成独立类型」
(见项目记忆「Swift 代码组织」),按**关注点**拆出三个 extension 文件:
| 新文件 | 内容 | 行数 |
|---|---|---|
| `NewsFlashService+Probe.swift` | 免 spawn 进程探测(P0-3 节能):probeProcessStatus / parseStatusJSON | 67 |
| `NewsFlashService+Exec.swift` | 子进程执行(含 30 秒超时守护):runCommand / runDetachedProcess | 87 |
| `NewsFlashService+Fetch.swift` | 脚本路径解析 + 快讯拉取:findPython/findScript/compute*/fetchNews*/loadMoreNews | 113 |
只搬移代码,未改动任何逻辑。
必须同步做的两处可见性调整(Swift 的 `private` 是**文件作用域**)
----------------------------------------------------------------
1. 搬走的方法在新文件里要由 `private func` 放宽为 `func`(internal)。
否则留在原文件的调用点报:
error: 'runCommand' is inaccessible due to 'private' protection level
(首次编译得到 30 条此类 error,逐条定位后修正。)
2. 主文件中 `private lazy var cachedPython` / `cachedScriptBase` 改为 `private(set) lazy var`:
同模块 extension 需要读取,写保护不变。其初始化器调用的
`computePythonPath` / `computeScriptBase` 随 +Fetch 搬走并放宽为 internal。
验证
----
`swift build -c release --disable-sandbox` → **Build complete!(0 error)**。
(注:本机完整重建较慢,首次用 900s 前台超时构建被中断、只留下
`error: Build failed` 而无明细,误判为编译错误;改为后台构建 + 轮询后取到真实结果。)
|
2026-09-19 22:56:34 +08:00 |
|
FinDataHub
|
33d20beab7
|
fix(client): 修复 findatahub-client 的 SQL 注入 + 建 venv + 补 38 条测试(P2-20)
缺陷一:服务根本无法安装(这是它长期无 venv、无测试的根因)
------------------------------------------------------------
`pyproject.toml` 依赖 `findatahub-core`,但**没有 `[tool.uv.sources]`** 指向本地路径,
而该包不在 PyPI 上。实测:
$ uv sync --dry-run
error: No solution found when resolving dependencies
cause: Because findatahub-core was not found in the package registry and your
project depends on findatahub-core>=0.1.0, we can conclude that your
project's requirements are unsatisfiable.
已补 `findatahub-core = { path = "../findatahub-core" }`(与 tushare_fetcher 一致,
**刻意不加 editable = true** —— 本机 uv+setuptools 下 build_editable 会 EEXIST 失败),
并补 `[dependency-groups] dev = ["pytest>=8"]`。
安装命令:`uv sync --no-editable`(`uv sync` 默认 editable 构建本项目自身会触发
上述 EEXIST 缺陷)。
缺陷二:**SQL 注入**(实测可复现)
----------------------------------
两个位置把调用方传入的名字直接拼进 SQL、未做校验:
1. `export(**filters)` 的**过滤键**:`` conditions.append(f"`{k}` = %s") ``
实测 `filters = {"x` = 1 OR `1`=`1` -- ": 1}` 生成:
SELECT * FROM `stock_daily` WHERE `x` = 1 OR `1`=`1` -- ` = %s
→ `OR 1=1` 生效、后续条件被注释掉,export 返回**整张表**。
2. `daily/weekly/monthly/fina_indicator` 的 **`fields`(列名列表)**:
`cols = ", ".join(f"`{f}`" for f in (fields or [...]))` —— 同一类突破。
实测 `fields=["a` FROM `stock_daily` WHERE 1=1 -- "]` 可注入。
修法:新增 `Client._check_identifier()`(`^[A-Za-z_][A-Za-z0-9_]*$`),
在 `export` 的过滤键循环与 4 处 `fields` 拼接上先行校验,非法即 `ValueError`、
**不产生任何 SQL**。
测试(新增 `tests/`,38 条,全部离线不连库)
--------------------------------------------
- 表名白名单 / 标识符校验(含 9 种非法键、5 种非法列名)
- **安全回归**:反引号注入必须被拒绝,且不产生 SQL
(修复前 10 条 FAIL、修复后全绿 —— 构成证伪)
- `search_stock` 的 by=auto/code/name 分支、keyword 去空格、limit 透传
- `export` 的 IN 子句、不支持格式、空结果、默认路径表名清洗
- `daily` 等 4 个方法的 fields 校验与正常放行
注:包以**非 editable** 方式安装,改源码后需
`uv sync --no-editable --reinstall-package findatahub-client` 再跑测试
(本次实测:改完未重装时测试仍按旧代码运行、误以为修复无效)。
|
2026-09-19 22:46:58 +08:00 |
|
FinDataHub
|
03d7db9cd4
|
perf(P1-11): 评估「ngram 全文检索提速搜索」——**实测不等价,故不采纳**(附证据工具)
背景
----
`api_search` 对 5 张 ACTIVE_TABLES 用 `title LIKE '%q%' OR content LIKE '%q%'`,
无法走索引;且 data 与 count 各扫一遍。数据库已有 `ngram` 插件
(`ngram_token_size=2`,STATUS=ACTIVE),故尝试用
`MATCH(title,content) AGAINST(... IN BOOLEAN MODE)` 走索引预筛、再用 LIKE 精确复核,
期望在不改变结果集的前提下提速。
实测结论:**不等价,不能采纳**
------------------------------
新增 `Scripts/bin/verify_fulltext_equivalence.py` 对 9 个查询做四写法对照
(like 基准 / 两阶段OR / 裸OR / 引号短语),结果:
| 查询 | LIKE 基准 | 两阶段 OR | 差异 |
|---|---|---|---|
| `股`(单字) | 54,015 | **1** | ★ −54,014(ngram token_size=2,单字无法命中) |
| `A股`(中英混合) | 5,882 | **0** | ★ −5,882(跨中英边界无法成 gram) |
| `人工智能`(纯中文 4 字) | 3,923 | **3,916** | ★ **−7**(即便最优情形也不精确) |
| `股市`/`涨停`/`央行`/`美股`/`IPO`/`5G` | — | 相等 | OK |
裸 MATCH(OR 语义)也**不是** LIKE 的超集(同上三项为负)→ 无法作为安全预筛。
速度确有大改善(0.25s → 0.03s),但**静默丢结果比慢更糟**,故不采纳。
处置
----
- **不修改** `api_search`(保持 `LIKE` 语义正确)
- **撤掉**本次为验证而建的 5 个 `ft_title_content` FULLTEXT 索引
(无查询会用到,只会增加高频写入表的重建与写入开销);
`news_flash` / `news_flash_archive` 上**原有的**同名索引保持不动
- 保留 `verify_fulltext_equivalence.py` 作为可复跑的决策证据工具
当前基准(实测,5 表合计)
--------------------------
- count 语句:**0.20–0.23s**;叠加 data 查询后单次搜索约 **0.4–0.5s**
- 结论:以 5 张表(合计约 40 万行)的规模,0.25s 量级的全扫描**可接受**。
若确有提速需求,可选路径是**刻意改变检索语义**(改产品决策并同步前端提示),
或改用应用侧倒排索引 —— 二者都属独立工作量,不在"透明提速"范畴。
|
2026-09-19 22:42:00 +08:00 |
|
FinDataHub
|
da9642c2bc
|
fix(packaging): 裁剪内联运行时(72M → 59M),解决 codesign --deep 失败
问题
----
上一步把 Python 运行时内联进 `Contents/Frameworks/python312/` 后,
`codesign --force --deep` 失败:
bundle format unrecognized, invalid, or unsuitable
In subcomponent: .../Frameworks/python312/lib/thread3.0.4
把完整运行时(含 Tcl/Tk)放进 bundle 超出了 `--deep` 的处理能力。
改动
----
`relocate_bundle_venvs.py` 增加 `prune_runtime()`,裁掉运行期不需要的内容:
- Tcl/Tk 家族:`tcl9.0` `tk9.0` `tcl9` `itcl4.3.5` `thread3.0.4`
`libtcl9.0.dylib` `libtcl9tk9.0.dylib`
- `include/`(供编译 C 扩展,运行期不需要)、`share/`
- stdlib 无关部分:`idlelib` `turtledemo` `ensurepip` `lib2to3` `tkinter` `test`
- `bin/` 中的开发工具:`idle3*` `2to3*`(保留 `pip` / `pydoc`)
裁剪依据(非拍脑袋):三个 venv 只用 pandas / numpy / pymysql / sqlalchemy /
fastapi / akshare 等纯计算与网络库,**不用 tkinter** ——
全仓已安装包中仅 `tqdm/tk.py` 引用它,那是可选的 GUI 进度条、未启用。
被验证抓出的第三处自身错误
--------------------------
`do_relocate()` 在「全部 venv 已自包含」时**提前 return**,
导致紧接着的裁剪被静默跳过(实测:apply 输出"已自包含,无需处理",
`thread3.0.4` 仍在,签名照旧失败)。
→ 已改为:venv 无需重写时**仍执行裁剪**。教训:早退分支里别忘掉并行的收尾动作。
结果与验证
----------
- 运行时 72M → **59M**(裁剪 15 项,释放 6.8 MB)
- 三个 venv 功能回归通过:python 3.12.13 / pymysql 2.2.8 均可导入
- `tushare-fetcher --help`、`akshare-fetcher --help` 均正常
- `tkinter` 确认已不可导入(符合裁剪预期,未被使用)
签名现状(如实说明)
--------------------
`codesign --deep` 仍无法覆盖内联的完整 stdlib(报
`In subcomponent: .../lib/python3.12`)。该 bundle 现为 **ad-hoc 签名 + 主可执行文件已签**。
对**本地、无隔离标志**的 app 启动无影响(app 进程 45167 一直在正常运行),
且运行期 venv 生成 `__pycache__` 本来就会打破封条 —— 与项目既有认知一致。
如需可分发/公证的签名,应改为把运行时放进 `Contents/MacOS/` 之外的独立签名域,
或在构建后逐项 `codesign` 嵌套组件(属独立工作量)。
|
2026-09-19 22:37:19 +08:00 |
|
FinDataHub
|
39a28becc7
|
fix(packaging): 让 .app 的 venv 自包含(内联 Python 运行时 + 修 editable .pth)+ 补打包 common/
缺陷一:bundle 内 venv 依赖用户级 Python
----------------------------------------
三个 bundle venv 的 `python` 都是软链接,指向
`~/.local/share/uv/python/cpython-3.12-macos-aarch64-none/...`,
`pyvenv.cfg` 的 `home` 与 `.venv/bin/*` 的 **shebang 同为绝对外部路径**
(`build-app.sh` 用 `rsync -a`,会原样保留软链接)。
→ 用户执行 `uv python uninstall 3.12`、升级,或清理 `~/.local/share/uv`,
**整个 app 的 Python 侧全部失效**,而这是日常会做的操作。
缺陷二:akshare venv 的 editable .pth 指向源码仓库
---------------------------------------------------
`_editable_impl_akshare_fetcher.pth` → `<repo>/Services/akshare_fetcher/src`。
即**部署的 akshare 采集器依赖源码仓库存在于原路径**(tushare 侧则已指向 bundle 内 src)。
换机器 / 移动仓库即 `ModuleNotFoundError`。
改动
----
1. 新增 `Scripts/bin/relocate_bundle_venvs.py`(`--check` / `--dry-run` / `--apply`)
- 把 Python 运行时整个拷入 `Contents/Frameworks/python312/`(103 MB)
- 重写 `.venv/bin/python*` 软链接、`pyvenv.cfg` 的 home/executable、
`.venv/bin/*` 的 shebang、指向 bundle 外的 editable `.pth`
- `--check` 可作启动自检 / CI 门禁
2. `build-app.sh` 第 5 段补 `common/`:该段此前只 rsync 了 `extend` 与 `app`,
**漏了 `common/` 包**(服务自身入口 `app/main.py` 需要 `from common.config import ...`)。
落实与验证(部署位已执行 --apply)
----------------------------------
- 三个 venv 全部 `--check` 通过「已自包含」;解释器均解析到
`<app>/Contents/Frameworks/python312/bin/python3.12`
- 三个 venv 的关键依赖可导入:pandas 3.0.3 / akshare 1.18.64 / fastapi 0.141.1
- `tushare-fetcher --help`、`akshare-fetcher --help` 均正常
(akshare 侧修复前报 `ModuleNotFoundError: No module named 'akshare_fetcher'`)
- 真实取数:`tushare-fetcher fetch --type daily --dry-run` 正常解析 6 张表
实现过程中被验证抓出的两处自身错误
----------------------------------
1. **shebang 误指向基础解释器**:首版把 `.venv/bin/*` 的 shebang 改成
`<app>/Contents/Frameworks/python312/bin/python3.12`。该路径虽在 bundle 内,
但**执行时不带 venv 上下文** → venv 的 site-packages 不在 sys.path、
editable `.pth` 不生效 → console script 直接 ModuleNotFoundError。
正解:shebang 必须指向 **venv 自己的** `<venv>/bin/python`(它本身是指向
bundle 内解释器的软链接,能正确带出 venv 上下文)。
`--check` 的判据也据此收紧为「shebang 必须恰为本 venv python」。
2. **只看首个 venv 就提前返回**:`--apply` 原先用第一个 venv 的自包含状态决定
是否跳过,导致「tushare 已自包含、akshare 未自包含」时整批跳过、漏修。
关于"缺 common/"的更正(不夸大)
--------------------------------
实测确认:**桌面后端并不导入 `app/main.py`**(它导入 `app.dependencies` /
`app.routers.*`),因此缺 `common/` **并未导致任何当前故障**,后端一直正常工作。
本次补齐属"bundle 相对服务自身入口不完整"的正确性修复与前瞻加固。
另注:`Scripts/bin/common.py`(单文件模块)与 `kuaixun-service/common/`(包)**同名**,
两者同时进入 sys.path 时前者会遮蔽后者 —— 这是既有架构约束,已在 build-app.sh 注明。
对应地,本批另一处真实故障是**采集器被注入了失效的会话代理**(臣上一轮重启时
误带 `HTTP_PROXY=127.0.0.1:50782`),导致所有新闻源 `All connection attempts failed`、
13 小时零写入(`stuck: true`)。已用**不含代理**的环境重启:
`stuck: false`,近 10 分钟写入 346 条 / 去重入库 43 条,采集恢复正常。
境内新闻源应直连,不应走会话代理。
|
2026-09-19 22:34:08 +08:00 |
|
FinDataHub
|
2d3da7ecbb
|
docs: 记录 uv run 日更中止、唯一键缺陷与 P2-22 资源复测
- 清单:新增「uv run 运行期联网导致日更整体中止」(升为最高优先级)、
「tables_registry 唯一键声明与真实数据不符」、以及 P2-22 交易时段复测结果
(含 table_rows/日志时间戳两条方法学踩坑)
- 记忆:生产运行期不得有包解析/下载行为;replace 报错 vs upsert 静默合并的危险差异
|
2026-09-19 18:10:16 +08:00 |
|
FinDataHub
|
076114488f
|
fix(schedule): 摘除 fut_wsr(无自然唯一键)并重载 us_basic
fut_wsr 摘除
------------
其接口返回行在 (trade_date, symbol, warehouse) 下**可重复**——
同一仓库同一日存在多条仓单记录(vol 不同),**没有自然唯一键**。
而写入走的是"唯一键 upsert" → 键相同的行互相覆盖,**静默丢行**。
实测该表在错误唯一键下已写入 **1,849,006 行**,属失真数据(无法判断丢了多少),
已 TRUNCATE 清空。如需采集须先改为快照表(无唯一键 + 写入前对完全相同行去重)。
已从排期摘除并在脚本内写明原因。
us_basic 重载
-------------
唯一键由 ["ts_code"] 修正为 ["ts_code","list_date","name"] 后
(见前次提交),用新键重载成功写入 **6,000 行**,
此前用旧键会报 (1062, Duplicate entry) 导致**整批失败**。
17 张新表最终状态(精确 COUNT)
-------------------------------
合计 **14,507,764 行,16/17 张有数据**(fut_wsr 按上述原因清空):
opt_daily 10,499,696 / fut_holding 1,800,000 / fut_daily 960,979 /
fut_settle 850,048 / fx_daily 337,229 / fut_trade_cal 13,527 /
sge_daily 10,250 / fut_basic 10,000 / us_basic 6,000 / us_tradecal 6,000 /
fut_mapping 5,000 / rate_wz_index 3,000 / hk_basic 2,785 /
hk_tradecal 2,000 / rate_gz_index 1,181 / fx_obasic 69
验证
----
- 排期前飞行检查:排期涉及 **79 张表,库中 0 张缺失** ✓
- 回归:tushare 174、core 121、akshare 183、Scripts 214、kuaixun 410 = 1102 passed
|
2026-09-19 18:09:56 +08:00 |
|
FinDataHub
|
e7186e5ce6
|
fix(prod): 调度脚本改用 bundle venv 直调,修复「uv run 运行期联网」导致日更整体中止
故障(2026-09-19 15:40 实测)
------------------------------
`tushare_sync.sh daily` **一张表都没抓**,日志只有一行:
── T0 每日同步 (日线 + 资金面 + 事件公告) ──
table_group: daily (6 tables)
error: Request failed after 3 retries in 4.5s
cause: Failed to fetch: `https://pypi.tuna.tsinghua.edu.cn/simple/structlog/`
cause: dns error
cause: failed to lookup address information: nodename nor servname provided
根因
----
两个调度脚本都用 `uv run <entrypoint>` 调用。**`uv run` 会在运行时同步/重建依赖
环境**,需要联网下载缺失包 —— 当天该请求 DNS 解析失败;又因脚本带
`set -eo pipefail`,这条 error 直接**中止整条任务**。
手动复现同一条命令还看到额外危害:
Building tushare-fetcher @ file:///...
Downloading mypy (13.0MiB)
Installed 14 packages in 31ms
即**生产运行期会联网改写自身 venv 的依赖**(把 mypy 装进生产环境)。
改动
----
`tushare_sync.sh`(17 处)与 `akshare_fetcher/scripts/sync_all.sh`(14 处)
全部改为直调 bundle 内的可执行文件:
TUSHARE_BIN="$PROJECT_DIR/.venv/bin/tushare-fetcher" # 原 uv run tushare-fetcher
AKSHARE_BIN="$PROJECT_DIR/.venv/bin/akshare-fetcher" # 原 uv run akshare-fetcher
VENV_PY="$PROJECT_DIR/.venv/bin/python" # 原 uv run python3 / --no-sync python
并在使用前校验可执行文件存在,缺失即明确报错退出(而不是让 uv 去"修好它")。
→ 不联网、不重建、也不依赖 uv 是否在 PATH 上(launchd 环境 PATH 极简,这本身也是风险)。
验证(端到端实跑)
------------------
- 当日日志现为:15:40 那次(第 1–8 行,含 dns error)之后,17:46:50 起新运行
至 17:48:45 **完成,exit 0,464 行,0 error**
- 6 个 table_group 全部执行到(含本批新增的「事件公告」与「补排期-日频」两组)
- 银层迁移同批完成:11,246 行写入 fin_data_clean.stock_monthly,blocked=False
- akshare 路径(18:00 任务)冒烟通过,无 uv 报错
说明:今天为周六(非交易日),故本次故障的**数据**影响有限(无新行情可采),
但该缺陷在每个工作日都会造成**整日零采集**,属最高优先级修复。
附带提交(同日稍早完成、一并入库)
----------------------------------
- `tables_registry.json`:`us_basic` 唯一键由 ["ts_code"] 修正为
["ts_code","list_date","name"]。同一 ticker 会被不同公司复用(AEM/ABX/AGI 等),
旧键导致静态表 replace 写入报 (1062, Duplicate entry),整批失败。
已按 MySQL NULL 语义实测验证为 0 冲突,DB 侧唯一索引同步重建。
- `fut_wsr` 追加警告:接口返回行在 (trade_date,symbol,warehouse) 下可重复且
**无自然唯一键**(同仓库同日多条仓单,vol 不同),按唯一键 upsert 会静默覆盖丢行。
- 新增 `Scripts/bin/verify_unique_keys.py`(校验 registry 声明的唯一键在真实数据上
是否唯一,含 MySQL NULL 语义与组合搜索建议)与 `Scripts/bin/inspect_dup_rows.py`
(打印重复行以确定真实唯一键)。
|
2026-09-19 17:49:33 +08:00 |
|
FinDataHub
|
eed4ecc16b
|
docs: 更正新表行数为精确 COUNT,并记录两条方法学教训
- 清单:新表行数由估算值改为精确 COUNT(*)(2,708,881 → 3,893,727);
补充「table_rows 是估算值、误杀健康长任务」的方法学教训、
「自动恢复不覆盖主动 stop」的观察、以及终止信号留痕的部署效果
- 记忆:同步上述两条教训
|
2026-09-19 08:48:35 +08:00 |
|
FinDataHub
|
9e05a153c3
|
feat(observability): 采集器记录终止信号与线程堆栈,补上崩溃排查的最后一块盲区
背景
----
部署 faulthandler 崩溃捕获后实测:`collector-crash.log` **一直是空的**。
这说明采集器**不是 Python 异常退出**,而是被**外部信号终止** ——
faulthandler 默认只接管 SIGSEGV/SIGABRT/SIGBUS 这类致命信号,**不覆盖 SIGTERM**。
于是应用日志里只有一句"采集器异常停止","谁终止了它、当时卡在哪一步"依旧无从判断。
改动
----
`_install_crash_handlers()` 增加 SIGTERM/SIGINT 处理:
- 记录信号名、时间、pid
- **记录所有线程的当前堆栈**(这是区分"被外部终止"与"自己卡死"的关键证据)
- 记录后恢复 SIG_DFL 再自杀,保持原有退出语义(父进程仍观测到相同退出信号)
- 仅在常驻模式(`start`)安装,不影响 stop/status 等短命令
验证
----
新增跨进程用例 `TestTerminationSignalLogging`:子进程装处理器 → 父进程发 SIGTERM
→ 断言日志含信号名与线程堆栈。
**信号处理必须在子进程验证**:pytest 主进程装上处理器后向自己发信号会直接终止测试运行。
已证伪:移除处理器安装后该用例报 `assert '收到终止信号' in ''`。
运行态说明
----------
已同步部署位并重启采集器(新 PID 22361)使处理器生效,采集正常(30 分钟 136 条)。
另记录一条观察:**App 的自动恢复不覆盖"主动 stop"** —— 用 CLI 正常停止采集器后
75 秒内未被自动拉起(这是合理行为:不该重启用户主动停的服务),
故主动停后需手动启动。
回归
----
Scripts/bin 214(+1)、core 121、tushare 174、akshare 183、kuaixun 410 = 1102 passed
|
2026-09-19 08:46:10 +08:00 |
|
FinDataHub
|
fc142630a0
|
docs: 记录排期缺表的排查与处置(17 张建表 + 7 张摘除)
- 未解决问题清单:新增第三批排查章节(含飞行检查的局限说明、
17 张建表结果、7 张不可采的确切原因、两处自身错误的更正)
- 记忆:记录「dry-run 不校验表是否存在」这一盲区、TABLE_MAP 解析与 dtype
判断两类自身错误、以及代理端口随会话变化的夜间失败模式
|
2026-09-19 08:38:47 +08:00 |
|
FinDataHub
|
a4b2cf11d7
|
fix(schedule): 建齐 17 张缺表让排期生效,并摘除 7 张不可采表
问题
----
排期前飞行检查(dry-run 只校验表名解析,不校验表是否存在)发现:
`tushare_sync.sh` 引用了 **21 张库中不存在的表**(fut_* / fx_* / hk_* / us_* /
opt_daily / sge_daily / ths_index / rate_* …)。因缺表只记 `写入失败` 日志、
**退出码仍为 0**,带 pipefail 的脚本也不会 abort —— 表现为这些行**长期静默空转**,
与用户报的 stk_holdertrade 停摆是同一类缺陷。仓库此前没有任何机制发现它。
改动
----
1. 新增 `Scripts/bin/create_missing_tables.py`
- `--list` 列出「排期引用但库中不存在」的表
- `--dry-run` 打印将执行的 DDL
- `--create` 幂等建表(已存在则跳过,不 DROP、不改既有结构)
- 从 **Tushare 接口实际返回的 DataFrame 推导列与类型**:仓库里
`tables_registry.json` 只登记表名/唯一键/日期列、**不含字段定义**,
而 DDL 必须与写入时的 `df.columns` 一致,否则 upsert 整批失败。
- 入参采用**阶梯重试**(ts_code / trade_date / start+end / 无参):
实测不同接口要求不同入参,且部分接口有 1 次/分钟限流,单次失败不代表不可用。
2. 建表结果:**成功 17 张**,首次加载已入库 **2,708,881 行**
(fut_daily 960,979 / fut_holding 1,800,000 / fut_trade_cal 13,527 /
fut_basic 10,000 / hk_basic 2,785 / rate_gz_index 1,181 …,其余后台继续)
3. 摘除 7 张**确认不可采**的表,并在 config.yaml 标注确凿原因(附实测报文):
- `libor` / `ths_index` / `ths_daily` / `stk_factor` —— **无接口访问权限**
- `hibor` / `shibor_lpr` / `us_daily` —— 配额 **1 次/小时**,无法按日迭代采集
复核(两条独立检查)
--------------------
- 排期涉及的 **80 张表在库中全部存在** → 下午 15:40 的日更不再有空转行
- 15 条 fetch 命令 dry-run 全部可解析;配置经 YAML 解析校验
过程中修正的两处自身错误
------------------------
1. 首次探测把 `TABLE_MAP` 元组里**第 1 个**引号串当作 api_name(实为库表名),
导致 `shibor`/`stk_managers` 等被误判为「接口名错误」;取第 2 个才是 api_name。
2. DDL 推导首版对 `str(dtype)` 做字符串匹配 —— pandas 3.x 字符串列 dtype 显示为
`str`,导致 `ts_code`/`name` 被误生成为 `double`。
改用 `pd.api.types.is_numeric_dtype()` 等类型谓词后,可疑列归零。
回归
----
tushare 174、core 121、akshare 183、Scripts 213、kuaixun 410 = 1101 passed
|
2026-09-19 08:38:27 +08:00 |
|
FinDataHub
|
43fadeff7a
|
docs: 记录停摆表扩大排查、连接复用与重复实现收敛
- 未解决问题清单:新增「23 张表曾经在采集、后来静默停摆」章节(含判据升级说明)、
P1-12 与 P2-18 章节;总表标记 P1-12/P2-18(tushare 侧) 已修
- 记忆:判据从 config 改为查库;连接复用中被既有测试抓到的异常语义回归;
sys.modules 别名 vs from-import 转发的取舍
|
2026-09-19 00:44:55 +08:00 |
|
FinDataHub
|
e00cc4ad9c
|
perf(dedup): unified_writer 7 处各自建连改为复用(每条新建省 4~5 次连接)
缺陷(P1-12)
-------------
`extend/unified_writer.py` 有 7 处各自 `pymysql.connect()` + `close()`,
而它们指向的是**同一个库 news_flash**:
_check_l1 / _verify_match_via_db / _check_l3 / _check_l4 /
_insert_new / _merge_sources / _load_window
单条新闻的去重链是 L1 → L3 → L4 → 写入 → 合并,即**每条新建开 4~5 次连接**;
采集器每 30s 一轮、每轮数十条,TCP 握手 + 认证 + 会话初始化被放大成可观开销。
改动
----
新增 `_conn()` 复用同一条连接,带 `ping(reconnect=True)` 自愈
(MySQL 空闲超时断开后自动重建);新增 `close()` 供进程退出时释放。
7 处调用点统一改为 `conn = self._conn()`,移除方法内的 close。
**事务边界不变**:写路径(_insert_new / _merge_sources)仍各自显式 commit。
对无 `ping` 的连接对象(测试替身)直接复用、不做假的探活。
过程中被既有测试抓到一处真实回归
--------------------------------
首版把 `conn = self._conn()` 放在了 `try:` **之外**,而原实现是
`conn = pymysql.connect(...)` 在 `try:` **之内** —— 连接失败原本会被
`except Exception` 吞掉、该层判定为"未命中",改后却会把
OperationalError 抛给调用方(测试报 `Access denied for user 'root'`)。
已把连接获取挪回 try 内,与原有异常语义一致。
验证
----
新增 3 条连接复用契约用例(复现次数、close 后可重建、ping 失败自愈),
均已证伪:还原为"每次新建"时 `test_connection_is_reused_across_calls`
报 `assert 3 == 1`。
kuaixun 410 passed、core 121、tushare 174、akshare 183、Scripts 213
—— 合计 1101 passed。
已同步 unified_writer.py 与 tushare core 三个转发层到部署位。
|
2026-09-19 00:44:22 +08:00 |
|
FinDataHub
|
48f00d3abf
|
refactor(core): 收敛 tushare 侧三处重复实现(574 → 66 行)+ 补排期 23 张静默停摆表
一、重复实现收敛
----------------
`findatahub_core` 抽取公共库后,`tushare_fetcher/core/` 下的旧副本原封未删:
cache.py 167 行 与 findatahub_core/cache.py **diff 差异 0 行**
validator.py 246 行 与 findatahub_core/validator.py **diff 差异 0 行**
report.py 161 行 与 findatahub_core/reporter.py **diff 差异 0 行**
合计 574 行纯重复(同目录的 db.py 早已改成薄封装,这三个被漏掉)。
akshare 侧的同名文件是**有差异的分支版本**(199/150/120),
属另一类问题,本次不动 —— 需先甄别差异是真实功能还是裁剪。
实现方式:sys.modules 别名(不是 from-import 转发)
首版写成 `from findatahub_core.cache import SyncCache, TradeDateCache`,
结果 4 个报告测试红了:monkeypatch 打的是
`tushare_fetcher.core.report.REPORT_DIR`(转发层上的**值副本**),
而 `ReportGenerator` 读的是 core 模块自己的全局 —— 写不进去。
改用 `sys.modules[__name__] = _core`,两个名字指向同一模块对象,读写同源。
二、补排期 23 张静默停摆表(tushare_sync.sh)
---------------------------------------------
用「库里有数据 + 逐表查新鲜度」作判据,从未覆盖的孤儿表里筛出
**23 张曾经在采集、后来静默停摆**的表(与用户报的 stk_holdertrade 同类):
daily += margin_secs,hk_hold,top_inst,disclosure_date,daily_info,
sz_daily_info,index_dailybasic,stk_limit,moneyflow_hsgt,ggt_daily,bak_basic
weekly += index_weekly,index_monthly
monthly += stk_rewards,fina_mainbz,yearly,stock_company,new_share,
bse_mapping,cn_m,sf_month,fund_adj,slb_len
停摆实证(补排期前):stock_margin_secs 停在 20260610(553 万行)、
stock_hk_hold 20260618(392 万行)、stock_rewards 20260430(150 万行)…
排期覆盖审计:调度覆盖 61 → **84** 张;「启用但无排期」真缺陷 7 → 5 张。
验证
----
- `tushare_fetcher.core.report is findatahub_core.reporter` → True;
`REPORT_DIR is REPORT_DIR` → True;cache / validator 同样
- tushare 174 passed(含此前因转发层失效而红的 4 条报告用例)
- 全量 core 121 / akshare 183 / Scripts 213 / kuaixun 407 —— 合计 1098 passed
|
2026-09-19 00:40:15 +08:00 |
|
FinDataHub
|
591d437929
|
docs: 记录 collation 统一、采集停摆排查与泄漏清理
- 未解决问题清单:P0-5 标记全部完成(格式+collation);
P1-13/P2-16/P2-17/P2-21 标记已修;新增「表启用却从未被调度」章节
与「本批更正」三章(含先前报告的三处不准确引用)
- 记忆:记录排期缺陷根因(config 开关被显式传参绕过)、
collation 迁移的前置与实测速度、工具时限下的排查方式、自查发现的残留缺陷
|
2026-09-19 00:27:53 +08:00 |
|
FinDataHub
|
f43b442bf1
|
fix: 修连接/句柄泄漏 + 修脚本版本契约 + 去掉 stk_factor 假启用
P1-13 连接池位泄漏(Scripts/bin/backfill.py)
---------------------------------------------
三处 `get_conn()` 都把 `put_conn(conn)` 放在**成功路径**,异常时连接永不归还:
get_source_map / get_existing_ids / insert_news
连接池有容量上限,异常路径不归还即永久占住池位,反复失败会耗尽池,
表现为后续所有取连接的操作阻塞(与 db_pool 取连接超时修复是同一类问题)。
修法:全部改为 try/finally 无条件归还。
**自查发现并修正一处残留缺陷**:`finally` 里写成 `cur.close(); put_conn(conn)`,
若 close 本身抛错(连接已断时常见)仍会跳过归还 → 改为嵌套 try/finally,
归还作为无条件最后一步。回归用例正是靠这一点抓出来的。
P1-13 后端 stderr 句柄泄漏(Scripts/bin/newsflash_collect.py)
--------------------------------------------------------------
`stderr=open(BACKEND_STDERR_LOG, "w")` 把 open() 返回值直接丢给 Popen,
父进程这份句柄没有任何引用可关 → 每启动一次后端泄漏一个 fd。
改为先存局部变量、Popen 后立即 close(子进程已持有独立 fd 副本,写入不受影响)。
P2-16 bundle-python.sh 版本契约冲突
------------------------------------
`uv venv --python 3.14` 与 .python-version(=3.12) 及所有 pyproject 的
requires-python=">=3.12,<3.13" 冲突。改为读取 .python-version 作为单一事实来源。
同时 `set -e` → `set -eo pipefail`:原先 `uv venv ... | tail -3` 会吞掉失败退出码,
与文件头注释声称的"确保失败中止"相矛盾。
并注明该脚本**当前不在构建链路内**(build-app.sh 未调用,全仓仅文档提及),
避免后人误以为它是打包的一个阶段。
stk_factor / stk_factor_pro 假启用
----------------------------------
config.yaml 与 config/default.yaml 写 `true`,但 fetcher.py:306 对该表显式跳过
(接口限 5 次/天,无法批量)。改为 false 并注明原因。
另外查明:`tables:` 开关只在「未显式指定表」时才生效(fetcher.py:239-243),
调度脚本总是显式传参 → **不能以 config 为准判断某项是否在跑**。
回归
----
Scripts/bin 213 passed(新增 6 条连接归还用例,已证伪:
旧写法下 `test_get_source_map_returns_conn_on_error` 报 assert 0 == 1)、
core 121、tushare 174、akshare 183、kuaixun 407 —— 合计 1098 passed。
本次一并完成的非代码清理
------------------------
- P2-17 陈旧可写 venv:`~/Library/Application Support/FinDataHub/venvs/` 下
akshare_fetcher(3.14.6, 308M) 与 kuaixun-service(3.14.5, 156M) 两个副本
来自 v1.4.7 时期,与 bundle 的 3.12 不符、且每次启动刷 WARN。
已移出(464M → 12K,保留 venv_versions.json)。应用日志本身提示
"如需 pip install,请在设置中重建可写副本",故可安全清理。
- P2-21 构建残留:findatahub-core/build、findatahub_core.egg-info、
findatahub_client.egg-info 已移出(移出前先 mv 到 /tmp 再后台删除,规避沙箱批量删除限制)。
|
2026-09-19 00:27:21 +08:00 |
|