diff --git a/README.md b/README.md index 86ecf64f..79750903 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ Where each capability stands today. Live module inventory and the end-to-end dec | ✅ Shipped | Scheduler / cron agent mode | D-9 | `scheduler_jobs` + advisory lock + `/api/scheduler/*` management plane | | ✅ Shipped | RiskGuard per-account isolation | D-9.1a | `RiskGuardFactory` removes cross-account state bleed | | ✅ Shipped | Multi-market data sources — web search + financial fundamentals | D-10 | zero-key DDGS web search · `baostock` is the logical A-share venue: Tencent HTTPS bars/ticker + Baostock fundamentals/calendar/constituents; yfinance covers global markets including HK · analyst integration + fallback · per-market lookbackDays | +| ✅ Shipped | Unified multi-market financial news | D-12 | `GET /news` + `data.get_news` · Eastmoney / SEC / HKEX / Crypto feeds · current-snapshot `as_of` filtering with explicit incomplete-history coverage · observable provider failures · explicit index/ETF proxy labels for market-level stock news | | ✅ Shipped | Risk engine — all 5 rules live in HTTP path | D-9 closed | `closed_trades` writes from HTTP order flow; `RoutingCalendar` for US equity + crypto; all trade-based rules trigger on real data | | ✅ Shipped | `askUserChoice` — `ask` permission path | D-11 (issue #2) | pending-permission flow resolves the `ask` state (no longer a workaround) | | ✅ Shipped | `permissions.yaml` configuration | D-11 (issue #4) | `config/permissions.default.yaml` + `yaml_loader.ts` replace the hard-coded `defaults.ts` | diff --git a/README.zh-CN.md b/README.zh-CN.md index 78a687b8..2119da77 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -233,6 +233,7 @@ Inalpha 把*调度*和*算力*分开:agent runtime 负责扇出网格、聚合 | ✅ 已上线 | Scheduler / cron agent 模式 | D-9 | `scheduler_jobs` + advisory lock + `/api/scheduler/*` 管理面 | | ✅ 已上线 | RiskGuard 账户级隔离 | D-9.1a | `RiskGuardFactory` 去除跨账户状态串联 | | ✅ 已上线 | 多市场数据源 — web 搜索 + 财报基本面 | D-10 | DDGS 零 key web search · `baostock` 是 A 股逻辑 venue:腾讯 HTTPS 行情/最新价 + Baostock 基本面/日历/成分;yfinance 覆盖全球(含港股)· analyst 接入 + 兜底 · lookbackDays 按市场分化 | +| ✅ 已上线 | 统一多市场财经新闻 | D-12 | `GET /news` + `data.get_news` · 东财 / SEC / HKEX / Crypto feed · 当前来源快照上的 `as_of` 截止过滤并显式标注历史覆盖不足 · provider 故障可观察 · 股票市场级新闻明确标注指数/ETF 代理口径 | | ✅ 已上线 | 风控引擎 — 5 条规则全在 HTTP 路径激活 | D-9 收口 | `closed_trades` 由 HTTP 订单流写入;`RoutingCalendar` 覆盖美股 + crypto;trade-based 规则全在真实数据上触发 | | ✅ 已上线 | `askUserChoice` — `ask` 权限路径 | D-11(issue #2) | pending-permission 流程把 `ask` 状态从 workaround 救回(已收口) | | ✅ 已上线 | `permissions.yaml` 配置化 | D-11(issue #4) | `config/permissions.default.yaml` + `yaml_loader.ts` 替代 `defaults.ts` 硬编码 | diff --git a/apps/dashboard/messages/en.json b/apps/dashboard/messages/en.json index 2755c665..fa9ce046 100644 --- a/apps/dashboard/messages/en.json +++ b/apps/dashboard/messages/en.json @@ -473,6 +473,18 @@ "toolResult": "output", "errorGeneric": "Agent reply failed. Please retry — likely an upstream LLM error or dropped stream.", "errorDismiss": "Dismiss", + "marketNews": { + "partial": "Some sources failed.", + "snapshotOnly": "The historical window is not fully covered.", + "empty": "No news data available.", + "providerFailure": "{provider}: {status}", + "status": { + "timeout": "timed out", + "rate_limited": "rate limited", + "upstream_error": "upstream error", + "unsupported": "unsupported scope" + } + }, "context": { "viewing": "Viewing", "dismiss": "Don't include this page", diff --git a/apps/dashboard/messages/zh.json b/apps/dashboard/messages/zh.json index 12c916d6..12f3c1a1 100644 --- a/apps/dashboard/messages/zh.json +++ b/apps/dashboard/messages/zh.json @@ -473,6 +473,18 @@ "toolResult": "输出", "errorGeneric": "Agent 回复失败,请重试 —— 多半是上游 LLM 报错或连接中断。", "errorDismiss": "关闭", + "marketNews": { + "partial": "部分来源失败。", + "snapshotOnly": "历史窗口覆盖不完整。", + "empty": "暂无可用新闻数据。", + "providerFailure": "{provider}:{status}", + "status": { + "timeout": "请求超时", + "rate_limited": "触发限流", + "upstream_error": "上游错误", + "unsupported": "不支持该范围" + } + }, "context": { "viewing": "正在看", "dismiss": "不带本页上下文", diff --git a/apps/dashboard/src/components/chat/tool-views/MarketAttributionViews.tsx b/apps/dashboard/src/components/chat/tool-views/MarketAttributionViews.tsx index 2ea6dd9d..c5fac450 100644 --- a/apps/dashboard/src/components/chat/tool-views/MarketAttributionViews.tsx +++ b/apps/dashboard/src/components/chat/tool-views/MarketAttributionViews.tsx @@ -1,5 +1,7 @@ "use client"; +import { useTranslations } from "next-intl"; + import { cn } from "@/lib/cn"; import { shortTimestamp } from "./format"; @@ -22,19 +24,68 @@ interface NewsItem { summary?: string; published_at?: string | null; related_codes?: string[]; + symbols?: string[]; } -export function isMarketNews(v: unknown): v is { items: NewsItem[] } { - if (!isObj(v) || !Array.isArray(v.items) || v.items.length === 0) return false; +interface NewsProviderStatus { + provider: string; + status: string; + coverage?: string; +} + +interface MarketNewsPayload { + items: NewsItem[]; + providers?: NewsProviderStatus[]; + is_partial?: boolean; + coverage_complete?: boolean; + error?: string; +} + +export function isMarketNews(v: unknown): v is MarketNewsPayload { + if (!isObj(v) || typeof v.error === "string" || !Array.isArray(v.items)) return false; + const hasNewsState = + Array.isArray(v.providers) || + typeof v.is_partial === "boolean" || + typeof v.coverage_complete === "boolean"; + if (v.items.length === 0) return hasNewsState; return v.items.every((it) => isObj(it) && typeof it.title === "string"); } const NEWS_CAP = 8; -export function MarketNewsView({ v }: { v: { items: NewsItem[] } }) { +const NEWS_PROVIDER_FAILURES = new Set([ + "timeout", + "rate_limited", + "upstream_error", + "unsupported", +]); + +export function MarketNewsView({ v }: { v: MarketNewsPayload }) { + const t = useTranslations("chat.marketNews"); const items = v.items.slice(0, NEWS_CAP); + const unavailable = (v.providers ?? []).filter((provider) => + NEWS_PROVIDER_FAILURES.has(provider.status), + ); return (