Newsletter Curation Agent
本页说明 Newsletter Curation Agent 如何以静默委托巡检公开行业来源,按内容数据库中的已收录清单去重,生成待编辑确认的策展草稿;巡检是长任务,用 run.id 随时重连。读完本页,你能理解这个场景什么时候才需要交互式确认、候选内容如何收集与去重,以及已收录 URL 这类数据为什么应该放在内容数据库而不是 Memory。
适用场景
Marketing 团队需要定期从产品更新、博客、活动、行业新闻和客户故事中策划 newsletter。候选内容来源分散且更新频繁,人工逐源巡检既慢又容易漏;同一篇内容经不同渠道转载后还会重复出现,需要按来源去重。
典型触发时机:
- 新一期 newsletter 截稿日临近,需要产出候选内容清单和栏目草稿。
- 行业出现值得报道的动态,需要快速判断是否纳入本期。
- 栏目结构或选材标准调整后,需要按新配置重新筛选候选。
工程挑战
- 巡检是跨小时甚至跨天的长任务:候选来源多、更新时间不定,一次收集可能横跨部署重启和定时任务窗口,任务句柄必须可保存、可重连,不能因为进程退出就从头再来。
- 转载去重需要确定性依据:同一篇内容在不同渠道有不同 URL 和标题,去重要按规范化 URL 判定并保留原始来源,否则同一内容会在多期反复出现。
- 无来源内容混入候选池:聚合页和转载页经常丢失原始出处,缺少可访问链接和采集时间的候选无法支撑编辑决策,必须在入池前剔除。
模块组合
| 模块 | 角色 | 说明 |
|---|---|---|
| GenAuth | 核心 | 运行时以静默委托签发短时效凭证(所有产品调用必需);本场景默认无需交互式确认,接入登录态或写动作时才升级 interactive。凭证短时效、可撤销、留审计链。 |
| Web Agent | 核心 | 通过 WebSearch 巡检公开博客、新闻和行业来源,每条候选保留链接和采集时间;长任务保存 run.id 随时重连。 |
| GUMem | 不使用 | 已收录 URL 清单、栏目结构属于业务状态,放在你的内容数据库里按期次管理,不属于 Memory。本场景没有需要跨任务沉淀的用户个人偏好。 |
何时需要交互式确认
所有产品调用都需要 GenAuth 委托令牌;公开只读场景用静默委托即可。本场景的默认路径只读取公开网页:应用用与 Qoni 凭证绑定的 GenAuth 用户 ID 直接换取运行时凭证,不需要用户跳转确认。凭证是显式、短时效、可撤销的,只覆盖公开读取与任务执行;发送、群发和订阅者操作不在任何委托范围内。
需要升级为交互式确认(mode: 'interactive')的情况:
- 候选收集要接入登录态的内容池、CMS 或付费订阅源。
- 任务要执行写动作,例如把策展草稿写进 CMS 草稿箱。
升级方式与其他场景相同:mode: 'interactive' + redirectUri,用户在 Qoni Console 确认后由服务端 completeDelegateToken 兑换凭证,完整流程见 快速开始。
工作流程
用户选择 newsletter 期次、主题和候选来源范围。
应用以静默委托换取运行时凭证(公开读取,无需用户跳转确认)。
Web Agent 启动公开来源巡检长任务;应用保存
run.id,进程重启或定时任务窗口到来时用attach重连。巡检返回候选清单,每条候选带可访问链接和采集时间。
检查点:每条候选都应有可访问的来源链接和采集时间;无来源的条目不进入候选池。
应用从内容数据库读取已收录 URL 清单和栏目结构,注入策展任务。
Agent 按规范化 URL 对候选去重、合并转载并保留原始来源,再按栏目结构生成建议、摘要和标题草稿。
Agent 输出策展草稿和来源清单,附 audit id;编辑确认后,采纳与排除决定写回你的内容数据库。
检查点:草稿只到"待确认"为止;任何指向发送或群发的动作都应被拒绝并留痕。
示例代码
下面的示例使用官方 Qoni SDK(@qoniai/qoni)把这个场景接到你的服务端:静默委托 → webSearch.run() 启动巡检长任务并保存 run.id → 之后用 attach 重连取结果 → 一次 doAnything.run() 按内容数据库配置去重建稿 → 解析并校验候选。
import { Qoni, QoniScopes } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
export async function startCurationSweep(
userId: string,
newsletterId: string,
topics: string[],
) {
// 1. 静默委托:公开内容收集不涉及站点登录
// (接入登录态内容池或写动作时才升级 interactive)
const { data: grant } = await qoni.delegateToken({
user: { id: userId },
agent: 'newsletter-curation',
products: ['webSearch'],
scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
})
// 2. 启动公开来源巡检长任务,保存 run.id 以便随时重连
const sweep = await qoni.webSearch.run({
token: grant.token,
prompt: `
Find recent public posts and industry news about
${topics.join(', ')}. Record the link and collection time
for every candidate.
`,
maxResultsPerQuery: 8,
})
await saveSweepState(newsletterId, {
runId: sweep.id,
token: grant.token,
auditId: grant.auditId,
grantedScopes: grant.grantedScopes,
}) // 你的任务存储
return sweep.id
}
// 之后——进程重启、定时任务窗口或另一台实例——用 run.id 重连同一个长任务
export async function resumeCurationSweep(newsletterId: string) {
const state = await loadSweepState(newsletterId)
const sweep = qoni.webSearch.attach(state.runId, { token: state.token })
const candidates = await sweep.wait()
// 3. 从你的内容数据库读取已收录 URL 与栏目结构(业务数据,不是 Memory)
const config = await loadCurationConfig(newsletterId) // 例如 { publishedUrls: [...], sections: [...] }
// 4. 一次 doAnything 去重、筛选并生成栏目草稿
const run = await qoni.doAnything.run({
token: state.token,
prompt: `
Curate the next newsletter issue from these candidates.
Deduplicate by normalized URL, merging syndicated copies while
keeping the original source; skip anything already in the
published list. Draft section ideas, summaries and headlines
following the section layout. Return entries as a JSON array of
{ section, title, summary, sourceUrl, collectedAt } objects.
Do not send, bulk mail, or modify any subscriber list — stop at
a draft for review.
Public candidates: ${JSON.stringify(candidates.output)}
Published URLs: ${JSON.stringify(config.publishedUrls)}
Section layout: ${JSON.stringify(config.sections)}
`,
capture: { screenshots: true },
})
const result = await run.wait()
// 5. 应用侧解析并校验输出契约:缺少来源链接或采集时间的条目不进草稿
const entries = parseCurationDraft(result.output).filter(
(e) => e.sourceUrl && e.collectedAt,
)
return {
entries,
artifacts: result.artifacts,
audit: { auditId: state.auditId, permissionBoundary: state.grantedScopes },
}
}输出结构由任务描述约定:这里约定返回 { section, title, summary, sourceUrl, collectedAt } 数组,应用侧 parseCurationDraft 负责解析与校验,缺少 sourceUrl 或 collectedAt 的条目被直接丢弃。委托凭证短时效:如果重连时凭证已过期(QoniTokenExpiredError),先重新静默委托,再 attach 同一个 run.id。
数据与记忆边界
这个场景涉及四类数据,本场景不使用 GUMem:
- 版本化配置:栏目结构、选材标准——放在你的内容数据库或配置库里按期次管理,草稿引用配置版本。
- 业务状态:候选清单、已收录 URL、编辑的采纳与排除决定——写回你的内容数据库,用于下一期去重与复盘。
- 审计记录:
grantId与auditId构成的委托与行为链——由 GenAuth 维护。 - 用户 Memory(可选):只有用户明确确认的长期个人偏好才属于 GUMem;选材标准和栏目结构是团队级配置,不是个人记忆,本场景默认不召回也不写回。
失败处理
| 情况 | 推荐处理 |
|---|---|
进程重启或 wait() 超时 | 用保存的 run.id 调 attach 重连;任务在服务端继续执行,不重复启动。 |
| 重连时委托凭证已过期 | 重新静默委托换取新凭证,再 attach 同一个 run.id。 |
| 候选来源无法访问或内容已下线 | 从候选池剔除并记录原因,不引用无法核实的内容。 |
| 输出条目缺少来源链接或采集时间 | 应用侧校验直接丢弃该条目,并在草稿中标注丢弃数量。 |
生产注意点
不要自动发布或群发。外部来源需要保留链接和采集时间。策展草稿必须经编辑确认后才能进入发送流程;对外部来源的巡检频率应设置上限,避免对目标站点造成压力。长任务的 run.id 与凭证应存放在服务端任务存储中,不要下发给浏览器。
下一步
- 阅读 快速开始 跑通 Agent 身份与委托的最短路径。
- 阅读 授权与浏览器沙盒 了解受控会话的安全边界。
- 继续查看 社媒草稿 Agent 了解相邻场景。