Skip to content

每日简报功能说明 ​

每日简报用于从配置的数据源中抓取热点内容,结合可配置的网页搜索平台和 OpenAI SDK 生成结构化产业简报。重点面向财经、产业链和 A 股关联分析。

功能概览 ​

生成链路:

text
定时或手动触发
  -> 读取 BRIEF_SOURCES
  -> 刷新热点源数据并保存历史
  -> 读取最近 BRIEF_LOOKBACK_HOURS 的热点
  -> 按来源交错选择候选,去掉跨来源同标题主题
  -> 按配置顺序调用 Tavily / DeepSeek 搜索补充背景材料(有结果的查询缓存 1 小时)
  -> OpenAI SDK 生成结构化 JSON 和 Markdown
  -> 保存到 MongoDB
  -> API 返回前端可用的简报内容

默认面向前端的响应不会返回 rawInputItems 和 searchEvidence。这两个字段仍会保存到数据库,用于后端排查和审计,需要显式传 includeDebug=true 才会返回。

每个来源先取配置的前 N 条,再交错放入 AI 输入,避免一个来源按时间排序后占满候选。搜索最多选择 BRIEF_MAX_TOPICS 个不同标题;送入模型的条目、描述和搜索摘要还会截断,原始输入仍保留在数据库调试字段中。

BRIEF_SEARCH_PROVIDERS 决定平台顺序。例如 tavily,deepseek 会先查 Tavily;未配置密钥、请求失败或返回空结果时,再查 DeepSeek。每个主题一旦拿到有效结果就停止尝试,所有平台都失败时仍继续生成简报。成功结果会按搜索词与平台顺序缓存 1 小时;调换平台顺序不会复用旧顺序的缓存。调试响应的每条 searchEvidence 包含实际命中的 provider(均无结果时为 null)。

DeepSeek 适配器调用官方 Search API,使用 POST /v1/tools/search、Bearer 鉴权和搜索摘要;不调用 Fetch 接口。

当简报模型使用 DeepSeek 官方 API 地址时,结构化 JSON 生成会关闭思考模式、设置输出 token 上限,并在提示词中提供 JSON 示例,以降低模型返回无效 JSON 的概率。其他 OpenAI 兼容端点不发送 DeepSeek 专属参数。

提示词预算定义在 src/daily-brief/clients/ai-analysis.client.ts 的 PROMPT_BUDGET:最多 60 条热点条目、12 条搜索证据。BRIEF_MAX_TOPICS 的校验上限与它对齐,配成更大的值会在启动时报配置错误,而不是跑完一次简报才发现主题被静默截断。BRIEF_TOP_ITEMS_PER_SOURCE × 来源数 超过 60 时(默认 7 个来源 × 10 条 = 70)多出的条目不会进入提示词,由于候选是按来源交错排列的,被丢掉的是每个来源排名最末的条目。调整这两个变量时需要一并考虑 PROMPT_BUDGET。

来源必须处于启用状态 ​

简报刷新走调度器的抓取任务(FetchHotDataTask),因此 BRIEF_SOURCES 里的来源还必须在 source_configs 中是启用状态。被禁用的来源不会被抓取,该来源只能贡献回看窗口内的历史条目,调度停抓之后这部分数据也会逐渐枯竭。这种情况会在生成日志里给出告警:

text
WARN [DailyBriefService] Brief source cls contributed no fresh items: Source 'cls' is disabled

同一条日志也用于区分"来源确实没有数据"和"来源已损坏"。核对来源状态:

bash
curl http://localhost:6689/api/scheduler/configs

启用某个来源:

bash
curl -X PUT http://localhost:6689/api/scheduler/config/cls \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true}'

另外 IGNORE_SAVE_SOURCES 中的来源仍会被抓取并参与简报,只是不写入历史库。

环境变量 ​

bash
# 每日简报配置
BRIEF_ENABLED=true
BRIEF_CRON_EXPRESSION=0 12 * * *
BRIEF_TIMEZONE=Asia/Shanghai
BRIEF_SOURCES=cls,yicai,wallstreet,jin10,tonghuashun,eastmoney,gelonghui
BRIEF_LOOKBACK_HOURS=24
BRIEF_TOP_ITEMS_PER_SOURCE=10
BRIEF_MAX_TOPICS=12
BRIEF_STOCK_RANKING_CACHE_TTL=43200
BRIEF_GENERATING_TIMEOUT_MINUTES=30
BRIEF_SOURCE_CONCURRENCY=3
BRIEF_SEARCH_CONCURRENCY=3
BRIEF_SEARCH_PROVIDERS=tavily,deepseek

# OPENAI 配置
OPENAI_API_KEY=skxxx
OPENAI_API_BASE_URL=https://api.deepseek.com
AI_MODEL=deepseek-v4-flash
AI_TIMEOUT_MS=120000
AI_MAX_RETRIES=2

# Tavily Search API Key
TAVILY_API_KEY=tvly-dev-xxx
TAVILY_MAX_RESULTS=5
TAVILY_TIMEOUT_MS=10000

# DeepSeek Search API Key;可与 DeepSeek 模型 Key 相同,但需单独配置
DEEPSEEK_SEARCH_API_KEY=
DEEPSEEK_SEARCH_MAX_RESULTS=5
DEEPSEEK_SEARCH_TIMEOUT_MS=10000

字段说明:

配置说明
BRIEF_ENABLED是否在服务启动时启用每日简报定时任务
BRIEF_CRON_EXPRESSION简报生成 cron 表达式,默认每天 12 点
BRIEF_TIMEZONE简报日期和定时任务使用的时区
BRIEF_SOURCES参与简报的数据源名称,逗号分隔,需在 source_configs 中启用
BRIEF_LOOKBACK_HOURS读取历史热点的回看窗口
BRIEF_TOP_ITEMS_PER_SOURCE每个源最多纳入多少条热点,默认只分析前 10 条
BRIEF_MAX_TOPICS最多对多少个候选主题做搜索增强,取值 1–12
BRIEF_STOCK_RANKING_CACHE_TTL股票历史排名接口缓存时长(秒),默认 43200(12 小时)
BRIEF_GENERATING_TIMEOUT_MINUTES简报卡在 generating 多久后视为失败并允许重新生成,默认 30 分钟
BRIEF_SOURCE_CONCURRENCY抓取热榜源时的最大并发数,默认 3
BRIEF_SEARCH_CONCURRENCY同时搜索的主题数,默认 3;每个主题内的平台依次尝试
BRIEF_SEARCH_PROVIDERS搜索平台优先级,默认 tavily,deepseek;可只选其一或留空关闭搜索增强
OPENAI_API_KEYOpenAI SDK 使用的 API Key
OPENAI_API_BASE_URL兼容 OpenAI 协议的 API Base URL
AI_MODEL生成简报使用的模型
AI_TIMEOUT_MS单次 AI 请求超时(毫秒),默认 120000
AI_MAX_RETRIESAI 请求失败重试次数,默认 2,设为 0 关闭重试
TAVILY_API_KEYTavily 搜索 API Key
TAVILY_MAX_RESULTS每个搜索请求最多返回多少条结果
TAVILY_TIMEOUT_MS单次 Tavily 搜索超时(毫秒),默认 10000
DEEPSEEK_SEARCH_API_KEYDeepSeek Search API Key;与 OPENAI_API_KEY 独立配置,可填相同 Key
DEEPSEEK_SEARCH_MAX_RESULTSDeepSeek 每次搜索的返回条数,默认 5,取值 1–50
DEEPSEEK_SEARCH_TIMEOUT_MS单次 DeepSeek 搜索超时(毫秒),默认 10000

数据结构 ​

简报保存在 MongoDB 的 dailybriefs collection 中,核心字段包括:

ts
{
  briefDate: string;
  period: string;
  status: 'generating' | 'success' | 'failed';
  sources: string[];
  inputWindow: {
    start: Date;
    end: Date;
    lookbackHours: number;
  };
  analysis: Record<string, unknown>;
  markdown: string;
  rawInputItems: Record<string, unknown>[];
  searchEvidence: Record<string, unknown>[];
  model: string;
  tavilyUsed: boolean;
  searchProvidersUsed: string[];
  error?: string;
  generationId?: string;
  generationHeartbeatAt?: Date;
}

briefDate + period 有唯一索引,用于避免同一天同一类型简报重复写入。

生成时使用原子 claim 和 generationId 防止并发任务互相覆盖,心跳用于识别失联任务。超过 BRIEF_GENERATING_TIMEOUT_MINUTES 后,旧任务的结果也不能覆盖新任务。强制重新生成已有成功简报时,失败会保留先前的成功内容。普通响应隐藏原始输入、搜索证据和生成状态字段;调试时可以使用 includeDebug=true。

analysis 里包含:

  • summary:核心结论
  • highlights:重点摘要
  • topics:主题拆解
  • risks:整体风险
  • followUpSignals:后续跟踪信号
  • markdown:可直接展示的 Markdown 内容

API 接口 ​

基础地址示例:

text
http://localhost:6689

写接口没有内建鉴权

服务本身不对任何接口做鉴权。下列接口会消耗真实费用或删除数据,必须在反向代理 / 网关层限制来源,不要直接暴露到公网:

接口风险
POST /api/briefs/generate触发 AI 与配置的搜索平台调用,可能产生费用
DELETE /api/briefs/:date删除指定日期的简报
DELETE /api/briefs/history按条件批量删除历史简报
POST /api/briefs/scheduler/start
POST /api/briefs/scheduler/stop
POST /api/briefs/scheduler/reconfigure
改变定时任务运行状态

GET 接口只读,可以按需开放给前端。

获取简报配置 ​

bash
curl http://localhost:6689/api/briefs/config

响应示例:

json
{
  "enabled": true,
  "cronExpression": "0 12 * * *",
  "timezone": "Asia/Shanghai",
  "sources": ["cls", "yicai", "wallstreet"],
  "lookbackHours": 24,
  "topItemsPerSource": 10,
  "maxTopics": 12,
  "stockRankingCacheTtl": 43200,
  "model": "deepseek-v4-flash",
  "tavilyConfigured": true,
  "deepseekSearchConfigured": true,
  "searchProviders": ["tavily", "deepseek"]
}

手动生成简报 ​

生成正式日报:

bash
curl -X POST http://localhost:6689/api/briefs/generate \
  -H 'Content-Type: application/json' \
  -d '{"force":true}'

指定数据源和 period,用于测试:

bash
curl -X POST http://localhost:6689/api/briefs/generate \
  -H 'Content-Type: application/json' \
  -d '{"sources":["cls"],"period":"manual-test","force":true}'

请求参数:

字段类型说明
datestring可选,格式 YYYY-MM-DD
periodstring可选,默认 daily
sourcesstring[]可选,覆盖 .env 中的 BRIEF_SOURCES
forceboolean可选,是否强制重新生成

默认返回前端可用字段,不包含 rawInputItems 和 searchEvidence。

获取最新简报 ​

bash
curl http://localhost:6689/api/briefs/latest

不传 period 时,返回所有成功简报中最新的一条。

只获取正式日报:

bash
curl 'http://localhost:6689/api/briefs/latest?period=daily'

获取调试字段:

bash
curl 'http://localhost:6689/api/briefs/latest?includeDebug=true'

获取指定日期简报 ​

bash
curl 'http://localhost:6689/api/briefs/2026-07-05'

指定 period:

bash
curl 'http://localhost:6689/api/briefs/2026-07-05?period=manual-test'

包含调试字段:

bash
curl 'http://localhost:6689/api/briefs/2026-07-05?period=manual-test&includeDebug=true'

分页查询简报 ​

bash
curl 'http://localhost:6689/api/briefs?page=1&limit=10'

按 period 查询:

bash
curl 'http://localhost:6689/api/briefs?period=daily&page=1&limit=10'

按状态查询:

bash
curl 'http://localhost:6689/api/briefs?status=success'

获取历史股票出现次数排名 ​

统计成功简报中 analysis.topics[].aShareMapping[] 的股票出现次数,并按次数从高到低返回排名:

bash
curl 'http://localhost:6689/api/briefs/statistics/stocks'

支持按 period、简报日期范围和返回数量筛选:

bash
curl 'http://localhost:6689/api/briefs/statistics/stocks?period=daily&startDate=2026-01-01&endDate=2026-07-18&limit=20'

接口按完整查询条件缓存结果,默认缓存 12 小时。缓存复用现有缓存模块(Redis 不可用时降级为进程内存缓存);成功生成简报或实际删除简报后,会清理股票排名缓存(生成失败不会清,因为排名数据没有变化)。

同一只股票的归并规则 ​

AI 给出的 aShareMapping 里,同一家公司在不同简报中可能写法不一致,聚合会先做归一化再统计:

  • 代码格式:去掉 .SH / .SZ 之类的后缀,以及 SH / SZ / BJ 前缀。600519、SH600519、600519.SH、sh600519 视为同一只。
  • 有时带代码、有时不带:先按公司名归并,把「带代码那次」学到的代码补给同一家公司的其他记录,再按代码归并。
  • 只有代码没有公司名(company 为 待验证 等占位值):也会并入同代码的那一条,并显示已知的公司名。
  • 始终没有代码:按公司名单独成条,code 返回 null。
  • appearanceCount 统计出现次数(同一份简报里出现两次算两次),briefCount 统计去重后的简报数。

公司名和代码取该股票最近一次出现时的有效值。

股票排名聚合兼容 MongoDB 6.0(服务器与本机均为 6.0):代码归一化用 $replaceAll 去空格、$regexFind 剥掉 SH/SZ/BJ 前缀,.SH 之类的后缀用 $split 截掉,于是 600519、SH600519、600519.SH 会归到同一只股票。只有当前缀后面剩下恰好 6 个字符时才剥离,SH12345 这种长度不符的保持原样。

简报列表接口(GET /api/briefs)的投影聚合用到 $$REMOVE、$ifNull、$size,同样在 6.0 可用范围内。

这条约束由 src/database/repositories/mongo-compatibility.spec.ts 守护:它把管道实际发出的算子全部抽出来,既检查没有出现 7.0 及以后才有的算子,也用白名单确保不会悄悄引入未经确认的新算子。改动聚合管道后请保证该测试通过。

查询参数:

参数类型默认值说明
periodstring全部只统计指定类型的成功简报
startDatestring不限制起始简报日期,格式 YYYY-MM-DD,包含当天
endDatestring不限制截止简报日期,格式 YYYY-MM-DD,包含当天
limitnumber50返回排名数量,范围 1-200

响应示例:

json
{
  "filters": {
    "period": "daily",
    "startDate": "2026-01-01",
    "endDate": "2026-07-18",
    "limit": 20
  },
  "summary": {
    "briefCount": 120,
    "uniqueStockCount": 86,
    "totalAppearances": 438
  },
  "rankings": [
    {
      "rank": 1,
      "company": "拓普集团",
      "code": "601689",
      "appearanceCount": 18,
      "briefCount": 13,
      "firstAppearedDate": "2026-01-08",
      "lastAppearedDate": "2026-07-17"
    }
  ]
}

统计口径:

  • 仅统计 status=success 的历史简报。
  • 同一股票每出现在一个主题的 aShareMapping 中一次,appearanceCount 增加一次。
  • briefCount 是包含该股票的不同简报数量;同一简报内出现多次只计一份简报。
  • 优先按去除空格并转为大写的股票代码合并;代码为空、待验证 或无效占位值时,按公司名合并。
  • 没有有效股票代码和公司名的映射会被忽略。
  • 次数相同时,依次按覆盖简报数、最近出现日期、股票代码和公司名排序。

删除指定日期简报 ​

删除某一天所有 period 的简报:

bash
curl -X DELETE http://localhost:6689/api/briefs/2026-07-05

只删除某一天的指定 period:

bash
curl -X DELETE 'http://localhost:6689/api/briefs/2026-07-05?period=manual-test'

响应示例:

json
{
  "mode": "date",
  "briefDate": "2026-07-05",
  "period": "manual-test",
  "deletedCount": 1
}

批量清理历史简报 ​

按保留周期清理:

bash
# 删除早于 1 个月前的简报
curl -X DELETE 'http://localhost:6689/api/briefs/history?olderThan=1m'

# 删除早于 1 年前的简报
curl -X DELETE 'http://localhost:6689/api/briefs/history?olderThan=1y'

# 删除早于 30 天前的简报
curl -X DELETE 'http://localhost:6689/api/briefs/history?olderThan=30d'

按截止日期清理:

bash
# 删除 briefDate 小于 2025-07-05 的简报
curl -X DELETE 'http://localhost:6689/api/briefs/history?beforeDate=2025-07-05'

只清理指定 period:

bash
curl -X DELETE 'http://localhost:6689/api/briefs/history?olderThan=1y&period=daily'

响应示例:

json
{
  "mode": "olderThan",
  "olderThan": "1y",
  "beforeDate": "2025-07-05",
  "period": "daily",
  "deletedCount": 12
}

olderThan 支持:

  • 30d:30 天前
  • 1m:1 个月前
  • 1y:1 年前
  • month / one-month:等价于 1m
  • year / one-year:等价于 1y

简报调度状态 ​

bash
curl http://localhost:6689/api/briefs/scheduler/status

响应示例:

json
{
  "isRunning": false,
  "enabled": true,
  "cronExpression": "0 12 * * *",
  "timezone": "Asia/Shanghai",
  "cronJobExists": true
}

启停简报调度 ​

启动:

bash
curl -X POST http://localhost:6689/api/briefs/scheduler/start

停止:

bash
curl -X POST http://localhost:6689/api/briefs/scheduler/stop

重新加载 cron 配置:

bash
curl -X POST http://localhost:6689/api/briefs/scheduler/reconfigure

前端响应字段 ​

默认响应适合前端直接使用,核心字段如下:

json
{
  "briefDate": "2026-07-05",
  "period": "daily",
  "status": "success",
  "sources": ["cls"],
  "inputWindow": {
    "start": "2026-07-04T04:00:00.000Z",
    "end": "2026-07-05T04:00:00.000Z",
    "lookbackHours": 24
  },
  "analysis": {
    "summary": "今日核心结论...",
    "highlights": [],
    "topics": [],
    "risks": [],
    "followUpSignals": [],
    "markdown": "# 每日热点简报..."
  },
  "markdown": "# 每日热点简报...",
  "model": "deepseek-v4-flash",
  "tavilyUsed": true,
  "searchProvidersUsed": ["tavily"],
  "createdAt": "2026-07-05T06:00:00.000Z",
  "updatedAt": "2026-07-05T06:01:00.000Z"
}

默认不返回:

  • rawInputItems
  • searchEvidence

需要排查生成输入或搜索证据时,使用 includeDebug=true。

测试命令 ​

检查服务健康:

bash
curl http://localhost:6689/health

检查配置:

bash
curl http://localhost:6689/api/briefs/config

用财联社单源生成测试简报:

bash
curl -X POST http://localhost:6689/api/briefs/generate \
  -H 'Content-Type: application/json' \
  -d '{"sources":["cls"],"period":"manual-test","force":true}'

验证默认不返回调试字段:

bash
curl -s http://localhost:6689/api/briefs/latest | node -e '
let s="";
process.stdin.on("data", d => s += d);
process.stdin.on("end", () => {
  const x = JSON.parse(s);
  console.log({
    hasRawInputItems: Object.prototype.hasOwnProperty.call(x, "rawInputItems"),
    hasSearchEvidence: Object.prototype.hasOwnProperty.call(x, "searchEvidence")
  });
});
'

验证调试字段可按需返回:

bash
curl -s 'http://localhost:6689/api/briefs/latest?includeDebug=true' | node -e '
let s="";
process.stdin.on("data", d => s += d);
process.stdin.on("end", () => {
  const x = JSON.parse(s);
  console.log({
    rawInputItems: x.rawInputItems?.length,
    searchEvidence: x.searchEvidence?.length
  });
});
'

注意事项 ​

  1. 修改 .env 后需要重启服务。
  2. BRIEF_ENABLED=true 只控制服务启动时是否自动注册简报定时任务。
  3. 手动启停调度只在当前进程内生效,服务重启后会重新读取 .env。
  4. POST /api/briefs/generate 会调用热点源、配置的搜索平台和模型,耗时可能达到几十秒。
  5. includeDebug=true 可能返回较大的响应体,不建议前端常规使用。
  6. A 股关联由模型基于热点和搜索证据生成,代码会提示模型不确定时标记为 待验证,但展示层仍建议保留风险提示。
  7. 删除接口会直接删除 MongoDB 中的简报记录,操作前建议先用分页查询接口确认范围。

仅供技术研究与开发测试使用