feat: 每日AI分析改造——定调摘要、环比快照、完整连板梯队带涨停原因、生成次数记账
This commit is contained in:
@@ -70,4 +70,11 @@
|
||||
- 成交额备用计算:当资金流向API获取失败时,使用 `chartData` 的 `volume × close` 近似计算成交额(单位:元)
|
||||
- 资金流向数据获取失败时,前端通过 `fundFlowError` 状态显示错误信息,便于排查问题
|
||||
- ❌ 东方财富 API(push2his.eastmoney.com)在 Edge Function 环境被拒绝访问(peer closed connection),主力净流入数据无法获取,显示为 "-"
|
||||
- ✅ 替代方案:使用腾讯实时行情API的外盘(索引7)和内盘(索引8)数据计算净主动买入额 = (外盘 - 内盘) × 当前价 × 100(单位:元)
|
||||
- ✅ 替代方案:使用腾讯实时行情API的外盘(索引7)和内盘(索引8)数据计算净主动买入额 = (外盘 - 内盘) × 当前价 × 100(单位:元)
|
||||
## 每日 AI 分析(backend FastAPI,15:10 自动触发)
|
||||
|
||||
- 报告结构约定:正文标题(#)后第一行必须是引用块 `> 今日定调:<80字内核心结论+关键数字>`;保存时由 `_extract_summary()` 正则提取进 `summary` 字段,前端顶部高亮展示,缺失时退回正文截断
|
||||
- 环比数据:`collect_ai_analysis` 每次运行先采集当日盘面快照(指数+涨跌统计 JSON)写入 `daily_market_stats` 表(UNIQUE trade_date,upsert),再读上一交易日快照格式化为 prompt 中的"环比数据段";首跑无昨日数据时 AI 须如实标注"暂无昨日基准"
|
||||
- 连板梯队:`get_market_dashboard` 返回 `limitLadder` 字段(完整版,2连板以上全量+首板前8),涨停原因/封单金额来自涨停池按 thscode 匹配;看板 events 里的天梯仍只取每层前2(保持 UI 精简),两处用途不同不要合并
|
||||
- tokens 口径:`ai_reports.tokens_used` 只记"当次生成"消耗(约7万/份);同日重新生成走 UPSERT,`generation_count` 自增、`updated_at` 刷新、`created_at` 保留首次生成时间;表有 UNIQUE(trade_date, report_type),禁止改回 INSERT OR REPLACE 之外还要注意别用 lastrowid(UPSERT 更新时不可靠,须回查 id)
|
||||
- 旧库补列用 init_db 里的 try/except ALTER TABLE 轻量迁移(CREATE TABLE IF NOT EXISTS 不会更新旧表结构)
|
||||
|
||||
@@ -82,9 +82,19 @@ CREATE TABLE IF NOT EXISTS ai_reports (
|
||||
tools_used TEXT,
|
||||
model TEXT NOT NULL,
|
||||
tokens_used INTEGER,
|
||||
generation_count INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||
updated_at TEXT,
|
||||
UNIQUE(trade_date, report_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_market_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trade_date TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||
UNIQUE(trade_date)
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
@@ -107,6 +117,16 @@ def init_db():
|
||||
conn = get_connection()
|
||||
conn.executescript(SCHEMA_SQL)
|
||||
|
||||
# 轻量迁移:为已存在的旧表补列(CREATE TABLE IF NOT EXISTS 不会更新旧表结构)
|
||||
for alter_sql in (
|
||||
"ALTER TABLE ai_reports ADD COLUMN generation_count INTEGER NOT NULL DEFAULT 1",
|
||||
"ALTER TABLE ai_reports ADD COLUMN updated_at TEXT",
|
||||
):
|
||||
try:
|
||||
conn.execute(alter_sql)
|
||||
except sqlite3.OperationalError:
|
||||
pass # 列已存在
|
||||
|
||||
# 清理过期缓存
|
||||
from services.cache import clean_expired
|
||||
clean_expired()
|
||||
|
||||
@@ -143,7 +143,8 @@ async def admin_list_reports(request: Request):
|
||||
conn = get_connection()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT id, trade_date, report_type, title, summary, tools_used, model, tokens_used, created_at "
|
||||
"SELECT id, trade_date, report_type, title, summary, tools_used, model, tokens_used, "
|
||||
"generation_count, created_at, updated_at "
|
||||
"FROM ai_reports ORDER BY trade_date DESC, id DESC LIMIT 50"
|
||||
).fetchall()
|
||||
items = []
|
||||
|
||||
@@ -60,7 +60,8 @@ async def list_reports():
|
||||
conn = get_connection()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT id, trade_date, report_type, title, summary, tools_used, model, tokens_used, created_at "
|
||||
"SELECT id, trade_date, report_type, title, summary, tools_used, model, tokens_used, "
|
||||
"generation_count, created_at, updated_at "
|
||||
"FROM ai_reports ORDER BY trade_date DESC, id DESC LIMIT 50"
|
||||
).fetchall()
|
||||
items = []
|
||||
|
||||
@@ -485,6 +485,67 @@ async def _build_dashboard() -> dict:
|
||||
"detail": f"涨停 {limit_up_count} 跌停 {limit_down_count} 炸板 {break_count}",
|
||||
})
|
||||
|
||||
# ── 连板梯队(完整版,供 AI 分析用;上方 events 天梯只取前2保持看板精简) ──
|
||||
# 涨停池里带 limit_up_reason / seal_money,按代码匹配给梯队个股
|
||||
reason_by_code = {}
|
||||
if isinstance(limit_up_data, dict):
|
||||
for lu in limit_up_data.get("item", []) or []:
|
||||
code = lu.get("thscode", "")
|
||||
if code:
|
||||
reason_by_code[code] = {
|
||||
"reason": (lu.get("limit_up_reason") or "").strip(),
|
||||
"sealWan": round(_safe_float(lu.get("seal_money")) / 10000),
|
||||
}
|
||||
|
||||
_LADDER_LEVELS = [
|
||||
("seven_over", 7, "7连板+"),
|
||||
("six_board", 6, "6连板"),
|
||||
("five_board", 5, "5连板"),
|
||||
("four_board", 4, "4连板"),
|
||||
("three_board", 3, "3连板"),
|
||||
("two_board", 2, "2连板"),
|
||||
("first_board", 1, "首板"),
|
||||
]
|
||||
limit_ladder = []
|
||||
if isinstance(limit_ladder_data, dict):
|
||||
ladder_items = limit_ladder_data.get("item", [])
|
||||
if ladder_items:
|
||||
today_boards = ladder_items[0].get("boards", {}) or {}
|
||||
seen_keys = set()
|
||||
for key, board_num, label in _LADDER_LEVELS:
|
||||
seen_keys.add(key)
|
||||
# 首板数量多(几十家)只取前8家;2连板以上全量保留
|
||||
cap = 8 if board_num == 1 else None
|
||||
entries = today_boards.get(key, []) or []
|
||||
if cap is not None:
|
||||
entries = entries[:cap]
|
||||
for item in entries:
|
||||
code = item.get("thscode", "")
|
||||
extra = reason_by_code.get(code, {})
|
||||
limit_ladder.append({
|
||||
"board": board_num,
|
||||
"label": label,
|
||||
"name": item.get("name", ""),
|
||||
"code": code,
|
||||
"reason": extra.get("reason", ""),
|
||||
"sealWan": extra.get("sealWan"),
|
||||
})
|
||||
# 兜底:天梯返回了未知层级 key 时也带上(跳过已处理的已知 key)
|
||||
for key, entries in today_boards.items():
|
||||
if key in seen_keys:
|
||||
continue
|
||||
for item in entries or []:
|
||||
code = item.get("thscode", "")
|
||||
extra = reason_by_code.get(code, {})
|
||||
limit_ladder.append({
|
||||
"board": _safe_float(item.get("board_num")) or 1,
|
||||
"label": f"{int(_safe_float(item.get('board_num')) or 1)}连板",
|
||||
"name": item.get("name", ""),
|
||||
"code": code,
|
||||
"reason": extra.get("reason", ""),
|
||||
"sealWan": extra.get("sealWan"),
|
||||
})
|
||||
|
||||
# ── 组装结果 ──
|
||||
result = {
|
||||
"indices": indices,
|
||||
@@ -492,6 +553,7 @@ async def _build_dashboard() -> dict:
|
||||
"sectorStrength": sector_strength[:31],
|
||||
"conceptStrength": concept_strength[:10],
|
||||
"events": events,
|
||||
"limitLadder": limit_ladder,
|
||||
"updateTime": datetime.now(BJT).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
|
||||
+152
-16
@@ -3,10 +3,14 @@
|
||||
负责:
|
||||
1. 调用 OpenAI 兼容 API 进行分析
|
||||
2. Function Calling 循环(AI 可主动获取数据)
|
||||
3. 保存报告到数据库
|
||||
3. 采集当日盘面快照(供次日环比)
|
||||
4. 保存报告到数据库
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
|
||||
import httpx
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
@@ -25,7 +29,7 @@ SYSTEM_PROMPT = """你是一位专业的A股市场分析师,擅长从数据中
|
||||
- 风险提示,每次推荐都需说明风险点
|
||||
|
||||
可用工具:
|
||||
- get_market_dashboard: 获取市场整体数据(指数/涨跌统计/行业强度/事件情报/市场温度)
|
||||
- get_market_dashboard: 获取市场整体数据(指数/涨跌统计/市场温度/连板梯队/行业强度/事件情报)
|
||||
- get_theme_history: 获取指定日期的题材涨幅排行
|
||||
- get_active_core_stocks: 获取核心股追踪数据(10日涨幅矩阵+所属题材)
|
||||
- get_stock_quote: 获取个股实时行情
|
||||
@@ -40,26 +44,39 @@ SYSTEM_PROMPT = """你是一位专业的A股市场分析师,擅长从数据中
|
||||
DAILY_ANALYSIS_PROMPT = """请对 {trade_date} 的A股市场进行收盘分析,生成一份完整的分析报告。
|
||||
|
||||
{prev_report_section}
|
||||
{prev_snapshot_section}
|
||||
|
||||
请先调用以下工具获取数据:
|
||||
1. get_market_dashboard - 获取市场整体数据
|
||||
1. get_market_dashboard - 获取市场整体数据(含涨跌统计、市场温度、连板梯队 limitLadder)
|
||||
2. get_theme_history(date="{trade_date}") - 获取今日题材涨幅
|
||||
3. get_active_core_stocks - 获取核心股数据
|
||||
|
||||
生成报告时必须遵守以下格式规则:
|
||||
|
||||
1. 报告标题(# 一级标题)之后的第一行,必须是一个引用块"定调摘要",格式严格为:
|
||||
> 今日定调:<一句话核心结论,不超过80字,必须包含1-2个关键数字(如成交额、涨停家数、市场温度)>
|
||||
|
||||
2. 量能与情绪类数字必须给环比:若上方提供了"前一交易日盘面数据快照",成交额、涨跌家数、涨停数、市场温度等在与昨日对比后表述(如"成交额2.05万亿,较昨日缩量约700亿");没有昨日快照则如实说明"暂无昨日数据"。
|
||||
|
||||
3. 连板梯队必须完整呈现 get_market_dashboard 返回的 limitLadder:从最高连板到2连板逐级列表格,每只标注涨停原因(reason字段)与封单金额(sealWan,单位万,为空则不写);首板只挑3-5只人气最高的点评。
|
||||
|
||||
4. 适当使用表格展示数据对比。
|
||||
|
||||
然后基于数据生成报告,结构如下:
|
||||
|
||||
## 一、市场总览
|
||||
- 主要指数表现(上证、深证、创业板)
|
||||
- 涨跌家数统计
|
||||
- 主要指数表现(上证、深证、创业板、科创50)
|
||||
- 涨跌家数统计(含涨停/跌停/炸板率,须环比)
|
||||
- 市场温度评估
|
||||
|
||||
## 二、题材热点分析
|
||||
- 今日涨幅前5题材
|
||||
- 持续活跃的题材
|
||||
- 新兴热点题材
|
||||
- 明显退潮的题材(警示)
|
||||
|
||||
## 三、核心股追踪
|
||||
- 连板股分析
|
||||
- 连板梯队分析(按格式规则3完整呈现)
|
||||
- 核心股表现
|
||||
- 龙头股辨识
|
||||
|
||||
@@ -80,6 +97,9 @@ DAILY_ANALYSIS_PROMPT = """请对 {trade_date} 的A股市场进行收盘分析
|
||||
|
||||
请用 Markdown 格式输出,适当使用表格展示数据对比。"""
|
||||
|
||||
# 报告头部的"今日定调"引用行,保存时提取为 summary
|
||||
_TONE_LINE_RE = re.compile(r"^>\s*今日定调[::]\s*(.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
async def call_llm(messages: list, tools: list = None) -> dict:
|
||||
"""调用 OpenAI 兼容 API"""
|
||||
@@ -119,11 +139,16 @@ async def collect_ai_analysis(trade_date: str) -> dict:
|
||||
{"id": int, "tokens_used": int, "tools_used": list}
|
||||
"""
|
||||
prev_report_section = _get_prev_report_section(trade_date)
|
||||
# 采集当日盘面快照(供次日环比),并读取上一交易日快照注入 prompt
|
||||
await _capture_market_snapshot(trade_date)
|
||||
prev_snapshot_section = _get_prev_snapshot_section(trade_date)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": DAILY_ANALYSIS_PROMPT.format(
|
||||
trade_date=trade_date,
|
||||
prev_report_section=prev_report_section
|
||||
prev_report_section=prev_report_section,
|
||||
prev_snapshot_section=prev_snapshot_section,
|
||||
)}
|
||||
]
|
||||
|
||||
@@ -171,20 +196,127 @@ async def collect_ai_analysis(trade_date: str) -> dict:
|
||||
"content": result
|
||||
})
|
||||
|
||||
summary = final_content[:200].replace("\n", " ") if final_content else ""
|
||||
summary = _extract_summary(final_content)
|
||||
report_id = _save_report(trade_date, final_content, summary, tools_used, total_tokens)
|
||||
|
||||
return {"id": report_id, "tokens_used": total_tokens, "tools_used": tools_used, "truncated": was_truncated}
|
||||
|
||||
|
||||
def _save_report(trade_date: str, content: str, summary: str, tools_used: list, tokens_used: int) -> int:
|
||||
"""保存报告到数据库"""
|
||||
def _extract_summary(content: str) -> str:
|
||||
"""提取报告头部的"今日定调"引用行作为摘要;缺失时退回正文截断"""
|
||||
match = _TONE_LINE_RE.search(content or "")
|
||||
if match:
|
||||
text = match.group(1).strip()
|
||||
if text:
|
||||
return text[:200]
|
||||
return (content or "")[:200].replace("\n", " ")
|
||||
|
||||
|
||||
def _num(v) -> str:
|
||||
"""快照数值安全转字符串"""
|
||||
if isinstance(v, float):
|
||||
return f"{v:g}"
|
||||
return str(v if v is not None else "-")
|
||||
|
||||
|
||||
def _fmt_amount(v) -> str:
|
||||
"""成交额(元)→ 万亿/亿 可读格式"""
|
||||
try:
|
||||
v = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return "-"
|
||||
if v >= 1e12:
|
||||
return f"{v / 1e12:.2f}万亿"
|
||||
if v >= 1e8:
|
||||
return f"{v / 1e8:.0f}亿"
|
||||
return f"{v:.0f}元"
|
||||
|
||||
|
||||
def _fmt_index(idx: dict) -> str:
|
||||
if not idx:
|
||||
return "-"
|
||||
sign = "+" if idx.get("changePct", 0) >= 0 else ""
|
||||
return f"{idx.get('price', '-')}({sign}{idx.get('changePct', 0)}%)"
|
||||
|
||||
|
||||
async def _capture_market_snapshot(trade_date: str) -> bool:
|
||||
"""采集当日盘面快照入库(指数+涨跌统计),供次日分析做环比"""
|
||||
try:
|
||||
from routes.market_dashboard import _build_dashboard
|
||||
data = await _build_dashboard()
|
||||
payload = json.dumps(
|
||||
{"indices": data.get("indices", []), "marketStats": data.get("marketStats", {})},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"""INSERT INTO daily_market_stats (trade_date, payload) VALUES (?, ?)
|
||||
ON CONFLICT(trade_date) DO UPDATE SET payload = excluded.payload""",
|
||||
(trade_date, payload),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
print(f"[ai-service] 市场快照采集失败 {trade_date}:")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def _get_prev_snapshot_section(trade_date: str) -> str:
|
||||
"""读取上一交易日盘面快照,格式化为 prompt 中的环比数据段"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.execute(
|
||||
"""INSERT OR REPLACE INTO ai_reports
|
||||
(trade_date, report_type, title, content, summary, tools_used, model, tokens_used)
|
||||
VALUES (?, 'daily', ?, ?, ?, ?, ?, ?)""",
|
||||
row = conn.execute(
|
||||
"SELECT trade_date, payload FROM daily_market_stats WHERE trade_date < ? ORDER BY trade_date DESC LIMIT 1",
|
||||
(trade_date,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return ""
|
||||
try:
|
||||
snap = json.loads(row["payload"])
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
stats = snap.get("marketStats") or {}
|
||||
indices = {i.get("name"): i for i in (snap.get("indices") or []) if isinstance(i, dict)}
|
||||
idx_line = "、".join(
|
||||
f"{name} {_fmt_index(indices.get(name))}"
|
||||
for name in ("上证指数", "深证成指", "创业板指", "科创50")
|
||||
)
|
||||
return f"""以下是前一交易日({row["trade_date"]})的盘面数据快照,报告中的量能与情绪数字必须给出与它的环比对比:
|
||||
|
||||
- 两市成交额:{_fmt_amount(stats.get("totalTurnover"))}
|
||||
- 上涨/下跌/平盘:{_num(stats.get("upCount"))}/{_num(stats.get("downCount"))}/{_num(stats.get("flatCount"))},涨停 {_num(stats.get("limitUp"))} 家、跌停 {_num(stats.get("limitDown"))} 家、炸板 {_num(stats.get("limitBreak"))} 家(炸板率 {_num(stats.get("breakRate"))}%)
|
||||
- 强势/弱势股:{_num(stats.get("strongCount"))}/{_num(stats.get("weakCount"))},市场宽度 {_num(stats.get("marketBreadth"))}%
|
||||
- 市场温度:{_num(stats.get("temperature"))} 分,竞价信号:{stats.get("auctionSignal") or "-"}
|
||||
- 指数收盘:{idx_line}
|
||||
|
||||
---
|
||||
"""
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _save_report(trade_date: str, content: str, summary: str, tools_used: list, tokens_used: int) -> int:
|
||||
"""保存报告到数据库(同日重生成:覆盖内容、generation_count+1、tokens 记当次消耗)"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"""INSERT INTO ai_reports
|
||||
(trade_date, report_type, title, content, summary, tools_used, model, tokens_used, updated_at)
|
||||
VALUES (?, 'daily', ?, ?, ?, ?, ?, ?, datetime('now','localtime'))
|
||||
ON CONFLICT(trade_date, report_type) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
content = excluded.content,
|
||||
summary = excluded.summary,
|
||||
tools_used = excluded.tools_used,
|
||||
model = excluded.model,
|
||||
tokens_used = excluded.tokens_used,
|
||||
updated_at = excluded.updated_at,
|
||||
generation_count = ai_reports.generation_count + 1""",
|
||||
(
|
||||
trade_date,
|
||||
f"{trade_date} A股收盘分析",
|
||||
@@ -193,10 +325,14 @@ def _save_report(trade_date: str, content: str, summary: str, tools_used: list,
|
||||
json.dumps(tools_used),
|
||||
AI_MODEL,
|
||||
tokens_used,
|
||||
)
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
row = conn.execute(
|
||||
"SELECT id FROM ai_reports WHERE trade_date = ? AND report_type = 'daily'",
|
||||
(trade_date,),
|
||||
).fetchone()
|
||||
return row["id"]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ TOOLS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_market_dashboard",
|
||||
"description": "获取A股市场看板数据,包含主要指数行情、涨跌统计、行业强度、概念热度、事件情报、市场温度评分",
|
||||
"description": "获取A股市场看板数据,包含主要指数行情、全市场涨跌统计(涨跌家数/涨停/跌停/炸板率/成交额)、市场温度评分与竞价信号、行业强度榜、概念热度、完整连板梯队(limitLadder字段,含涨停原因与封单金额)、事件情报(热门股/龙虎榜/飙升/异动)",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,7 +14,9 @@ export interface AiReport {
|
||||
toolsUsed: string[];
|
||||
model: string;
|
||||
tokens_used: number;
|
||||
generation_count?: number;
|
||||
created_at: string;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
export async function fetchAiLatestReport(): Promise<AiReport> {
|
||||
|
||||
@@ -53,9 +53,12 @@ function AiAnalysisPage() {
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 sm:gap-4 text-xs sm:text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{report.created_at}</span>
|
||||
<span className="truncate">{report.updated_at || report.created_at}</span>
|
||||
</span>
|
||||
<span>{report.tokens_used?.toLocaleString()} tokens</span>
|
||||
{(report.generation_count ?? 1) > 1 && (
|
||||
<span>第 {report.generation_count} 次生成</span>
|
||||
)}
|
||||
{report.toolsUsed && report.toolsUsed.length > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Wrench className="h-3.5 w-3.5 shrink-0" />
|
||||
@@ -66,6 +69,18 @@ function AiAnalysisPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 今日定调摘要 */}
|
||||
{report.summary && (
|
||||
<div className="mb-3 sm:mb-4 rounded-lg border border-primary/30 bg-primary/5 px-3 sm:px-4 py-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<span className="shrink-0 mt-0.5 rounded bg-primary/10 px-1.5 py-0.5 text-[11px] font-semibold text-primary">
|
||||
今日定调
|
||||
</span>
|
||||
<p className="text-sm leading-relaxed">{report.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report Content */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-3 sm:p-4 md:p-6 ai-report-content">
|
||||
|
||||
Reference in New Issue
Block a user