diff --git a/.env.example b/.env.example
index 4143a41..0d93ac5 100644
--- a/.env.example
+++ b/.env.example
@@ -13,8 +13,7 @@ SIGNALSCOUT_GITHUB_RECENT_DAYS=30
SIGNALSCOUT_GITHUB_MIN_STARS=20
SIGNALSCOUT_GITHUB_MAX_PROJECTS=5
SIGNALSCOUT_AGENT_TIMEZONE=Asia/Shanghai
-SIGNALSCOUT_AGENT_CRON_HOUR=8
-SIGNALSCOUT_AGENT_CRON_MINUTE=30
+SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30
SIGNALSCOUT_REPORT_DIR=reports
SIGNALSCOUT_LOG_FILE=logs/app.log
SIGNALSCOUT_LOG_TAIL_LINES=200
diff --git a/README.md b/README.md
index 955b007..3c945d4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# SignalScout
-SignalScout 是一个定时运行的 AI 情报 Agent。它先用动态新闻搜索和 GitHub 官方搜索抓取候选证据,再使用 Grok 通过中转 API 生成最新 AI 新闻、模型发布、Agent 工具、融资动态和 GitHub 热门项目日报,并把结构化情报保存到 MySQL。
+SignalScout 是一个自动运行的 AI 情报 Agent。它先用动态新闻搜索和 GitHub 官方搜索抓取候选证据,再使用 Grok 通过中转 API 生成最新 AI 新闻、模型发布、Agent 工具、融资动态和 GitHub 热门项目日报,并把结构化情报保存到 MySQL。
## 主流程
@@ -35,6 +35,7 @@ SIGNALSCOUT_NEWS_SEARCH_MAX_RECORDS=20
SIGNALSCOUT_GITHUB_RECENT_DAYS=30
SIGNALSCOUT_GITHUB_MIN_STARS=20
SIGNALSCOUT_GITHUB_MAX_PROJECTS=5
+SIGNALSCOUT_AGENT_INTERVAL_MINUTES=30
```
## 本机运行
@@ -64,8 +65,8 @@ http://127.0.0.1:8010/docs
## API
- `GET /health`:服务健康检查
-- `POST /agent/runs/daily`:手动触发每日 Agent
-- `POST /agent/runs/daily/stream`:以 SSE 方式触发每日 Agent
+- `POST /agent/runs/daily`:运维触发一次 Agent
+- `POST /agent/runs/daily/stream`:运维以 SSE 方式触发一次 Agent
- `GET /agent/runs`:查看最近运行记录
- `GET /signals`:查看结构化情报
- `GET /reports/latest`:查看最新日报
diff --git a/app/agent/scheduler.py b/app/agent/scheduler.py
index 6115502..7f5f50b 100644
--- a/app/agent/scheduler.py
+++ b/app/agent/scheduler.py
@@ -1,3 +1,5 @@
+from datetime import datetime
+
from apscheduler.schedulers.background import BackgroundScheduler
from sqlalchemy.orm import Session
@@ -16,15 +18,17 @@ def run_scheduled_job(settings: Settings) -> None:
def build_scheduler(settings: Settings) -> BackgroundScheduler:
- # APScheduler负责每日后台运行,服务启动后自动挂载任务。
+ # APScheduler负责自动循环运行,启动后先跑一次,再按固定间隔持续采集。
scheduler = BackgroundScheduler(timezone=settings.agent_timezone)
scheduler.add_job(
run_scheduled_job,
- trigger="cron",
- hour=settings.agent_cron_hour,
- minute=settings.agent_cron_minute,
+ trigger="interval",
+ minutes=settings.agent_interval_minutes,
args=[settings],
- id="daily-intelligence",
+ id="continuous-intelligence",
+ next_run_time=datetime.now(scheduler.timezone),
replace_existing=True,
+ max_instances=1,
+ coalesce=True,
)
return scheduler
diff --git a/app/core/config.py b/app/core/config.py
index 5bad03b..fe7a9de 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -24,8 +24,7 @@ class Settings(BaseSettings):
github_min_stars: int = 20
github_max_projects: int = 5
agent_timezone: str = "Asia/Shanghai"
- agent_cron_hour: int = 8
- agent_cron_minute: int = 30
+ agent_interval_minutes: int = 30
report_dir: Path = Path("reports")
log_file: Path = Path("logs/app.log")
log_tail_lines: int = 200
diff --git a/app/main.py b/app/main.py
index 5b62835..eba59ab 100644
--- a/app/main.py
+++ b/app/main.py
@@ -17,8 +17,12 @@ from app.core.log_setup import configure_logging # noqa: E402
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
- # The scheduler lives with the web process for the demo deployment profile.
+ # 测试环境不启动后台采集,避免接口测试触发真实外部请求。
settings = get_settings()
+ if settings.env == "test":
+ yield
+ return
+ # 调度器随Web进程常驻,负责自动周期性整理情报。
scheduler = build_scheduler(settings)
scheduler.start()
app.state.scheduler = scheduler
diff --git a/app/ui/home.py b/app/ui/home.py
index 685346a..ec02dcf 100644
--- a/app/ui/home.py
+++ b/app/ui/home.py
@@ -15,7 +15,6 @@ HOME_PAGE_HTML = """
--panel: oklch(99% 0.006 190);
--panel-2: oklch(94.5% 0.018 176);
--accent: oklch(52% 0.12 174);
- --accent-ink: oklch(19% 0.035 190);
--warn: oklch(67% 0.13 70);
--bad: oklch(58% 0.16 26);
--good: oklch(53% 0.12 150);
@@ -79,7 +78,8 @@ HOME_PAGE_HTML = """
}
.brand span,
- .nav-note {
+ .nav-note,
+ .auto-card span {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
@@ -91,17 +91,37 @@ HOME_PAGE_HTML = """
margin-top: 24px;
}
- .nav a {
- text-decoration: none;
+ .nav button {
+ border: 0;
+ width: 100%;
+ text-align: left;
+ background: transparent;
padding: 10px 12px;
border-radius: 8px;
color: var(--muted);
+ font: inherit;
font-size: 14px;
+ cursor: pointer;
}
- .nav a.active {
+ .nav button.active {
background: var(--ink);
color: white;
+ 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 {
@@ -134,36 +154,44 @@ HOME_PAGE_HTML = """
line-height: 1.7;
}
- .actions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- justify-content: flex-end;
- }
-
- button {
- border: 1px solid var(--ink);
+ .pulse {
+ min-width: 184px;
+ border: 1px solid var(--line);
border-radius: 8px;
- background: var(--accent);
- color: white;
- min-height: 42px;
- padding: 0 16px;
- font-weight: 700;
- cursor: pointer;
- box-shadow: 4px 4px 0 var(--ink);
- transition: transform 160ms ease, box-shadow 160ms ease;
+ padding: 12px 14px;
+ background: var(--panel);
+ color: var(--muted);
+ line-height: 1.55;
+ font-size: 13px;
}
- button:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); }
- button:disabled { opacity: 0.58; cursor: wait; transform: none; box-shadow: 2px 2px 0 var(--ink); }
+ .pulse strong {
+ display: block;
+ color: var(--ink);
+ font-size: 18px;
+ line-height: 1.2;
+ }
.layout {
display: grid;
- grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr);
+ grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
gap: 18px;
align-items: start;
}
+ .view {
+ display: none;
+ gap: 18px;
+ }
+
+ .view.active {
+ display: grid;
+ }
+
+ .view[data-view="signals"].active {
+ grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
+ }
+
.panel {
background: var(--panel);
border: 1px solid var(--line);
@@ -235,36 +263,28 @@ HOME_PAGE_HTML = """
.pill {
display: inline-flex;
align-items: center;
- height: 24px;
+ min-height: 24px;
border: 1px solid var(--line);
border-radius: 999px;
- padding: 0 9px;
+ padding: 2px 9px;
background: oklch(98% 0.01 180);
color: var(--muted);
max-width: 100%;
}
.importance {
- color: var(--accent-ink);
+ color: oklch(19% 0.035 190);
border-color: oklch(72% 0.08 174);
background: oklch(91% 0.045 174);
}
.report {
padding: 18px;
- min-height: 320px;
+ min-height: 520px;
line-height: 1.75;
white-space: pre-wrap;
}
- .report h1,
- .report h2,
- .report h3 {
- font-size: 16px;
- margin: 0 0 10px;
- line-height: 1.35;
- }
-
.runs { display: grid; }
.run-row {
@@ -291,28 +311,6 @@ HOME_PAGE_HTML = """
line-height: 1.7;
}
- .toast {
- position: fixed;
- right: 22px;
- bottom: 22px;
- max-width: 360px;
- background: var(--ink);
- color: white;
- padding: 12px 14px;
- border-radius: 8px;
- opacity: 0;
- transform: translateY(12px);
- pointer-events: none;
- transition: opacity 160ms ease, transform 160ms ease;
- line-height: 1.55;
- z-index: 20;
- }
-
- .toast.show {
- opacity: 1;
- transform: translateY(0);
- }
-
@media (max-width: 980px) {
.shell { grid-template-columns: 1fr; }
.rail {
@@ -322,9 +320,9 @@ HOME_PAGE_HTML = """
border-bottom: 1px solid var(--line);
}
.nav { grid-template-columns: repeat(3, minmax(0, 1fr)); }
- .layout { grid-template-columns: 1fr; }
+ .view[data-view="signals"].active { grid-template-columns: 1fr; }
.topbar { display: grid; }
- .actions { justify-content: flex-start; }
+ .pulse { min-width: 0; }
}
@media (max-width: 560px) {
@@ -332,7 +330,6 @@ HOME_PAGE_HTML = """
.rail { padding: 18px; }
.nav { grid-template-columns: 1fr; }
h1 { font-size: 30px; }
- button { width: 100%; }
}
@@ -345,26 +342,31 @@ HOME_PAGE_HTML = """
AI 情报采集与研判工作台
-
每天自动整理 AI 新闻、模型发布、Agent 工具和 GitHub 热门项目。
+
+ 自动整理
+ 启动后自动运行,之后约每 30 分钟整理一次。
+
+ 聚合 AI 新闻、模型发布、Agent 工具和 GitHub 热门项目。
今日 AI 情报
-
聚合最新新闻和开源热度,由 Agent 统一筛选、去重和生成日报。
+
Agent 自动筛选、去重和生成日报;页面会持续更新最新结果。
-
-
+
+ 等待运行
+ 正在读取运行记录。
-
-
+
+
情报信号
0 条
@@ -375,39 +377,65 @@ HOME_PAGE_HTML = """
-
+
最新日报
待生成
-
正在读取日报。
+
正在读取日报。
-
+
+
+
+
+
+
最新日报
+ 待生成
+
+
正在读取日报。
+
+
+
+
-
-