按关注话题生成晨间简报
此应用使用 Grok API 制作个人 AI 新闻简报,以简短的语音摘要播报关注话题的最新动态。同一方法也适用于其他定时调研任务:按固定时间范围搜索、记录已报道内容,并限制每次运行的支出。
选择几个话题,应用每天早晨会在 𝕏 和网页上搜索上次简报之后的新进展,跳过已播报的新闻,将其余内容整理为约两分钟的语音,并附上每条新闻的来源帖子和文章。网页版像播客一样播放当前话题,同时由 Grok 调研下一话题;终端版可按计划运行。
你将学到
用 X Search 的以下两个参数指定搜索时段:
from_date和to_date记录每期简报的内容,让 Grok 判断一条新闻是新事件、后续进展还是已经报道过的消息
根据引用只保留搜索实际返回的帖子和文章
实时显示每次搜索,使用 SDK 的
server_tool_call事件依据 API 对每个请求报告的费用控制单次运行支出
使用
client.voice.speak()和少量语音标签为各部分配音,同时调研下一话题通过 cron 定时运行,或使用
--every
运行示例
需要 Node.js 22.13 或更高版本。将 API 密钥保存在 .env(仓库根目录),或导出环境变量 XAI_API_KEY。
cd examples/topics-to-morning-briefing/typescript
npm install
npm run web打开 http://localhost:3000,点击 Brief me。页面初始话题来自 topics.txt,包括 SpaceX、一级方程式和 AI 芯片。点击 × 可移除话题,也可以输入新话题并按回车。页面类似播客播放器,会实时展示各话题的搜索过程,再列出新闻及对应的 𝕏 帖子和文章。第一个话题准备好便开始播放,高亮当前话题,逐词标出朗读进度。再次点击 Brief me 时,若已报道的新闻没有进展,应用会告诉你没有新消息。起始页面列出已记住的内容,可用 Forget them 清空。点击 Stop 或关闭页面即可取消本次运行。
在终端制作简报的方法如下:
npm start默认播报 topics.txt 中的话题,每行一个,也可以传入话题,例如 npm start -- "SpaceX" "Formula 1"。两个版本都将简报保存到 output/<time>/briefing.mp3,已报道的内容保存到 output/memory.json。终端版还会将含全部来源链接的节目说明写入 briefing.md,与 MP3 位于同一目录。
示例实测三个话题需一至两分钟,API 繁忙时最长约五分钟。费用约 0.45 美元:X Search 和网页搜索合计约 0.20 美元,token 约 0.20 美元,语音约 0.03 美元。没有新动态时费用约减半。X Search 按获取的帖子数计费,每 1,000 条 5 美元;网页搜索每 1,000 次调用 5 美元;语音每百万字符 15 美元。
定时运行
如果继续处理新话题预计会超过费用上限,应用便会停止启动新话题。上限默认为 1 美元,可通过 --max-cost 调整。网页版在 Cost cap 菜单中设置。要每天早晨 7 点生成简报,可用 crontab -e 添加如下条目。cron 执行命令时使用较短的 PATH,因此请将 /path/to/node/bin 替换为 dirname "$(which node)" 输出的目录:
0 7 * * * cd /path/to/xai-cookbook/examples/topics-to-morning-briefing/typescript && PATH="/path/to/node/bin:$PATH" npm start -- --max-cost 0.75 >> "$HOME/morning-briefing.log" 2>&1也可在终端持续运行并指定 --every。每次简报开始后,程序会在指定间隔后再次生成简报,直到按下 Ctrl+C:
npm start -- --every 1d工作原理
共享代码位于 src/briefing.ts,记忆逻辑位于 src/memory.ts。makeBriefing() 依次调研各个话题,通过回调报告每一步,并在调研下一话题的同时为当前话题配音:
loadMemory()读取output/memory.json,其中列出各期简报的时间、费用、调研话题和已报道新闻。searchWindow()为每个话题划定从上次报道当天(首次运行则从昨天)到今天的搜索时段。X Search 以 UTC 整日为单位,而to_date不计入范围,所以将结束日期设为明天才能涵盖今天。X Search 文档称范围包含to_date,但示例实测将to_date设为今天查询最新帖子时,没有返回今天的帖子。researchTopic()流式发出一个grok-4.7请求,使用xSearch({ from_date, to_date })和webSearch()并在text.format中设置 JSON Schema。提示词列出该话题过去一周报道过的新闻和上次简报的发布时间。Grok 为找到的新闻分别标记“新事件”“后续进展”或“旧闻”,附上来源帖子、文章和主播口播稿。由于 X Search 只接受日期,Grok 会根据上次播报时间判断哪些进展是新发生的,并经常用 𝕏 的since:运算符将查询精确到分钟。server_tool_call事件实时报告各次搜索。citation事件收集搜索返回的全部 URL;没有匹配 URL 的帖子或文章会被丢弃。主要工作由搜索完成,因此请求采用较低的推理强度。max_turns限制每个话题的搜索轮数,idleTimeout防止停滞的数据流阻塞定时任务。每个响应会通过
usage.cost_in_usd_ticks返回包含搜索在内的费用,SDK 将其换算为美元并存入usage.cost_usd。只有在请求结束后才能知道费用,不能处理到一半再停下话题。因此,应用按本次迄今最贵话题的费用估算,下个话题预计不超预算才会开始。若尚未处理任何话题,则使用上次简报的平均话题费用。片尾会报出被跳过的话题;下次简报将向前扩大这些话题的搜索范围。recordClip()会在每个话题的脚本写好后立即调用client.voice.speak()配音。Grok 可使用少量语音标签,例如在新闻之间插入[pause],或在某个词上使用<emphasis>。语音 API 会把不能识别的标签念出来,所以先通过 SDK 的stripInvalidSpeechTags()移除无效标签。Grok 偶尔也会在脚本中留下引用标记,如<citation id="web:23"/>,语音合成会将其读作“citation ID web 23”,因此用stripCitations()清除。语音 API 不报告费用,故按字符单价计算。片头和片尾文案由程序直接生成。所有话题完成后,程序会将片段合并为一个 MP3,并由
remember()把新事件和后续进展添加到记忆文件。中途停止不会改动记忆。
src/server.ts 会运行 makeBriefing() 来处理页面提交的话题,并通过服务器发送事件逐步报告进度。由于每次都会重写记忆文件,同时只运行一期简报。服务器给每个请求传入 AbortSignal,因此关闭页面或点击 Stop 都会取消进行中的任务。服务器只提供本次写入的音频文件,按字节范围传输以支持浏览器跳转。public/index.html 使用原生 HTML 和 JavaScript 显示这些事件,按顺序播放各片段。src/index.ts 在终端输出相同步骤并写入节目说明。
如需更换声音,请修改 VOICE,位置在 src/briefing.ts。client.voice.list() 可列出内置声音。