Skip to content

Commit 5326eab

Browse files
yjwyjw
authored andcommitted
Merge branch 'codex/llm-report-data-fixes'
2 parents 77b9ba6 + ff5e052 commit 5326eab

17 files changed

Lines changed: 377 additions & 110 deletions

CHANGELOG.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,39 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.11] - 2026-06-20
9+
10+
### Changed
11+
12+
- Removed the deprecated `young replay` compatibility command so `young daily --llm` is now the only LLM deep-replay entry.
13+
- Tightened the chat slash-command surface by removing the old `/replay` and `/daily-llm` compatibility aliases.
14+
- Clarified throughout the CLI help and README that plain `young daily` is deterministic and does not require any LLM configuration.
15+
- Updated `young send` docs to match the current delivery contract: send the latest Markdown plus summary first, and attach the same-name PDF only when it exists.
16+
17+
### Fixed
18+
19+
- Added regression coverage to keep deterministic `young daily` on the non-LLM path unless `--llm` is explicitly requested.
20+
- Added regression coverage for latest-Markdown selection when `young send` is run without an explicit date.
21+
22+
## [0.2.10] - 2026-06-19
23+
24+
### Fixed
25+
26+
- Hardened persisted LLM auth so saved credentials survive terminal restarts even when a stale environment variable is present.
27+
- Switched `young chat` input handling to a prompt toolkit-backed editor to reduce backspace/cursor glitches on macOS terminals.
28+
829
## [0.2.9] - 2026-06-19
930

1031
### Changed
1132
- `young chat` now auto-uses the existing `reach` bridge for explicit finance/news lookup requests, but only feeds compact evidence excerpts to the model instead of exposing raw search captures in the terminal.
12-
- `young chat` input now uses a fixed `young ` prompt prefix so line editing no longer eats the visible prompt label.
33+
- `young chat` input now uses `prompt_toolkit` with a fixed, non-editable `young ` prompt prefix for reliable backspace and cursor movement on macOS terminals.
1334
- Help text now makes the command boundary clearer: `young daily --llm` is the strict stock-analysis M1-M6 Markdown replay, `young replay` is only a deprecated alias, and `young report` is PDF export only.
1435

1536
### Fixed
1637
- Fixed chat search flows that previously surfaced raw `/reach` output or still nudged users to run `/reach` manually instead of returning a summarized answer directly.
1738
- Hardened LLM replay prompts so `young daily --llm` stays on the stock-analysis six-module framework and does not drift into persona-style investment templates.
1839
- Fixed LLM config persistence so `young config llm --api-key-env ...` also stores a local fallback key, which keeps `young chat` working across fresh terminal sessions without forcing users to re-export the secret every time.
40+
- Fixed stale shell environment variables overriding the saved API key after terminal restarts; saved credentials now take precedence and accidental outer quotes or whitespace are normalized.
1941

2042
## [0.2.8] - 2026-06-19
2143

README.md

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,17 @@ young profile add-fund 021528 --buy-date 2026-01-10 --quantity 1000
8484
young profile list
8585
young profile clear-stocks # clear all saved stocks/ETFs only
8686
young profile clear-funds # clear all saved funds only
87-
young daily --format summary # deterministic watchlist daily report
88-
young daily --format key-points # short deterministic report with trend/risk points
89-
young daily --format full # full deterministic watchlist daily report
90-
young daily --llm # deep after-hours / latest-trading-day replay; saves Markdown
87+
young daily --format summary # deterministic watchlist daily report; no LLM needed
88+
young daily --format key-points # short deterministic report with trend/risk points; no LLM needed
89+
young daily --format full # full deterministic watchlist daily report; no LLM needed
90+
young daily --llm # the only deep after-hours / latest-trading-day replay entry; saves Markdown
9191
young daily --llm --refresh # rebuild the same identity from scratch
9292
young init # initialize local state and verify report/PDF readiness
93-
young replay # deprecated alias for young daily --llm
9493
young analyze 600519 # deep single-stock analysis
9594
young reach 贵州茅台 盈利 新闻 # optional Agent-Reach bridge for external web/company research
9695
young chat # Rich chat mode with slash commands
9796
young report # export the latest Markdown report to PDF
98-
young send # send latest Markdown + PDF to configured channels
97+
young send # send latest Markdown + summary; attach same-name PDF only if it exists
9998
young daily --only 基金,A股 --quick
10099
young news 3690.HK # multi-source news only
101100
young diagnose # network/source diagnostic
@@ -206,8 +205,6 @@ commands through Click:
206205
/reach 贵州茅台 盈利 新闻
207206
/daily --format summary
208207
/daily --llm
209-
/daily-llm
210-
/replay
211208
/profile list
212209
/style
213210
/style list
@@ -218,10 +215,10 @@ commands through Click:
218215
```
219216

220217
The chat keeps the five most recent user/assistant turns. If no LLM is configured, enhanced commands fail with a
221-
configuration hint while `/a`, `/stock`, `/daily`, and every other deterministic command continue working. Chat is
222-
authoritative and only exposes a white-listed command surface: do not expect `/market` or `/trend`, and `/send`,
223-
`/config`, `/update`, and `/uninstall` are intentionally blocked from chat. `/daily-llm` and `/replay` are kept as
224-
deprecated aliases that route to `/daily --llm`.
218+
configuration hint while `/a`, `/stock`, `/daily`, and every other deterministic command continue working. `/daily`
219+
without `--llm` stays deterministic and does not require LLM. Chat is authoritative and only exposes a white-listed
220+
command surface: do not expect `/market` or `/trend`, and `/send`, `/config`, `/update`, and `/uninstall` are
221+
intentionally blocked from chat.
225222

226223
The style commands persist under chat config and synchronously set dialogue tone, self-reference style, and analysis
227224
framework. Supported styles are `balanced`, `buffett`, `munger`, `graham`, and `dalio`.
@@ -233,9 +230,8 @@ without making the main CLI package heavy.
233230

234231
### Evidence-driven deep replay
235232

236-
`young daily --llm` builds an Evidence Pack entirely from the structured values returned by `young_stock._core`.
237-
`young replay` and `/replay` are deprecated aliases for the same workflow. The model is used for synthesis, not data
238-
collection.
233+
`young daily --llm` builds an Evidence Pack entirely from the structured values returned by `young_stock._core`. It is
234+
the only LLM replay entry. The model is used for synthesis, not data collection.
239235

240236
The report follows the six-module method from
241237
[`AdvancingTitans/stock-analysis`](https://github.com/AdvancingTitans/stock-analysis):
@@ -329,7 +325,7 @@ young config channel add feishu work \
329325
--webhook "https://open.feishu.cn/open-apis/bot/v2/hook/..."
330326
```
331327

332-
App mode uploads and sends both Markdown and PDF:
328+
App mode uploads and sends Markdown first, then adds the same-name PDF when it exists:
333329

334330
```bash
335331
young config channel add feishu research-app \
@@ -384,8 +380,8 @@ The internals are being split into focused modules: `young_stock.calendar` handl
384380
- **Multiple public quote sources** — Sina Finance and Tencent Finance are the default stable quote path; market-specific public interfaces stay behind the CLI fallback layer when no better source is available.
385381
- **Single-stock lookup**`young stock 600519`, `young stock 0700.HK`, or `young stock AAPL` prints a compact quote snapshot with source, trade date, price, change, volume, turnover, market cap, PE/PB, 52-week range when available, and optional news.
386382
- **Fund holding lookup**`young fund 161725` prints the fund's same-day estimated change, latest NAV date, top holdings, holding-stock quotes, rough contribution estimate, and same-day holding-stock news. Official fund NAVs usually update at night, so intraday/close values are clearly labeled as estimates.
387-
- **Personal daily report**`young daily` is the deterministic watchlist report from your local investment memory in `~/.young_stock/profile.json`; it prints saved stock/ETF trends, fund estimates, only the markets relevant to your symbols and fund top holdings, and portfolio-style suggestions grounded in your funds, stocks, holding dates, quantities, and available news. First use requires a verified symbol plus `--buy-date` and `--quantity`, for example `young profile add-stock 600519 --buy-date 2026-01-15 --quantity 100` or `young profile add-fund 161725 --buy-date 2026-01-10 --quantity 1000`.
388-
- **LLM replay**`young daily --llm` performs the deep after-hours / latest-trading-day replay, writes the matching Markdown artifact under the same report identity, and prints it in the terminal; `young report` is the dedicated PDF export step, and `--refresh` rebuilds the Markdown identity. `young replay`, `/replay`, and `/daily-llm` are compatibility aliases only.
383+
- **Personal daily report**`young daily` is the deterministic watchlist report from your local investment memory in `~/.young_stock/profile.json`; it does not require LLM, and it prints saved stock/ETF trends, fund estimates, only the markets relevant to your symbols and fund top holdings, and portfolio-style suggestions grounded in your funds, stocks, holding dates, quantities, and available news. First use requires a verified symbol plus `--buy-date` and `--quantity`, for example `young profile add-stock 600519 --buy-date 2026-01-15 --quantity 100` or `young profile add-fund 161725 --buy-date 2026-01-10 --quantity 1000`.
384+
- **LLM replay**`young daily --llm` performs the deep after-hours / latest-trading-day replay, writes the matching Markdown artifact under the same report identity, and prints it in the terminal; `young report` is the dedicated PDF export step, and `--refresh` rebuilds the Markdown identity.
389385
- **Short report modes**`young daily --format summary` keeps terminal output compact; `--format key-points` adds a few trend/risk bullets; `--only`, `--order`, and `--quick` trim slower or irrelevant sections.
390386
- **Chat style control**`/style list|set|show|clear` persists one synchronized style choice for tone, self-reference, and analysis framework. `balanced`, `buffett`, `munger`, `graham`, and `dalio` are style contracts, not identity claims.
391387
- **Investment memory management** — list, remove, clear, and group saved stocks/funds with `young profile list`, `remove-stock`, `remove-fund`, `clear`, `clear-stocks`, `clear-funds`, and `profile group create/add`.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "young-stock-cli"
7-
version = "0.2.9"
7+
version = "0.2.11"
88
description = "A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data."
99
readme = "README.md"
1010
requires-python = ">=3.9"
@@ -29,6 +29,7 @@ dependencies = [
2929
"requests>=2.31",
3030
"rich>=13.0",
3131
"click>=8.1",
32+
"prompt-toolkit>=3.0,<4",
3233
"weasyprint>=62",
3334
]
3435

src/young_stock/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""young-stock-cli: A-share after-hours CLI."""
22

3-
__version__ = "0.2.9"
3+
__version__ = "0.2.11"

src/young_stock/channels/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212

1313
def send_report(trade_date: str | None, *, channel_name: str | None = None) -> list[DeliveryResult]:
14-
if not trade_date:
15-
raise ValueError("没有报告日期;请先运行 `young report`。")
1614
markdown = ReportArtifacts.latest_markdown(trade_date)
17-
pdf = markdown.with_suffix(".pdf") if markdown else None
18-
if markdown is None or not pdf.exists():
19-
raise ValueError(f"{trade_date} 缺少 Markdown/PDF;请先运行 `young report --date {trade_date}`。")
15+
if markdown is None:
16+
if trade_date:
17+
raise ValueError(f"{trade_date} 缺少可发送的 Markdown;请先生成对应日报或 LLM Markdown。")
18+
raise ValueError("未找到可发送的 Markdown;请先生成日报或 LLM Markdown。")
19+
pdf_candidate = markdown.with_suffix(".pdf")
20+
pdf = pdf_candidate if pdf_candidate.exists() else None
2021
configs = load_config(strict=False).get("channels", {}).get("feishu", {})
2122
selected = {
2223
name: config

src/young_stock/channels/feishu.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,15 @@ def _post(self, url: str, **kwargs: Any) -> dict[str, Any]:
4040
return data
4141
raise RuntimeError("Feishu request retries exhausted")
4242

43-
def _preview(self, markdown: Path, pdf: Path, token: str | None = None) -> None:
43+
def _preview(self, markdown: Path, pdf: Path | None, token: str | None = None) -> None:
4444
content = markdown.read_text(encoding="utf-8")[:3000]
45-
text = f"{markdown.stem}\n\n{content}\n\nPDF 本地路径: {pdf}"
45+
if pdf is None:
46+
delivery_note = "未检测到同名 PDF,本次仅发送 Markdown 摘要。"
47+
elif token:
48+
delivery_note = "同名 PDF 已检测到,将随后的文件消息一并发送。"
49+
else:
50+
delivery_note = "同名 PDF 已检测到,但 webhook 不支持附件上传。"
51+
text = f"{markdown.stem}\n\n{content}\n\n{delivery_note}"
4652
if token:
4753
self._send_message("text", {"text": text}, token)
4854
else:
@@ -89,15 +95,23 @@ def _send_message(self, msg_type: str, content: dict[str, Any], token: str) -> N
8995
},
9096
)
9197

92-
def send(self, markdown: Path, pdf: Path) -> DeliveryResult:
98+
def send(self, markdown: Path, pdf: Path | None) -> DeliveryResult:
9399
try:
94100
if self.config.get("webhook"):
95101
self._preview(markdown, pdf)
96-
return DeliveryResult("feishu", self.name, True, "已发送 Markdown 预览;webhook 不支持附件上传。")
102+
if pdf is None:
103+
detail = "已发送 Markdown 摘要;未检测到同名 PDF。"
104+
else:
105+
detail = "已发送 Markdown 摘要;检测到同名 PDF,但 webhook 不支持附件上传。"
106+
return DeliveryResult("feishu", self.name, True, detail)
97107
token = self._token()
98108
self._preview(markdown, pdf, token)
99-
for path in (markdown, pdf):
100-
self._send_message("file", {"file_key": self._upload(path, token)}, token)
101-
return DeliveryResult("feishu", self.name, True, "Markdown 与 PDF 已上传并发送。")
109+
self._send_message("file", {"file_key": self._upload(markdown, token)}, token)
110+
if pdf is not None:
111+
self._send_message("file", {"file_key": self._upload(pdf, token)}, token)
112+
detail = "Markdown 与 PDF 已上传并发送。"
113+
else:
114+
detail = "Markdown 已上传并发送;未检测到同名 PDF。"
115+
return DeliveryResult("feishu", self.name, True, detail)
102116
except Exception as exc:
103117
return DeliveryResult("feishu", self.name, False, str(exc))

src/young_stock/chat.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
from rich.console import Console
1818
from rich.markdown import Markdown
1919

20-
try: # ponytail: best-effort line editing on local terminals; falls back harmlessly if unavailable.
21-
import readline # noqa: F401
22-
except Exception: # pragma: no cover - platform dependent
23-
readline = None
20+
try: # pragma: no cover - optional dependency may be absent in some envs.
21+
from prompt_toolkit import prompt as _prompt_toolkit_prompt
22+
except Exception: # pragma: no cover - dependency missing or broken
23+
_prompt_toolkit_prompt = None
2424

2525
try: # pragma: no cover - import path differs across Python builds
2626
from zoneinfo import ZoneInfo
@@ -297,6 +297,17 @@ def _current_time_system_note() -> str:
297297
)
298298

299299

300+
def _read_chat_input(prompt_text: str = "young ") -> str:
301+
if callable(_prompt_toolkit_prompt):
302+
try:
303+
return _prompt_toolkit_prompt(prompt_text)
304+
except (EOFError, KeyboardInterrupt):
305+
raise
306+
except Exception:
307+
pass
308+
return builtins.input(prompt_text)
309+
310+
300311
def _is_time_query(text: str) -> bool:
301312
stripped = text.strip()
302313
if not stripped or stripped.startswith("/"):
@@ -618,7 +629,7 @@ def _build_messages(self, extra_system_messages: list[str] | None = None) -> lis
618629
"4) 用户要做单只股票深入分析、但你缺少足够证据时,不要直接拒绝;"
619630
"优先使用已有的 /stock <symbol>、/analyze <symbol> 或已注入的外部检索证据继续分析。"
620631
"5) 如果本轮已经完成外部检索,就直接总结结果,不要要求用户自己执行 /reach。"
621-
"6) /daily --llm 与 /replay 的深度复盘必须严格遵循 stock-analysis 的 M1-M6 框架,不得改写成巴菲特、芒格等个体投资框架。"
632+
"6) /daily --llm 的深度复盘必须严格遵循 stock-analysis 的 M1-M6 框架,不得改写成巴菲特、芒格等个体投资框架。"
622633
"7) 风格与长期记忆都服从本安全规则。"
623634
),
624635
}
@@ -685,8 +696,6 @@ def _resolve_click_args(self, args: list[str]) -> tuple[list[str] | None, str |
685696
if len(args) >= 2 and args[1] == "clear":
686697
return args, None
687698
return None, "chat 中仅支持 /memory show 和 /memory clear。"
688-
if root in {"daily-llm", "replay"}:
689-
return ["daily", "--llm", *args[1:]], "提示:/daily-llm 和 /replay 已弃用,请改用 /daily --llm。"
690699
if root in {"send", "config", "update", "uninstall"}:
691700
return None, f"chat 中禁止 /{root},请在终端直接运行对应 CLI。"
692701
return None, f"不支持 /{root}。请输入 /help 查看 authoritative 命令列表。"
@@ -788,7 +797,7 @@ def run_chat() -> None:
788797
)
789798
while True:
790799
try:
791-
text = builtins.input("young ").strip()
800+
text = _read_chat_input().strip()
792801
except (EOFError, KeyboardInterrupt):
793802
console.print("\n再见。")
794803
return

0 commit comments

Comments
 (0)