每日简报功能说明
每日简报用于从配置的数据源中抓取热点内容,结合可配置的网页搜索平台和 OpenAI SDK 生成结构化产业简报。重点面向财经、产业链和 A 股关联分析。
功能概览
生成链路:
定时或手动触发
-> 读取 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 中是启用状态。被禁用的来源不会被抓取,该来源只能贡献回看窗口内的历史条目,调度停抓之后这部分数据也会逐渐枯竭。这种情况会在生成日志里给出告警:
WARN [DailyBriefService] Brief source cls contributed no fresh items: Source 'cls' is disabled同一条日志也用于区分"来源确实没有数据"和"来源已损坏"。核对来源状态:
curl http://localhost:6689/api/scheduler/configs启用某个来源:
curl -X PUT http://localhost:6689/api/scheduler/config/cls \
-H 'Content-Type: application/json' \
-d '{"enabled":true}'另外 IGNORE_SAVE_SOURCES 中的来源仍会被抓取并参与简报,只是不写入历史库。
环境变量
# 每日简报配置
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_KEY | OpenAI SDK 使用的 API Key |
OPENAI_API_BASE_URL | 兼容 OpenAI 协议的 API Base URL |
AI_MODEL | 生成简报使用的模型 |
AI_TIMEOUT_MS | 单次 AI 请求超时(毫秒),默认 120000 |
AI_MAX_RETRIES | AI 请求失败重试次数,默认 2,设为 0 关闭重试 |
TAVILY_API_KEY | Tavily 搜索 API Key |
TAVILY_MAX_RESULTS | 每个搜索请求最多返回多少条结果 |
TAVILY_TIMEOUT_MS | 单次 Tavily 搜索超时(毫秒),默认 10000 |
DEEPSEEK_SEARCH_API_KEY | DeepSeek Search API Key;与 OPENAI_API_KEY 独立配置,可填相同 Key |
DEEPSEEK_SEARCH_MAX_RESULTS | DeepSeek 每次搜索的返回条数,默认 5,取值 1–50 |
DEEPSEEK_SEARCH_TIMEOUT_MS | 单次 DeepSeek 搜索超时(毫秒),默认 10000 |
数据结构
简报保存在 MongoDB 的 dailybriefs collection 中,核心字段包括:
{
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 接口
基础地址示例:
http://localhost:6689写接口没有内建鉴权
服务本身不对任何接口做鉴权。下列接口会消耗真实费用或删除数据,必须在反向代理 / 网关层限制来源,不要直接暴露到公网:
| 接口 | 风险 |
|---|---|
POST /api/briefs/generate | 触发 AI 与配置的搜索平台调用,可能产生费用 |
DELETE /api/briefs/:date | 删除指定日期的简报 |
DELETE /api/briefs/history | 按条件批量删除历史简报 |
POST /api/briefs/scheduler/startPOST /api/briefs/scheduler/stopPOST /api/briefs/scheduler/reconfigure | 改变定时任务运行状态 |
GET 接口只读,可以按需开放给前端。
获取简报配置
curl http://localhost:6689/api/briefs/config响应示例:
{
"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"]
}手动生成简报
生成正式日报:
curl -X POST http://localhost:6689/api/briefs/generate \
-H 'Content-Type: application/json' \
-d '{"force":true}'指定数据源和 period,用于测试:
curl -X POST http://localhost:6689/api/briefs/generate \
-H 'Content-Type: application/json' \
-d '{"sources":["cls"],"period":"manual-test","force":true}'请求参数:
| 字段 | 类型 | 说明 |
|---|---|---|
date | string | 可选,格式 YYYY-MM-DD |
period | string | 可选,默认 daily |
sources | string[] | 可选,覆盖 .env 中的 BRIEF_SOURCES |
force | boolean | 可选,是否强制重新生成 |
默认返回前端可用字段,不包含 rawInputItems 和 searchEvidence。
获取最新简报
curl http://localhost:6689/api/briefs/latest不传 period 时,返回所有成功简报中最新的一条。
只获取正式日报:
curl 'http://localhost:6689/api/briefs/latest?period=daily'获取调试字段:
curl 'http://localhost:6689/api/briefs/latest?includeDebug=true'获取指定日期简报
curl 'http://localhost:6689/api/briefs/2026-07-05'指定 period:
curl 'http://localhost:6689/api/briefs/2026-07-05?period=manual-test'包含调试字段:
curl 'http://localhost:6689/api/briefs/2026-07-05?period=manual-test&includeDebug=true'分页查询简报
curl 'http://localhost:6689/api/briefs?page=1&limit=10'按 period 查询:
curl 'http://localhost:6689/api/briefs?period=daily&page=1&limit=10'按状态查询:
curl 'http://localhost:6689/api/briefs?status=success'获取历史股票出现次数排名
统计成功简报中 analysis.topics[].aShareMapping[] 的股票出现次数,并按次数从高到低返回排名:
curl 'http://localhost:6689/api/briefs/statistics/stocks'支持按 period、简报日期范围和返回数量筛选:
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 及以后才有的算子,也用白名单确保不会悄悄引入未经确认的新算子。改动聚合管道后请保证该测试通过。
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
period | string | 全部 | 只统计指定类型的成功简报 |
startDate | string | 不限制 | 起始简报日期,格式 YYYY-MM-DD,包含当天 |
endDate | string | 不限制 | 截止简报日期,格式 YYYY-MM-DD,包含当天 |
limit | number | 50 | 返回排名数量,范围 1-200 |
响应示例:
{
"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 的简报:
curl -X DELETE http://localhost:6689/api/briefs/2026-07-05只删除某一天的指定 period:
curl -X DELETE 'http://localhost:6689/api/briefs/2026-07-05?period=manual-test'响应示例:
{
"mode": "date",
"briefDate": "2026-07-05",
"period": "manual-test",
"deletedCount": 1
}批量清理历史简报
按保留周期清理:
# 删除早于 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'按截止日期清理:
# 删除 briefDate 小于 2025-07-05 的简报
curl -X DELETE 'http://localhost:6689/api/briefs/history?beforeDate=2025-07-05'只清理指定 period:
curl -X DELETE 'http://localhost:6689/api/briefs/history?olderThan=1y&period=daily'响应示例:
{
"mode": "olderThan",
"olderThan": "1y",
"beforeDate": "2025-07-05",
"period": "daily",
"deletedCount": 12
}olderThan 支持:
30d:30 天前1m:1 个月前1y:1 年前month/one-month:等价于1myear/one-year:等价于1y
简报调度状态
curl http://localhost:6689/api/briefs/scheduler/status响应示例:
{
"isRunning": false,
"enabled": true,
"cronExpression": "0 12 * * *",
"timezone": "Asia/Shanghai",
"cronJobExists": true
}启停简报调度
启动:
curl -X POST http://localhost:6689/api/briefs/scheduler/start停止:
curl -X POST http://localhost:6689/api/briefs/scheduler/stop重新加载 cron 配置:
curl -X POST http://localhost:6689/api/briefs/scheduler/reconfigure前端响应字段
默认响应适合前端直接使用,核心字段如下:
{
"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"
}默认不返回:
rawInputItemssearchEvidence
需要排查生成输入或搜索证据时,使用 includeDebug=true。
测试命令
检查服务健康:
curl http://localhost:6689/health检查配置:
curl http://localhost:6689/api/briefs/config用财联社单源生成测试简报:
curl -X POST http://localhost:6689/api/briefs/generate \
-H 'Content-Type: application/json' \
-d '{"sources":["cls"],"period":"manual-test","force":true}'验证默认不返回调试字段:
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")
});
});
'验证调试字段可按需返回:
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
});
});
'注意事项
- 修改
.env后需要重启服务。 BRIEF_ENABLED=true只控制服务启动时是否自动注册简报定时任务。- 手动启停调度只在当前进程内生效,服务重启后会重新读取
.env。 POST /api/briefs/generate会调用热点源、配置的搜索平台和模型,耗时可能达到几十秒。includeDebug=true可能返回较大的响应体,不建议前端常规使用。- A 股关联由模型基于热点和搜索证据生成,代码会提示模型不确定时标记为
待验证,但展示层仍建议保留风险提示。 - 删除接口会直接删除 MongoDB 中的简报记录,操作前建议先用分页查询接口确认范围。