- 删除 5 个过时/残留文档(project_plan/agent_prompt/optimization_plan/report_db_design/deploy/README) - 新建 docs/architecture.md(项目架构:11 包职责+数据模型+配置+产物) - 重写 docs/user-guide.md(CLI 全量+增量/断点续跑+MCP+FAQ) - 重写 README.md(精简入口+文档索引) - 更新 continuation.md(追加本次记录) - 更新 .gitignore(排除 data/* 运行产物)
24 KiB
项目架构文档
版本:v2.0 | 最后更新:2026-08-22
目录
1. 整体架构
┌─────────────────────────────────────────────────────────────────┐
│ 统一 CLI (a_share_cli) │
│ crawl | extract | dedup | events | embed | ingest | pipeline │
│ search | status | report | cninfo | watchlist | discover │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────┼──────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐
│ crawler │──▶│extractor │──▶│ dedup │──▶│ llm │──▶│embedding │
│ (M1) │ │ (M2) │ │ (M3) │ │ (M4) │ │ (M5) │
└─────────┘ └──────────┘ └────────┘ └──────────┘ └─────┬────┘
│
▼
┌──────────┐
│vectorstore│
│ (M6) │
└─────┬────┘
│
┌──────────────────────────────────────┤
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ mcp_server│ │scheduler │
│ (M8) │ │ (M7) │
└──────────┘ └─────┬────┘
│
┌─────────────────┤
▼ ▼
┌──────────┐ ┌──────────────┐
│ reporter │ │stock_reporter │
│ (日报) │ │ (个股日报) │
└─────┬────┘ └──────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│report_db │ │report_ │ │ MySQL │
│ (M10) │ │ import │ │(myquant) │
└──────────┘ └──────────┘ └──────────┘
11 个包(按管道顺序):
| 序号 | 包 | 模块 | 概述 |
|---|---|---|---|
| 1 | crawler/ |
M1 | 新闻抓取:httpx 静态直连 + Playwright JS 渲染 + cninfo 公告 API |
| 2 | extractor/ |
M2 | GNE 中文新闻正文提取,输出 Article |
| 3 | dedup/ |
M3 | 三层去重:URL Hash → Content Hash → SimHash |
| 4 | llm/ |
M4 | DeepSeek/Qwen 投资事件抽取,输出 ExtractedEvent |
| 5 | embedding/ |
M5 | DashScope 远程 / 本地 BGE-M3 向量化 |
| 6 | vectorstore/ |
M6 | Qdrant 本地文件模式,语义检索 |
| 7 | scheduler/ |
M7 | APScheduler 定时任务 + pipeline 编排 + 日报生成 |
| 8 | mcp_server/ |
M8 | MCP 5 工具,供 Cherry Studio/Claude Code 调用 |
| 9 | a_share_cli/ |
CLI | 统一命令行入口(argparse),所有子命令 |
| 10 | report_db/ |
M10 | 日报 MySQL 结构化入库(pymysql) |
| 11 | report_import/ |
M10 | 历史日报 HTML 解析与批量导入 |
辅助目录:
| 目录 | 用途 |
|---|---|
configs/ |
sources.yaml(新闻源)、watchlist.yaml(关注列表)、llm_models.yaml(LLM 场景配置)、loader.py |
prompts/ |
4 个 LLM Prompt 模板(event_extraction/company_analysis/industry_analysis/risk_analysis) |
scripts/ |
独立运行脚本(每个模块一个入口) + systemd 服务文件 |
tests/ |
pytest 测试(225+ passed) |
api/ |
占位(空包,预留给未来 API 服务) |
app/ |
空目录(预留) |
2. 数据流全景
财经网站 / cninfo API
│
▼
┌───────────┐
│ M1 抓取 │ → data/raw/{source}/{YYYYMMDD}/*.html + index.jsonl
└─────┬─────┘
│
▼
┌───────────┐
│ M2 提取 │ → data/processed/{source}/{YYYYMMDD}/{url_hash}.json (Article)
└─────┬─────┘
│
▼
┌───────────┐
│ M3 去重 │ → data/deduped/{YYYYMMDD}/uniques/{url_hash}.json
└─────┬─────┘ data/dedup/fingerprints.sqlite3 (指纹库)
│
▼
┌───────────┐
│ M4 LLM │ → data/events/{YYYYMMDD}/{url_hash}.json (ExtractedEvent)
└─────┬─────┘
│
▼
┌───────────┐
│ M5 向量化 │ → data/embeddings/{YYYYMMDD}/{url_hash}.json (1024 维向量)
└─────┬─────┘
│
▼
┌───────────┐
│ M6 入库 │ → data/qdrant_storage/ (Qdrant 本地文件)
└─────┬─────┘
│
┌────┴────┐
▼ ▼
┌──────┐ ┌──────┐
│ MCP │ │ 日报 │
│ 检索 │ │ 生成 │
└──────┘ └──┬───┘
│
▼
┌────────┐
│ MySQL │ → news_report / news_event (myquant 库)
└────────┘
关键设计:
- 增量处理:M2/M4/M5 产物存在即跳过(2026-08-12 新增),
--force全量重建 - 断点续跑:
pipeline --once --resume从上次失败步骤继续,状态文件data/pipeline/state.json - 去重多源记录:M3 指纹库
source_ids列记录同一篇新闻的多个来源
3. 包职责与导出 API
3.1 crawler/ — M1 新闻抓取
| 文件 | 职责 |
|---|---|
engine.py |
Crawl4AI 异步引擎:httpx 静态直连(js_render=false) / Playwright(js_render=true) |
config.py |
加载 configs/sources.yaml |
models.py |
SourceConfig / CrawlerConfig / CrawlResult / ArticleLink / CninfoItem / CrawlStage |
storage.py |
保存 raw HTML + index.jsonl,URL 去重 |
cninfo.py |
cninfo 公告/调研/互动易 API 抓取 |
公开 API:
from crawler import (
crawl_all, crawl_source, extract_article_links,
load_crawler_config,
SourceConfig, CrawlerConfig, CrawlResult, CrawlStage, ArticleLink, CninfoItem,
)
抓取分流规则:
| 条件 | 引擎 | 特点 |
|---|---|---|
js_render=False 且无 wait_for |
httpx 直连 | 快,不触发反爬 |
js_render=True 或有 wait_for |
Playwright | 支持 JS 渲染 |
新闻源:14 个(13 Web + 1 API:新闻联播),见 configs/sources.yaml
3.2 extractor/ — M2 正文提取
| 文件 | 职责 |
|---|---|
parser.py |
GNE 中文正文提取,模板文本过滤 |
models.py |
Article / ExtractError |
公开 API:
from extractor import (
extract_article, Article, ExtractError, MIN_CONTENT_LENGTH, SOURCE_NAME_MAP,
)
Article 模型(M2 输出,M3+ 输入的唯一格式):
| 字段 | 类型 | 说明 |
|---|---|---|
source_id |
str | M1 源 ID |
url |
str | 文章 URL |
url_hash |
str | SHA1 前 16 位,主键 |
title |
str | 标题 |
content |
str | 清理后纯文本正文 |
author |
str|None | 作者 |
publish_time |
datetime|None | 标准化发布时间 |
word_count |
int | 中文字符数 |
item_type |
str|None | cninfo 类型:announcement/research/irm |
3.3 dedup/ — M3 三层去重
| 文件 | 职责 |
|---|---|
deduper.py |
Deduper 主类:check / ingest / stats |
hasher.py |
SimHash 64 位 + 汉明距离 + content_hash |
store.py |
FingerprintStore,SQLite 持久化 |
models.py |
DedupLayer / DedupResult / DedupStats / Fingerprint |
公开 API:
from dedup import (
Deduper, article_to_fingerprint, FingerprintStore,
DedupLayer, DedupResult, DedupStats, Fingerprint,
simhash64, hamming, content_hash, normalize_content,
DEFAULT_HAMMING_THRESHOLD, DEFAULT_TIME_WINDOW_DAYS,
)
三层去重逻辑:
| 层 | 算法 | 命中条件 |
|---|---|---|
| L1 URL | URL Hash (SHA1) | 完全相同 URL |
| L2 Content | 标准化正文 SHA1 | 正文完全一致 |
| L3 SimHash | 64 位 SimHash + 汉明距离 | 距离 ≤ 3 (默认) |
3.4 llm/ — M4 投资事件抽取
| 文件 | 职责 |
|---|---|
client.py |
OpenAI 兼容客户端,指数退避重试,LLMConfig 加载 |
extractor.py |
Prompt 模板加载,JSON 解析,extract_event / extract_event_async |
models.py |
EventExtraction / ExtractedEvent / Sentiment / LLMCallError / EVENT_TYPES |
公开 API:
from llm import (
extract_event, extract_event_async, parse_event_json,
PromptTemplate, load_llm_config, LLMConfig,
make_sync_client, make_async_client,
EventExtraction, ExtractedEvent, Sentiment, LLMCallError,
EVENT_TYPES, MAX_CONTENT_CHARS, MAX_IMPORTANCE, MIN_IMPORTANCE,
SCENE_EVENT_EXTRACTION, SCENE_DAILY_REPORT, SCENE_STOCK_REPORT,
)
ExtractedEvent 模型(M4 落盘格式):
| 字段 | 类型 | 说明 |
|---|---|---|
source_id |
str | 主源 |
url |
str | 原文 URL |
url_hash |
str | SHA1 前 16 位 |
title |
str | 标题 |
publish_time |
datetime|None | 发布时间 |
sources |
list[str] | 全部来源(去重合并) |
event |
EventExtraction | LLM 抽取结果 |
provider |
str | deepseek / qwen |
model |
str | 模型名 |
attempts |
int | LLM 实际调用次数(含重试) |
EventExtraction(LLM 输出 JSON 结构):
| 字段 | 类型 | 说明 |
|---|---|---|
stock_codes |
list[str] | 6 位代码,可带 .SH/.SZ/.BJ |
company_names |
list[str] | 公司中文简称 |
industries |
list[str] | 行业(申万二级) |
sentiment |
Sentiment | positive/neutral/negative |
importance |
int | 1-5 重要程度 |
event_type |
str | 23 种事件类型之一 |
summary |
str | 一句话摘要(≤200 字) |
23 种事件类型:业绩预告/业绩快报/财报披露/合作签约/投资并购/重大合同/产品发布/技术突破/监管处罚/诉讼仲裁/股东减持/股东增持/回购/分红/高管变动/资产重组/停牌复牌/ST警示/退市风险/宏观政策/行业政策/国际局势/其他
LLM 场景配置(configs/llm_models.yaml):
| 场景 | 用途 | 调用方 |
|---|---|---|
event_extraction |
投资事件抽取(M4) | llm/extractor.py |
daily_report |
日报 AI 摘要 | scheduler/reporter.py |
stock_report |
个股 AI 要点 | scheduler/stock_reporter.py |
embedding |
文本向量化(M5) | embedding/factory.py |
3.5 embedding/ — M5 向量化
| 文件 | 职责 |
|---|---|
base.py |
EmbeddingProvider / AsyncEmbeddingProvider ABC + compose_text |
remote.py |
DashScope 远程 Embedding(OpenAI 兼容) |
local.py |
本地 BGE-M3(可选,需 uv sync --extra local-embedding) |
factory.py |
resolve_provider_type / make_sync_provider / make_async_provider |
models.py |
EmbeddingProviderType / EmbeddingResult / EmbeddingError |
公开 API:
from embedding import (
make_sync_provider, make_async_provider, resolve_provider_type,
compose_text, EmbeddingProvider, AsyncEmbeddingProvider,
EmbeddingResult, EmbeddingError, EmbeddingProviderType,
DashScopeEmbeddingProvider, DashScopeAsyncEmbeddingProvider,
DASHSCOPE_DEFAULT_DIM, DASHSCOPE_DEFAULT_MODEL, DASHSCOPE_BATCH_LIMIT,
MAX_TEXT_CHARS,
)
向量维度:1024(DashScope text-embedding-v3 / 本地 BGE-M3)
文本组装:compose_text() 统一策略——标题 + 正文 + 事件摘要,最大 8000 字符
3.6 vectorstore/ — M6 Qdrant 知识库
| 文件 | 职责 |
|---|---|
client.py |
VectorStore 封装(init/upsert/query/count/info) + make_qdrant_client 工厂 |
models.py |
SearchFilter / SearchResult / CollectionInfo |
公开 API:
from vectorstore import (
VectorStore, make_qdrant_client,
SearchFilter, SearchResult, CollectionInfo,
DEFAULT_COLLECTION, DEFAULT_VECTOR_DIM,
)
Qdrant 模式:默认本地文件模式(data/qdrant_storage/),零依赖,ARM64 兼容
SearchFilter 支持:source_id / stock_codes / company_names / industries / sentiment / importance_min / event_types / publish_date_from / publish_date_to
3.7 scheduler/ — M7 定时任务与 Pipeline
| 文件 | 职责 |
|---|---|
pipeline.py |
run_pipeline / run_step,编排 M1→M6,断点续跑 |
reporter.py |
日报生成(收集→AI 摘要→MySQL 入库) |
stock_reporter.py |
个股日报生成(关注列表) |
公开 API:
from scheduler import (
run_pipeline, run_step, PipelineResult, StepResult,
STEP_COMMANDS, STEP_TIMEOUTS,
)
Pipeline 步骤:
| 步骤名 | 中文 | 默认超时 | 说明 |
|---|---|---|---|
crawler |
M1 新闻抓取 | 900s | 13 源,含 Playwright |
xwlb |
M1 新闻联播 | 60s | 纯 HTTP API |
extractor |
M2 正文提取 | 300s | GNE 提取 |
dedup |
M3 新闻去重 | 120s | 三层去重 |
llm |
M4 LLM 抽取 | 900s | API 调用 |
embedding |
M5 向量化 | 300s | DashScope |
qdrant |
M6 Qdrant 入库 | 300s | 本地文件写入 |
report |
日报生成 | 30s | 仅 07:00 执行 |
cninfo_crawl |
cninfo 公告抓取 | 900s | 06:30 执行 |
cninfo_extract |
cninfo 正文提取 | 300s | — |
cninfo_pdf |
cninfo PDF 补充 | — | — |
超时优先级:TIMEOUT_{NAME} 环境变量 > PIPELINE_STEP_TIMEOUT > 硬编码默认值 > 1800s
调度时间表:
| 时间 | 步骤 |
|---|---|
| 06:30 | cninfo 公告管道 |
| 07:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant→日报 |
| 07:30 | 个股日报 |
| 12:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
| 18:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
| 22:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
3.8 mcp_server/ — M8 MCP 服务
| 文件 | 职责 |
|---|---|
tools.py |
FastMCP 服务器,5 个 MCP 工具 |
5 个 MCP 工具:
| 工具 | 参数 | 说明 |
|---|---|---|
search_news |
query, top_k | 通用语义检索 |
search_company_news |
query, company, top_k | 按公司过滤 |
search_industry_news |
query, industry, top_k | 按行业过滤 |
search_stock_events |
query, stock_code, top_k | 按股票代码过滤 |
search_sentiment_trend |
query, sentiment, top_k | 情绪趋势 + 统计 |
启动方式:stdio 模式(Cherry Studio/Claude Code 自动管理进程)或 SSE 模式(调试:--sse 8765)
3.9 a_share_cli/ — 统一 CLI
| 文件 | 职责 |
|---|---|
main.py |
argparse 子命令路由,所有命令行操作 |
全部子命令:
| 命令 | 功能 | 关键参数 |
|---|---|---|
crawl |
M1 抓取 | --source, --no-save |
extract |
M2 提取 | --date, --source |
dedup |
M3 去重 | --date, --reset |
events |
M4 LLM 抽取 | --date, --provider, --model, --limit, --concurrency |
embed |
M5 向量化 | --date, --provider, --model |
ingest |
M6 入库 | --date, --recreate |
pipeline |
全链路/守护 | --once, --resume, --steps, --report, --cninfo-once |
search |
检索知识库 | query, --top, --source, --sentiment, --stock, --industry, --min-importance |
status |
数据总览 | 无参数 |
report |
生成日报 | --date, --no-upload |
report-import |
历史日报导入 | --dir, --date, --type, --force |
cninfo |
公告抓取 | --no-save, --enrich-pdf, --pdf-limit |
stock-report |
个股日报 | --no-upload |
watchlist |
关注列表管理 | add/remove/list |
discover |
站点分析 | url, --name, --extra, --add |
add-entry |
追加入口 | source, urls... |
3.10 report_db/ — M10 日报 DB 层
| 文件 | 职责 |
|---|---|
db.py |
连接/事务/建表/保存/查询 |
models.py |
EventRow / ReportData (Pydantic) |
schema.py |
DDL(CREATE TABLE) |
公开 API:
from report_db import (
connect, init_schema, save_report, load_db_config,
exists_report, fetch_report,
EventRow, ReportData,
)
3.11 report_import/ — M10 历史日报导入
| 文件 | 职责 |
|---|---|
parser.py |
BeautifulSoup 解析 finance/intl 历史 HTML |
importer.py |
批量导入,幂等,ImportStats 统计 |
公开 API:
from report_import import (
import_history, ImportStats,
parse_report, parse_finance_report, parse_intl_report, ReportParseError,
)
4. 核心数据模型
4.1 管道数据模型关系
CrawlResult (M1) ──→ Article (M2) ──→ DedupResult (M3)
│
▼
ExtractedEvent (M4)
│
▼
EmbeddingResult (M5)
│
▼
SearchResult (M6)
4.2 日报数据模型
ReportData (Pydantic)
├── report_date: date
├── report_type: str (finance | intl)
├── file_name: str
├── generated_at: datetime
├── ai_summary: str | None
├── stats: dict (JSON)
└── events: list[EventRow]
├── section: str (xwlb | news | cninfo | intl)
├── rank: int
├── importance: int | None
├── event_type: str | None
├── title: str
├── summary: str | None
├── sentiment: str | None
└── source: str | None
4.3 全部 Pydantic 模型清单
| 包 | 模型 | 用途 |
|---|---|---|
| crawler | SourceConfig | 单个新闻源配置 |
| crawler | CrawlerConfig | 全局抓取配置 |
| crawler | CrawlerSettings | 抓取设置(并发/重试/UA) |
| crawler | CrawlResult | 单次抓取结果 |
| crawler | ArticleLink | 列表页发现的链接 |
| crawler | CninfoItem | 公告/调研/互动易条目 |
| extractor | Article | 标准化文章(M2 输出) |
| dedup | Fingerprint | 指纹记录(SQLite) |
| dedup | DedupResult | 判重结果 |
| dedup | DedupStats | 指纹库统计 |
| llm | EventExtraction | LLM 输出 JSON 结构 |
| llm | ExtractedEvent | M4 落盘格式(文章+事件) |
| embedding | EmbeddingResult | 向量化结果 |
| vectorstore | SearchFilter | 检索过滤条件 |
| vectorstore | SearchResult | 单条检索结果 |
| vectorstore | CollectionInfo | Qdrant Collection 信息 |
| scheduler | StepResult | 单步执行结果 |
| scheduler | PipelineResult | 全链路执行结果 |
| report_db | EventRow | 日报事件行 |
| report_db | ReportData | 完整日报数据 |
5. 配置体系
5.1 配置文件
| 文件 | 格式 | 用途 | 热更新 |
|---|---|---|---|
configs/sources.yaml |
YAML | 14 个新闻源配置 | 每次抓取重读 |
configs/llm_models.yaml |
YAML | 4 个 LLM 场景配置 | 每次调用重读 |
configs/watchlist.yaml |
YAML | cninfo 公告关注列表 | 每次操作重读 |
.env |
dotenv | API Key + 调度/超时/DB 配置 | 重启服务生效 |
5.2 配置优先级(LLM 场景)
CLI 显式参数 (--provider / --model)
↓
configs/llm_models.yaml scenes.<场景>.xxx
↓
.env 环境变量 (LLM_PROVIDER / DEEPSEEK_MODEL 等)
↓
代码内置默认值 (仅超时/温度等参数)
模型名无内置兜底:缺失即报错,绝不静默使用错误模型。
5.3 关键环境变量
| 变量 | 用途 | 生产值示例 |
|---|---|---|
DASHSCOPE_API_KEY |
百炼 LLM + Embedding | sk-xxx |
DEEPSEEK_API_KEY |
DeepSeek LLM | sk-xxx |
SCHEDULE_TIMES |
调度时间 | 07:00,12:00,18:00,22:00 |
PIPELINE_STEP_TIMEOUT |
全局超时 | 1800 |
NEWS_DB_HOST |
日报 DB 主机 | 192.168.1.10(pi5) / 127.0.0.1(Mac) |
NEWS_DB_PORT |
日报 DB 端口 | 13306 |
6. 产物目录结构
data/
├── raw/ # M1 原始抓取
│ ├── cls/{YYYYMMDD}/ # 按源分目录
│ │ ├── index.jsonl # 文章列表(CrawlResult 摘要)
│ │ └── *.html # 原始 HTML
│ ├── eastmoney/{YYYYMMDD}/
│ ├── ...
│ └── cninfo/{YYYYMMDD}/ # 公告独立目录
│
├── processed/ # M2 正文提取
│ ├── cls/{YYYYMMDD}/
│ │ └── {url_hash}.json # Article
│ └── ...
│
├── dedup/ # M3 指纹库(跨日)
│ └── fingerprints.sqlite3
│
├── deduped/ # M3 去重产物
│ └── {YYYYMMDD}/
│ ├── uniques/
│ │ └── {url_hash}.json # 唯一文章
│ ├── duplicates.jsonl # 重复记录
│ └── sources.json # 多源映射
│
├── events/ # M4 事件抽取
│ └── {YYYYMMDD}/
│ └── {url_hash}.json # ExtractedEvent
│
├── embeddings/ # M5 向量化
│ └── {YYYYMMDD}/
│ └── {url_hash}.json # 1024 维向量 + 事件
│
├── qdrant_storage/ # M6 Qdrant 本地文件
│
├── pipeline/ # 断点续跑状态
│ └── state.json
│
└── reports_history/ # 历史日报 HTML(可选)
7. 运行环境
| 项目 | 值 |
|---|---|
| Python | 3.11 |
| 包管理器 | uv |
| 虚拟环境 | .venv/ |
| 生产服务器 | 树莓派 5 (ARM64), pi@192.168.1.160 |
| 数据库 | MySQL/MariaDB (myquant 库), 通过 pi@192.168.1.10 autossh 隧道 |
| 系统服务 | systemd a-share-research + a-share-db-tunnel |
| 测试 | pytest (asyncio_mode=auto, integration 标记默认跳过) |
| 静态检查 | ruff + mypy |
| LLM 模型 | deepseek-v4-flash(锁定,不可修改) |
—— 架构文档结束 ——