优化情报分栏和日报展示
This commit is contained in:
+4
-4
@@ -5,13 +5,13 @@ SIGNALSCOUT_LLM_BASE_URL=https://api.zayuapi.com/v1
|
|||||||
SIGNALSCOUT_LLM_API_KEY=
|
SIGNALSCOUT_LLM_API_KEY=
|
||||||
SIGNALSCOUT_LLM_MODEL=grok-4.3
|
SIGNALSCOUT_LLM_MODEL=grok-4.3
|
||||||
SIGNALSCOUT_LLM_TIMEOUT_SECONDS=90
|
SIGNALSCOUT_LLM_TIMEOUT_SECONDS=90
|
||||||
SIGNALSCOUT_LLM_MAX_TOKENS=4000
|
SIGNALSCOUT_LLM_MAX_TOKENS=8000
|
||||||
SIGNALSCOUT_NEWS_RECENT_DAYS=3
|
SIGNALSCOUT_NEWS_RECENT_DAYS=3
|
||||||
SIGNALSCOUT_NEWS_MAX_ITEMS=12
|
SIGNALSCOUT_NEWS_MAX_ITEMS=60
|
||||||
SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=20
|
SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=100
|
||||||
SIGNALSCOUT_GITHUB_RECENT_DAYS=30
|
SIGNALSCOUT_GITHUB_RECENT_DAYS=30
|
||||||
SIGNALSCOUT_GITHUB_MIN_STARS=20
|
SIGNALSCOUT_GITHUB_MIN_STARS=20
|
||||||
SIGNALSCOUT_GITHUB_MAX_PROJECTS=5
|
SIGNALSCOUT_GITHUB_MAX_PROJECTS=40
|
||||||
SIGNALSCOUT_AGENT_TIMEZONE=Asia/Shanghai
|
SIGNALSCOUT_AGENT_TIMEZONE=Asia/Shanghai
|
||||||
SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30
|
SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30
|
||||||
SIGNALSCOUT_REPORT_DIR=reports
|
SIGNALSCOUT_REPORT_DIR=reports
|
||||||
|
|||||||
@@ -29,15 +29,18 @@ SIGNALSCOUT_DATABASE_URL=mysql+pymysql://user:password@host:3306/signalscout?cha
|
|||||||
SIGNALSCOUT_LLM_BASE_URL=https://api.zayuapi.com/v1
|
SIGNALSCOUT_LLM_BASE_URL=https://api.zayuapi.com/v1
|
||||||
SIGNALSCOUT_LLM_API_KEY=你的中转站 key
|
SIGNALSCOUT_LLM_API_KEY=你的中转站 key
|
||||||
SIGNALSCOUT_LLM_MODEL=grok-4.3
|
SIGNALSCOUT_LLM_MODEL=grok-4.3
|
||||||
|
SIGNALSCOUT_LLM_MAX_TOKENS=8000
|
||||||
SIGNALSCOUT_NEWS_RECENT_DAYS=3
|
SIGNALSCOUT_NEWS_RECENT_DAYS=3
|
||||||
SIGNALSCOUT_NEWS_MAX_ITEMS=12
|
SIGNALSCOUT_NEWS_MAX_ITEMS=60
|
||||||
SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=20
|
SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=100
|
||||||
SIGNALSCOUT_GITHUB_RECENT_DAYS=30
|
SIGNALSCOUT_GITHUB_RECENT_DAYS=30
|
||||||
SIGNALSCOUT_GITHUB_MIN_STARS=20
|
SIGNALSCOUT_GITHUB_MIN_STARS=20
|
||||||
SIGNALSCOUT_GITHUB_MAX_PROJECTS=5
|
SIGNALSCOUT_GITHUB_MAX_PROJECTS=40
|
||||||
SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30
|
SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30
|
||||||
```
|
```
|
||||||
|
|
||||||
|
单次运行的目标上限是新闻 60 条、GitHub 项目 40 条;实际入库数量会根据候选质量、链接去重和模型筛选结果浮动。
|
||||||
|
|
||||||
## 本机运行
|
## 本机运行
|
||||||
|
|
||||||
按项目规则,本机 Python 使用:
|
按项目规则,本机 Python 使用:
|
||||||
@@ -66,7 +69,7 @@ http://127.0.0.1:8010/docs
|
|||||||
|
|
||||||
- `GET /health`:服务健康检查
|
- `GET /health`:服务健康检查
|
||||||
- `GET /agent/runs`:查看最近运行记录
|
- `GET /agent/runs`:查看最近运行记录
|
||||||
- `GET /signals`:查看结构化情报
|
- `GET /signals`:查看结构化情报,可用 `source_type=news` 或 `source_type=github` 查看单类信号
|
||||||
- `GET /reports/latest`:查看最新日报
|
- `GET /reports/latest`:查看最新日报
|
||||||
- `GET /dashboard`:获取仪表盘数据
|
- `GET /dashboard`:获取仪表盘数据
|
||||||
- `POST /topics`:新增 Agent 跟踪主题
|
- `POST /topics`:新增 Agent 跟踪主题
|
||||||
|
|||||||
+28
-7
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
@@ -42,11 +42,19 @@ def list_runs(db: Session = Depends(get_db)) -> list[AgentRun]:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/signals", response_model=list[SignalRead])
|
@router.get("/signals", response_model=list[SignalRead])
|
||||||
def list_signals(limit: int = 50, db: Session = Depends(get_db)) -> list[Signal]:
|
def list_signals(
|
||||||
|
limit: int = 100,
|
||||||
|
source_type: Optional[Literal["news", "github"]] = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> list[Signal]:
|
||||||
# 情报列表按采集时间倒序返回,是仪表盘的主要阅读入口。
|
# 情报列表按采集时间倒序返回,是仪表盘的主要阅读入口。
|
||||||
safe_limit = max(1, min(limit, 200))
|
safe_limit = max(1, min(limit, 200))
|
||||||
return list(
|
return list(
|
||||||
db.scalars(_visible_signals_query().order_by(Signal.id.desc()).limit(safe_limit)).all()
|
db.scalars(
|
||||||
|
_visible_signals_query(source_type=source_type)
|
||||||
|
.order_by(Signal.id.desc())
|
||||||
|
.limit(safe_limit)
|
||||||
|
).all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,12 +68,22 @@ def latest_report(db: Session = Depends(get_db)) -> Optional[Report]:
|
|||||||
def dashboard(db: Session = Depends(get_db)) -> DashboardRead:
|
def dashboard(db: Session = Depends(get_db)) -> DashboardRead:
|
||||||
# 单个仪表盘接口返回运行、信号、日报和主题,前端不需要理解内部调度细节。
|
# 单个仪表盘接口返回运行、信号、日报和主题,前端不需要理解内部调度细节。
|
||||||
runs = list(db.scalars(select(AgentRun).order_by(AgentRun.id.desc()).limit(10)).all())
|
runs = list(db.scalars(select(AgentRun).order_by(AgentRun.id.desc()).limit(10)).all())
|
||||||
signals = list(db.scalars(_visible_signals_query().order_by(Signal.id.desc()).limit(80)).all())
|
news_signals = list(
|
||||||
|
db.scalars(
|
||||||
|
_visible_signals_query(source_type="news").order_by(Signal.id.desc()).limit(80)
|
||||||
|
).all()
|
||||||
|
)
|
||||||
|
github_signals = list(
|
||||||
|
db.scalars(
|
||||||
|
_visible_signals_query(source_type="github").order_by(Signal.id.desc()).limit(80)
|
||||||
|
).all()
|
||||||
|
)
|
||||||
report = db.scalar(select(Report).order_by(Report.id.desc()).limit(1))
|
report = db.scalar(select(Report).order_by(Report.id.desc()).limit(1))
|
||||||
topics = list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
|
topics = list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
|
||||||
return DashboardRead(
|
return DashboardRead(
|
||||||
runs=runs,
|
runs=runs,
|
||||||
signals=signals,
|
news_signals=news_signals,
|
||||||
|
github_signals=github_signals,
|
||||||
latest_report=report,
|
latest_report=report,
|
||||||
topics=topics,
|
topics=topics,
|
||||||
)
|
)
|
||||||
@@ -96,6 +114,9 @@ def list_topics(db: Session = Depends(get_db)) -> list[Topic]:
|
|||||||
return list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
|
return list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
|
||||||
|
|
||||||
|
|
||||||
def _visible_signals_query():
|
def _visible_signals_query(source_type: Optional[Literal["news", "github"]] = None):
|
||||||
# 运行中的信号和完成后的信号走同一张表,查询函数保留统一排序入口。
|
# 运行中的信号和完成后的信号走同一张表,查询函数保留统一排序入口。
|
||||||
return select(Signal).join(AgentRun, Signal.run_id == AgentRun.id)
|
query = select(Signal).join(AgentRun, Signal.run_id == AgentRun.id)
|
||||||
|
if source_type is not None:
|
||||||
|
query = query.where(Signal.source_type == source_type)
|
||||||
|
return query
|
||||||
|
|||||||
+4
-4
@@ -16,13 +16,13 @@ class Settings(BaseSettings):
|
|||||||
llm_api_key: str = ""
|
llm_api_key: str = ""
|
||||||
llm_model: str = "grok-4.3"
|
llm_model: str = "grok-4.3"
|
||||||
llm_timeout_seconds: int = 90
|
llm_timeout_seconds: int = 90
|
||||||
llm_max_tokens: int = 4000
|
llm_max_tokens: int = 8000
|
||||||
news_recent_days: int = 3
|
news_recent_days: int = 3
|
||||||
news_max_items: int = 12
|
news_max_items: int = 60
|
||||||
news_search_max_records: int = 20
|
news_search_max_records: int = 100
|
||||||
github_recent_days: int = 30
|
github_recent_days: int = 30
|
||||||
github_min_stars: int = 20
|
github_min_stars: int = 20
|
||||||
github_max_projects: int = 5
|
github_max_projects: int = 40
|
||||||
agent_timezone: str = "Asia/Shanghai"
|
agent_timezone: str = "Asia/Shanghai"
|
||||||
agent_interval_minutes: int = 30
|
agent_interval_minutes: int = 30
|
||||||
report_dir: Path = Path("reports")
|
report_dir: Path = Path("reports")
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Signal(Base):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
run_id: Mapped[int] = mapped_column(ForeignKey("agent_runs.id"), nullable=False, index=True)
|
run_id: Mapped[int] = mapped_column(ForeignKey("agent_runs.id"), nullable=False, index=True)
|
||||||
|
source_type: Mapped[str] = mapped_column(String(20), nullable=False, default="news", index=True)
|
||||||
topic: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
|
topic: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
|
||||||
title: Mapped[str] = mapped_column(String(300), nullable=False)
|
title: Mapped[str] = mapped_column(String(300), nullable=False)
|
||||||
summary: Mapped[str] = mapped_column(Text, nullable=False)
|
summary: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class AgentRepository:
|
|||||||
if existing and existing.run_id != run.id:
|
if existing and existing.run_id != run.id:
|
||||||
return None
|
return None
|
||||||
if existing:
|
if existing:
|
||||||
|
existing.source_type = item.source_type
|
||||||
existing.topic = item.topic
|
existing.topic = item.topic
|
||||||
existing.title = item.title
|
existing.title = item.title
|
||||||
existing.summary = item.summary
|
existing.summary = item.summary
|
||||||
@@ -68,6 +69,7 @@ class AgentRepository:
|
|||||||
return existing
|
return existing
|
||||||
signal = Signal(
|
signal = Signal(
|
||||||
run_id=run.id,
|
run_id=run.id,
|
||||||
|
source_type=item.source_type,
|
||||||
topic=item.topic,
|
topic=item.topic,
|
||||||
title=item.title,
|
title=item.title,
|
||||||
summary=item.summary,
|
summary=item.summary,
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ class GitHubHotProjectClient:
|
|||||||
|
|
||||||
def _search(self, query: str) -> dict[str, Any]:
|
def _search(self, query: str) -> dict[str, Any]:
|
||||||
# GitHub REST搜索直接返回排序后的仓库元数据,便于后续模型研判。
|
# GitHub REST搜索直接返回排序后的仓库元数据,便于后续模型研判。
|
||||||
url = f"{self.endpoint}?{urlencode({'q': query, 'sort': 'stars', 'order': 'desc', 'per_page': '10'})}"
|
per_page = min(max(self.settings.github_max_projects * 2, 20), 100)
|
||||||
|
params = {"q": query, "sort": "stars", "order": "desc", "per_page": str(per_page)}
|
||||||
|
url = f"{self.endpoint}?{urlencode(params)}"
|
||||||
request = Request(
|
request = Request(
|
||||||
url,
|
url,
|
||||||
headers={
|
headers={
|
||||||
@@ -92,6 +94,7 @@ class GitHubHotProjectClient:
|
|||||||
f"主要语言为 {language}。"
|
f"主要语言为 {language}。"
|
||||||
)
|
)
|
||||||
return SignalItem(
|
return SignalItem(
|
||||||
|
source_type="github",
|
||||||
topic="GitHub 热门项目",
|
topic="GitHub 热门项目",
|
||||||
title=full_name,
|
title=full_name,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class NewsSearchClient:
|
|||||||
comments = int(story.get("num_comments") or 0)
|
comments = int(story.get("num_comments") or 0)
|
||||||
published_at = self._parse_hn_date(story.get("created_at"))
|
published_at = self._parse_hn_date(story.get("created_at"))
|
||||||
return SignalItem(
|
return SignalItem(
|
||||||
|
source_type="news",
|
||||||
topic="AI 新闻候选",
|
topic="AI 新闻候选",
|
||||||
title=title[:300],
|
title=title[:300],
|
||||||
summary=f"Hacker News 最新技术讨论,作者 {author},约 {points} points、{comments} 条评论。",
|
summary=f"Hacker News 最新技术讨论,作者 {author},约 {points} points、{comments} 条评论。",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class GrokIntelligenceClient:
|
|||||||
]
|
]
|
||||||
github_payload = [
|
github_payload = [
|
||||||
{
|
{
|
||||||
|
"source_type": item.source_type,
|
||||||
"title": item.title,
|
"title": item.title,
|
||||||
"summary": item.summary,
|
"summary": item.summary,
|
||||||
"source_url": item.source_url,
|
"source_url": item.source_url,
|
||||||
@@ -142,6 +143,7 @@ class GrokIntelligenceClient:
|
|||||||
]
|
]
|
||||||
news_payload = [
|
news_payload = [
|
||||||
{
|
{
|
||||||
|
"source_type": item.source_type,
|
||||||
"title": item.title,
|
"title": item.title,
|
||||||
"summary": item.summary,
|
"summary": item.summary,
|
||||||
"source_url": item.source_url,
|
"source_url": item.source_url,
|
||||||
@@ -170,6 +172,7 @@ class GrokIntelligenceClient:
|
|||||||
"title": "字符串,日报标题",
|
"title": "字符串,日报标题",
|
||||||
"signals": [
|
"signals": [
|
||||||
{
|
{
|
||||||
|
"source_type": "news 或 github;新闻候选必须为 news,GitHub 候选必须为 github",
|
||||||
"topic": "AI 新闻 / GitHub 热门项目 / 模型发布 / Agent 工具 / 融资动态等",
|
"topic": "AI 新闻 / GitHub 热门项目 / 模型发布 / Agent 工具 / 融资动态等",
|
||||||
"title": "字符串",
|
"title": "字符串",
|
||||||
"summary": "中文摘要,说明为什么重要",
|
"summary": "中文摘要,说明为什么重要",
|
||||||
@@ -189,6 +192,8 @@ class GrokIntelligenceClient:
|
|||||||
"新闻必须来自日期窗口内或接近日内发生的事件。",
|
"新闻必须来自日期窗口内或接近日内发生的事件。",
|
||||||
"AI新闻只能从 news_candidates 中选择,不要新增候选外新闻。",
|
"AI新闻只能从 news_candidates 中选择,不要新增候选外新闻。",
|
||||||
"GitHub 热门项目只能从 github_candidates 中选择,不要新增候选外仓库。",
|
"GitHub 热门项目只能从 github_candidates 中选择,不要新增候选外仓库。",
|
||||||
|
"news 和 github 是两个独立栏目,分别尽量输出到对应上限,不要把 GitHub 项目写成新闻。",
|
||||||
|
"只有候选重复、链接不可验证或明显不相关时,才少于对应上限。",
|
||||||
"每条 signal 必须有真实 source_url。",
|
"每条 signal 必须有真实 source_url。",
|
||||||
"report_markdown 只能基于 signals 写,不要加入 signals 外的新事实。",
|
"report_markdown 只能基于 signals 写,不要加入 signals 外的新事实。",
|
||||||
"输出必须是可被 json.loads 解析的 JSON object。",
|
"输出必须是可被 json.loads 解析的 JSON object。",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ class SearchTopic(BaseModel):
|
|||||||
|
|
||||||
class SignalItem(BaseModel):
|
class SignalItem(BaseModel):
|
||||||
# 模型产出的结构化情报会通过这个契约进入数据库和日报。
|
# 模型产出的结构化情报会通过这个契约进入数据库和日报。
|
||||||
|
source_type: Literal["news", "github"]
|
||||||
topic: str
|
topic: str
|
||||||
title: str
|
title: str
|
||||||
summary: str
|
summary: str
|
||||||
@@ -51,6 +52,7 @@ class RunRead(BaseModel):
|
|||||||
class SignalRead(BaseModel):
|
class SignalRead(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
run_id: int
|
run_id: int
|
||||||
|
source_type: Literal["news", "github"]
|
||||||
topic: str
|
topic: str
|
||||||
title: str
|
title: str
|
||||||
summary: str
|
summary: str
|
||||||
@@ -77,7 +79,8 @@ class ReportRead(BaseModel):
|
|||||||
|
|
||||||
class DashboardRead(BaseModel):
|
class DashboardRead(BaseModel):
|
||||||
runs: list[RunRead]
|
runs: list[RunRead]
|
||||||
signals: list[SignalRead]
|
news_signals: list[SignalRead]
|
||||||
|
github_signals: list[SignalRead]
|
||||||
latest_report: Optional[ReportRead]
|
latest_report: Optional[ReportRead]
|
||||||
topics: list["TopicRead"]
|
topics: list["TopicRead"]
|
||||||
|
|
||||||
@@ -95,4 +98,3 @@ class TopicRead(BaseModel):
|
|||||||
enabled: bool
|
enabled: bool
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|||||||
+188
-69
@@ -77,9 +77,7 @@ HOME_PAGE_HTML = """
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand span,
|
.brand span {
|
||||||
.nav-note,
|
|
||||||
.auto-card span {
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
@@ -110,20 +108,6 @@ HOME_PAGE_HTML = """
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-card {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: 24px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: oklch(99% 0.006 180 / 0.78);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auto-card strong {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -155,7 +139,7 @@ HOME_PAGE_HTML = """
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pulse {
|
.pulse {
|
||||||
min-width: 184px;
|
min-width: 220px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
@@ -172,6 +156,11 @@ HOME_PAGE_HTML = """
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pulse span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
||||||
@@ -189,7 +178,7 @@ HOME_PAGE_HTML = """
|
|||||||
}
|
}
|
||||||
|
|
||||||
.view[data-view="signals"].active {
|
.view[data-view="signals"].active {
|
||||||
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@@ -282,7 +271,70 @@ HOME_PAGE_HTML = """
|
|||||||
padding: 18px;
|
padding: 18px;
|
||||||
min-height: 520px;
|
min-height: 520px;
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
white-space: pre-wrap;
|
color: oklch(25% 0.028 210);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
max-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc h2,
|
||||||
|
.report-doc h3,
|
||||||
|
.report-doc h4,
|
||||||
|
.report-doc p,
|
||||||
|
.report-doc ul,
|
||||||
|
.report-doc ol {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.25;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc h3 {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc h4 {
|
||||||
|
color: oklch(34% 0.035 190);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc p {
|
||||||
|
color: oklch(30% 0.025 210);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc ul,
|
||||||
|
.report-doc ol {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc li {
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc a {
|
||||||
|
color: oklch(42% 0.12 174);
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-doc code {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
background: oklch(96% 0.012 180);
|
||||||
|
font-size: 0.92em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runs { display: grid; }
|
.runs { display: grid; }
|
||||||
@@ -342,57 +394,52 @@ HOME_PAGE_HTML = """
|
|||||||
<span>AI 情报采集与研判工作台</span>
|
<span>AI 情报采集与研判工作台</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav" aria-label="主导航">
|
<nav class="nav" aria-label="主导航">
|
||||||
<button class="active" type="button" data-view-target="signals">情报</button>
|
<button class="active" type="button" data-view-target="signals">信号</button>
|
||||||
<button type="button" data-view-target="report">日报</button>
|
<button type="button" data-view-target="report">日报</button>
|
||||||
<button type="button" data-view-target="runs">记录</button>
|
<button type="button" data-view-target="runs">记录</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="auto-card">
|
|
||||||
<strong>自动整理</strong>
|
|
||||||
<span>约每 30 分钟自动整理一次。</span>
|
|
||||||
</div>
|
|
||||||
<p class="nav-note">聚合 AI 新闻、模型发布、Agent 工具和 GitHub 热门项目。</p>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<section class="topbar">
|
<section class="topbar">
|
||||||
<div>
|
<div>
|
||||||
<h1>今日 AI 情报</h1>
|
<h1>今日 AI 信号</h1>
|
||||||
<p class="subtitle">Agent 自动筛选、去重和生成日报;页面会持续更新最新结果。</p>
|
<p class="subtitle">新闻动态与 GitHub 项目分开阅读,日报保留适合快速复盘的重点判断。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pulse">
|
<div class="pulse">
|
||||||
<strong id="lastRunText">等待运行</strong>
|
<strong id="todayCountText">读取中</strong>
|
||||||
<span id="lastRunDetail">正在读取运行记录。</span>
|
<span id="todayCountDetail">正在整理新闻与项目。</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="view active" data-view="signals">
|
<section class="view active" data-view="signals">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>情报信号</h2>
|
<h2>AI 新闻</h2>
|
||||||
<span class="count" id="signalCount">0 条</span>
|
<span class="count" id="newsCount">0 条</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="signals" id="signalList">
|
<div class="signals" id="newsList">
|
||||||
<div class="empty">正在读取情报。</div>
|
<div class="empty">正在读取新闻。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>最新日报</h2>
|
<h2>GitHub 热门项目</h2>
|
||||||
<span class="count" id="reportTime">待生成</span>
|
<span class="count" id="githubCount">0 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="signals" id="githubList">
|
||||||
|
<div class="empty">正在读取项目。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="report" id="reportPreview">正在读取日报。</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>自动记录</h2>
|
<h2>最新日报</h2>
|
||||||
<span class="count" id="runCount">0 次</span>
|
<span class="count" id="reportTime">待生成</span>
|
||||||
</div>
|
|
||||||
<div class="runs" id="runPreview">
|
|
||||||
<div class="empty">正在读取运行记录。</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="report" id="reportPreview">正在读取日报。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -410,7 +457,7 @@ HOME_PAGE_HTML = """
|
|||||||
<section class="view" data-view="runs">
|
<section class="view" data-view="runs">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>自动记录</h2>
|
<h2>运行记录</h2>
|
||||||
<span class="count" id="runCountFull">0 次</span>
|
<span class="count" id="runCountFull">0 次</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="runs" id="runList">
|
<div class="runs" id="runList">
|
||||||
@@ -424,18 +471,18 @@ HOME_PAGE_HTML = """
|
|||||||
<script>
|
<script>
|
||||||
const navButtons = Array.from(document.querySelectorAll("[data-view-target]"));
|
const navButtons = Array.from(document.querySelectorAll("[data-view-target]"));
|
||||||
const views = Array.from(document.querySelectorAll("[data-view]"));
|
const views = Array.from(document.querySelectorAll("[data-view]"));
|
||||||
const signalList = document.querySelector("#signalList");
|
const newsList = document.querySelector("#newsList");
|
||||||
const signalCount = document.querySelector("#signalCount");
|
const newsCount = document.querySelector("#newsCount");
|
||||||
|
const githubList = document.querySelector("#githubList");
|
||||||
|
const githubCount = document.querySelector("#githubCount");
|
||||||
const reportPreview = document.querySelector("#reportPreview");
|
const reportPreview = document.querySelector("#reportPreview");
|
||||||
const reportBody = document.querySelector("#reportBody");
|
const reportBody = document.querySelector("#reportBody");
|
||||||
const reportTime = document.querySelector("#reportTime");
|
const reportTime = document.querySelector("#reportTime");
|
||||||
const reportTimeFull = document.querySelector("#reportTimeFull");
|
const reportTimeFull = document.querySelector("#reportTimeFull");
|
||||||
const runPreview = document.querySelector("#runPreview");
|
|
||||||
const runList = document.querySelector("#runList");
|
const runList = document.querySelector("#runList");
|
||||||
const runCount = document.querySelector("#runCount");
|
|
||||||
const runCountFull = document.querySelector("#runCountFull");
|
const runCountFull = document.querySelector("#runCountFull");
|
||||||
const lastRunText = document.querySelector("#lastRunText");
|
const todayCountText = document.querySelector("#todayCountText");
|
||||||
const lastRunDetail = document.querySelector("#lastRunDetail");
|
const todayCountDetail = document.querySelector("#todayCountDetail");
|
||||||
|
|
||||||
const stateText = {
|
const stateText = {
|
||||||
completed: "完成",
|
completed: "完成",
|
||||||
@@ -452,6 +499,67 @@ HOME_PAGE_HTML = """
|
|||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderInlineMarkdown(value) {
|
||||||
|
return escapeHtml(value)
|
||||||
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||||
|
.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>")
|
||||||
|
.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdownReport(markdown) {
|
||||||
|
const lines = String(markdown || "").split(/\\r?\\n/);
|
||||||
|
const blocks = [];
|
||||||
|
let paragraph = [];
|
||||||
|
let listItems = [];
|
||||||
|
let listTag = "ul";
|
||||||
|
|
||||||
|
const flushParagraph = () => {
|
||||||
|
if (!paragraph.length) return;
|
||||||
|
blocks.push(`<p>${renderInlineMarkdown(paragraph.join(" "))}</p>`);
|
||||||
|
paragraph = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushList = () => {
|
||||||
|
if (!listItems.length) return;
|
||||||
|
blocks.push(`<${listTag}>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</${listTag}>`);
|
||||||
|
listItems = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.forEach((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const heading = /^(#{1,4})\\s+(.+)$/.exec(trimmed);
|
||||||
|
const bullet = /^[-*]\\s+(.+)$/.exec(trimmed);
|
||||||
|
const ordered = /^\\d+[.)]\\s+(.+)$/.exec(trimmed);
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
flushParagraph();
|
||||||
|
flushList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (heading) {
|
||||||
|
flushParagraph();
|
||||||
|
flushList();
|
||||||
|
const level = Math.min(heading[1].length + 1, 4);
|
||||||
|
blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bullet || ordered) {
|
||||||
|
flushParagraph();
|
||||||
|
const nextTag = ordered ? "ol" : "ul";
|
||||||
|
if (listItems.length && listTag !== nextTag) flushList();
|
||||||
|
listTag = nextTag;
|
||||||
|
listItems.push((bullet || ordered)[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flushList();
|
||||||
|
paragraph.push(trimmed);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushParagraph();
|
||||||
|
flushList();
|
||||||
|
return `<div class="report-doc">${blocks.join("")}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return "未标注时间";
|
if (!value) return "未标注时间";
|
||||||
const text = String(value);
|
const text = String(value);
|
||||||
@@ -473,13 +581,13 @@ HOME_PAGE_HTML = """
|
|||||||
if (updateHash) history.replaceState(null, "", `#${name}`);
|
if (updateHash) history.replaceState(null, "", `#${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSignals(signals) {
|
function renderSignalGroup(signals, target, counter, emptyText) {
|
||||||
signalCount.textContent = `${signals.length} 条`;
|
counter.textContent = `${signals.length} 条`;
|
||||||
if (!signals.length) {
|
if (!signals.length) {
|
||||||
signalList.innerHTML = '<div class="empty">Agent 正在按固定节奏自动整理,完成后这里会出现情报信号。</div>';
|
target.innerHTML = `<div class="empty">${escapeHtml(emptyText)}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signalList.innerHTML = signals.map((item) => `
|
target.innerHTML = signals.map((item) => `
|
||||||
<article class="signal-row">
|
<article class="signal-row">
|
||||||
<div class="signal-meta">
|
<div class="signal-meta">
|
||||||
<span class="pill">${escapeHtml(item.topic)}</span>
|
<span class="pill">${escapeHtml(item.topic)}</span>
|
||||||
@@ -500,16 +608,16 @@ HOME_PAGE_HTML = """
|
|||||||
if (!report) {
|
if (!report) {
|
||||||
reportTime.textContent = "待生成";
|
reportTime.textContent = "待生成";
|
||||||
reportTimeFull.textContent = "待生成";
|
reportTimeFull.textContent = "待生成";
|
||||||
reportPreview.textContent = "Agent 正在自动整理,日报生成后会显示在这里。";
|
reportPreview.textContent = "日报生成后会显示在这里。";
|
||||||
reportBody.textContent = "Agent 正在自动整理,日报生成后会显示在这里。";
|
reportBody.textContent = "日报生成后会显示在这里。";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timeText = formatDate(report.created_at);
|
const timeText = formatDate(report.created_at);
|
||||||
const content = report.content_md || "日报内容为空。";
|
const content = report.content_md || "日报内容为空。";
|
||||||
reportTime.textContent = timeText;
|
reportTime.textContent = timeText;
|
||||||
reportTimeFull.textContent = timeText;
|
reportTimeFull.textContent = timeText;
|
||||||
reportPreview.textContent = content;
|
reportPreview.innerHTML = renderMarkdownReport(content);
|
||||||
reportBody.textContent = content;
|
reportBody.innerHTML = renderMarkdownReport(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runRows(runs) {
|
function runRows(runs) {
|
||||||
@@ -522,27 +630,38 @@ HOME_PAGE_HTML = """
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderRuns(runs) {
|
function renderRuns(runs) {
|
||||||
runCount.textContent = `${runs.length} 次`;
|
|
||||||
runCountFull.textContent = `${runs.length} 次`;
|
runCountFull.textContent = `${runs.length} 次`;
|
||||||
if (!runs.length) {
|
if (!runs.length) {
|
||||||
runPreview.innerHTML = '<div class="empty">Agent 会按固定节奏自动整理。</div>';
|
runList.innerHTML = '<div class="empty">暂无运行记录。</div>';
|
||||||
runList.innerHTML = '<div class="empty">Agent 会按固定节奏自动整理。</div>';
|
|
||||||
lastRunText.textContent = "等待记录";
|
|
||||||
lastRunDetail.textContent = "约每 30 分钟自动整理一次。";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runPreview.innerHTML = runRows(runs.slice(0, 4));
|
|
||||||
runList.innerHTML = runRows(runs);
|
runList.innerHTML = runRows(runs);
|
||||||
const latest = runs[0];
|
}
|
||||||
lastRunText.textContent = stateText[latest.status] || latest.status;
|
|
||||||
lastRunDetail.textContent = `最近一次:${formatDate(latest.started_at)}`;
|
function renderSignalSummary(newsSignals, githubSignals) {
|
||||||
|
const newsTotal = newsSignals.length;
|
||||||
|
const githubTotal = githubSignals.length;
|
||||||
|
todayCountText.textContent = `${newsTotal + githubTotal} 条信号`;
|
||||||
|
todayCountDetail.textContent = `新闻 ${newsTotal} · GitHub ${githubTotal}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
const response = await fetch("/dashboard", { headers: { Accept: "application/json" } });
|
const response = await fetch("/dashboard", { headers: { Accept: "application/json" } });
|
||||||
if (!response.ok) throw new Error("读取情报失败");
|
if (!response.ok) throw new Error("读取情报失败");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
renderSignals(data.signals || []);
|
renderSignalGroup(
|
||||||
|
data.news_signals || [],
|
||||||
|
newsList,
|
||||||
|
newsCount,
|
||||||
|
"Agent 正在整理 AI 新闻,完成后这里会出现新闻信号。"
|
||||||
|
);
|
||||||
|
renderSignalGroup(
|
||||||
|
data.github_signals || [],
|
||||||
|
githubList,
|
||||||
|
githubCount,
|
||||||
|
"Agent 正在整理 GitHub 热门项目,完成后这里会出现项目线索。"
|
||||||
|
);
|
||||||
|
renderSignalSummary(data.news_signals || [], data.github_signals || []);
|
||||||
renderReport(data.latest_report);
|
renderReport(data.latest_report);
|
||||||
renderRuns(data.runs || []);
|
renderRuns(data.runs || []);
|
||||||
}
|
}
|
||||||
@@ -557,10 +676,10 @@ HOME_PAGE_HTML = """
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadDashboard().catch(() => {
|
loadDashboard().catch(() => {
|
||||||
signalList.innerHTML = '<div class="empty">情报读取失败,请稍后再试。</div>';
|
newsList.innerHTML = '<div class="empty">新闻读取失败,请稍后再试。</div>';
|
||||||
|
githubList.innerHTML = '<div class="empty">项目读取失败,请稍后再试。</div>';
|
||||||
reportPreview.textContent = "日报读取失败。";
|
reportPreview.textContent = "日报读取失败。";
|
||||||
reportBody.textContent = "日报读取失败。";
|
reportBody.textContent = "日报读取失败。";
|
||||||
runPreview.innerHTML = '<div class="empty">运行记录读取失败。</div>';
|
|
||||||
runList.innerHTML = '<div class="empty">运行记录读取失败。</div>';
|
runList.innerHTML = '<div class="empty">运行记录读取失败。</div>';
|
||||||
});
|
});
|
||||||
window.setInterval(loadDashboard, 60000);
|
window.setInterval(loadDashboard, 60000);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""add signal source type
|
||||||
|
|
||||||
|
Revision ID: 202607081700
|
||||||
|
Revises: 202607081530
|
||||||
|
Create Date: 2026-07-08 17:00:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision: str = "202607081700"
|
||||||
|
down_revision: Union[str, None] = "202607081530"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# source_type让新闻和GitHub项目拥有稳定分类,仪表盘和API都按这个字段分组。
|
||||||
|
op.add_column(
|
||||||
|
"signals",
|
||||||
|
sa.Column("source_type", sa.String(length=20), nullable=False, server_default="news"),
|
||||||
|
)
|
||||||
|
op.execute("UPDATE signals SET source_type = 'github' WHERE source_name = 'GitHub'")
|
||||||
|
op.create_index("ix_signals_source_type", "signals", ["source_type"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# 回滚时移除分类索引和字段,恢复迁移前的signals结构。
|
||||||
|
op.drop_index("ix_signals_source_type", table_name="signals")
|
||||||
|
op.drop_column("signals", "source_type")
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
|
from app.db.models import AgentRun, Base, Signal
|
||||||
|
from app.db.session import get_db
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +34,9 @@ def test_home_page_returns_dashboard_shell(monkeypatch) -> None:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "SignalScout" in response.text
|
assert "SignalScout" in response.text
|
||||||
assert "今日 AI 情报" in response.text
|
assert "今日 AI 信号" in response.text
|
||||||
|
assert "renderMarkdownReport" in response.text
|
||||||
|
assert "自动记录" not in response.text
|
||||||
assert "采集今日情报" not in response.text
|
assert "采集今日情报" not in response.text
|
||||||
|
|
||||||
|
|
||||||
@@ -42,3 +51,75 @@ def test_manual_agent_run_endpoint_is_not_exposed(monkeypatch) -> None:
|
|||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
assert stream_response.status_code == 404
|
assert stream_response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_returns_news_and_github_signals_separately(monkeypatch) -> None:
|
||||||
|
# 仪表盘接口直接返回分组后的业务数据,前端不再用标题或来源名自行猜测分类。
|
||||||
|
monkeypatch.setenv("SIGNALSCOUT_ENV", "test")
|
||||||
|
get_settings.cache_clear()
|
||||||
|
engine = create_engine(
|
||||||
|
"sqlite:///:memory:",
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
poolclass=StaticPool,
|
||||||
|
future=True,
|
||||||
|
)
|
||||||
|
TestingSessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
with TestingSessionLocal() as session:
|
||||||
|
run = AgentRun(status="completed", started_at=datetime(2026, 7, 8, 8, 0), request_payload={})
|
||||||
|
session.add(run)
|
||||||
|
session.flush()
|
||||||
|
session.add_all(
|
||||||
|
[
|
||||||
|
Signal(
|
||||||
|
run_id=run.id,
|
||||||
|
source_type="news",
|
||||||
|
topic="AI 新闻",
|
||||||
|
title="New model release",
|
||||||
|
summary="模型发布动态。",
|
||||||
|
source_url="https://example.com/news",
|
||||||
|
source_url_hash="news-hash",
|
||||||
|
source_name="Example News",
|
||||||
|
published_at=datetime(2026, 7, 8, 9, 0),
|
||||||
|
importance=4,
|
||||||
|
entities=["Example"],
|
||||||
|
),
|
||||||
|
Signal(
|
||||||
|
run_id=run.id,
|
||||||
|
source_type="github",
|
||||||
|
topic="GitHub 热门项目",
|
||||||
|
title="example/agent-kit",
|
||||||
|
summary="开源项目动态。",
|
||||||
|
source_url="https://github.com/example/agent-kit",
|
||||||
|
source_url_hash="github-hash",
|
||||||
|
source_name="GitHub",
|
||||||
|
published_at=datetime(2026, 7, 8, 10, 0),
|
||||||
|
importance=5,
|
||||||
|
entities=["example/agent-kit"],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
# 测试依赖把接口请求限制在内存库里,避免触碰开发数据库。
|
||||||
|
db = TestingSessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
with TestClient(app) as client:
|
||||||
|
dashboard_response = client.get("/dashboard")
|
||||||
|
github_response = client.get("/signals", params={"source_type": "github"})
|
||||||
|
get_settings.cache_clear()
|
||||||
|
|
||||||
|
dashboard = dashboard_response.json()
|
||||||
|
assert dashboard_response.status_code == 200
|
||||||
|
assert [item["source_type"] for item in dashboard["news_signals"]] == ["news"]
|
||||||
|
assert [item["source_type"] for item in dashboard["github_signals"]] == ["github"]
|
||||||
|
assert github_response.status_code == 200
|
||||||
|
assert [item["title"] for item in github_response.json()] == ["example/agent-kit"]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ def test_github_client_maps_repository_to_signal() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert signal.topic == "GitHub 热门项目"
|
assert signal.topic == "GitHub 热门项目"
|
||||||
|
assert signal.source_type == "github"
|
||||||
assert signal.source_name == "GitHub"
|
assert signal.source_name == "GitHub"
|
||||||
assert signal.source_url == "https://github.com/example/agent-kit"
|
assert signal.source_url == "https://github.com/example/agent-kit"
|
||||||
assert "2400 stars" in signal.summary
|
assert "2400 stars" in signal.summary
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ class StubGrokClient(GrokIntelligenceClient):
|
|||||||
def _chat_json(self, messages):
|
def _chat_json(self, messages):
|
||||||
# 测试只验证提示词输入和结果契约,不访问真实模型服务。
|
# 测试只验证提示词输入和结果契约,不访问真实模型服务。
|
||||||
assert "github_candidates" in messages[1]["content"]
|
assert "github_candidates" in messages[1]["content"]
|
||||||
|
assert '"source_type": "news"' in messages[1]["content"]
|
||||||
|
assert '"source_type": "github"' in messages[1]["content"]
|
||||||
return {
|
return {
|
||||||
"title": "AI 情报日报 2026-07-08",
|
"title": "AI 情报日报 2026-07-08",
|
||||||
"signals": [
|
"signals": [
|
||||||
{
|
{
|
||||||
|
"source_type": "github",
|
||||||
"topic": "GitHub 热门项目",
|
"topic": "GitHub 热门项目",
|
||||||
"title": "example/agent-kit",
|
"title": "example/agent-kit",
|
||||||
"summary": "该项目提供生产级 Agent 工作流能力。",
|
"summary": "该项目提供生产级 Agent 工作流能力。",
|
||||||
@@ -43,6 +46,7 @@ def test_grok_client_returns_agent_result() -> None:
|
|||||||
],
|
],
|
||||||
news_candidates=[
|
news_candidates=[
|
||||||
SignalItem(
|
SignalItem(
|
||||||
|
source_type="news",
|
||||||
topic="AI 新闻候选",
|
topic="AI 新闻候选",
|
||||||
title="New model release",
|
title="New model release",
|
||||||
summary="News candidate from example.com.",
|
summary="News candidate from example.com.",
|
||||||
@@ -55,6 +59,7 @@ def test_grok_client_returns_agent_result() -> None:
|
|||||||
],
|
],
|
||||||
github_projects=[
|
github_projects=[
|
||||||
SignalItem(
|
SignalItem(
|
||||||
|
source_type="github",
|
||||||
topic="GitHub 热门项目",
|
topic="GitHub 热门项目",
|
||||||
title="example/agent-kit",
|
title="example/agent-kit",
|
||||||
summary="Agent framework.",
|
summary="Agent framework.",
|
||||||
@@ -69,5 +74,6 @@ def test_grok_client_returns_agent_result() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.title == "AI 情报日报 2026-07-08"
|
assert result.title == "AI 情报日报 2026-07-08"
|
||||||
|
assert result.signals[0].source_type == "github"
|
||||||
assert result.signals[0].source_name == "GitHub"
|
assert result.signals[0].source_name == "GitHub"
|
||||||
assert result.report_markdown.startswith("# AI 情报日报")
|
assert result.report_markdown.startswith("# AI 情报日报")
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ def test_news_search_client_maps_article_to_candidate() -> None:
|
|||||||
|
|
||||||
assert signal is not None
|
assert signal is not None
|
||||||
assert signal.topic == "AI 新闻候选"
|
assert signal.topic == "AI 新闻候选"
|
||||||
|
assert signal.source_type == "news"
|
||||||
assert signal.source_name == "Hacker News"
|
assert signal.source_name == "Hacker News"
|
||||||
assert signal.published_at is not None
|
assert signal.published_at is not None
|
||||||
assert signal.published_at.year == 2026
|
assert signal.published_at.year == 2026
|
||||||
|
|||||||
Reference in New Issue
Block a user