Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
787fb96
feat: add intelligence source ingestion baseline
ZhuLinsen Jun 17, 2026
e1d3dcc
feat: feed local intelligence into analysis contexts
ZhuLinsen Jun 17, 2026
8926ec9
fix(review-feedback-1708): Reject DNS names that resolve privately an…
ZhuLinsen Jun 17, 2026
807839a
fix(review-feedback-1709): 处理大盘复盘本地资讯可能被搜索结果截断掉的问题,并澄清结构化检测到的外部模型/API…
ZhuLinsen Jun 17, 2026
6995e14
fix(review-feedback-1708): Pin DNS resolution before fetching and Str…
ZhuLinsen Jun 17, 2026
ac89316
fix(review-feedback-1709): filter by published at for analysis eviden…
ZhuLinsen Jun 17, 2026
6572a20
fix(review-feedback-1708): Sanitize fetch errors before returning the…
ZhuLinsen Jun 17, 2026
b2e6616
fix(review-feedback-1709): Normalize symbol scope before lookup
ZhuLinsen Jun 17, 2026
cfa1253
fix(review-feedback-1708): Avoid rolling back prior item inserts on d…
ZhuLinsen Jun 17, 2026
9ad6229
fix(review-feedback-1709): Use the effective news window for local ev…
ZhuLinsen Jun 17, 2026
08f344f
fix(review-feedback-1708): 落地可配置 RSS/Atom 情报源、存储、查询、retention 和基础安全边界
ZhuLinsen Jun 17, 2026
c128303
fix(review-feedback-1709): 补充“revert 本 PR 或移除本地资讯接入入口/清退本地资讯源配置数据”级别说明即可
ZhuLinsen Jun 17, 2026
604b46a
fix(review-feedback-1708): 落地 RSS/Atom 情报源的存储、拉取、查询、retention 和基础安全边界
ZhuLinsen Jun 17, 2026
5cac614
fix(review-feedback-1709): 补 Refs 1707
ZhuLinsen Jun 17, 2026
aa57b25
fix(review-feedback-1709): 解决冲突后再合入
ZhuLinsen Jun 17, 2026
7562c5a
fix(review-feedback-1709): 解决冲突后再合入
ZhuLinsen Jun 17, 2026
954a330
fix(review-feedback-1709): src/services/intelligence service.py 回退了资讯…
ZhuLinsen Jun 17, 2026
3d1ef78
fix: harden intelligence source ingestion
ZhuLinsen Jun 18, 2026
aab08db
Merge remote-tracking branch 'origin/main' into feat/news-intelligenc…
ZhuLinsen Jun 18, 2026
ef5c22b
fix(review-feedback-1709): Sanitize upstream fetch errors before retu…
ZhuLinsen Jun 18, 2026
45554c1
feat: add NewsNow intelligence sources
ZhuLinsen Jun 18, 2026
5898c0d
fix(review-feedback-1709): 确认并修复
ZhuLinsen Jun 18, 2026
cdfd42b
fix(review-feedback-1709): 补一个未命中敏感规则的异常回归测试
ZhuLinsen Jun 18, 2026
03647a6
fix(review-feedback-1709): 处理
ZhuLinsen Jun 18, 2026
b9db9a0
docs: enhance NEWSNOW_BASE_URL compatibility guidance with official l…
ZhuLinsen Jun 18, 2026
7dc7a88
fix(review-feedback-1709): 补齐官方 NewsNow 实例链接文档或 API 契约确认,明确指出公开实例风险(需…
ZhuLinsen Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,12 @@ SEARXNG_PUBLIC_INSTANCES_ENABLED=true
# NEWS_STRATEGY_PROFILE=short
# 新闻最大时效(天),搜索时限制结果在近期内,避免使用过时信息
# NEWS_MAX_AGE_DAYS=3
# 本地资讯/情报池保留天数;只清理资讯池 intelligence_items,不影响历史报告
# NEWS_INTEL_RETENTION_DAYS=30
# 单个 RSS/Atom 资讯源拉取超时(秒)
# NEWS_INTEL_FETCH_TIMEOUT_SEC=8
# 单次每个资讯源最多采集条数
# NEWS_INTEL_MAX_ITEMS_PER_SOURCE=50
# 乖离率阈值(%),偏离 MA5 超过此值提示不追高;强势趋势股自动放宽到 1.5 倍
# BIAS_THRESHOLD=5.0

Expand Down
114 changes: 114 additions & 0 deletions api/v1/endpoints/intelligence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
"""Intelligence source API endpoints."""

from __future__ import annotations

import logging
from typing import Optional

from fastapi import APIRouter, HTTPException, Query

from api.v1.schemas.common import ErrorResponse
from api.v1.schemas.intelligence import (
IntelligenceFetchResponse,
IntelligenceItemListResponse,
IntelligenceSourceCreateRequest,
IntelligenceSourceItem,
IntelligenceSourceListResponse,
IntelligenceSourceTestResponse,
)
from src.services.intelligence_service import IntelligenceService, IntelligenceServiceError

logger = logging.getLogger(__name__)
router = APIRouter()


def _bad_request(exc: Exception) -> HTTPException:
return HTTPException(status_code=400, detail={"error": "validation_error", "message": str(exc)})


def _not_found(message: str) -> HTTPException:
return HTTPException(status_code=404, detail={"error": "not_found", "message": message})


def _internal_error(message: str, exc: Exception) -> HTTPException:
logger.error("%s: %s", message, exc, exc_info=True)
return HTTPException(status_code=500, detail={"error": "internal_error", "message": f"{message}: {str(exc)}"})


@router.post("/sources", response_model=IntelligenceSourceItem, responses={400: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, summary="Create intelligence source")
def create_source(request: IntelligenceSourceCreateRequest) -> IntelligenceSourceItem:
try:
return IntelligenceSourceItem(**IntelligenceService().create_source(request.model_dump()))
except IntelligenceServiceError as exc:
raise _bad_request(exc)
except Exception as exc:
raise _internal_error("Create intelligence source failed", exc)


@router.get("/sources", response_model=IntelligenceSourceListResponse, responses={500: {"model": ErrorResponse}}, summary="List intelligence sources")
def list_sources(
enabled: Optional[bool] = Query(None),
source_type: Optional[str] = Query(None),
scope_type: Optional[str] = Query(None),
market: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
) -> IntelligenceSourceListResponse:
try:
return IntelligenceSourceListResponse(**IntelligenceService().list_sources(
enabled=enabled, source_type=source_type, scope_type=scope_type,
market=market, page=page, page_size=page_size,
))
except Exception as exc:
raise _internal_error("List intelligence sources failed", exc)


@router.post("/sources/test", response_model=IntelligenceSourceTestResponse, responses={400: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, summary="Dry-run an intelligence source payload")
def test_source_payload(request: IntelligenceSourceCreateRequest) -> IntelligenceSourceTestResponse:
try:
return IntelligenceSourceTestResponse(**IntelligenceService().test_source(request.model_dump()))
except IntelligenceServiceError as exc:
raise _bad_request(exc)
except Exception as exc:
raise _internal_error("Test intelligence source failed", exc)


@router.post("/sources/{source_id}/fetch", response_model=IntelligenceFetchResponse, responses={400: {"model": ErrorResponse}, 404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, summary="Fetch one intelligence source")
def fetch_source(source_id: int, dry_run: bool = Query(False)) -> IntelligenceFetchResponse:
try:
return IntelligenceFetchResponse(**IntelligenceService().fetch_source(source_id, dry_run=dry_run))
except IntelligenceServiceError as exc:
message = str(exc)
if "not found" in message.lower():
raise _not_found(message)
raise _bad_request(exc)
except Exception as exc:
raise _internal_error("Fetch intelligence source failed", exc)


@router.post("/sources/fetch-enabled", response_model=IntelligenceFetchResponse, responses={500: {"model": ErrorResponse}}, summary="Fetch all enabled intelligence sources with fail-open semantics")
def fetch_enabled_sources() -> IntelligenceFetchResponse:
try:
return IntelligenceFetchResponse(**IntelligenceService().fetch_enabled_sources())
except Exception as exc:
raise _internal_error("Fetch enabled intelligence sources failed", exc)


@router.get("/items", response_model=IntelligenceItemListResponse, responses={500: {"model": ErrorResponse}}, summary="List persisted intelligence items")
def list_items(
scope_type: Optional[str] = Query(None),
scope_value: Optional[str] = Query(None),
market: Optional[str] = Query(None),
query: Optional[str] = Query(None),
days: Optional[int] = Query(None, ge=1),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
) -> IntelligenceItemListResponse:
try:
return IntelligenceItemListResponse(**IntelligenceService().list_items(
scope_type=scope_type, scope_value=scope_value, market=market,
query=query, days=days, page=page, page_size=page_size,
))
except Exception as exc:
raise _internal_error("List intelligence items failed", exc)
7 changes: 7 additions & 0 deletions api/v1/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
decision_signals,
health,
history,
intelligence,
portfolio,
stocks,
system_config,
Expand Down Expand Up @@ -103,6 +104,12 @@
tags=["AlphaSift"]
)

router.include_router(
intelligence.router,
prefix="/intelligence",
tags=["Intelligence"]
)

router.include_router(
health.router,
tags=["Health"]
Expand Down
98 changes: 98 additions & 0 deletions api/v1/schemas/intelligence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
"""Intelligence source API schemas."""

from __future__ import annotations

from typing import List, Literal, Optional

from pydantic import BaseModel, Field

SourceTypeValue = Literal["rss", "atom"]
ScopeTypeValue = Literal["symbol", "market", "sector"]
MarketValue = Literal["cn", "hk", "us", "global"]


class IntelligenceSourceCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
url: str = Field(..., min_length=1, max_length=1000)
source_type: SourceTypeValue = "rss"
enabled: bool = True
scope_type: ScopeTypeValue = "market"
scope_value: Optional[str] = Field(None, max_length=64)
market: MarketValue = "cn"
description: Optional[str] = None


class IntelligenceSourceItem(BaseModel):
id: int
name: str
source_type: str
url: str
enabled: bool
scope_type: str
scope_value: Optional[str] = None
market: str
description: Optional[str] = None
last_status: Optional[str] = None
last_error: Optional[str] = None
last_fetched_at: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None


class IntelligenceSourceListResponse(BaseModel):
items: List[IntelligenceSourceItem] = Field(default_factory=list)
total: int
page: int
page_size: int


class IntelligenceItem(BaseModel):
id: int
source_id: Optional[int] = None
source_name: Optional[str] = None
source_type: str
title: str
summary: Optional[str] = None
url: str
source: Optional[str] = None
published_at: Optional[str] = None
fetched_at: Optional[str] = None
scope_type: str
scope_value: Optional[str] = None
market: str


class IntelligenceSampleItem(BaseModel):
title: str
summary: Optional[str] = None
url: str
source: Optional[str] = None
published_at: Optional[str] = None


class IntelligenceItemListResponse(BaseModel):
items: List[IntelligenceItem] = Field(default_factory=list)
total: int
page: int
page_size: int


class IntelligenceFetchResponse(BaseModel):
ok: bool
source_id: Optional[int] = None
source_count: Optional[int] = None
fetched_count: Optional[int] = None
saved_count: Optional[int] = None
retention_deleted: Optional[int] = None
dry_run: Optional[bool] = None
sample_items: List[IntelligenceSampleItem] = Field(default_factory=list)
results: Optional[List[dict]] = None
error: Optional[str] = None


class IntelligenceSourceTestResponse(BaseModel):
ok: bool
source: dict
fetched_count: int
sample_items: List[IntelligenceSampleItem] = Field(default_factory=list)
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- [修复] 发布说明生成查询 PR 作者失败时保留降级并输出包含 PR 编号和异常类型的 warning,便于排查 token、权限、网络或 GitHub API 异常。
- [改进] DSA 数据源链路新增 Tencent 日 K 直连 fetcher、daily source health 短期熔断,并升级 AlphaSift 默认 pin/runtime bridge,默认启用 `DAILY_SOURCE=auto`、Sina snapshot 优先级和候选级 quote context。
- [文档] 补充 AlphaSift 迁移与回退边界:明确 `ALPHASIFT_INSTALL_SPEC` 显式覆盖语义、`requirements.txt + DEFAULT_ALPHASIFT_INSTALL_SPEC` 与运行时兼容边界、以及回滚路径(关闭功能/完整 revert)说明,覆盖旧 pin 用户升级行为。
- [新功能] #1707 新增合规 RSS/Atom 资讯源配置、拉取、去重、入库、查询、retention 与基础安全校验 API,作为个股/市场资讯情报池基线。
- [改进] #1707 个股分析、Agent 分析和大盘复盘会 fail-open 读取本地资讯/情报池,并把来源链接作为新闻上下文和 evidence 输入。

- [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。

Expand Down
1 change: 1 addition & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
| [Bot 命令与接入](bot-command.md) | Bot 命令、Webhook、平台接入和回调说明 |
| [Bot 平台配置](bot/) | 飞书、钉钉、Discord 等 Bot 配置截图和补充说明 |
| [实时告警中心](alerts.md) | EventMonitor 基线、Web 规则管理、通知结果、冷却状态和 Phase 边界 |
| [资讯 / 情报源](intelligence-sources.md) | RSS/Atom 合规资讯源配置、测试、拉取、去重、存储、查询与安全边界 |
| [分析上下文包契约、运行态消费与可见性](analysis-context-pack.md) | AnalysisContextPack 首版范围、字段质量状态、P1/P2 内部契约、P3 Prompt 摘要消费、P4 历史/API/Web 低敏可见性、P5 数据质量评分、P6 迁移回滚与源码锚点;完整指南补充 #1386 阶段感知分析、迁移与回滚入口 |
| [图片识别 Prompt](image-extract-prompt.md) | 图片识别股票信息的 Prompt 与使用边界 |
| [OpenClaw Skill 集成](openclaw-skill-integration.md) | OpenClaw / Skill 外部集成说明 |
Expand Down
55 changes: 55 additions & 0 deletions docs/intelligence-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 资讯 / 情报源 MVP

Issue #1707 的首版能力聚焦“合规资讯源采集、本地沉淀、可查询证据”,不把 RSS/Atom 混入按需搜索语义,也不默认新增独立舆情页。

## 能力范围

- 支持配置 RSS / Atom HTTP(S) 资讯源。
- 保存资讯源配置、启用状态、作用域和最近一次拉取状态。
- 拉取条目落库到 `intelligence_items`,保存标题、摘要、URL、来源、发布时间、拉取时间、市场与作用域。
- 按 URL 去重;无 URL 条目使用 `no-url:intel:<hash>` 兜底键。
- 支持 `symbol` / `market` / `sector` 作用域,以及 `cn` / `hk` / `us` / `global` 市场标记。
- 拉取批处理采用 fail-open:单个源失败不会阻塞其他源或主分析链路。
- 支持 retention 清理,避免资讯池无限增长。

## 安全边界

自定义 URL 会做基础校验:

- 只允许绝对 `http` / `https` URL;
- 禁止 URL 中携带 username/password;
- 禁止 `localhost`、`.local`、回环地址、内网地址、链路本地地址、保留地址和组播地址;
- 重定向后的最终 URL 也会再次校验;
- 错误消息会脱敏常见 `token` / `key` / `secret` 查询参数。

明确非目标:不做反爬、模拟登录、Cookie 抓取或非授权门户直抓。

## 配置项

```env
NEWS_INTEL_RETENTION_DAYS=30
NEWS_INTEL_FETCH_TIMEOUT_SEC=8
NEWS_INTEL_MAX_ITEMS_PER_SOURCE=50
```

## API

所有接口位于 `/api/v1/intelligence`。

- `POST /sources`:创建资讯源。
- `GET /sources`:查询资讯源。
- `POST /sources/test`:测试 payload,不落库。
- `POST /sources/{source_id}/fetch?dry_run=false`:拉取单个源。
- `POST /sources/fetch-enabled`:fail-open 拉取全部启用源。
- `GET /items?scope_type=market&market=cn&days=7`:查询资讯条目。

## 后续接入建议

首版基线之上,分析链路会 best-effort 读取本地资讯池:

- 个股传统分析会优先读取 `symbol=<股票代码>` 的资讯,并补充同市场 `market` 级资讯;内容追加到既有 `news_context`,随 AnalysisContextPack 摘要和历史 `news_content` 保存。
- Agent 分析同样通过 `news_context` 注入本地资讯证据,避免 Agent 必须重新搜索才能看到已沉淀新闻。
- 大盘复盘会把同市场 `market` 级资讯合并到市场新闻列表,Prompt、结构化 payload 和报告 news 字段都能看到来源链接。
- 本次能力仅新增本地资讯消费路径,不改模型名、provider/base URL、回退策略或运行时配置语义;兼容现有部署配置,回滚方式为清退本地资讯接入入口或移除本地资讯源配置/数据。

后续 PR 可以继续完善报告 evidence 展示和 Web 设置/报告查看入口。
24 changes: 24 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,9 @@ class Config:
# === 新闻与分析筛选配置 ===
news_max_age_days: int = 3 # 新闻最大时效(天)
news_strategy_profile: str = "short" # 新闻窗口策略档位:ultra_short/short/medium/long
news_intel_retention_days: int = 30 # 本地资讯池保留天数
news_intel_fetch_timeout_sec: float = 8.0 # 单个资讯源拉取超时
news_intel_max_items_per_source: int = 50 # 单次每个资讯源最多采集条数
bias_threshold: float = 5.0 # 乖离率阈值(%),超过此值提示不追高

# === Agent 模式配置 ===
Expand Down Expand Up @@ -1499,6 +1502,27 @@ def _load_from_env(cls) -> 'Config':
news_strategy_profile=cls._parse_news_strategy_profile(
os.getenv('NEWS_STRATEGY_PROFILE', 'short')
),
news_intel_retention_days=parse_env_int(
os.getenv('NEWS_INTEL_RETENTION_DAYS'),
30,
field_name='NEWS_INTEL_RETENTION_DAYS',
minimum=1,
maximum=365,
),
news_intel_fetch_timeout_sec=parse_env_float(
os.getenv('NEWS_INTEL_FETCH_TIMEOUT_SEC'),
8.0,
field_name='NEWS_INTEL_FETCH_TIMEOUT_SEC',
minimum=1.0,
maximum=30.0,
),
news_intel_max_items_per_source=parse_env_int(
os.getenv('NEWS_INTEL_MAX_ITEMS_PER_SOURCE'),
50,
field_name='NEWS_INTEL_MAX_ITEMS_PER_SOURCE',
minimum=1,
maximum=200,
),
bias_threshold=parse_env_float(os.getenv('BIAS_THRESHOLD'), 5.0, field_name='BIAS_THRESHOLD', minimum=1.0),
agent_litellm_model=agent_litellm_model,
agent_mode=os.getenv('AGENT_MODE', 'false').lower() == 'true',
Expand Down
Loading