Commit Graph

646 Commits

Author SHA1 Message Date
sunkai174634
fb4735a105 feat: add Futu OpenD as optional HK realtime and fundamental data source (#2269)
* feat: add Futu OpenD as an optional HK realtime and fundamental data source

Add FutuFetcher and FutuFundamentalAdapter behind FUTU_OPEND_HOST/PORT,
register the settings in Config and config_registry so the Web settings
page can expose host, port and HK realtime priority, and route HK
realtime quotes through a configurable futu/longbridge/akshare/yfinance
order while keeping A-share priority untouched. Include offline tests
for the adapter, config schema and HK routing/fallback, plus docs and
CHANGELOG entries.

* fix: wire Futu fundamentals into HK pipeline and restore quote supplementation

- _fetch_offshore_fundamental_bundle() prefers the Futu fundamental
  adapter for HK when FUTU_OPEND_HOST is configured, and falls back to
  yfinance when Futu is absent or returns no usable content.
- HK realtime priority loop now supplements missing quote fields
  (volume_ratio / turnover_rate / pe/pb / market cap) from later
  configured sources instead of returning after the first non-empty
  quote, matching the US path's _supplement_quote behavior.
- capital_flow / boards blocks are filled from the Futu bundle for HK
  instead of being hard-coded not_supported; status and missing_fields
  aggregation updated accordingly.
- Add regression tests for partial-quote supplementation and Futu
  fundamental bundle routing/fallback.

* test: expect boards block ok when bundle provides belong_boards

The Futu integration made the offshore boards block data-driven instead
of hard-coded not_supported; update the existing US/HK fundamental
context test to match (belong_boards from the bundle now surface as an
ok boards block).

* fix: preserve HK fallback_from metadata and normalize Futu quote timestamps

- HK realtime priority loop now records the failed preferred source token
  and passes it as fallback_from when a later source takes over, so the
  pipeline and analysis context can mark the quote as degraded.
- Futu snapshot update_time is a naive Beijing-time (UTC+8) string; attach
  the +08:00 offset before storing provider_timestamp so stale_seconds /
  is_stale / provider_timestamp freshness semantics are correct instead of
  being parsed as UTC.
- Add regression tests for fallback_from propagation and timestamp
  normalization.

* fix: normalize Futu belong_boards to name/type/code contract

OpenD owner_plate returns plate_code / plate_name / plate_type, but DSA
downstream consumers (notification, extract_board_detail_fields, market
structure) only read name/type/code. Map the fields in
FutuFundamentalAdapter._boards so HK Futu boards are actually consumed
instead of silently dropped, and add regression tests including an
end-to-end check through extract_board_detail_fields.

* fix: merge yfinance bundle when Futu fundamental returns partial blocks

Futu partial success (e.g. statements failed but static info worked) used
to short-circuit the whole bundle, silently dropping the growth/earnings
that the existing yfinance path could still provide. Now, when Futu
returns content but is missing growth or earnings, fetch the yfinance
bundle within the remaining budget and merge the missing blocks
(growth/earnings/institution/capital_flow/belong_boards), keeping
Futu-preferred values where both exist. Add regression test for the
partial-success merge path.

* fix: use field-level checks when deciding Futu-vs-yfinance growth/earnings

The previous merge condition only checked dict truthiness, so a truthy
growth/earnings shell (all-None core values or metadata-only keys such
as report_date/period/currency) would skip the yfinance supplement and
silently downgrade existing HK fundamentals. Add _earnings_block_has_values
(a core numeric field or a populated dividend is required) and reuse the
existing _has_meaningful_payload for growth; both the missing_core check
and the merge loop now use these. Add regression test for the
all-None-shell scenario.

* fix: fill HK fundamental field gaps from yfinance instead of block-level checks

Block-level meaningful checks still skipped the yfinance supplement when
Futu hit only part of the growth/earnings fields (e.g. revenue_yoy but
None net_profit_yoy, or earnings with only basic_eps), silently dropping
fields the main branch used to provide. Replace the missing_core decision
with a per-field gap list (growth: revenue_yoy/net_profit_yoy/gross_margin;
earnings.financial_report: revenue/net_profit_parent/basic_eps/gross_profit)
and make the merge field-level: keep Futu values, fill each missing field
from yfinance. Add regression tests for partial-hit and all-None shells.

* fix: normalize Futu dividends to the repo contract and treat dividend gaps as supplement triggers

Futu OpenD dividend_list carries raw fields (statement/ex_date/record_date)
which the notification/data_processing market-structure consumers do not
read; the repo contract is ttm_cash_dividend_per_share,
ttm_dividend_yield_pct and events[].cash_dividend_per_share /
ex_dividend_date / event_date. Normalize events in
FutuFundamentalAdapter._dividends_and_splits, compute TTM count/cash and
yield from the latest quote, and teach _field_gaps/_merge_bundles to treat
a dividend block that does not satisfy the contract as a gap so yfinance
supplements it. Also dedupe FUTU_OPEND_HOST/PORT in full-guide_EN.

* fix: read dividend yield price from UnifiedRealtimeQuote objects

FutuFetcher.get_realtime_quote returns a UnifiedRealtimeQuote dataclass,
not a dict, so the yield branch in _dividends_and_splits that guarded on
isinstance(quote, dict) never ran on the live Futu path, silently dropping
ttm_dividend_yield_pct while the contract check considered the dividend
block complete. Read price via getattr(quote, 'price', None) and keep the
dict fallback for other fetchers; add a regression test driving the real
UnifiedRealtimeQuote shape.

* fix: treat dividend blocks with TTM cash but no yield as supplement gaps

The repo contract consumes ttm_cash_dividend_per_share and
ttm_dividend_yield_pct together. When the Futu dividend path has events
and TTM cash but the extra realtime price snapshot failed (quote None /
no price), ttm_dividend_yield_pct cannot be computed and the block was
previously treated as complete, so yfinance was never consulted and the
notification rendered the yield as N/A.

_dividend_contract_has_values() now requires the paired yield whenever
TTM cash is present, so _field_gaps() triggers the yfinance supplement
and _merge_bundles() replaces the incomplete dividend block.

Add regression tests for the adapter-level gap shape (quote unavailable
leaves no yield) and the manager-level supplement path (Futu cash
without yield pulls yfinance and fills the yield).

* fix: skip unconfigured Futu in HK realtime routing

When FUTU_OPEND_HOST is not configured, the HK realtime priority loop
used to still attempt the futu source, record it as the failed primary,
and attach fallback_from='futu' to a successful quote from the next
enabled source (longbridge/akshare/yfinance). Consumers then wrongly
treated an enabled source's first success as degraded fallback data,
contradicting the documented contract that Futu only participates when
OpenD is configured.

The HK loop now checks FutuFetcher.has_configured_endpoint() once and
skips the futu token entirely when it is disabled, so no fallback_from
is written. Existing configured-Futu routing tests explicitly patch the
endpoint check; a new regression test asserts an unconfigured Futu is
never called and the enriched quote carries fallback_from=None.

* fix: release cached HK Futu fundamental fetcher in DataFetcherManager.close()

The HK Futu fundamental path lazily creates and caches its own
FutuFetcher (an OpenQuoteContext-backed OpenD connection) on
_futu_fundamental_fetcher, but close() only released the TickFlow
fetcher and the default fetchers snapshot. Explicit close / reload
paths therefore left the OpenD connection hanging.

close() now takes the cached _futu_fundamental_fetcher, clears the
reference and calls its close() best-effort. A regression test injects
an observable fetcher into _futu_fundamental_fetcher and asserts
close() invokes it and clears the attribute.

---------

Co-authored-by: BayMax local review <baymax-local@invalid>
2026-08-25 22:54:01 +08:00
Gach-Coder
f92ad2e7d4 feat:分词模块 (#2272) 2026-08-25 22:39:11 +08:00
Elvis Wang
1b42907605 feat: 扩展 A 股指数注册表与 CSI provider 路由 (#2267)
* feat: add JSON-backed index registry

* fix: address index registry review feedback

* fix: validate raw duplicate index aliases

* fix: converge CSI task identities and registry fallback

* docs: align index identity terminology
2026-08-25 22:32:04 +08:00
zhulinsen
262cf5d5de ci: skip heavy checks for docs-only changes (#2255)
* ci: skip heavy checks for docs-only changes

* test: cover docs-only CI path filters

* fix: preserve CI checks for contract assets

* fix: preserve CI checks for license notices
2026-08-25 22:03:03 +08:00
zhulinsen
e45d5ce202 fix: 修复 Linux 分享图中文缺失 (#2270)
* fix: render CJK text in Linux share images

* fix(review-feedback-2270): 补到 PR 附件或评论里,和本次渲染修复说明保持一致

* fix: preserve report language contract in share images
2026-08-24 23:20:56 +08:00
wvyan
5bb3139209 fix: isolate runtime scheduler with portable watchdog (#2219)
* fix: isolate runtime scheduler with portable watchdog

* fix: harden runtime scheduler watchdog shutdown

* fix: prevent scheduled analysis after shutdown

* fix: fence scheduler shutdown and harden watchdog

---------

Co-authored-by: wvyan <13724623+wvyan@users.noreply.github.com>
2026-08-24 22:43:46 +08:00
zhulinsen
9ab79b8299 docs: prepare v3.31.0 release (#2260)
* docs: prepare v3.31.0 release

* fix(review-feedback-2260): add the v3
2026-08-23 22:32:59 +08:00
Elvis Wang
55ad446855 feat: 增加 A 股指数多数据源 fallback 路由 (#2258)
* feat: add A-share index fallback routing

* docs: clarify index priority configuration scope
2026-08-23 21:42:07 +08:00
青玉案
d0e66a1dc3 feat: 新闻检索为空时在报告中如实标注 (#2229)
* feat: 新闻检索为空时在报告中如实标注

消息面章节此前是「有内容才渲染」,检索一条没拿到时整段直接消失,
读报告的人无从判断是确实没新闻,还是检索静默失败了(搜索源限流、
未配置可用渠道等)。这把「抓取失败」呈现成了「确实没有新闻」。

- src/analyzer.py: AnalysisResult 新增 news_result_count,默认 None
- src/core/pipeline.py: 把 Step 4 已算好的计数交给结果对象
  (此前只进了 diagnostic context snapshot,报告层拿不到)
- src/notification.py: news_lines 为空且计数为 0 时,渲染明确提示,
  并说明结论未纳入新闻维度证据
- tests: 新增 5 条用例,含两条负例——计数为 None 时不得报警
  (那是未配置搜索渠道,不是失败)、拿到新闻时行为与改动前一致

不触碰任何检索路径,纯展示层增量。

* fix: 把新闻缺失提示放进真实渲染路径,并独立于模型输出判定

按 review 三条意见修正:

P1-1 提示只存在于 generate_daily_report,而正常流程从不调用它——
_send_single_stock_notification 与聚合报告走的是 dashboard / brief /
single_stock。原实现对所有标准 REPORT_TYPE 都不生效。
改为抽出共享判定 _empty_news_disclosure,四个渲染器统一接入。

P2 检索零命中但模型按 schema 写出了 market_sentiment / hot_topics 时,
原 elif 分支被跳过,报告会展示模型生成的情绪判断却隐瞒无新闻证据。
改为独立判定 news_result_count == 0,与模型是否产出文字无关。

P1-2 补 docs/CHANGELOG.md [Unreleased] 条目,并在
docs/data-source-stability.md 的「用户可见提示建议」一节记录该行为,
含 None / 0 / >0 三态语义表。

测试从 5 条增至 10 条,新增覆盖 dashboard、brief、single_stock 三个真实
渲染器,以及「模型有输出但检索为空」这一最糟组合。39 passed

* fix: 把新闻零命中披露覆盖到模板链路与企业微信入口

按 review 指出的 blocker 修正。此前只接了字符串拼接分支,遗漏两类活路径:

1. REPORT_RENDERER_ENABLED=true 时,generate_dashboard_report /
   generate_brief_report / generate_wechat_dashboard 会先 return render(...),
   模板链路一路不渲染披露;
2. generate_wechat_dashboard 的非模板 fallback 从未接入,而 pipeline 在
   企业微信非 brief 场景会直接调用它。

后果是同一份分析结果在部分渠道披露、在另一些渠道沉默。

改法不再逐点打补丁,而是抽出单一事实来源:

- 新增 src/services/empty_news.py 持有判定与中英文案
- src/notification.py 的 _empty_news_disclosure 改为委托该模块
- src/services/report_renderer.py 为每条结果预计算 empty_news_disclosure,
  三个平台模板共用
- templates/report_markdown.j2 / report_brief.j2 / report_wechat.j2 各加渲染分支
- generate_wechat_dashboard 的 fallback 正文接入披露

新增 6 条回归测试:模板链路三个平台各一条、企业微信入口一条,
外加两条负例(未执行检索时模板与企业微信均不得提示)。

本文件测试 10 → 16 全过;全量 5824 passed,9 个既有失败与本 PR 无关
(干净 main 上同样失败,属测试顺序依赖)。

* fix: 修正计数源头的两处缺口(自查发现)

按 review 的 merge-base..HEAD 方法自查全链路,发现此前几轮都只盯着渲染出口,
从未核对计数源头,而源头本身在两条路径上是错的:

1. src/core/pipeline.py: news_result_count 只在 intel_results 非空时赋值,
   搜索服务整体失败(正是所有搜索源限流全挂的场景)时停留在 None,
   语义为「未执行检索」,于是本 PR 想解决的头号场景反而不提示。
   改为检索一发起即置 0。

2. _analyze_with_agent: Agent 模式自行调用 search_stock_news 完成检索,
   却从不回写计数,该路径下零命中永远静默。改为按检索结果回写 0 或实际条数。

渲染层再周全,源头数据不对则全部落空。

新增 2 条测试锁住这两处语义(18 passed,此前 16)。
全量 5826 passed,9 个既有失败与本 PR 无关。

* fix: disclose missing news search configuration

* chore: remove unrelated agent guidance

* test: run all empty news tests directly

* fix: preserve empty news disclosure across reports

* fix: 让 Agent 模式的新闻披露跟随实际消费的证据

原问题:agent_arch=multi 等受支持的 Agent 配置下,报告可能声称「未纳入新闻
面证据」而分析其实用了新闻,或反过来该提示而不提示。

根因:news_result_count 取自 executor.run() 结束后为持久化情报补打的一次
search_stock_news()。真实情报由 IntelAgent 通过 search_comprehensive_intel
取得,两者不等价,因此披露与真实证据链可能相反。

修复点:新增 src/agent/news_evidence.py,以运行期证据作用域收集 Agent 搜索
工具的真实返回条数;搜索渠道不可用为 None(未执行检索),可用则从 0 起步、
拿到多少算多少。pipeline 在 executor.run() 前后开启并读取该作用域,事后的
持久化补查不再回写计数。

回归风险:工具在 ThreadPoolExecutor 中执行,runner.py 以
contextvars.copy_context() 提交,故作用域中必须是可变累加器对象,换成不可变
值会让父线程读不到;已加回归测试锁住该机制。原 test_agent_path_records_count
断言的正是被修复的错误行为,已替换为反向断言。

Refs #2225

* fix: 让新闻披露以实际证据为准而非搜索命中数

原问题:本地已落库的资讯池或社交情绪进入 news_context 参与分析后,报告仍可能
声称「未配置搜索渠道,本次分析未纳入新闻面证据」或「零命中」。

根因:news_context 由三路来源拼成——实时检索、社交情绪(美股)、本地资讯池,
但只有实时检索会更新 news_result_count。披露断言的是「结论有没有用到新闻面
证据」,而计数只是「搜索命中了几条」,两者是不同命题,后两路参与时必然失真。

修复点:AnalysisResult 新增 news_evidence_present,由 news_context 是否非空
得出,pipeline 两条路径共用 src/services/empty_news.news_evidence_present()
这一个判定函数。披露改为先看有无证据;确无证据时才用计数解释原因
(None=未配置渠道,0=检索零命中)。历史重建同步恢复该字段。

回归风险:旧记录没有该字段,按计数回退推断,与该记录当时的报告表现一致,不会
追溯改变旧报告;已有用例锁住。review 只点名了本地资讯池,社交情绪属同一缺陷类,
本次一并修复并加测试。另加源码断言:任一 pipeline 路径改回只传计数即失败。

Refs #2225

* fix: 按来源登记新闻证据,不让零命中占位文本冒充证据

原问题:普通分析链路在「搜索已执行但一条证据都没拿到」时,报告不再显示零命中
披露——正是本 PR 要修的核心场景,反而比改动前更差。

根因:src/search_service.py 的 format_intel_report() 即使所有维度失败或为空,
也会输出「【XX 情报搜索结果】」标题和每个维度的「未找到相关信息」占位文本,
整段永远非空。上一版把拼好的 news_context 整段交给 news_evidence_present()
判定,于是 news_result_count == 0 时 evidence 被翻成 true,披露被吞掉,错误
状态还会经 to_dict() 持久化,继续影响历史、详情 API 与 Web。

修复点:判定改为按来源逐个登记——实时检索的真实命中数、社交情绪内容、本地
资讯池内容,任一为真才算有证据;两条 pipeline 路径都不再传拼好的整段。
news_evidence_present() 的契约随之改为接收各来源,并在文档串里写明为什么不能
传整段。

回归风险:新增反例用真实的 format_intel_report() 产出占位文本(不用 mock),
断言其不得被判成证据、且报告必须出现零命中披露。另有源码断言:谁把整段
news_context 交回判定函数即失败。上一版两条测试实际在保护该缺陷(一条名为
「任何非空 context 都算证据」,一条要求必须传入 news_context),已一并纠正。

Refs #2225

---------

Co-authored-by: Mach-Chan <zz-b240@zz-b240deMacBook-Air.local>
2026-08-22 21:27:04 +08:00
Gach-Coder
f6b719d1fe 股票实体解析增强 (#2245)
Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
2026-08-22 21:23:55 +08:00
Elvis Wang
cd1fd2229c feat: 数据库 stock_daily 新增 canonical_id 列与双写(#2207) (#2251)
* feat: add canonical_id column and dual-write for stock daily data (#2207)

* fix: unify bare index codes to index canonical_id via matched_index (review OR-COR-4f9ffc38)

* docs: update docstrings for index-aware derivation and fix orphaned inspection-failure test (review OR-COR-4f9ffc38)
2026-08-22 21:21:15 +08:00
zhulinsen
dfff177cef fix: 记录大盘复盘实际生成后端 (#2241)
* fix: report actual market review backend

* fix: preserve exhausted fallback diagnostics

* fix(review-feedback-2241): update the attempt loop to retain the current model even when the and

* fix: preserve resolved market review provider

* fix(review-feedback-2241): Resolve aliased LiteLLM providers before recording

* fix(review-feedback-2241): preserve the failed LiteLLM route's resolved provider

* fix(review-feedback-2241): preserve template fallback for primary LiteLLM exhaustion and Use the

* fix(review-feedback-2241): preserve the fallback provider for unqualified response models

* fix(review-feedback-2241): Derive exhausted aliases from the router's last deployment and

* fix(review-feedback-2241): preserve the provider for gateway-owned slash model IDs

* fix(review-feedback-2241): preserve the explicit provider for unqualified failure models

* fix(review-feedback-2241): fix several earlier route-alias and router-failure diagnostics gaps,

* fix(review-feedback-2241): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:Full

* fix(review-feedback-2241): 补一组成功路径回归:同一 alias 下首个 deployment 为 openai/~...、后续 deployment 为直连

* fix(review-feedback-2241): add a regression test that injects an analyzer exposing only is

* fix(review-feedback-2241): add a regression that asserts the legacy injected-analyzer path does

* fix(review-feedback-2241): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:基于当前 HEAD

* fix(review-feedback-2241): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:基于完整

* fix(review-feedback-2241): add a regression test that drives call litellm
2026-08-22 21:20:59 +08:00
zhulinsen
7fd376f669 fix: sanitize market review history summaries (#2242) 2026-08-22 14:58:20 +08:00
zhulinsen
96bc532dfc fix: restore persisted runtime schedules (#2231) 2026-08-19 23:46:49 +08:00
tseliangmin-star
5d14873f17 docs: add xAI Grok LiteLLM examples and Grok Bot integration notes (#2228)
* docs: add xAI Grok LiteLLM examples and Grok Bot integration notes

* docs: link Grok Bot notes from SKILL, INDEX, and LiteLLM YAML

* docs: add Grok Bot index/openclaw pointers and restore SKILL analyzer_service path

* docs: add Grok Bot pointers in INDEX_EN and openclaw guide

* docs: prefer async Grok Bot analysis and poll market-review status

* docs: prefer async Grok Bot analysis and poll market-review status
2026-08-19 23:39:48 +08:00
mrlonely
5d3e4e374a fix: count DingTalk webhook as notification channel (#2230) 2026-08-19 22:52:10 +08:00
青玉案
b740beea2a fix: 公共 SearXNG 实例发现默认关闭 (#2226)
* fix: 公共 SearXNG 实例发现默认关闭

公共实例普遍存在限流(429)、下线(503)或未开启 JSON 输出的情况,
默认开启会让未配置任何搜索 key 的用户每次分析多耗 30~60 秒,
且新闻面最终为空却不在报告中提示(详见 #2225 的实测数据)。

- .env.example: 默认值改为 false,并说明原因与推荐做法
- docs/full-guide.md、docs/full-guide_EN.md: 同步默认值与说明
- tests: 放宽段落定位断言以适配新默认值,并新增一条测试固化该默认值

* fix: 公共 SearXNG 实例发现改在运行时层面默认关闭

按 review 意见修正:仅改 .env.example 只影响复制新模板的用户,
已有安装与 GitHub Actions(未配置 Variable/Secret 时变量为空)
仍会走 default=True 的运行时分支,继续承受 30~60 秒失败重试。

- src/config.py: parse_env_bool 的 default 由 True 改为 False(真正生效处)
- src/core/config_registry.py: 示例顺序调整为 false 优先
- .github/workflows/00-daily-analysis.yml: 变量未设置时诊断显示「默认关闭」,
  与运行时行为保持一致,不再误报为已开启
- tests: 新增三条回归测试,分别锁住运行时默认值、config.py 源码中的
  default=False、以及工作流诊断文案
- docs/CHANGELOG.md: 在 [Unreleased] 追加条目

显式设为 true 的用户行为不变。27 passed

* fix: align SearXNG public instance defaults

* fix: complete SearXNG opt-in defaults

---------

Co-authored-by: Mach-Chan <zz-b240@zz-b240deMacBook-Air.local>
Co-authored-by: ZhuLinsen <zhuls97@163.com>
2026-08-19 21:59:10 +08:00
lmx-2077
cfd6b0a5fb feat(agent): per-category tool timeouts with graceful degradation (#1890) (#2134) 2026-08-17 21:41:19 +08:00
zhulinsen
294210883c fix: 分析后无报告生成 (#2201) (#2211)
* fix(issue-2201): [bug]-分析后无报告生成

* fix(review-feedback-2211): Keep the TTM cutoff anchored to the as-of date and Propagate report

* fix(review-feedback-2211): Move the empty-watchlist check before the trading-day return

* fix(review-feedback-2211): preserve the failure status, but defer returning until after the and

* fix(review-feedback-2211): preserve the failure result, but defer returning until after the

* fix(review-feedback-2211): Handle failures in the market-review-only CLI branch
2026-08-14 22:08:51 +08:00
zhulinsen
3b98aa1d77 docs: update AIHubMix referral links to InferEra (#2182) 2026-08-10 21:18:03 +08:00
zhulinsen
396d43a4c7 docs: prepare v3.30.0 release (#2180)
* docs: prepare v3.30.0 release

* docs: clarify default Xiaohongshu handle
2026-08-09 22:05:16 +08:00
zhulinsen
071c5aa3c7 fix: stabilize Xiaohongshu share image caption (#2179) 2026-08-09 21:40:13 +08:00
Carl_Wu
40b8c6c3cd fix: 修复首页移动端个股栏触摸滚动 (#2171)
* fix: restore mobile stock sidebar scrolling

* fix: keep history stock bar shrinkable

---------

Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
2026-08-09 19:43:04 +08:00
s4kura
5698068fec fix: split level-one headings without recursion (#2176)
* fix: split level-one headings without recursion

* fix: avoid duplicate content after chunk flush
2026-08-09 19:13:57 +08:00
zhulinsen
46d5bf3472 fix: 恢复桌面端报告分享图 (#2169)
* fix: enable desktop report share images

* fix: include default share image branding

* fix(review-feedback-2169): Keep custom branding paired with its QR code

* fix: show default share image nickname

* fix(review-feedback-2169): Document the atomic branding fallback

* fix(review-feedback-2169): Remove the hard-coded social account default

* fix(review-feedback-2169): Remove the bundled account QR fallback

* fix(review-feedback-2169): update docs/share-images

* fix(review-feedback-2169): [Verification blocker] 当前 Head 的阻断型 CI

* fix(review-feedback-2169): 补一条回归:用包含超长 fenced-code 行和超长原始 URL 的完整/通用报告分别走 build share image html
2026-08-09 13:53:00 +08:00
lyl2104626211
ed848da6f0 feat: persist Agent Chat Skill selection by session (#2160)
* feat: persist Agent Chat skill selection by session

* test: satisfy ChatPage mock immutability lint

* fix: preserve implicit Skill state for legacy sessions

* fix: make top-level Skill selection authoritative

* fix: preserve session skills for invalid requests

---------

Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
2026-08-05 23:12:42 +08:00
zhulinsen
ae19329d66 fix: stabilize watchlist details and workspace state (#2126) 2026-08-05 22:45:12 +08:00
zhulinsen
03dd26ac2e ci: shard backend tests across runners (#2165) 2026-08-05 22:33:50 +08:00
zhulinsen
4dda5d7148 feat: add explicit Responses API channel routing (#2157) 2026-08-05 19:15:08 +08:00
zhulinsen
8052d1a0ac ci: accelerate selective test gates (#2158) 2026-08-05 19:14:40 +08:00
Nicholas-Xiong
a8f084bf08 fix: close #2122 explicit-suffix reject review blockers (OR-COR-607f1395 / OR-COR-26596201 / OR-COR-d6afd0d6) (#2129)
* feat: 新增 STOCK_LIST 单条目解析契约(issue #2063 Phase 1)

新增 src/services/stock_list_parser.py 中的 parse_analysis_target() 单条目解析入口,
按 issue #2063 Phase 1 三段契约实现:

1. 前缀白名单指数:sh/sz 前缀且命中 IndexRegistry 的代码 → INDEX,
   canonical_id 同步指数稳定 ID(sh000300→sh000300、sz399001→sz399001);
2. 裸码默认个股:未带前缀的代码一律 STOCK,即使裸码与已知指数代码冲突
   (000300、000016 等)也仅通过 matched_index 暴露冲突,不翻转 asset_type;
3. 前缀未命中降级为股票:sh/sz/bj/hk/us 前缀但 registry 未收录的代码 → STOCK,
   不再产生 UNSUPPORTED,避免把 typo 或新代码误判为不可处理。

对外暴露 IndexRegistry、IndexEntry、AnalysisTarget、ParseStatus 以及
default_index_registry()(默认收录 sh000300、sh000016、sh000688、sz399001、
sz399006 五个核心指数),上层可注入自定义 registry 扩展白名单。

保留 split_stock_list() / serialize_stock_list() 两个 legacy helper 的签名与
行为不变,tests/test_stock_list_parser.py 在原有两个 legacy 测试的基础上新增
36 个测试覆盖三条契约、6 个 maintainer spec 样例(sh000300、sz399300、sh600519、
000300、000001、920xxx)及边界场景,全部通过。

* fix: 修复 stock_list_parser 三个 review blocker

修复 maintainer 在 PR #2094 (issue #2063 phase 1) review
中指出的三个解析正确性问题:

1. OR-COR-d24a4e9a — 美股 ticker 前缀冲突
   _split_prefix 在切前缀时把 1-5 字母的裸美股 ticker 误判
   为 (前缀, 剩余) 形式:SHOP -> (sh, OP)、HKD -> (hk, D)、
   BJRI -> (bj, RI)、USM -> (us, M)、SHAK -> (sh, AK)、
   USFD -> (us, FD)。修复:在切前缀扫描之前,整串为 1-5
   ASCII 大写字母时直接短路返回 (None, token),让它走裸码
   分支的 US 路径。isupper() 是关键鉴别器——大写字母才符
   合仓库 is_code_like 的 US ticker 形状 '^[A-Z]{1,5}$',
   混合大小写 (usAAPL) 与含数字码 (sh000300) 仍走前缀拆分
   以保留 contract #3 的「prefix supplied → degrade to
   stock」语义。

2. OR-COR-1b643ee6 — 裸 A 股 ETF 路由错误
   _classify_bare_code 把 51/52/56/58/15/16/18 开头的 6 位
   裸 ETF 码统一归到 'CN/STOCK',_canonicalize_for_stock
   随后合成出 'cn510300' / 'cn159915',没有任何上游 fetcher
   接受这种形式。修复:在 6 位分支增加与
   data_provider/baostock_fetcher.py、data_provider/
   yfinance_fetcher.py 以及 data_provider/base.py 中
   ETF_PREFIXES 一致的前缀路由——51/52/56/58 -> SH,
   15/16/18 -> SZ——canonical_id 直接生成 'sh510300' /
   'sz159915',可被 BaostockFetcher._convert_stock_code
   原样接收(往返一致)。

3. OR-COR-403bd018 — 空 IndexRegistry 被静默覆盖
   parse_analysis_target 用 'registry = registry or
   default_index_registry()',导致显式传入的 IndexRegistry([])
   (空白名单配置) 被 falsy 短路替换为默认 registry,
   sh000300 / sz399001 仍被 elevate 为 index,调用方配置
   被破坏。修复:改为 'if registry is None: registry =
   default_index_registry()' 显式 None 判断,空白名单被尊重
   为合法的「不 whitelist 指数」配置——sh000300 在空 registry
   下按 contract #3 degrade 为 stock;默认 registry 行为保持
   不变 (sh000300 仍为 index)。

测试:新增 TestReviewBlockerRegressions 覆盖以上三条回归用
例 (USFD/SHAK/BJDX/SZKMY + 维持用例 SHOP/HKD/BJRI/USM/AAPL
/TSLA/BRK/A/Z;510300/159915/510050/520000/562000/588000/
159919/160000/164000/184000 + 与 BaostockFetcher 的往返一致
性;IndexRegistry([]) 下 sh000300/sz399001 degrade、默认
registry 下 sh000300 仍 elevate、自定义子集 registry 仅
匹配子集)。原 130 tests 保持全过,无回归。

属于 #2063 phase 1 收尾

* fix: make parse_analysis_target reuse normalize_code for lowercase/suffix inputs

PR #2094 (issue #2063 Phase 1) addressed three review blockers, but
maintainer flagged a fourth: lowercase/mixed-case/suffix inputs were
misrouted.

Before this commit:
- 'shop' / 'hkd' / 'aapl' (lowercase) was caught by the 2-char prefix
  scan in _split_prefix and stripped to ('sh', 'op'), 'hk', 'd', etc.
- 'usAAPL' (mixed-case) flowed through to _classify_bare_code which
  matched its alphanumeric pattern and routed to US.
- '600519.SH' / '00700.HK' / '7203.T' (suffix form) never had the suffix
  stripped; '.'-containing tokens were discarded by the empty fallback
  branch and the inner code was misclassified.

Fix: at the top of parse_analysis_target, call
stock_code_utils._normalize_code_and_exchange to uppercase, strip the
suffix and extract the exchange. The normalized form is then rewritten
via _EXCHANGE_NORMALIZER into the legacy sh600519/hk00700 prefix
canonical so _split_prefix stays the source of truth for the prefix →
contract flow. Special-case 000300.SH (index) is rebuilt into sh000300
so the index lookup still fires. Pure alphabetic suffix forms
(7203.T / 005930.KS / 2330.TW / 6505.TWO) short-circuit straight to
the Yahoo-style BASE.SUFFIX canonical.

Tests: new TestNormalizationReviewBlocker covers shop/hkd/aapl
uppercase, usBRK contract #3 degrade, USFD/USM bare-ticker
preservation (the isupper() short-circuit keeps them safe from prefix
scanning), all six CN/HK suffix forms, 000300.SH → sh000300 INDEX,
and all four JP/KR/TW alphabetic suffix forms. All 72 parser tests pass.

Refs: PR #2094 review blocker OR-COR-5f9691af.

* fix(#2063): 显式后缀 reject 不再静默改写为 sh<digits>

关闭 PR #2122 maintainer review 三个高置信度 correctness blocker:

- OR-COR-607f1395: 600519.BJ / 600000.HK / 1234567.SH 这类带显式
  .SH/.SZ/.BJ/.HK 后缀、但与该交易所规则不匹配或位数非法的数字
  代码,以前 parse_analysis_target 在 norm_code is None 分支里
  无条件抽 raw 数字重建为 sh<digits>,把被规范化层明确拒绝的
  输入静默变成上交所股票。修复:仅在 SH/SS alias 路径上、且
  重建后的 sh<base> 在 IndexRegistry.find_by_prefixed_code 命中
  时才重建(保留 000300.SH → sh000300 INDEX alias 语义);其他
  BJ/HK 不匹配或 SH 非识别 alias 一律返回 unsupported 并携带
  可定位 reason。

- OR-COR-26596201: abc.SH 这种带显式交易所后缀、但主体本身不合法
  的 token,以前 norm_code is None 后没在 reject 点终止,继续走
  _split_prefix / _classify_bare_code,_classify_bare_code 对非
  数字默认返回 US,导致 abc.SH 被静默改判为 US stock。修复:在
  显式后缀 + base 非纯数字 + 无 registry alias 命中时直接
  unsupported,终止后续分类。

- OR-COR-d6afd0d6: 1234567.SH / abc.SH 这类规范化层已经拒绝的
  显式后缀输入以前仍会继续走后续分类,最终被当成有效股票而不是
  unsupported。修复:同上,在 _split_explicit_exchange 命中且 raw
  含 '.' 时强制走严格的 alias-lookup-or-reject 路径,阻断后续
  _split_prefix / _classify_bare_code 误判。

非阻断建议同时处理:把 docs/CHANGELOG.md 中原放在 [3.28.0] 段的
STOCK_LIST 解析 [新功能] 条目移回 [Unreleased] 扁平列表,并新增
本轮 [修复] 条目描述显式后缀 reject 修复。

测试:新增 TestExplicitExchangeSuffixRejections 6 个用例覆盖
600519.BJ / 600000.HK / 1234567.SH / abc.SH 4 个 reject 路径,
以及 000300.SH / sh000300.SH 2 个 INDEX alias 仍命中的回归保
护。原 72 个 stock_list_parser 测试保持全过,无回归。

* fix(#2063): collapse 3 review blocker variants in parse_analysis_target

Round-2 review blocker closure for PR #2129 (issue #2063 Phase 1):

* OR-COR-d83a3580 — malformed mixed prefix+suffix tokens like
  sh0x00300.SH no longer get reconstructed into a registered
  index alias. The base_digits == base_after_prefix guard
  requires raw to be a clean <prefix><6digits>.<suffix> shape;
  any embedded hex-like garbage between the prefix and the digits
  short-circuits to unsupported.

* OR-COR-b3e32200 — dotted-prefix tokens (SH.000999) now honour
  contract #3 (prefix supplied → degrade to stock) instead of being
  misrouted through strict-suffix reject. has_explicit_suffix
  now requires both a literal . AND no dotted-prefix lead, so
  SH.000999 falls through to _split_prefix and resolves as a
  SH stock candidate (canonical sh.000999).

* OR-COR-e21e9de5 — foreign-exchange suffixes (.T / .KS / .KQ / .TW /
  .TWO) whose base failed _valid_exchange_code are no longer
  silently flipped to US stock by _classify_bare_code. A new
  elif norm_exchange: branch returns unsupported with the
  offending foreign suffix surfaced in exchange / reason.

Regression tests cover all 3 blocker categories alongside the
existing OR-COR-607f1395 family.

* fix(#2063): close OR-COR-6f4d6b12 + OR-COR-4b91e5a0 review blockers

OR-COR-6f4d6b12 (dotted-prefix invalid base silent acceptance):
parse_analysis_target previously let dotted-prefix inputs like
`SH.000999` / `BJ.600519` / `HK.600519` / `SS.000999` fall through
to `_split_prefix` after `_normalize_code_and_exchange` rejected the
base. The fall-through produced malformed canonical ids
(`sh.000999`, `bj.600519`, `hk.600519`), and for `SS.000999` —
since lowercase `ss` isn't a known exchange prefix — the bare-code
classifier silently flipped the token to `asset_type='stock',
exchange='US'`. Both outcomes mask user typos as round-trippable
canonical ids.

Fix: extend the explicit-suffix reject branch to also catch dotted-
prefix form (`<EXCHANGE>.base`) when `_split_explicit_exchange`
returned a token whose base failed `_valid_exchange_code`. The same
alias rebuild / reject logic now applies uniformly to strict-suffix
and dotted-prefix shapes.

OR-COR-4b91e5a0 (SZ index mixed prefix+suffix alias rejection):
`sz399001.SZ` and `sz399006.SZ` were rejected with "explicit exchange
suffix 'SZ' rejects base 'SZ399001'" despite the default registry
listing both as SZ indices. The alias rebuild whitelist was hardcoded
to {"SH", "SS"}, so the SZ mixed prefix+suffix path never got a
chance to rebuild into `sz399001` / `sz399006`.

Fix: extend the whitelist to {"SH", "SS", "SZ"}. The existing
`base_after_prefix` / `base_digits` / `clean_alias_shape` checks
already guard against malformed hex-like garbage (OR-COR-d83a3580)
and require the rebuilt base to actually hit the registry; SZ now
rebuilds symmetrically to SH/SS.

Test changes:
- `test_dotted_prefix_degrades_to_stock_per_contract3` is replaced
  by `test_dotted_prefix_with_invalid_base_is_unsupported` covering
  SH.000999 / BJ.600519 / HK.600519 / SS.000999. `SZ.000001` is
  removed because SZ is now in the rebuild whitelist and resolves
  through the index/alias path (it's a legitimate SZ stock alias).
- New `test_sz_mixed_prefix_suffix_resolves_through_index_alias`
  covers sz399001.SZ / sz399006.SZ rebuilding into the canonical
  SZ index ids with the right display_name.

Local: tests/test_stock_list_parser.py 91 passed; tests/test_stock_code_utils.py 96 passed; tests/test_agent_executor.py 63 passed.

* docs(changelog): 与代码行为对齐 PR #2129 dotted-prefix 描述

reviewer 指出当前 Unreleased 条目写着 SH.000999 按 contract #3 降级为该交易所股票候选,但 src/services/stock_list_parser.py 与 tests/test_stock_list_parser.py 已把 SH.000999 / BJ.600519 / HK.600519 / SS.000999 固定为 unsupported,白名单交易所的合法 dotted-prefix(如 sz399001.SZ / sz399006.SZ)才命中 INDEX。

同步修正文案:
- invalid base dotted-prefix 不被 strict-suffix 误拒、亦不静默降级为畸形 canonical stock,统一返回 unsupported
- 白名单交易所的合法 dotted-prefix(如 sz399001.SZ / sz399006.SZ)才命中 INDEX

非阻断修订,不改代码。

* fix(#2063): close PR #2129 review blocker OR-COR-9c3d2c44 (lowercase us bare ticker / lowercase explicit-prefix)

reviewer 在新 head ec7959aa 上发现新 blocker:parse_analysis_target()
在 norm_exchange == '' 的 elif 分支用 raw.startswith('us') 判定显式前缀,
但全小写 bare US ticker(如 usfd / usm)也命中这个分支并被错误剥前缀,
得到 canonical_id='FD' / 'M',造成 USFD / USM 真实 ticker 被无声改写为
不同 symbol。

修复:
1. lowercase us 前缀分支仅在 raw 是 mixed case(既非全 lower 也非全 upper)
   时才进入剥离分支。全小写 bare ticker 由 normalize 层处理为合规 uppercase
   bare US ticker(^[A-Z]{1,5}$ 形态)。全大写裸 ticker 由 _split_prefix 短路。
2. _canonicalize_for_stock US 分支统一把 bare upper(),让 lowercase 显式前缀
   形态(usaapl / usshop)也得到大写 canonical id,与 mixed-case 显式前缀
   (usAAPL → AAPL)与 bare lowercase(aapl → AAPL)一致。

测试:
- tests/test_stock_list_parser.py 新增 4 个 regression case:
  - usfd → USFD / usm → USM(blocker close)
  - usaapl → AAPL / usshop → SHOP(non-blocking 修复)

完整本地测试:
- tests/test_stock_list_parser.py 95 passed
- tests/test_stock_code_utils.py 96 passed
- tests/test_stock_code_bse.py 17 passed
- tests/test_stock_index_loader.py 14 passed
- tests/test_agent_executor.py 63 passed
- 共 285 passed

文档:docs/CHANGELOG.md [Unreleased] 段追加新条目描述 OR-COR-9c3d2c44。

* fix(#2063): close PR #2129 review blocker OR-COR-2f0d1a7e (lowercase us-prefix split bifurcation) via Phase 1 contract unification

Phase 1 maintainer clarification (issue #2063, 2026-08-01): the "us"
exchange prefix is case-insensitive on the prefix itself, but the ticker
base following an "us" prefix must arrive in canonical uppercase US
symbol shape. Fully lowercase "us"-prefixed tokens are surfaced as
"unsupported" rather than silently rewritten — closes two prior blockers
under one consistent rule without a US ticker whitelist:

- OR-COR-9c3d2c44 (closed): "usfd" / "usm" were silently upper-cased to
  bare "USFD" / "USM" (i.e. synthesised different US tickers like
  US Foods / USM Holdings when the user may have meant something else).
- OR-COR-2f0d1a7e (new): "usibm" / "usamd" / "usge" / "usbk" were silently
  length-dependent — 5-letter lowercase "us"-prefix inputs fell back
  to bare treaty producing "USIBM" / "USAMD" / "USGE" / "USBK", while
  longer lowercase "usmsft" / "usmeta" split cleanly to "MSFT" / "META".

After this commit:

| input | behaviour |
|-------|-----------|
| "usAAPL" / "usBRK" / "usFD" / "usM" (mixed case) | explicit "us" prefix, splits to "AAPL" / "BRK" / "FD" / "M" |
| "USAAPL" (all upper, >5) | explicit "us" prefix, splits to "AAPL" |
| "USFD" / "USM" (all upper, ≤5) | bare US ticker, preserved as "USFD" / "USM" |
| "usfd" / "usm" / "usibm" / "usamd" / "usge" / "usbk" / "usaapl" / "usshop" (all lower) | "unsupported" with diagnostic "must use uppercase ticker base" |
| "aapl" / "shop" (no "us" prefix) | bare lowercase ticker normalized to "AAPL" / "SHOP" (unchanged) |

Implementation:

- parse_analysis_target() early-return guard: when raw.isalpha() and
  raw.startswith("us") and len(raw) > 2 and raw.islower(), surface as
  ParseStatus.UNSUPPORTED with exchange="US" and a human-readable
  unsupported_reason mentioning the canonical uppercase forms
  ("usAAPL" / "usBRK" / "USFD").
- Simplified the elif norm_code and norm_exchange == "": branch to
  re-split only mixed-case "us"-prefix inputs (all-lowercase is already
  short-circuited by the early guard; all-uppercase stays on the bare
  ticker short-circuit in _split_prefix ≤5 letters or splits as
  explicit prefix >5 letters).
- Updated parse_analysis_target() docstring to reflect the new
  contract: exchange prefix is case-insensitive, but "us"-prefix base
  must be uppercase US symbol shape, while bare codes may be either
  case (the normalizer upper-cases them).

Tests:

- Renamed test_lowercase_us_ticker_regression →
  test_lowercase_us_prefix_is_unsupported with 8 parametrized cases
  ("usfd" / "usm" / "usibm" / "usamd" / "usge" / "usbk" / "usaapl" /
  "usshop"). All assert asset_type == UNSUPPORTED, exchange == "US",
  normalized_prefix is None, and unsupported_reason mentioning
  the uppercase base hint. 289 parser/code-utils/index-loader/agent
  tests pass locally (the lone test_multi_agent.py::TestIntelAgent
  PostProcess::test_repairs_json_and_caches_intel_context failure
  reproduces on HEAD pre-patch and is unrelated — issue #2131
  AlphaSift test pollution).

CHANGELOG:

- Replaced the OR-COR-9c3d2c44 close bullet with a single consolidated
  entry that closes both OR-COR-9c3d2c44 and OR-COR-2f0d1a7e under one
  contract rule from issue #2063.

* fix(#2063): close PR #2129 review blocker OR-COR-7b45f5c1 (mixed-case
us prefix with lowercase base) by extending guard to "case-insensitive
us prefix + base must be uppercase"

OpenReview Bot on PR #2129 head e642648c9d358ffde41c4e29750f55bf27745418
flagged a remaining correctness gap in the August 1, 2026 contract
implementation: the new early-return guard only rejected fully
lowercase "us"-prefixed tokens, so mixed-case prefixes with lowercase
bases such as "Usfd", "USibm", and "Usaapl" still slipped through and
were silently rewritten into different US tickers ("USFD", "USIBM")
or accepted as explicit prefixes with lowercase bases ("us"+"aapl")
instead of returning "unsupported" under the stated Phase 1 contract.

Root cause:

- parse_analysis_target() docstring (src/services/stock_list_parser.py:
  451-456) and the new guard comments claim the "us" prefix is
  case-insensitive AND the ticker base must be uppercase. The
  implementation only fired the reject path when raw.startswith("us")
  AND raw.islower() were both true (src/services/stock_list_parser.py:
  522-527). Mixed-case prefixes like "Usfd" / "USibm" / "Usaapl" have
  raw.islower()==False, so they bypassed the guard.
- The downstream elif branch (src/services/stock_list_parser.py:763-767)
  used raw.startswith("us") (case-sensitive) for its recovery split,
  so those mixed-case prefixes were re-canonicalised to bare "USFD" /
  "USIBM" (when norm_code was non-empty and ≤5 letters fit the bare
  treaty) or accepted as explicit "us" prefix with lowercase base
  (when norm_code was None and the downstream _split_prefix split the
  prefix from the lowercase base).

After this commit the contract is enforced uniformly:

| input | behaviour |
|-------|-----------|
| "usAAPL" / "UsAAPL" / "uSAAPL" / "usBRK" / "UsBRK" / "uSBRK" / "usFD" / "usM" (prefix any case + uppercase base) | explicit "us" prefix, splits to "AAPL" / "BRK" / "FD" / "M" |
| "USAAPL" (all upper, >5) | explicit "us" prefix, splits to "AAPL" |
| "USFD" / "USM" (all upper, ≤5) | bare US ticker, preserved as "USFD" / "USM" |
| "usfd" / "usm" / "usibm" / "usamd" / "usge" / "usbk" / "usaapl" / "usshop" (all-lowercase) | "unsupported" — closed OR-COR-9c3d2c44 + OR-COR-2f0d1a7e |
| "Usfd" / "USibm" / "Usaapl" / "uSfd" / "USaapl" (mixed-case prefix + lowercase base) | "unsupported" — closes OR-COR-7b45f5c1 |
| "aapl" / "shop" (no "us" prefix) | bare lowercase ticker normalised to "AAPL" / "SHOP" (unchanged) |

Implementation:

- Extended the early-return guard in parse_analysis_target() from
  `raw.startswith("us") and raw.islower()` (only fully-lowercase
  rejection) to `raw[:2].lower() == "us" and not raw[2:].isupper()`
  (case-insensitive prefix + base must be entirely uppercase). Any
  token whose base contains lowercase letters — whether the prefix is
  all-lowercase, mixed-case, or all-uppercase — is now uniformly
  rejected up-front as `ParseStatus.UNSUPPORTED` with `exchange="US"`
  and a human-readable `unsupported_reason` mentioning the suggested
  uppercase base form. This closes OR-COR-9c3d2c44, OR-COR-2f0d1a7e
  AND OR-COR-7b45f5c1 under one consistent rule.
- Simplified the elif norm_code and norm_exchange == ""`: branch to
  re-split only mixed/upper-case `us`-prefix inputs whose base is fully
  uppercase. The condition now requires `raw[2:].isupper()` (base must
  be uppercase — already enforced by the early-return guard for the
  reject path, so this clause is a defensive confirmation) and
  excludes the bare-US-ticker short circuit
  `not (raw.isupper() and len(raw) <= 5)` so `USFD` / `USM` ≤5-letter
  all-uppercase bare tickers continue to flow through `_split_prefix`
  short-circuit rather than to the explicit-prefix split.

Tests:

- Extended `test_lowercase_us_prefix_is_unsupported` with 5 new
  parametrized cases (Usfd / USibm / Usaapl / uSfd / USaapl) covering
  the OR-COR-7b45f5c1 mixed-case prefix + lowercase base scenario.
  All 13 cases assert `asset_type == UNSUPPORTED`,
  `exchange == "US"`, `normalized_prefix is None`, `canonical_id == raw`
  (verbatim passthrough), and `unsupported_reason` mentioning
  "uppercase".
- 254 parser/code-utils/index-loader/yfinance/bse tests pass locally
  (249 + 5 new OR-COR-7b45f5c1 cases).

CHANGELOG: updated the existing consolidated bullet to mention the
third closed blocker (OR-COR-7b45f5c1) and the explicit acceptance of
mixed-case prefix + uppercase base forms ("UsBRK" / "uSBRK" / "usFD")
now that the contract is enforced uniformly on the base case.

* fix(#2063): close PR #2129 review blocker OR-COR-us-prefix-nonalpha-guard-gap by extending us-prefix guard to all non-canonical bases

OpenReview Bot 在 PR #2129 head 49e3da6e 上重新复核后给出 1 个未关闭的高置信度 correctness blocker(OR-COR-us-prefix-nonalpha-guard-gap),同时关闭前轮的 4 个 blocker(OR-COR-9c3d2c44 / 2f0d1a7e / 7b45f5c1 三个 round-1/2 blocker 已关闭,本轮只闭合 OR-COR-us-prefix-nonalpha-guard-gap)。本 commit 闭环该剩余 blocker。

== OR-COR-us-prefix-nonalpha-guard-gap root cause ==

src/services/stock_list_parser.py:526-545 的 us-prefix reject guard 用 raw.isalpha() 作为前置条件:

    if (
        raw.isalpha()                                  #  前置 isalpha 过滤
        and len(raw) > 2
        and raw[:2].lower() == "us"
        and not raw[2:].isupper()
    ):
        return AnalysisTarget(..., asset_type=UNSUPPORTED, ...)

这意味着含标点或数字的 us-prefix 输入走不到 reject 路径,被 silently rewrite 为不同的 stock:
- parse_analysis_target("usbrk.b") -> stock canonical="BRK.B"  (lowecase base+标点)
- parse_analysis_target("usshop.us") -> stock canonical="SHOP.US"  (lowercase base+标点)
- parse_analysis_target("us1") -> stock canonical="1"  (lowercase prefix+数字 base)

reviewer 指出这些 us-prefixed 输入应被 surfacing 为 unsupported,以免 callers 收到误导性的 canonical stock id(特别是 canonical="1" 不是合法 US symbol shape)。

reviewer 同时给出非阻断建议:补 dotted/numeric us-prefixed inputs 回归测试覆盖。

== 修法 ==

把 guard 从「raw.isalpha() AND base 含 lowercase letter」改为「base 不匹配 canonical US ticker regex」:

    _US_TICKER_SHAPE_RE = re.compile(r"^[A-Z]{1,5}(\.[A-Z]{1,2})?$")
    if (
        len(raw) > 2
        and raw[:2].lower() == "us"
        and _US_TICKER_SHAPE_RE.match(raw[2:]) is None           #  改为 regex match
    ):
        return AnalysisTarget(..., asset_type=UNSUPPORTED, ...)

新 _US_TICKER_SHAPE_RE 模块级常量与 data_provider/us_index_mapping.py:16-17 和 stock_code_utils._normalize_code_and_exchange 用的同一 regex 一致——canonical US symbol shape:1-5 个大写字母可选跟一个 . + 1-2 个大写字母(covers AAPL/BRK.B/SHOP.US/HKD/USFD 等)。

这个 guard 比 raw.isalpha() + lowercase-letter 检查更严格:
- usbrk.b:base "brk.b" 不 match regex(含 lowercase)→ unsupported ✓
- usshop.us:base "shop.us" 不 match → unsupported ✓
- us1:base "1" 不 match(不是 1-5 大写字母)→ unsupported ✓
- US1:base "1" 不 match → unsupported ✓(含数字的 US-prefix 也被 reject,与 reviewer 期望一致)
- usfd / usibm / Usaapl:base 含 lowercase → 不 match → unsupported ✓(保持原 reject)
- usAAPL / usBRK.B / usSHOP.US:base match → 不 reject → 走原 split-prefix 路径 ✓
- USFD / USBRK.B / AAPL / BRK.B:raw[:2].lower()=="us" false 或 base match → 不 reject → 走原路径 ✓

测试覆盖:
- tests/test_stock_list_parser.py::test_lowercase_us_prefix_is_unsupported parametrize list 加 7 个 new case:
  * usbrk.b、usshop.us(lowercase base + punctuation)
  * us1、us1a、us12a(lowercase base + digits)
  * US1、US12345(all-uppercase but 含数字 invalid US shape)
- 所有 case 断言 asset_type==UNSUPPORTED、exchange=="US"、canonical_id==raw、unsupported_reason 含 "uppercase"
- docstring 与 parametrize 注释同步更新加 OR-COR-us-prefix-nonalpha-guard-gap 解释

== 验证 ==

本地:
- 111 个 stock_list_parser 测试全过(104 已有 + 7 新增 parametrize case)
- PYTHONPATH=src python3 -c "from services.stock_list_parser import parse_analysis_target; for s in [...]: ..." 26 个 case 手动验证全部预期通过
- python3 -m flake8 src/services/stock_list_parser.py tests/test_stock_list_parser.py 我的改动无新增 lint 错误(pre-existing F401 'json'/'Path'/'Optional'/'AnalysisTarget' 不在本 commit 范围)
- backend-gate local run 因 1.8G 内存机 OOM killed(pre-existing limitation,与 PR 2140 解决的 issue #2131 同源)留给 CI web-gate 跑

CI 状态留给 push 后看。

== 真实路径 ==

- PR #2129 review 在 head 49e3da6e 收到 OpenReview Bot OR-COR-us-prefix-nonalpha-guard-gap blocker
- 本 commit 在分支 fix/pr-2122-blockers-r3 上修复并 push
- CI 全绿后请 maintainer 在新 head 复审

* fix(#2063): close PR #2129 round-4 blocker OR-COR-bare-us-suffix-prefix-collision in _split_prefix

OpenReview Bot 在 head 04d86b11 上给出新的 blocker:合法的 bare .US 美股代码如 SHOP.US / HKD.US / BJRI.US / USFD.US 被 _split_prefix() 按 sh/hk/bj/us 前缀错误拆分。

root cause: _split_prefix() 1-5 字母 bare US ticker short-circuit 只覆盖纯字母形态 USFD/SHAK/AAPL;带 .US 后缀的 dotted form 走到已知前缀扫描,前两字符碰巧撞上 sh/hk/bj/us 就被错误拆分。

fix: 用同一 regex _US_TICKER_SHAPE_RE = /^[A-Z]{1,5}(\.[A-Z]{1,2})?$/ 同时覆盖 bare 和 dotted form 作为 short-circuit gate。

回归测试新增 6 个 parametrize case:SHOP.US / HKD.US / BJRI.US / USFD.US / AAPL.US / BRK.B。

本地 117 测试全过(含 6 个新 case),flask8 我的改动无新增 error。CI 交给 push 后看。

* fix(#2129 round-5): extend us-prefix recovery to dotted uppercase US base

OpenReview Bot round-5 review of PR #2129 flagged a remaining
correctness blocker (OR-COR-0e285b84): mixed-case us-prefixed
inputs with a dotted uppercase US base such as usBRK.B /
usABC.US were silently rewritten to bare dotted US tickers
(USBRK.B / USABC.US) after _normalize_code_and_exchange
upper-cased them. _split_prefix then short-circuited on
_US_TICKER_SHAPE_RE and swallowed the user-supplied us prefix,
producing a different canonical id — the same bug pattern that
round-4 already fixed for bare form USFD.US / SHOP.US.

Root cause: the us-prefix recovery branch at
src/services/stock_list_parser.py:~825 guarded its eligible form
with raw.isalpha() (only all-alpha inputs). A dotted base
(e.g. BRK.B) is not all-alpha, so it fell through to raw =
norm_code (USBRK.B) and was then consumed by _split_prefix'
s bare-dotted short-circuit.

Fix: replace the raw.isalpha() + raw[2:].isupper() gate with a
single _US_TICKER_SHAPE_RE.match(raw[2:]) gate (same shape the
upfront guard at lines 566-585 already accepts), while preserving
the bare all-uppercase short-circuit exclusion (not raw[:2].isupper())
so that genuine bare dotted US tickers like SHOP.US / HKD.US /
USFD.US still skip the recovery path and flow through
_split_prefix'
s short-circuit as before.

Add 4 regression test cases covering:
- usAAPL / usBRK — bare US base, explicit prefix preserved
- usBRK.B / usABC.US — dotted uppercase US base, the new
  accepted path, normalized_prefix='us' and canonical id is the
  bare dotted form (e.g. BRK.B), not the mistaken USBRK.B.

Validation:
  python -m pytest tests/test_stock_list_parser.py
  -> 121 passed (was 117, +4 new regression cases)
  No other tests touched — diff is restricted to the parser
  recovery branch and the new regression test suite.
Signed-off-by: xxiaoxiong <2482929840@qq.com>

---------

Signed-off-by: xxiaoxiong <2482929840@qq.com>
Co-authored-by: xxiaoxiong <xxiaoxiong@nicholasxiong.cn>
Co-authored-by: xxiaoxiong <xxiaoxiong@users.noreply.github.com>
2026-08-04 19:38:22 +08:00
zhulinsen
01ee2fa86e fix: route Lark stream bot through international domain (#2148) 2026-08-03 22:36:53 +08:00
zhulinsen
2e6b361a1a docs: add macOS quarantine troubleshooting (#2156) 2026-08-03 22:14:02 +08:00
zhulinsen
84ba462b06 docs: prepare v3.29.0 release 2026-08-02 21:41:41 +08:00
zhulinsen
b61ebf9fc5 fix: preserve native share user activation (#2151) 2026-08-02 21:16:54 +08:00
zhulinsen
3c849dec19 fix: 按需生成分享图并恢复基础设置中的选股开关 2026-08-02 20:59:13 +08:00
zhulinsen
e430fcfe48 fix: 收敛选股排序、缓存与热点并发契约 (#2145)
* fix: stabilize screening ranking and hotspot workflows

* fix(review-feedback-2145): Stop timed-out constituent workers before returning and Keep the

* fix: close screening review contract gaps

* fix: bound screening hotspot search workers

* fix(review-feedback-2145): [Verification blocker] 当前 Head 的阻断型 CI 已最终失败:backend-gate。请在本地运行对应

* fix: close screening contract review gaps

* fix: unify hotspot timeout contracts

* fix: bound hotspot search end to end

* fix: bound hotspot detail fallbacks
2026-08-02 18:32:30 +08:00
zhulinsen
91988da149 feat: 添加报告分享图片与 Web 一键分享 (#2146)
* feat: add report image sharing

* fix(review-feedback-2146): 评审结论 - 代码检查 :当前整个 PR 仍有 2 个未关闭的高置信度代码 blocker。最新复核摘要:基于 merge-base

* fix(review-feedback-2146): preserve the Markdown fallback for unavailable snapshots and preserve

* fix(review-feedback-2146): 补一组通知回归测试,至少覆盖

* fix(review-feedback-2146): 在源码树执行 npm ci 和 npx playwright install chromium,这与预编译 desktop 发行包环境不一致

* fix(review-feedback-2146): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:基于 merge-base

* fix(review-feedback-2146): 补上明确的 CLI/Chromium 安装提示,避免当前日志和 503 文案继续把用户引向

* fix(review-feedback-2146): 补到与 docs/share-images.md 一致,直接提示 cd apps/dsa-web && npm ci 和 npx
2026-08-02 16:45:37 +08:00
Nicholas-Xiong
8d8fa56bc7 ci(#2131): 给 backend-gate offline pytest 加 --timeout=120 + faulthandler_timeout=300 watchdog (#2140)
* ci(#2131): 给 backend-gate offline pytest 加 --timeout=120 + faulthandler_timeout=300

issue #2131 报告 backend-gate 在执行 `Offline test suite` 步骤时两次
在同一提交 `e6abcef17fbc5d655c1c49429182079f02d6552e`(PR #2123)
上间歇性无 traceback 卡住:

  workflow run 30551305640(2026-07-30):
    - Offline test suite 开始于 14:23:13 UTC
    - 最后一条测试输出 14:23:58 UTC(AlphaSift hotspot PASSED [11%])
    - job 在 22 分 38 秒无输出后被取消

  workflow run 30553352307(2026-07-30):同样在 11% 位置卡住

GitHub Actions runner 在被取消前没有 pytest 进度的任何信号,也没
有 traceback。本地 16 个 AlphaSift hotspot 测试都能稳过,意味着
问题是 CI-only 的测试执行顺序、进程级全局状态、线程/事件循环
清理或依赖行为 — 没有稳定的 assertion failure 可调试。

issue #2131 已经列出推荐排查方向之一:给 pytest 加单测试超时与
卡住时的线程栈 dump(`pytest-timeout`、`faulthandler_timeout`)。
但当前 `scripts/ci_gate.sh` 的 `offline_test_suite()` 仅运行
`python -m pytest -m "not network"`,没有任何超时或 watchdog。

本 PR 实施 issue #2131 推荐的 watchdog 改造,目标不是修复根因
(那需要 issue #2131 的等线程栈 dump 复现才能定位),而是让任何
未来 CI hang 都会留下可定位的失败信息或 post-mortem 栈,而不是
静默消亡到 GitHub Actions workflow timeout 才被取消。

改动:

1. `.github/requirements-ci.txt`:新增 `pytest-timeout>=2.3.0`
   依赖。CI 的 `setup-python` + `pip install -r` 步骤会自动拉取。

2. `scripts/ci_gate.sh` 的 `offline_test_suite()`:
     python -m pytest -m "not network" \
       --timeout=120 -o timeout_method=thread \
       -o faulthandler_timeout=300

   - `--timeout=120`:单个测试如果执行超过 2 分钟直接 fail,
     生成 pytest-timeout 的 traceback 指出是哪个 case。
   - `-o timeout_method=thread`:pytest-timeout 用 watcher 线程
     而非 signal 方法,对吞了 SIGINT/SIGTERM 的测试更可靠
     (yfinance、AlphaSift 这类的 Threads/eventloop 都
     有可能 swallow signal)。
   - `-o faulthandler_timeout=300`:pytest 内置 faulthandler
     的 watchdog,整体 pytest 5 分钟无任何输出(最末一个测试
     结束到下一个测试开始之间的「沉默期」超过 300 秒)就 dump
     全部 Python 线程栈到 stderr。这是定位卡住位置的关键信号。

3. `docs/CHANGELOG.md`:在 [Unreleased] 段加一条 [修复] entry
   描述本次改动并指明 issue #2131。

本地验证:

  $ pip install pytest-timeout
  $ bash -n scripts/ci_gate.sh && echo syntax ok
  $ python -m pytest -m "not network" --timeout=120 \
      -o timeout_method=thread -o faulthandler_timeout=300 \
      tests/test_stock_list_parser.py tests/test_stock_code_utils.py -q
  ============================== 98 passed in 2.00s ==============================

  $ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
  0

行为不变项:

- 本地开发 `python -m pytest ...` 不带 `--timeout` 仍可任意长跑调试
  (`setup.cfg` 没在 `[tool:pytest]` 全局加 timeout — 仅 CI 路径加)。
- `scripts/ci_gate.sh` 的 syntax/flake8/deterministic phases 不受影响。
- `pytest-timeout` 只在 `scripts/ci_gate.sh` 的 offline_test_suite 被
  调用时生效,不会污染本地开发流程。

注:本 PR 不修复 issue #2131 的根本 hang 原因(仍需 thread dump 复
现定位),而是把未来的 hang 转成可定位的失败。reviewer 在 issue
#2131 上对类似方向说过「即使再次复现,定位价值很低」—— 但本 PR
的 watchdog 至少把「无信息消亡」转成「带 stacktrace 的 fail」,
当 hang 再次发生时能立刻看到卡在哪个测试的哪一行。

* ci(#2131): close PR #2140 review blocker OR-COM-cc22d635 + OR-COM-c76d8eff — docker-publish.yml 对齐 backend-gate 依赖安装

按 reviewer 在 head `e01a1835` 上的 OpenReview Bot 复核反馈,关闭以下 2 个
高置信度 compatibility blocker:

- OR-COM-cc22d635: Docker Release Publish workflow 的 Install backend gate
  dependencies 仍按旧依赖集合执行 ./scripts/ci_gate.sh,新加的
  pytest-timeout 没覆盖到发布入口
- OR-COM-c76d8eff: 与 OR-COM-cc22d635 同源,cache-dependency-path 也
  缺失 .github/requirements-ci.txt

## 改了什么

.github/workflows/docker-publish.yml:
- setup-python cache-dependency-path 对齐 ci.yml backend-gate:
  加入 requirements.txt + .github/requirements-ci.txt 两个文件
  作为 pip cache key,命中缓存
- Install backend gate dependencies 改用与 ci.yml 完全相同的
  pattern:retry loop (3 attempts, 15s backoff) + 单一
  pip install -r .github/requirements-ci.txt(该文件已 -r 递归拉
  requirements.txt,所以无需重复 pip install -r requirements.txt)
- 加注释说明 issue #2131 引入 pytest-timeout 的关联

docs/CHANGELOG.md:
- [Unreleased] #2131 entry 末尾补一句:同步修正 docker-publish.yml
  的 install 与 cache-dependency-path 对齐

## 为什么这么改

issue #2131 让 scripts/ci_gate.sh 的 offline_test_suite 用
`--timeout=120 -o timeout_method=thread -o faulthandler_timeout=300`,
这要求 pytest-timeout>=2.3.0 插件。

ci.yml 的 backend-gate 已通过 .github/requirements-ci.txt 安装该
插件,但 docker-publish.yml 仍用旧的 `pip install flake8 pytest`,
没有 pytest-timeout,发布前 gate 跑 ./scripts/ci_gate.sh 会直接
报 unrecognized --timeout=120 fail,阻断镜像发布。

reviewer OR-COM-cc22d635 / OR-COM-c76d8eff 都指这是 PR 引入的
compatibility regression(不是已有旧债),需要 PR 同步修。

ci.yml 的 install pattern 是 retry 3 次带 backoff,对齐到
docker-publish 让两个工作流完全统一,未来新加 CI-only 依赖只需改
.github/requirements-ci.txt。

## 验证情况

已本地验证:
- python -c "import yaml; yaml.safe_load(open('.github/workflows/
  docker-publish.yml'))" — YAML 语法 OK
- 对比 ci.yml 的 backend-gate install 步骤,pattern 完全一致

已 CI 验证(待 push 后跑):
- 本 PR 触发 backend-gate / docker-build / ai-governance / Change
  Detection,但 docker-publish.yml 只在 v*.*.* tag 或 workflow_dispatch
  触发,本 PR CI 不会真跑该 workflow。reviewer 复核时会做静态比对

未验证 / 风险点:
- 真实发布流程跑不通:需要 maintainer 推 v*.*.* tag 或手动
  workflow_dispatch 触发,才能验证发布前 gate 真的工作
- 但 install pattern 与 ci.yml 完全对齐,ci.yml 那边过则该边也
  应该过;问题概率很低

## 风险点与回滚

回滚:
1. revert 本 commit
2. docker-publish.yml install step 改回 pip install -r requirements.txt
   + pip install flake8 pytest
3. cache-dependency-path 删除新增两行
4. CHANGELOG.md entry 末尾去掉补充句

潜在风险:
- .github/requirements-ci.txt 递归 -r requirements.txt,release
  runner 之前装过 requirements.txt;retry loop + cache 应该吸收掉
  任何 pip 网络抖动,但首次 release 可能比之前略慢(多一次冗余
  install)。Trade-off 可接受:与 ci.yml 完全统一比省一次冗余
  install 更重要。

* docs(#2131): 收窄 PR #2140 faulthandler_timeout 措辞 + 补 PR 描述 Refs/回滚

OpenReview Bot 在 head 14416440 上已给「可以直接合入」结论,剩 2 个非阻断建议:
1. CHANGELOG 与 PR 描述里 -o faulthandler_timeout=300 写成「整体超过 5 分钟无输出 watchdog」与 pytest 实际语义不一致,应改为「单个测试(含其 teardown)超过 5 分钟时 dump 线程栈」
2. 把 issue 关联补成显式 Refs #2131 + 加最小回滚方案

本 commit 同步两条:
- docs/CHANGELOG.md:把 faulthandler_timeout 描述从「整体超过 5 分钟无输出时 dump 全部线程栈」改为「单个测试(含其 teardown)超过 5 分钟时 dump 全部线程栈」,与 python -m pytest --help 中 faulthandler_timeout 的语义对齐
- PR #2140 描述:在末尾追加 "## 关联 issue Refs #2131" 与 "## 最小回滚方案" 两段(用 gh pr edit --body-file 更新),按 .github/PULL_REQUEST_TEMPLATE.md 模板完整化

无代码逻辑改动,仅文案同步。

---------

Co-authored-by: xxiaoxiong <xxiaoxiong@nicholasxiong.cn>
2026-08-01 22:41:41 +08:00
Nicholas-Xiong
5edee53550 fix(#1970): 关闭认证强制要求当前管理员密码二次确认 (#2050)
* fix(#1970): 关闭认证强制要求当前管理员密码二次确认

后端 api/v1/endpoints/auth.py 的 auth_update_settings 在 disable 路径上即使携带有效 session cookie 也强制要求 current_admin_password,否则返回 400;密码错误统一返回 401,命中 rate limit 与 enable 路径一致返回 429。enable 与 initial setup 路径行为保持不变。

前端 AuthSettingsCard 在关闭认证场景下若 currentPassword 缺失,submit 按钮保持可点击(disabled 仅由 isDirty 决定),handleSubmit 校验后给出内联错误,避免用户面对一个长期 disabled 的按钮但不知所缺。新增 i18n key settings.authDisableRequiredCurrentPassword 中英文本,同步修订 authHelperTurnOff / authPasswordHintOff 文案以反映新契约,并避免 hint 文案与 inline error 文案完全重复导致测试 findByText 多匹配。

测试覆盖:

- tests/test_auth_api.py 新增 disable 路径在有/无 session、有/无 current_password、密码对错、rate limit 命中 6 种分支用例,全部通过。

- apps/dsa-web/__tests__/AuthSettingsCard.test.tsx 把原 'missing current password when session valid' 反向为 'blocks disabling when current password missing',并补 'disables auth with current password provided'。本机 vitest run AuthSettingsCard.test.tsx 6/6 通过。

issue #1970

* test(#1970): 关闭认证回归补真实 ASGI 端到端用例并清理无效 mock

针对 PR #2050 review 反馈,补齐两条回归用例并修正既有用例的误导性 mock:

1. 删除 AuthApiTestCase 三个 valid-session 用例中对 verify_session 的 patch。
   Disable 分支不会调用 verify_session(仅在 enable 分支的 TOCTOU 复检里用到),
   旧 mock 既不生效也容易让读者误以为 disable 路径会做 session 校验。

2. 新增 AuthDisableViaRealASGITestCase 通过真实 ASGI / AuthMiddleware / auth
   路由组合链路(create_app + httpx.ASGITransport,与 test_api_health.py 同路径)
   验证 Issue #1970 修复:
   - 真实 POST /api/v1/auth/login 拿到签名 cookie 后,仅带 session 不带
     currentPassword 调 /api/v1/auth/settings 关闭认证 -> 400 current_required;
   - 同上下文携带正确 currentPassword 关闭认证 -> 200,.env 翻转至
     ADMIN_AUTH_ENABLED=false,响应头携带 Set-Cookie 轮换 session secret。

3. 同步 /api/v1/auth/settings OpenAPI description:明确「关闭认证时
   currentPassword 必填、有效 session 不足够」这一新契约,与 endpoint
   行为及 Issue #1970 上下文对齐。

测试:tests/test_auth_api.py 35/35 全过。

* test(auth): fix rate-limit trigger test — needs MAX+1 iterations to reach 429

Previously range(RATE_LIMIT_MAX_FAILURES) ran only 5 iterations, but
check_rate_limit returns False only when count >= MAX. This means the
5th request enters with count=4 (4 < 5), passes check_rate_limit,
runs verify_stored_password + record_login_failure, and returns 401.
Only a 6th request - entering with count=5 (5 >= 5) - is rejected
early by check_rate_limit and returns 429.

Fix the loop range to RATE_LIMIT_MAX_FAILURES + 1 and update the
assertion: the first MAX attempts return 401 (each recording a
failure), and the final attempt returns 429. Also normalise a
mixed Chinese/English docstring to English-only. 35 tests pass.

* test: tighten ASGI auth disable coverage

- Drop the middleware-is_auth_enabled patch now that the endpoint
  disables auth by rotating the session secret in a single in-memory
  transition. Subsequent middleware checks in the same client see the
  disabled state via the auth module, so the patch was masking a
  state-leak instead of testing the real path.
- Make the positive disable test assert cookie deletion semantics
  (empty value + Max-Age=0/Expires-past + jar cleared) instead of just
  a presence check on Set-Cookie. A leaked pre-disable cookie must not
  remain usable after disable, and the previous assertion would still
  pass if the endpoint rotated to a fresh session id.

* test(auth): strip surrounding quotes when asserting empty dsa_session value

Starlette's delete_cookie serializes the empty cookie value as
dsa_session=""; Max-Age=0; ... — i.e. with surrounding double quotes.
The previous assertion expected the raw value to equal '' and so
failed on CI (which uses Starlette's stock serializer); locally the
TestClient happened to round-trip the same way but the strip happened
to make the assertion spuriously true (or the assertion was correct
against an older Starlette). Strip surrounding double quotes before
comparison so the test matches the actual deletion-form emitted by
delete_cookie.

* docs(changelog): move #1970 entry to [Unreleased] + drop 5 stray 3.28.0 文档段 entries

OR-COR-3defa936 blocker fix: 上轮 commit (`5219a43b`) 把 6 条 bullet 错误地追加到了已发布的 `## [3.28.0] - 2026-07-26` -> `### 文档` 段落,而不是 `[Unreleased]`。其中:

- 1 条属于本 PR (#1970) — 应放进 [Unreleased]
- 5 条属于其他 PR / issue (#2026 / #1985 / #2051 / Windows mimetypes / TUSHARE_HTTP_URL 工作流映射),且这些条目在 `3.28.0` 的 `### 新功能` / `### 改进` / `### 修复` 段已经发布过,再放进 `### 文档` 段属于重复 + 跨段误归类。

修复:
- 删除 `3.28.0 -> ### 文档` 段下的 6 条新增 bullet,恢复该段原本只剩「修复文档中的失效相对链接。」的 upstream/main 原貌;
- 在 `[Unreleased]` 段追加本 PR 的 #1970 单条扁平条目(与仓库约定一致:本 PR 自身只追加自身条目,不替其他 PR 处理)。

合并后 `docs/CHANGELOG.md` 的 `3.28.0 -> ### 文档` 段恢复 1 条原貌;`[Unreleased]` 段只新增 1 条本 PR 的条目,不再污染已发布版本历史。

* chore: trigger CI re-run after changelog fix (e01e0cf7 didn't fire pull_request event)

---------

Co-authored-by: xxiaoxiong <xxiaoxiong@users.noreply.github.com>
2026-08-01 14:33:38 +08:00
zhulinsen
bcb7ae4e13 feat: 将参考 AlphaSift 的选股实现纳入主项目 (#2136)
* feat: integrate built-in stock screening engine

* test: verify screening routes via public contract

* refactor: make screening integration fully native

* fix(review-feedback-2136): [Verification blocker] 当前 Head 的阻断型 CI 已最终失败:backend-gate。请在本地运行对应

* feat: persist screening runs and reuse DSA analysis

* fix(review-feedback-2136): 对选股重排应用统一的模型参数兼容逻辑

* fix(review-feedback-2136): 将字段缺失计入数据源失败

* fix(review-feedback-2136): 评审结论 - 代码检查 :当前整个 PR 仍有 3 个未关闭的高置信度代码 blocker。最新复核摘要:当前整个 PR 仍有 3

* fix(review-feedback-2136): 延迟写入候选直到覆盖率通过校验 and 将 DSA 日线成功结果写入 last-good 缓存

* fix(review-feedback-2136): 将直连日线源纳入统一调用超时 and 将新增的选股缓存目录加入桌面更新备份

* fix(review-feedback-2136): 保留完整热点缓存而非请求切片 and 遵守 DSA 上下文声明的候选上限

* fix(review-feedback-2136): 补一条端到端回归:把真实 ScreenResult 经 ScreeningService.screen 落到 /history 和

* fix(review-feedback-2136): 补一个 refresh=true 且 live fetch 失败时返回 stale cache 的回归测试,避免当前只更新 stale
2026-08-01 14:13:23 +08:00
ObVious55
831ada5370 feat: apply Bayesian skill outcome weights (#2123)
* feat: apply Bayesian skill outcome weights

* fix: align skill auto-weight help with outcome model

* fix: limit outcome stats to requested skills
2026-07-31 19:59:48 +08:00
Nicholas-Xiong
7fa29c7e3e fix(longbridge): 修正 history_candlesticks_by_offset 位置参数顺序,导致 volume_ratio 静默失败 (fixes #2100) (#2109)
* fix(longbridge): 修正 history_candlesticks_by_offset 位置参数顺序 (fixes #2100)

_compute_volume_ratio 调用 ctx.history_candlesticks_by_offset 时把
time(datetime, 第 5 个位置参数)与 count(int, 第 6 个位置参数)值传反——time 槽位
被传入 6, count 槽位被传入 datetime.now()。PyO3 在 PyDateTime 转换层抛
'argument "time": "int" object cannot be converted to "PyDateTime"',
异常被 try/except 吞到 logger.debug, 上层只看到 '未获取到 HK01109 的数据'
DataFetchError, 量比字段恒为 None, 港股实时行情链路上整体退化。

实测发现 Windows 下因 SDK 行为宽容差异未必必复现, 但参数顺序 error 是
SDK 签名 (symbol, period, adjust_type, forward, time, count) 与官方 .pyi
annotation 直接违反的硬 bug——Docker/Linux 下用户长期命中。

修复:
1. 按正确顺序传入 datetime.now() 作为 time、6 作为 count, 在源码注释里写
   清参数语义与旧 bug 模式, 避免未来再次翻位传反。
2. 新增 test_volume_ratio_history_candlesticks_by_offset_arg_order 回归测试,
   对 call_args.args[4]/[5] 做 isinstance + 值断言, 任何回退都会立即 CI 失败。
   旧测试 test_realtime_quote_with_volume_ratio 用 MagicMock 不约束参数顺序,
   是 bug 一直逃过测试的根本原因。

* feat: history_candlesticks_by_offset keyword args cross-SDK compat

- Switch from positional args to keyword args (symbol=, period=,
  adjust_type=, forward=, time=, count=) in _compute_volume_ratio,
  immune to positional signature drift between longbridge 0.2.74
  (forward, time, count) and 4.x (forward, count, time).
- Update test_volume_ratio_history_candlesticks_by_offset_arg_order
  to assert keyword args instead of positional slots.
- Add test_volume_ratio_keyword_args_cross_sdk_compat covering
  4.x positional signature with keyword args.
- Narrow CHANGELOG: keyword args compat + cross-version regression tests.

* docs(changelog): move #2100 fix entry to [Unreleased] + drop 8 stray 3.28.0 entries

[Unreleased] 段此前为空,而 #2100 的修复 entry 被错误地放进已发布的
[3.28.0] - 2026-07-26 段下的 ### 文档 子段中,且伴随 8 条与 #2100 无关
的旧 issue 条目(#2026/#1985/#2051/#2070/#2091 等)也被一并错塞。

按仓库规则修复:
- 把 #2100 的 [修复] 条目(描述:LongbridgeFetcher._compute_volume_ratio
  调用 history_candlesticks_by_offset 位置参数 time/count 传反,导致港股/
  美股实时量比字段恒为 None;改用 adaptive keyword args 兼容 0.2.74 与
  4.x,并补回归测试)作为单条扁平条目写入 [Unreleased] 段;
- 删除 [3.28.0] ### 文档 段下错塞的 9 条无关条目(8 条历史 issue + #2100
  自身),恢复 3.28.0 已发布段原貌;
- 保留 3.28.0 ### 文档 段原有的「修复文档中的失效相对链接」一条。

PR #2109 review feedback from @ZhuLinsen.

---------

Co-authored-by: xxiaoxiong <xxiaoxiong@users.noreply.github.com>
Co-authored-by: xxiaoxiong <xxiaoxiong@nicholasxiong.cn>
2026-07-31 19:59:31 +08:00
Nicholas-Xiong
ee3d3da1c5 fix: redact short credentials in CLI diagnostics (#2118)
* fix: redact short credentials in CLI diagnostics

* fix(review-feedback-2111): Redact indented values under empty sensitive YAML fields and preserve

* fix: close structured diagnostic redaction gaps

* fix(review-feedback-2111): Treat comment-only YAML values as empty blocks and Redact indentless

* fix: redact YAML node property blocks

* fix: redact normalized proxy authorization fields

* fix: close multiline diagnostic redaction gaps

* fix: redact spaced credential labels

* fix(review-feedback-2111): Consume YAML blocks with node properties

* fix: include registered spaced credential labels

* fix: close remaining structured redaction gaps

* fix: redact shell words and explicit YAML mappings

* fix: redact single-quoted structured keys

* fix: redact shell append assignments

* fix: redact quoted YAML explicit keys

* fix(review-feedback-2111): add suffix text or drop segments from the env name, such as DeepSeek

* fix(review-feedback-2111): trimming the new docs/CHANGELOG

* fix(review-feedback-2111): update the PR description's verification counts/ranges to match the

* fix(review-feedback-2111): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:基于

* fix(review-feedback-2111): 补上 helper 级和 non-zero-exit preview 级回归用例,避免文档与运行时行为再次漂移

* fix(review-feedback-2111): add focused helper-level and non-zero-exit preview regressions for

* fix(review-feedback-2111): 评审结论 - 代码检查 :当前整个 PR 仍有 1 个未关闭的高置信度代码 blocker。最新复核摘要:On the current

* fix(review-feedback-2111): update the PR description's reported current head from 27a013fbf to

* fix: redact sensitive env names embedded inside command substitutions across multi-segment diagnostics

- 覆盖 OPENAI_API_KEY=sk-12345 这种首段为非敏感赋值、值里又嵌敏感名的情况
- 当 $(...) 的前置赋值是敏感名时跳过尾扫避免双重改写,非敏感名仍需进入尾扫
- 新增测试覆盖多段 + 同函数敏感+非敏感赋值的复合诊断文本
- 使用 [A-Z][A-Z0-9_]* token 扫描找到 ALL  中的敏感 env 名引用

* fix(review-2111): redact export SENSITIVE=$(...) without dropping trailing fields

Round-3 review blocker closure for PR #2118 (issue #1784):

OR-COR-7c0a5d41 — the form

    export SENSITIVE_ENV=$(printenv OTHER_SECRET) session_id=dup1 token_budget=1000

previously lost ``session_id=dup1`` (case 1) and ALL trailing fields
(case 2 with ``echo OPENAI_API_KEY=sk-12345``) because the second-pass
``$(...)`` scan re-added the same span that the first pass had already
replaced, and ``_replace_spans`` silently dropped the duplicated
region's width worth of trailing characters.

Two fixes:

1. Track first-pass sensitive-assignment replacement spans
   (``first_pass_spans``) and skip any ``$(...)`` whose start lies
   inside one of those spans. This is the principled guard against
   the overlap regardless of where the leading assignment sits.
   Previously the second-pass computed a "prior prefix" via three
   independent regex branches (semicolon-separated / newline-
   separated / head-of-string), each of which only matched bare
   ``NAME=`` — so ``export NAME=`` slipped through and the second
   pass double-rewrote the same span.

2. Add ``(?:export[ \t]+)?`` to all three prior-prefix regexes so
   that even if the first-pass span guard were ever evaded, the
   leading ``export SENSITIVE=`` would still be recognised and the
   second pass would skip the inner ``$(...)``.

Regression tests cover the two exact reproductions from the review
(``session_id=dup1 token_budget=1000`` and ``session_id=dup3``), plus
a non-``export`` control case to lock in the existing behaviour. Full
``tests/test_local_cli_backend.py`` passes 363/363; the ``tests/test_stock_*``
subset is unaffected (460 passed across the CLI + stock subset).

---------

Co-authored-by: zhulinsen <zhuls97@163.com>
Co-authored-by: xxiaoxiong <xxiaoxiong@nicholasxiong.cn>
2026-07-30 22:49:54 +08:00
ObVious55
03bae035a6 feat: add skill opinion outcome performance statistics (#2119)
* feat: add skill opinion outcome evaluation core

* feat: add skill opinion outcome evaluation core

* fix: unify expected-start resolution

* changelog

* fix: validate persisted daily start sessions

* fix: preserve legacy local backtest windows

* feat: add skill opinion outcome statistics

* docs: clarify outcome statistics stage boundary

* fix: clarify backtest-only legacy start fallback

* fix: prevent pending outcome retry starvation

* fix: preserve explicit backtest start contract

* fix: rotate failed outcome candidates
2026-07-30 22:12:57 +08:00
ObVious55
85ded1d70c feat: add skill opinion outcome evaluation core (#2116)
* feat: add skill opinion outcome evaluation core

* feat: add skill opinion outcome evaluation core

* fix: unify expected-start resolution

* changelog

* fix: validate persisted daily start sessions

* fix: preserve legacy local backtest windows
2026-07-28 19:55:15 +08:00
ObVious55
f4d9956c52 fix:backtest stock identity and daily-window correctness refactor (#2073)
* fix: unify local daily window stock code resolution

* fix: enforce authoritative daily window resolution

* add test

* fix: converge daily window resolution contract

* fix: preserve daily stock identity compatibility

* fix: rebuild legacy foreign market snapshots

* fix(backtest): preserve legacy JP/KR bare-code compatibility

* fix(backtest): disambiguate legacy offshore stock codes

* fix(backtest): prevent cross-market alias collisions
2026-07-27 21:40:38 +08:00
ZhuLinsen
905c339d80 docs: add v3.28.0 release changelog 2026-07-26 22:04:45 +08:00
zhulinsen
90f62349af fix: 将 Tencent 日 K 保持为最终兜底 (#2107)
* fix: keep Tencent daily data as final fallback

* fix(review-feedback-2107): 补一个覆盖 python scripts/check env.py --fetch 或“先 import data provider 再读取
2026-07-26 20:32:12 +08:00
zhulinsen
20c399e793 fix: 避免港股实时行情重复拉取全市场数据 (#2104)
* fix: cache HK realtime market data

* fix(review-feedback-2104): Coalesce failed cold-cache refreshes

* fix(review-feedback-2104): Reject empty market snapshots before caching success
2026-07-26 16:33:28 +08:00