Commit Graph
761 Commits
Author SHA1 Message Date
kevin9327 03f8966856 fix(backtest): 财务因子矩阵路径不再沿用上一期的过期值
矩阵侧逐列前向填充时跳过空值写入, 新一期财报缺某指标就继续沿用上一期,
同一行会混用两期报告 (pb 取新期净资产、roe 停在上一期); polars 侧
join_asof 只认最新一期整行, 该指标为 null。同一份配置两条路径给出不同
因子值。改为空值同样覆盖为 NaN, 与 attach_fundamental_factors 口径一致。
2026-09-10 07:50:04 +09:00
shy3130 d0a14b5c1b fix(ext-pull): 拉取循环的状态回写不再清策略缓存
上修复 (keep_strategy_cache 沿数据写入链放行) 后线上复现: 每轮拉取
成功后 12ms 缓存仍被清空。clear_cache 新增调用链日志抓到真凶 —
ExtConfigStore.upsert 无条件触发 _invalidate_ext_derived: 调度器每轮
拉取要回写 last_run/last_status/next_run 共 2-3 次配置, 每次都全清
策略结果缓存, 绕过了已放行的数据写入链路。

upsert 增加 keep_strategy_cache 参数 (默认 False 保持 UI 保存配置/
手动变更的全清语义), 拉取循环内 4 处例行回写全部传 True。
2026-09-09 16:30:20 +08:00
shy3130 71cec0dc35 ops(strategy): clear_cache 记录调用链日志
策略页依赖 strategy_cache 秒加载, 缓存被清空即整页回退到全量重算。
今日线上排查: 缓存在扩展表拉取后 12ms 消失, 但三个已知调用方均无
触发记录, 静态分析无法定位。clear_cache 打 WARNING 附最近 5 帧调用
链, 此类问题不再依赖复现现场。
2026-09-09 16:10:03 +08:00
shy3130 6345eb93ab fix(strategy): 渐进式 run_all 逐策略隔离失败
单个策略执行崩溃 (如自定义策略 filter_history 的数据类型错误) 会杀掉
整批剩余策略: handle 整体 fail, 后台线程结束, 页面剩余卡片永远不亮。
线上实证: 70 策略池跑到第 35 个 (custom_1782999589 pl.DataFrame 构造
schema 混杂) 崩溃, 后 36 个全部没算。

改为单策略 try/except: 失败记入 handle.errors 并移出待算队列, 其余
策略照常算完落缓存; 响应新增 errors 字段 (前端可忽略)。job 级失败
(context 构建崩溃) 仍走整体 500 语义不变。
2026-09-09 15:49:21 +08:00
shy3130 01347b72bc perf(strategy): 渐进式 run_all 只构建一次共享矩阵
_run_all_progressive 逐策略调用 engine.run_all(strategy_ids=[sid]),
run_all 不回写 context.market → 每个 matrix_native 策略都重建一次
全市场矩阵 (39 万行, 小服务器单次数秒到十余秒), 6 策略一轮多耗约
1 分钟。

从 run_all 抽出 build_shared_matrix (字段并集语义不变, 非渐进路径
行为一致), 渐进式循环外一次建好放入 context.market 复用。
2026-09-09 15:30:10 +08:00
shy3130 b531475ca8 fix(ext-pull): 定时拉取不再清空策略结果缓存
扩展表 PullScheduler 每轮成功写入都会经 invalidate_ext_caches 销毁
strategy_cache, 策略页随之下一次轮询整页空白, 且前端会话级防重入
(runAllDateRef) 不会自动补跑, 小服务器上全量重算需分钟级 → 页面长期黑屏。

例行数据刷新改为只失效扩展帧缓存与注册同步状态 (下次策略运行自然
读到新值); 手动上传/配置变更保持全清 (触发下次全量重算) 旧行为。
keep_strategy_cache 沿 fetch_and_ingest → rows_to_parquet →
write_ext_parquet → invalidate_ext_caches 显式传递。
2026-09-09 15:30:10 +08:00
wshy a5e725c738 Update LICENSE 2026-09-09 12:13:38 +08:00
shy3130 8d8b66c7f8 fix(kline): 分钟批量补全识别前部空洞, 重启跨开盘后分时可自愈
盘中重启/停机跨开盘后, 本地当日分钟K只剩重启时刻起的连续尾巴。
缺口判定只检查相邻K间隔, 连续尾巴被判"仅尾部落后"走增量 — 增量
锚定本地最新时间, 永远不会回看缺的开头, 上午的洞永久残留, 自选
分时只显示几根K。

- _has_holes 增加前部洞判定: 当日首根 > 开盘+6min 容差即视为洞,
  归入全天重拉 (取到即落盘, 一次自愈)。仅根数不足的分支才判定,
  稳态零额外请求; 停牌晚开票命中亦幂等无害
- 全量分钟服务健康时不再压制空洞类补拉 (服务增量补不了洞),
  纯尾部落后的压制维持不变

实测: 服务器重启窗口后策略页(本地为空)走全天拉取显示完整、
自选页(有连续尾巴)被增量锁死只显示 11 根, 修复后统一自愈。
2026-09-09 12:13:04 +08:00
wshy 3db7f38446 Merge pull request #281 from 0112020179/codex/fix-enriched-refresh-memory
fix(oom): 降低补算内存峰值并统一重任务并发控制
2026-09-09 11:36:22 +08:00
0112020179 771a95a6d1 fix(oom): 降低补算内存峰值并统一重任务并发控制 2026-09-09 11:09:48 +08:00
wshy 5ffe43d42d Merge pull request #280 from kevin9327/fix/backtest-stream-date-400
fix(backtest): SSE 端点非法 start/end 返回 400 而不是 500
2026-09-09 09:04:52 +08:00
wshy efd9f820d4 Merge pull request #279 from kevin9327/fix/screener-run-all-as-of
fix(screener): run_all 的 as_of 与 /custom、/preset 同口径校验 (数字 as_of 会写坏策略缓存)
2026-09-09 09:04:46 +08:00
wshy 6eb1508fd8 Merge pull request #278 from kevin9327/fix/alerts-query-bounds
fix(alerts): 触发记录查询的 days/limit 补上范围约束 (limit=-1 静默丢记录)
2026-09-09 09:04:40 +08:00
wshy 7dc11125bf Merge pull request #277 from kevin9327/fix/merge-history-null-announce-date
fix(financial): 公告日为空的旧行不再压过带公告日的新行
2026-09-09 09:04:35 +08:00
wshy 7e99a07270 Merge pull request #276 from kevin9327/fix/quotes-flush-quote-ts
fix(kline): 实时行情覆写当日日K分区时补写 quote_ts
2026-09-09 09:04:28 +08:00
wshy fe1410532f Merge pull request #275 from kevin9327/fix/factor-delete-fail-closed
fix(factors): 删除因子被拒绝时定义已被删掉 (删除路径未 fail-closed)
2026-09-09 09:04:22 +08:00
wshy 5467b0dca4 Merge pull request #274 from kevin9327/fix/fundamental-announce-day-hole
fix(fundamentals): 换报告期公告当日不再丢失上一期财务因子值 (与矩阵路径口径对齐)
2026-09-09 09:04:17 +08:00
wshy 464a5d9d50 Merge pull request #273 from kevin9327/fix/test-endpoint-cold-cache
fix(settings): 端点清单缓存未预热时端点测速 500
2026-09-09 09:04:11 +08:00
wshy 99f478b257 Merge pull request #272 from kevin9327/fix/report-created-at-beijing-time
fix(reports): AI 报告 created_at 改用北京墙钟, 修复 UTC 部署下「8 小时前」
2026-09-09 09:04:05 +08:00
wshy 970296e82e Merge pull request #271 from kevin9327/fix/minute-sync-beijing-window
fix(kline): 分钟K增量同步窗口统一按北京时区
2026-09-09 09:04:00 +08:00
wshy eebe533986 Merge pull request #270 from kevin9327/fix/rotation-signal-date-alignment
fix(rotation): 轮动信号缺失日按日期归位补齐, 新晋概念不再被误判为退潮
2026-09-09 09:03:54 +08:00
wshy 1b90f57501 Merge pull request #269 from 0112020179/codex/fix-halted-partition-rebuild
fix(pipeline): 避免停牌分区反复误删重算
2026-09-09 09:03:49 +08:00
kevin9327andClaude Opus 5 68005c92e6 fix(alerts): 触发记录查询的 days/limit 补上范围约束
GET /api/alerts 的 days、limit 是裸 int,没有任何约束,越界值不报错而是
静默返回错误结果:

- days=-1  → cutoff 被推到未来,200 但 alerts 为空(total 仍是 3)
- limit=-1 → out[:limit] 变成负数切片,3 条记录只返回 2 条,
             静默丢掉最旧一条,调用方无从察觉
- limit=0  → 200 但 alerts 为空

同类列表端点已有正确写法:abnormal.py 用 `limit: int = Query(500, ge=1,
le=2000)`,rps.py 用 `days: int = Query(12, ge=7, le=30)`,越界直接 422。
本次把 /api/alerts 补齐到同一口径,上下限取存储侧保留策略本身
(alert_store.MAX_DAYS / MAX_RECORDS),不新增常量,超出保留窗口的请求
本来也没有可返回的数据。

前端 alertsList 实际只传 days=7、limit=1/10/500,全部落在新区间内;
新增的 tests/test_alerts_query_bounds.py 同时锁住这些合法取值仍返回 200
且记录条数不变。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 07:37:17 +09:00
kevin9327andClaude Opus 5 fd5d883c8f fix(screener): run_all 的 as_of 与 /custom、/preset 同口径校验
/api/screener/run_all 收的是无类型 dict,as_of 只在 isinstance(str) 时才
解析,其余类型原样透传,造成两个问题:

1. 非法字符串("not-a-date"、"2026-13-01")抛未捕获 ValueError → 500;
2. JSON 数字({"as_of": 2026})不进解析分支,直接被当作日期用下去。
   实测:{"as_of": 2026} 返回 200,响应 as_of="2026",
   strategy_cache.json 也写入 as_of="2026"({"as_of": 20260904} → "20260904")。
   其它入口写进同一份缓存的是 "2026-09-04",cached-summary /
   cached-result 都按 as_of 字符串比对,格式不一致的缓存条目永远失配。

同一份 as_of 在 /api/screener/custom 和 /api/screener/preset 上走
Pydantic 模型(CustomRequest / PresetRequest 的 `as_of: Optional[date]`),
非法字符串和数字都会被 422 拦下;只有 run_all 这一处漏了。本次把 run_all
补齐到同一口径:非字符串直接 400,非法字符串 400「日期格式错误」。

补 tests/test_screener_run_all_as_of.py,同时锁住合法 as_of 仍然照常执行、
缓存仍写 ISO 日期,以及不传 as_of 时回退到 latest_date()。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 07:36:21 +09:00
kevin9327andClaude Opus 5 d2931aaadb fix(backtest): SSE 端点非法 start/end 返回 400 而不是 500
strategy/stream、optimize/stream、walkforward/stream 三个端点直接裸调
date.fromisoformat(start/end),前端传入 "not-a-date"、"2026-13-01"、
"2026/09/04" 之类的值会抛未捕获 ValueError,客户端只拿到 500 和
Internal Server Error,看不到是哪个参数不合法。

仓库内同类入口已有正确写法:signals.py 的 /intraday/replay 把
date.fromisoformat 包在 try 里返回 400「日期格式错误」,mining.py 的
MiningRunRequest 用 field_validator 走 422。本次只把 backtest 这三处
补齐到同一口径,不改其它行为。

补 tests/test_backtest_stream_date_guard.py:三个端点 × 非法 start/end
断言 400;另外用服务端范围保护(backtest_range_guard)让事件流立即收尾,
断言合法日期照旧返回 200,证明不是把入口收窄。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 07:35:32 +09:00
kevin9327 252872e84f fix(financial): 公告日为空的旧行不再压过带公告日的新行
_merge_report_history 用 nulls_last=True 排序, 而聚合是逐列
drop_nulls().last() — "排在最后"等于"最权威", 于是 announce_date 为空的
旧行反而胜出, 产出 announce_date 是新公告、数值仍是修正前的矛盾行,
点时因子在公告日之后放出的是旧数。改为 nulls_last=False: 公告日未知视为
最旧, 与 docstring 承诺的"新同步行有值则覆盖旧值"一致。
2026-09-09 07:33:28 +09:00
kevin9327 4bbc7d07a3 fix(kline): 实时行情覆写当日日K分区时补写 quote_ts
sync_daily_by_quotes 手工拼 record 时漏了行情响应的 timestamp, 而
flush_live_daily 是整分区覆写, 会把 QuoteService 之前写入的 quote_ts 一并
抹掉。data_integrity 的判据全建立在 quote_ts 上, 分区因此从"盘中快照"
退化为"权威历史": 盘中触发同步后停机, 次日自检漏判, 停机时刻的
close/volume 永久留存并污染 lookback 指标。
2026-09-09 07:25:35 +09:00
kevin9327 a49c8b3b15 fix(factors): 删除因子被拒时定义已被删掉
DELETE /api/factors/custom/{id} 的 404 守卫把 store.delete_one 当成存在性
探测, 但它会真的 unlink 文件并返回 True。当定义在盘上却没进注册表时
(load_into_registry 对注册失败的定义只告警跳过, 如复合因子的成员已被强制
删除), get_factor 为 None, 短路求值会走到 delete_one, 定义随即被删除。

随后的引用检查再返回 409「该因子仍有引用, 拒绝删除」—— 接口声称拒绝, 定义
却已经没了; 用户改带 force=true 重试只会得到 404, 定义无法恢复。

把 fail-closed 的引用检查移到存在性判定之前, 保证任何拒绝路径都不改磁盘。
2026-09-09 07:20:16 +09:00
kevin9327 7a13713903 fix(fundamentals): 换报告期公告当日不再丢失上一期财务因子值
attach_fundamental_factors 用公告日做 asof 回看再按「严格大于」门控:
日期正好等于新一期公告日时, join_asof 已匹配到「当天还不能用」的新记录,
门控随即置 null, 打断上一期的前向填充。矩阵路径 build_fundamental_matrices
按 searchsorted(side="right") 逐期覆盖, 公告当日保留上一期, 两条路径因此不一致
(其 docstring 声明与 attach_fundamental_factors 同口径)。

asof 键改用生效日 (公告日次日), 只命中已生效的报告期; 公告前仍为 null 的门控不变。
补 tests/test_fundamental_factors.py 的两条公告一致性用例。
2026-09-09 07:19:09 +09:00
kevin9327 ea27376e03 fix(kline): 分钟K增量同步窗口统一按北京时区
sync_and_persist_minute 的起点取自本地分钟K的北京墙钟, 终点却用服务器
本地墙钟 datetime.now(); _datetime_to_ms 按服务器时区解释两者, UTC 容器上
起点反而晚于终点, time_segments 为空, 增量补拉一个请求都发不出去。

_latest_minute_datetime / _earliest_minute_datetime 返回值带上 CN_TZ,
now 改用 cn_now(), 与 fetch_minute_single 已有的时区纪律保持一致。
2026-09-09 07:15:02 +09:00
kevin9327 e349f2ba9b fix(settings): 端点测速在清单缓存未预热时 500
_endpoints_cache 初值是 {"ts": 0.0, "data": None}, "data" 键存在, 所以
dict.get("data", {}) 的默认值不生效, 返回的是 None。进程启动后到第一次
GET /api/settings/endpoints 之前, POST /api/settings/test_endpoint 不带
rounds 会在 None.get("testRounds", 5) 上抛 AttributeError → HTTP 500。

前端 endpoints 查询有 5 分钟 staleTime, 后端重启后用户再点测速正好落在
这个窗口: 前端不会重新拉清单, 后端缓存却已清空。

改为先 `or {}` 兜底再取 testRounds, 保持原有默认 5 轮语义。
2026-09-09 07:12:54 +09:00
kevin9327 a0925cb0c8 fix(reports): AI 报告 created_at 改用北京墙钟
三类 AI 报告(财务分析 / 个股分析 / 大盘复盘)共用的 JsonReportStore 用
datetime.now() 补 created_at, 取的是宿主机时钟。容器默认 UTC 时写入的是
UTC 墙钟, 前端 fmtRelative 再按浏览器本地时区解析这串 naive 时间, 刚生成
的报告被显示成「8 小时前」; stockAnalysisStore 的「今天是否已生成过报告」
判定(created_at 前 10 位 == 浏览器今天)在北京 00:00-08:00 也会误判。

改用 app.market_time.cn_now(), 保持原有 naive 秒精度格式不变。
2026-09-09 07:10:55 +09:00
kevin9327 2266ecc1d8 fix(rotation): 轮动信号缺失日按日期归位补齐
_compute_rotation_signals 把缺失日的 (999, 0.0) 占位一律追加到序列末尾,
而下游按 ranks[0]=最早日 / ranks[-1]=最新日 解读。概念在部分日期缺席时
(build_rps_rotation 已过滤掉当日无有效 avg_pct 的成员) 实际排名被压到左端、
占位落到右端, 只在最近几日上榜的新晋概念因此被判成退潮。

改为按日期归位补齐, 并补充 tests/test_concept_rotation_signals.py 覆盖
早期缺席 / 最近缺席两个方向。
2026-09-09 07:10:35 +09:00
0112020179 f325a9676c fix(pipeline): 避免停牌分区反复误删重算 2026-09-08 23:49:55 +08:00
shy3130 9a4bdcd07d feat(backtest): 环境数据缺口一键补算并自动重跑
- 回测报错横幅识别环境数据缺失文案(提取缺失首日), 显示「补算环境数据并重跑」按钮
- 点击后调用已有 /api/regime/recompute 补算缺口区间, 完成后自动重跑本次回测
- 补算请求超时放宽到 5 分钟(需扫全市场 enriched 数据)
- 预热区间不足的报错变体不显示按钮(补算无法解决, 文案自说明)
2026-09-08 22:16:21 +08:00
shy3130 85c903704a feat(screener): 叠加策略创建与配置体验优化
- 新建默认交集合并模式, 选项顺序对调; 可选策略列表加高
- 策略名称默认子策略名用 + 连接, 手动编辑后停止跟随
- 子策略权重改为滑块 (0-100 百分比), 保存时自动按比例归一, 移除手动归一按钮
- 子策略名可点击弹出其配置编辑 (创建对话框与配置对话框均支持)
- 策略池: 叠加策略并入自定义分组并标记「叠加」徽标, 修复误显示为内置
- 创建策略/修改策略池后自动扫描新增日线策略, 免手动刷新
2026-09-08 21:45:38 +08:00
wshy 8cb834d6a1 Merge pull request #267 from 0112020179/codex/fix-neighbor-prefetch-priority
fix(frontend): 当前股票加载后串行预取邻股
2026-09-08 21:06:45 +08:00
wshy cdd41a6d15 Merge pull request #266 from 0112020179/codex/fix-kline-transfer-minute-capability
fix(kline): 压缩详情响应并限制无权限分钟回退
2026-09-08 21:06:40 +08:00
wshy de250f72bc Merge pull request #268 from kevin9327/fix/pull-window-beijing
fix(ext-data): 定时拉取的时间窗口与落盘日期改用北京时间
2026-09-08 21:06:36 +08:00
kevin9327 9a9903cf69 fix(ext-data): 定时拉取的时间窗口与落盘日期改用北京时间 2026-09-08 19:56:03 +09:00
0112020179 cda496ba99 fix(frontend): 当前股票加载后串行预取邻股 2026-09-08 18:46:44 +08:00
0112020179 a8bb7a25bf fix(kline): 压缩详情响应并限制无权限分钟回退 2026-09-08 18:09:59 +08:00
wshy a5374d4212 Merge pull request #264 from 0112020179/codex/fix-plugin-source-display
fix(frontend): display plugin data source names
2026-09-08 16:53:27 +08:00
shy3130 908b385501 feat(ext-data): 拉取接口支持 API Key 鉴权 + 扩展数据弹窗加宽可滚动
后端:
- PullConfig 新增 auth (none/bearer/header/query, 与自定义行情源
  AuthConfig 同口径); 拉取/测试/回补共用 _request_json, 鉴权注入
  只有一套口径, 用户自定义 Headers 仍优先于 UA 标识头
- Key 本体存 secrets_store (data/user_data/secrets.json, 0600),
  不落 config.json; EXT_{ID}_API_KEY 环境变量兜底; 配置了鉴权但
  未设 Key 时 fail-closed 拒绝请求
- 新增 GET/PUT /api/ext-data/{id}/api-key (GET 只返回脱敏值);
  删除配置时清理残留 Key; 历史 config.json 无 auth 字段读为 None
  行为不变; PUT /pull 不带 auth 时沿用现有鉴权

前端:
- 拉取面板新增"接口鉴权"区块: 方式下拉 + 请求头名/参数名 + Key
  密码框 (输入新 Key 覆盖, 清空保存删除), 随保存/测试/开关一起生效
- SettingsModal 加 width prop, 扩展数据设置弹窗与 EditExtDialog
  加宽至 max-w-2xl; SettingsModal 加 max-h-[88vh] + 内容区独立
  滚动, 矮视口下不再被截断
2026-09-07 22:54:05 +08:00
shy3130 f8a23b7ce5 fix(api): quote_service 缺失时 quote-interval 兜底返回默认档位 (#261)
PUT /preferences/quote-interval 的 if not qs 分支错误调用
qs.get_min_interval(),此时 qs 必为 None,触发 AttributeError → 500。
改为返回硬编码默认值 6.0,与 GET 接口兜底写法及
QuoteService.DEFAULT_INTERVAL 保持一致,并新增复现测试。
2026-09-07 22:01:12 +08:00
0112020179 614e088079 fix(frontend): display plugin data source names 2026-09-07 18:08:40 +08:00
shy3130 cd1cff0089 style(screener): 策略设置对话框加宽 980px → 1200px 2026-09-07 15:26:00 +08:00
shy3130 f03bc38a16 feat(screener): run_all 渐进式返回, 快策略先出、慢策略后台补算
策略页全量 run_all 需 ~2 分钟, 期间卡片全空。现在按历史耗时升序执行,
首返时限 (strategy_run_all_first_return_s, 默认 15s) 内算完的策略随
响应返回, 慢策略转后台继续算并逐个写入策略缓存, 前端轮询
cached-summary 逐个点亮卡片数字 (未出的显示脉冲占位)。

后端:
- services/strategy_run_queue: 单飞 daemon 工作线程 + handle 状态。
  相同 key (资产/周期/日期/策略集) 且未完成的请求搭车现有执行不重算;
  已完成的重跑即新执行。后端全局同时只跑一个 run_all, 补上前端
  防重入之外的第二道 Numba 并发防线
- run_all 渐进分支 (仅日线 + summary_only): 逐策略增量写缓存 (同日
  按 sid 合并), 收尾整体重写保持旧口径; 结果带 computed_at 时间戳;
  分钟周期与明细请求保持整段阻塞不变
- 历史耗时落盘 user_data/strategy_run_timings.json, 次日起快策略自动
  排前; cached-summary 透传 computed_at

前端:
- 请求通用 30s 超时 + 慢接口豁免清单 (run_all/run/backtest/factor 等
  300s), 避免一个挂起请求占满 HTTP/1.1 连接拖死全站
- Screener: 收到 pending 后 summaryQuery 每 2s 轮询, 以
  computed_at >= started_at 判新 (防同日旧缓存冒充), 8 分钟兜底;
  StrategyCard 三种尺寸新增 computing 脉冲占位

验证: 新增 13 测试 (排序/落盘/搭车/串行化/端点行为/旧路径兼容),
全量套件 1762 passed; pnpm build 通过。
2026-09-07 15:25:54 +08:00
shy3130 a1ef095334 perf(strategy): run_all 逐策略计时日志与可选并行执行
- 每策略计时: ≥1s 用 INFO、否则 DEBUG, 热点策略 (macd_below_zero_revival
  ~155s / platform_consolidation_breakout ~131s / bullish_alignment ~82s)
  可直接从日志定位; 矩阵构建单独计时
- parallel 参数 + strategy_run_all_workers 配置 (默认 1=串行): 实测
  2026-09-07 外层 4 worker 并发 41 策略 299.6s 慢于串行 ~112s (polars
  eager 内部已多线程, 外层并发属超订), 故默认串行、保留开关供调优
- 叠加策略递归 run_all 固定 parallel=False, 防嵌套线程数爆炸
- 单测: 并行与串行结果/失败语义一致
2026-09-07 15:25:48 +08:00
shy3130 5618b4ef1d perf(polars): 并发闸+写锁收缩+看门狗三层防死锁, 升级 polars 1.44
线上曾出现并发 LazyFrame.collect 触发 polars streaming 执行器死锁,
叠加 _write_lock 区间内做重活, 放大为全站请求冻结。本次按触发缩小、
爆炸半径收缩、自动恢复三层布防:

- polars_guard: BoundedSemaphore 并发闸 (总闸 4 + 后台车道 2, 后台
  先拿子闸再拿总闸防死锁); repository 18 处 collect 按交互/后台分级接入
- repository 写锁区间收缩: 分区合并移出锁外, 锁内 (mtime_ns,size)
  指纹校验 + 3 次乐观重试, 失败回退锁内合并; 5 处 _write_lock 重构
- watchdog: 周期探测 collect 闸与全局写锁, 连续 2 次失败退出交由
  supervisor 拉起 (可配置, 默认开)
- polars >=1.44,<1.45 (1.44.1); 附并发压测脚本
  scripts/stress_polars_concurrency.py 供复现验证

另: config 新增 polars_collect_permits / watchdog_* /
strategy_run_all_workers / strategy_run_all_first_return_s 旋钮
(后两者供后续 run_all 优化提交使用, 默认保持旧行为基准)。
2026-09-07 15:25:41 +08:00