From 8e15211f0724063ef287b492388d56abd99efa3f Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 8 Jul 2026 23:39:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=83=85=E6=8A=A5=E5=88=86?= =?UTF-8?q?=E6=A0=8F=E5=92=8C=E6=97=A5=E6=8A=A5=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +- README.md | 11 +- app/api/routes.py | 35 ++- app/core/config.py | 8 +- app/db/models.py | 1 + app/db/repository.py | 2 + app/integrations/github_client.py | 5 +- app/integrations/news_search_client.py | 1 + app/llm/grok_client.py | 5 + app/schemas/agent.py | 8 +- app/ui/home.py | 257 +++++++++++++----- .../202607081700_add_signal_source_type.py | 32 +++ tests/test_api_health.py | 83 +++++- tests/test_github_client.py | 1 + tests/test_grok_client.py | 6 + tests/test_news_search_client.py | 1 + 16 files changed, 371 insertions(+), 93 deletions(-) create mode 100644 migrations/versions/202607081700_add_signal_source_type.py diff --git a/.env.example b/.env.example index 0d93ac5..1d92b7b 100644 --- a/.env.example +++ b/.env.example @@ -5,13 +5,13 @@ SIGNALSCOUT_LLM_BASE_URL=https://api.zayuapi.com/v1 SIGNALSCOUT_LLM_API_KEY= SIGNALSCOUT_LLM_MODEL=grok-4.3 SIGNALSCOUT_LLM_TIMEOUT_SECONDS=90 -SIGNALSCOUT_LLM_MAX_TOKENS=4000 +SIGNALSCOUT_LLM_MAX_TOKENS=8000 SIGNALSCOUT_NEWS_RECENT_DAYS=3 -SIGNALSCOUT_NEWS_MAX_ITEMS=12 -SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=20 +SIGNALSCOUT_NEWS_MAX_ITEMS=60 +SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=100 SIGNALSCOUT_GITHUB_RECENT_DAYS=30 SIGNALSCOUT_GITHUB_MIN_STARS=20 -SIGNALSCOUT_GITHUB_MAX_PROJECTS=5 +SIGNALSCOUT_GITHUB_MAX_PROJECTS=40 SIGNALSCOUT_AGENT_TIMEZONE=Asia/Shanghai SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30 SIGNALSCOUT_REPORT_DIR=reports diff --git a/README.md b/README.md index c2cb63e..1eb1436 100644 --- a/README.md +++ b/README.md @@ -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_API_KEY=你的中转站 key SIGNALSCOUT_LLM_MODEL=grok-4.3 +SIGNALSCOUT_LLM_MAX_TOKENS=8000 SIGNALSCOUT_NEWS_RECENT_DAYS=3 -SIGNALSCOUT_NEWS_MAX_ITEMS=12 -SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=20 +SIGNALSCOUT_NEWS_MAX_ITEMS=60 +SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=100 SIGNALSCOUT_GITHUB_RECENT_DAYS=30 SIGNALSCOUT_GITHUB_MIN_STARS=20 -SIGNALSCOUT_GITHUB_MAX_PROJECTS=5 +SIGNALSCOUT_GITHUB_MAX_PROJECTS=40 SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30 ``` +单次运行的目标上限是新闻 60 条、GitHub 项目 40 条;实际入库数量会根据候选质量、链接去重和模型筛选结果浮动。 + ## 本机运行 按项目规则,本机 Python 使用: @@ -66,7 +69,7 @@ http://127.0.0.1:8010/docs - `GET /health`:服务健康检查 - `GET /agent/runs`:查看最近运行记录 -- `GET /signals`:查看结构化情报 +- `GET /signals`:查看结构化情报,可用 `source_type=news` 或 `source_type=github` 查看单类信号 - `GET /reports/latest`:查看最新日报 - `GET /dashboard`:获取仪表盘数据 - `POST /topics`:新增 Agent 跟踪主题 diff --git a/app/api/routes.py b/app/api/routes.py index cb62e56..f80c911 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional +from typing import Literal, Optional from fastapi import APIRouter, Depends 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]) -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)) 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: # 单个仪表盘接口返回运行、信号、日报和主题,前端不需要理解内部调度细节。 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)) topics = list(db.scalars(select(Topic).order_by(Topic.id.asc())).all()) return DashboardRead( runs=runs, - signals=signals, + news_signals=news_signals, + github_signals=github_signals, latest_report=report, 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()) -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 diff --git a/app/core/config.py b/app/core/config.py index fe7a9de..8a305fb 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -16,13 +16,13 @@ class Settings(BaseSettings): llm_api_key: str = "" llm_model: str = "grok-4.3" llm_timeout_seconds: int = 90 - llm_max_tokens: int = 4000 + llm_max_tokens: int = 8000 news_recent_days: int = 3 - news_max_items: int = 12 - news_search_max_records: int = 20 + news_max_items: int = 60 + news_search_max_records: int = 100 github_recent_days: int = 30 github_min_stars: int = 20 - github_max_projects: int = 5 + github_max_projects: int = 40 agent_timezone: str = "Asia/Shanghai" agent_interval_minutes: int = 30 report_dir: Path = Path("reports") diff --git a/app/db/models.py b/app/db/models.py index eb97105..412c641 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -44,6 +44,7 @@ class Signal(Base): id: Mapped[int] = mapped_column(Integer, primary_key=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) title: Mapped[str] = mapped_column(String(300), nullable=False) summary: Mapped[str] = mapped_column(Text, nullable=False) diff --git a/app/db/repository.py b/app/db/repository.py index 9178d59..69e5aaa 100644 --- a/app/db/repository.py +++ b/app/db/repository.py @@ -57,6 +57,7 @@ class AgentRepository: if existing and existing.run_id != run.id: return None if existing: + existing.source_type = item.source_type existing.topic = item.topic existing.title = item.title existing.summary = item.summary @@ -68,6 +69,7 @@ class AgentRepository: return existing signal = Signal( run_id=run.id, + source_type=item.source_type, topic=item.topic, title=item.title, summary=item.summary, diff --git a/app/integrations/github_client.py b/app/integrations/github_client.py index 44de3eb..03344e3 100644 --- a/app/integrations/github_client.py +++ b/app/integrations/github_client.py @@ -58,7 +58,9 @@ class GitHubHotProjectClient: def _search(self, query: str) -> dict[str, Any]: # 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( url, headers={ @@ -92,6 +94,7 @@ class GitHubHotProjectClient: f"主要语言为 {language}。" ) return SignalItem( + source_type="github", topic="GitHub 热门项目", title=full_name, summary=summary, diff --git a/app/integrations/news_search_client.py b/app/integrations/news_search_client.py index d9032a2..b3e4ea2 100644 --- a/app/integrations/news_search_client.py +++ b/app/integrations/news_search_client.py @@ -84,6 +84,7 @@ class NewsSearchClient: comments = int(story.get("num_comments") or 0) published_at = self._parse_hn_date(story.get("created_at")) return SignalItem( + source_type="news", topic="AI 新闻候选", title=title[:300], summary=f"Hacker News 最新技术讨论,作者 {author},约 {points} points、{comments} 条评论。", diff --git a/app/llm/grok_client.py b/app/llm/grok_client.py index 074b626..0410626 100644 --- a/app/llm/grok_client.py +++ b/app/llm/grok_client.py @@ -132,6 +132,7 @@ class GrokIntelligenceClient: ] github_payload = [ { + "source_type": item.source_type, "title": item.title, "summary": item.summary, "source_url": item.source_url, @@ -142,6 +143,7 @@ class GrokIntelligenceClient: ] news_payload = [ { + "source_type": item.source_type, "title": item.title, "summary": item.summary, "source_url": item.source_url, @@ -170,6 +172,7 @@ class GrokIntelligenceClient: "title": "字符串,日报标题", "signals": [ { + "source_type": "news 或 github;新闻候选必须为 news,GitHub 候选必须为 github", "topic": "AI 新闻 / GitHub 热门项目 / 模型发布 / Agent 工具 / 融资动态等", "title": "字符串", "summary": "中文摘要,说明为什么重要", @@ -189,6 +192,8 @@ class GrokIntelligenceClient: "新闻必须来自日期窗口内或接近日内发生的事件。", "AI新闻只能从 news_candidates 中选择,不要新增候选外新闻。", "GitHub 热门项目只能从 github_candidates 中选择,不要新增候选外仓库。", + "news 和 github 是两个独立栏目,分别尽量输出到对应上限,不要把 GitHub 项目写成新闻。", + "只有候选重复、链接不可验证或明显不相关时,才少于对应上限。", "每条 signal 必须有真实 source_url。", "report_markdown 只能基于 signals 写,不要加入 signals 外的新事实。", "输出必须是可被 json.loads 解析的 JSON object。", diff --git a/app/schemas/agent.py b/app/schemas/agent.py index 485c738..7c51e9b 100644 --- a/app/schemas/agent.py +++ b/app/schemas/agent.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime -from typing import Optional +from typing import Literal, Optional from pydantic import BaseModel, Field @@ -14,6 +14,7 @@ class SearchTopic(BaseModel): class SignalItem(BaseModel): # 模型产出的结构化情报会通过这个契约进入数据库和日报。 + source_type: Literal["news", "github"] topic: str title: str summary: str @@ -51,6 +52,7 @@ class RunRead(BaseModel): class SignalRead(BaseModel): id: int run_id: int + source_type: Literal["news", "github"] topic: str title: str summary: str @@ -77,7 +79,8 @@ class ReportRead(BaseModel): class DashboardRead(BaseModel): runs: list[RunRead] - signals: list[SignalRead] + news_signals: list[SignalRead] + github_signals: list[SignalRead] latest_report: Optional[ReportRead] topics: list["TopicRead"] @@ -95,4 +98,3 @@ class TopicRead(BaseModel): enabled: bool model_config = {"from_attributes": True} - diff --git a/app/ui/home.py b/app/ui/home.py index a6f52f3..3caae0d 100644 --- a/app/ui/home.py +++ b/app/ui/home.py @@ -77,9 +77,7 @@ HOME_PAGE_HTML = """ line-height: 1.1; } - .brand span, - .nav-note, - .auto-card span { + .brand span { color: var(--muted); font-size: 12px; line-height: 1.55; @@ -110,20 +108,6 @@ HOME_PAGE_HTML = """ 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 { padding: 28px; display: grid; @@ -155,7 +139,7 @@ HOME_PAGE_HTML = """ } .pulse { - min-width: 184px; + min-width: 220px; border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; @@ -172,6 +156,11 @@ HOME_PAGE_HTML = """ line-height: 1.2; } + .pulse span { + display: block; + margin-top: 5px; + } + .layout { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr); @@ -189,7 +178,7 @@ HOME_PAGE_HTML = """ } .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 { @@ -282,7 +271,70 @@ HOME_PAGE_HTML = """ padding: 18px; min-height: 520px; 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; } @@ -342,57 +394,52 @@ HOME_PAGE_HTML = """ AI 情报采集与研判工作台 -
- 自动整理 - 约每 30 分钟自动整理一次。 -
-
-

今日 AI 情报

-

Agent 自动筛选、去重和生成日报;页面会持续更新最新结果。

+

今日 AI 信号

+

新闻动态与 GitHub 项目分开阅读,日报保留适合快速复盘的重点判断。

- 等待运行 - 正在读取运行记录。 + 读取中 + 正在整理新闻与项目。
-

情报信号

- 0 条 +

AI 新闻

+ 0 条
-
-
正在读取情报。
+
+
正在读取新闻。
-

最新日报

- 待生成 +

GitHub 热门项目

+ 0 条 +
+
+
正在读取项目。
-
正在读取日报。
-

自动记录

- 0 次 -
-
-
正在读取运行记录。
+

最新日报

+ 待生成
+
正在读取日报。
@@ -410,7 +457,7 @@ HOME_PAGE_HTML = """
-

自动记录

+

运行记录

0 次
@@ -424,18 +471,18 @@ HOME_PAGE_HTML = """