Skip to content

财务报告 Agent

本页说明财务报告 Agent 如何在用户委托下从授权可见的财务或账单后台抽取报表数据,在应用侧对每个数字做结构化校验(period、currency、来源、抓取时间),写入带版本的报表数据库,并生成每个数字都能回溯到来源页面的周期报告。读完本页,你能理解只读财务数据需要哪些权限边界、为什么委托范围不含任何交易动作,以及口径与基线为什么进报表数据库而不是 Memory。

适用场景

分析团队需要按周期从多个财务或账单后台(SaaS 账单、支付平台、银行对账页)抽取数据汇总成周期报告,并结合公开财报与公告补充上下文。这些后台没有统一导出接口,人工逐个登录复制数字既慢又容易抄错,事后也说不清某个数字来自哪一页。

典型触发时机:

  • 月度或季度经营报告截止前,需要汇总各账单后台的费用与收入数字。
  • 关注公司发布财报或重大公告,需要更新分析摘要。
  • 审计或预算复核时,需要报告中每个数字对应的来源页面证据。

工程挑战

  • 口径一致性:同一个指标在不同后台的周期、币种和统计口径都可能不同;口径漂移的报告各期不可比,"环比涨了 12%"可能只是口径变了。
  • 数字可回溯:报告里的每个数字都要经得起"这个数从哪来"的追问。抄错一个数字或引用了错误页面版本,整份报告在审计面前失去支撑。
  • 后台异构与登录态:每个后台的报表结构、登录方式和 MFA 策略都不同;抽取逻辑要能在页面结构变化时按失败处理,而不是静默输出错误数字。

模块组合

模块角色说明
GenAuth核心财务后台登录态属于高风险委托:交互式授权、只读报表页、短时凭证、可撤销,且委托范围在签发时就不含任何交易能力。
Web Agent核心受控会话逐页抽取报表数字,每个数字保留来源 URL、截图和抽取时间;Profiles 复用登录态,公开财报与公告用 WebSearch 补充。
GUMem不使用数字、口径和周期基线是业务数据,进带版本的报表数据库,供各期对账与审计;Memory 不参与本场景。

财务报告 Agent 场景架构

权限与委托边界

Agent 本身不持有任何固有权限。每次取数任务的实际权限是三个集合的交集:用户真实权限 ∩ 显式委托范围 ∩ 企业批准边界。落到这个场景:

  • 委托范围只覆盖"读取指定财务或账单后台的报表页面",不包含任何交易、付款、退款、审批或账户设置动作。
  • 财务后台登录属于高风险操作,必须走 mode: 'interactive':用户在 Qoni Console 确认授权,凭证在服务端回调中兑换,不暴露给浏览器。
  • 委托凭证短时效,单期取数任务建议分钟级有效期;周期报告依靠按计划重新签发。
  • 越权尝试(例如访问付款或审批页面)会被拒绝并留痕——审计链覆盖全部尝试,不只是成功行为。

注意:SDK 示例申请的是产品级 scope(如 webagent.do_anything:read)。后台域名、报表页面范围这类细粒度边界由 GenAuth 的 Agent Profile 或策略层配置强制执行,不由任务 prompt 承担;本页示例未展示该配置。完整语义见 Delegate Token 与缩权

工作流程

财务报告 Agent 工作流程

  1. 用户选择报告周期和需要取数的财务后台清单。

  2. 应用发起交互式委托;用户在 Qoni Console 确认授权,服务端回调兑换凭证。

  3. 应用从报表数据库读取当前版本的报告口径(币种、指标定义、周期规则),注入任务描述。

  4. Web Agent 打开各财务后台;首次访问时用户在受控会话中完成登录,后续周期通过 Profiles 复用登录态。

    检查点:登录墙、验证码或风控页出现时,Web Agent 应升级给人处理,而不是静默绕过。

  5. Web Agent 逐页抽取报表数字,每个数字记录来源 URL、页面截图和抽取时间;公开财报与公告按需补充。

  6. 应用侧结构化校验每个数字:periodcurrencysourceUrlcapturedAt 任一缺失即标注 pending_verification,不冒充确认数据。

    检查点:报告中每个数字都应能回溯到具体来源页面;无法回溯的数字只能以待核验状态出现。

  7. 校验后的数字连同口径版本写入报表数据库;应用输出周期报告,附数字-来源对照表、待核验清单和 audit id。

示例代码

下面的示例使用官方 Qoni SDK@qoniai/qoni)接入这个场景:交互式委托(完整 callback)→ 从报表数据库读取带版本的报告口径 → 一次 doAnything.run() 完成只读取数 → 应用侧 parseFigures 结构化校验 → 写入报表数据库。

ts
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 顶层只返回通用的 RunResultrunIdstatusoutputartifacts 等)。

数据与记忆边界

这个场景涉及四类数据,没有一类属于 GUMem:

  • 版本化规则:报告口径(币种、指标定义、周期规则)——放在报表数据库里按版本管理,每期报告引用口径版本号,口径调整即版本升级。
  • 业务状态:报表数字、数字-来源对照、页面截图和各期基线——进报表数据库,供各期对账、审计与差异解释。
  • 审计记录:交互式授权、每次取数与越权拒绝构成的行为链——由 GenAuth 维护。
  • 用户 Memory:本场景不使用 GUMem。数字与口径是需要逐期对账的业务数据,不是用户偏好;写进 Memory 会失去版本对账能力。

失败处理

情况推荐处理
财务后台登录态失效任务挂起,通知用户重新登录,从断点继续取数。
报表页面结构变化导致抽取失败按失败处理并回放会话记录,该数据点标注缺失,不用估算值填充。
委托范围外的付款或审批页面请求直接拒绝并记录,事后可在审计链中查到未遂访问。
数字缺少 period、currency、来源或抓取时间应用侧校验标注 pending_verification,进入待核验清单,由人复核后再确认。

生产注意点

财务内容应保留来源和日期;Agent 输出不应作为投资建议。Agent 不执行任何交易、付款、退款或审批动作——委托范围在签发时就不包含这些能力。后台数字与公开财报口径冲突时,并列呈现两个来源与差异并标注待核验,不擅自取舍;报表数据库中的口径版本历史是解释各期差异的唯一依据。

下一步