财务报告 Agent
本页说明财务报告 Agent 如何在用户委托下从授权可见的财务或账单后台抽取报表数据,在应用侧对每个数字做结构化校验(period、currency、来源、抓取时间),写入带版本的报表数据库,并生成每个数字都能回溯到来源页面的周期报告。读完本页,你能理解只读财务数据需要哪些权限边界、为什么委托范围不含任何交易动作,以及口径与基线为什么进报表数据库而不是 Memory。
适用场景
分析团队需要按周期从多个财务或账单后台(SaaS 账单、支付平台、银行对账页)抽取数据汇总成周期报告,并结合公开财报与公告补充上下文。这些后台没有统一导出接口,人工逐个登录复制数字既慢又容易抄错,事后也说不清某个数字来自哪一页。
典型触发时机:
- 月度或季度经营报告截止前,需要汇总各账单后台的费用与收入数字。
- 关注公司发布财报或重大公告,需要更新分析摘要。
- 审计或预算复核时,需要报告中每个数字对应的来源页面证据。
工程挑战
- 口径一致性:同一个指标在不同后台的周期、币种和统计口径都可能不同;口径漂移的报告各期不可比,"环比涨了 12%"可能只是口径变了。
- 数字可回溯:报告里的每个数字都要经得起"这个数从哪来"的追问。抄错一个数字或引用了错误页面版本,整份报告在审计面前失去支撑。
- 后台异构与登录态:每个后台的报表结构、登录方式和 MFA 策略都不同;抽取逻辑要能在页面结构变化时按失败处理,而不是静默输出错误数字。
模块组合
| 模块 | 角色 | 说明 |
|---|---|---|
| GenAuth | 核心 | 财务后台登录态属于高风险委托:交互式授权、只读报表页、短时凭证、可撤销,且委托范围在签发时就不含任何交易能力。 |
| Web Agent | 核心 | 受控会话逐页抽取报表数字,每个数字保留来源 URL、截图和抽取时间;Profiles 复用登录态,公开财报与公告用 WebSearch 补充。 |
| GUMem | 不使用 | 数字、口径和周期基线是业务数据,进带版本的报表数据库,供各期对账与审计;Memory 不参与本场景。 |
权限与委托边界
Agent 本身不持有任何固有权限。每次取数任务的实际权限是三个集合的交集:用户真实权限 ∩ 显式委托范围 ∩ 企业批准边界。落到这个场景:
- 委托范围只覆盖"读取指定财务或账单后台的报表页面",不包含任何交易、付款、退款、审批或账户设置动作。
- 财务后台登录属于高风险操作,必须走
mode: 'interactive':用户在 Qoni Console 确认授权,凭证在服务端回调中兑换,不暴露给浏览器。 - 委托凭证短时效,单期取数任务建议分钟级有效期;周期报告依靠按计划重新签发。
- 越权尝试(例如访问付款或审批页面)会被拒绝并留痕——审计链覆盖全部尝试,不只是成功行为。
注意:SDK 示例申请的是产品级 scope(如 webagent.do_anything:read)。后台域名、报表页面范围这类细粒度边界由 GenAuth 的 Agent Profile 或策略层配置强制执行,不由任务 prompt 承担;本页示例未展示该配置。完整语义见 Delegate Token 与缩权。
工作流程
用户选择报告周期和需要取数的财务后台清单。
应用发起交互式委托;用户在 Qoni Console 确认授权,服务端回调兑换凭证。
应用从报表数据库读取当前版本的报告口径(币种、指标定义、周期规则),注入任务描述。
Web Agent 打开各财务后台;首次访问时用户在受控会话中完成登录,后续周期通过 Profiles 复用登录态。
检查点:登录墙、验证码或风控页出现时,Web Agent 应升级给人处理,而不是静默绕过。
Web Agent 逐页抽取报表数字,每个数字记录来源 URL、页面截图和抽取时间;公开财报与公告按需补充。
应用侧结构化校验每个数字:
period、currency、sourceUrl、capturedAt任一缺失即标注pending_verification,不冒充确认数据。检查点:报告中每个数字都应能回溯到具体来源页面;无法回溯的数字只能以待核验状态出现。
校验后的数字连同口径版本写入报表数据库;应用输出周期报告,附数字-来源对照表、待核验清单和 audit id。
示例代码
下面的示例使用官方 Qoni SDK(@qoniai/qoni)接入这个场景:交互式委托(完整 callback)→ 从报表数据库读取带版本的报告口径 → 一次 doAnything.run() 完成只读取数 → 应用侧 parseFigures 结构化校验 → 写入报表数据库。
import { Qoni, QoniScopes } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
// 1. 入口:发起交互式委托——涉及财务后台登录,让用户在 Qoni Console 确认授权
export async function startPeriodReport(userId: string, period: string, backendPages: string[]) {
// 任务参数存应用存储,state 只放任务 ID,回调时按 ID 取回
const taskId = await taskStore.save({ period, backendPages })
const { data: authorization } = await qoni.delegateToken({
mode: 'interactive',
agent: 'finance-report',
scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
redirectUri: 'https://app.example.com/qoni/callback',
state: taskId,
user: { id: userId },
expiresIn: 900, // 单期取数给分钟级有效期,周期报告按计划重新签发
})
redirectUserTo(authorization.authorizationUrl)
}
// 2. 用户同意后,在服务端回调中兑换委托令牌,并继续取数
export async function handleQoniCallback(request: Request) {
const query = new URL(request.url).searchParams
const { data: grant } = await qoni.completeDelegateToken({
grantId: query.get('grantId')!,
code: query.get('code')!,
state: query.get('state')!,
})
// 按 state 里的任务 ID 取回 startPeriodReport 存下的任务参数
const { period, backendPages } = await taskStore.load(query.get('state')!)
return extractPeriodReport(grant, period, backendPages) // grant.token 只保存在服务端
}
export async function extractPeriodReport(
grant: { token: string; auditId: string; grantedScopes: string[] },
period: string,
backendPages: string[],
) {
// 3. 取数前:从报表数据库读取当前版本的报告口径(不是 Memory)
const basis = await loadReportingBasis() // 例如 { version: '2026-Q2', currency: 'USD', metrics: [...] }
// 4. 一次调用完成只读取数:逐页抽取数字,每个数字带来源与截图
const run = await qoni.doAnything.run({
token: grant.token,
prompt: `
Extract report figures for period ${period} from these finance
backends: ${backendPages.join(', ')}.
Return figures as a JSON array of
{ metric, value, period, currency, sourceUrl, capturedAt } objects,
and keep a page screenshot for every figure. Strictly read only:
never perform any transaction, payment, refund, approval or
account-settings action.
Reporting basis (version ${basis.version}):
${JSON.stringify(basis)}
`,
capture: { screenshots: true },
})
const result = await run.wait({
// 登录墙 / MFA / 风控页:转发给用户,由人完成
onInteraction: (interaction) => notifyUserActionRequired(interaction),
})
// 5. 应用侧结构化校验:字段不全的数字标 pending_verification,不冒充确认数据
const figures = parseFigures(result.output).map((f) =>
f.period && f.currency && f.sourceUrl && f.capturedAt
? { ...f, status: 'verified_source' }
: { ...f, status: 'pending_verification' },
)
// 6. 数字与口径版本写入报表数据库(带版本),不写 Memory
await reportDb.saveFigures(period, figures, { basisVersion: basis.version })
return {
figures,
pending: figures.filter((f) => f.status === 'pending_verification'),
artifacts: result.artifacts, // 截图归档,供审计回溯
basisVersion: basis.version,
audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
}
}输出结构由任务描述约定:这里约定返回 { metric, value, period, currency, sourceUrl, capturedAt } 数组(Figure[]),应用侧 parseFigures 负责解析,四个校验字段任一缺失即标注 pending_verification。SDK 顶层只返回通用的 RunResult(runId、status、output、artifacts 等)。
数据与记忆边界
这个场景涉及四类数据,没有一类属于 GUMem:
- 版本化规则:报告口径(币种、指标定义、周期规则)——放在报表数据库里按版本管理,每期报告引用口径版本号,口径调整即版本升级。
- 业务状态:报表数字、数字-来源对照、页面截图和各期基线——进报表数据库,供各期对账、审计与差异解释。
- 审计记录:交互式授权、每次取数与越权拒绝构成的行为链——由 GenAuth 维护。
- 用户 Memory:本场景不使用 GUMem。数字与口径是需要逐期对账的业务数据,不是用户偏好;写进 Memory 会失去版本对账能力。
失败处理
| 情况 | 推荐处理 |
|---|---|
| 财务后台登录态失效 | 任务挂起,通知用户重新登录,从断点继续取数。 |
| 报表页面结构变化导致抽取失败 | 按失败处理并回放会话记录,该数据点标注缺失,不用估算值填充。 |
| 委托范围外的付款或审批页面请求 | 直接拒绝并记录,事后可在审计链中查到未遂访问。 |
| 数字缺少 period、currency、来源或抓取时间 | 应用侧校验标注 pending_verification,进入待核验清单,由人复核后再确认。 |
生产注意点
财务内容应保留来源和日期;Agent 输出不应作为投资建议。Agent 不执行任何交易、付款、退款或审批动作——委托范围在签发时就不包含这些能力。后台数字与公开财报口径冲突时,并列呈现两个来源与差异并标注待核验,不擅自取舍;报表数据库中的口径版本历史是解释各期差异的唯一依据。
下一步
- 阅读 快速开始 跑通 Agent 身份与委托的最短路径。
- 阅读 授权与浏览器沙盒 了解受控会话的安全边界。
- 继续查看 合规风险 Agent 了解相邻的审计型场景。