跳到正文

Quickstart:销售跟进助手 ​

本页用一个企业销售团队的场景,说明企业应用如何按顺序调用 Qoni SDK:销售把权限委托给 Agent,Agent 通过 Web Agent 每天核对客户和邮件、起草跟进邮件。登录 Airtable 和 Gmail、两步验证、发信这些关键时刻,Agent 会把决定交还给销售;往来邮件和双方承诺由 DoAnything 自动记进 GUMem。

迈向智能体 Web ​

Qoni 的目标是让 Agent 成为 Web 世界的一等公民。模型负责思考,Web 是 Agent 行动的地方,Qoni 是两者之间的那一层。下图是这个分层:人把有范围、有期限的权限委托给 Agent,每一步都可审计;Agent 在云端托管浏览器里替人操作网页,登录状态在任务之间复用;做完之后,把结果交还给人。

迈向智能体 Web一个分层模型:人把有范围、可审计的权限委托给 Agent,Agent 在云端托管浏览器里操作 Web,再把结果交还给人。HTMLWebAgent人委托给 Agent每一步可审计结果交还给人云端托管浏览器没有 API 也能干活登录态跨任务复用并行执行网页任务

Qoni 是面向个人 AI 智能体的托管式基础设施,为 Agent 提供身份、行动和记忆三样东西:GenAuth 让 Agent 拥有绑定真人的独立身份,授权有范围、有期限,每一步可审计;Web Agent 让 Agent 在云端托管浏览器里操作真实网页,没有 API 也能干活;GUMem 记住用户说过什么、做过什么,不用教第二遍。三者通过同一个 Qoni SDK 接入。

场景 ​

销售 Alice 对团队的跟进助手说:

帮我连接 Airtable 和邮箱。每天核对客户列表与邮件往来,找出需要发 follow-up、但尚未跟进的客户,避免重复发送。发送前跟我确认;发送成功后,更新 Airtable 的跟进状态和联系时间,再处理下一位。

跨会话记住客户需求、偏好、双方承诺和下一步安排。我问「上次和这位客户聊到哪了?」时,能召回相关记忆、展示来源邮件,并继续跟进。

把这段话变成每天自动运转的跟进流程,Agent 需要三样东西:

需要什么为什么需要对应步骤
销售本人的委托Agent 以 Alice 的名义办事,委托限定它能做什么、多久内有效①
能与销售协作的执行用 Alice 自己的账号打开 Airtable 和 Gmail,核对、起草、发信、回写;遇到登录、两步验证和发信前确认时请 Alice 出手②
跨会话的客户记忆记住需求、偏好、承诺和下一步,避免重复跟进,并能指回来源邮件② ③

整体流程 ​

下图是整体逻辑:上半部分是委托,每天为每位销售静默取得当天的 Token;下半部分是每天的跟进任务。

委托每天静默取得
  1. 1GenAuth委托 Agent以销售本人的身份静默取得,不经过授权页
当天的 Token交给每日跟进任务
每天每个工作日一次

委托不打扰销售:每个工作日早上,你的应用以销售本人的身份静默取得当天的委托 Token,不经过授权页。随后的任务在受控浏览器里用销售本人的账号操作 Airtable 和 Gmail,销售只在发信前点一次确认。任务里读到的往来邮件和发出的承诺,由 DoAnything 自动写进 GUMem,下一次核对和「上次聊到哪了」都会用到。

前置条件 ​

  • Node.js 20.11 或更高版本。
  • 一组 Qoni AccessKey ID 和 Secret。这组 AccessKey 的权限策略需要允许本页申请的全部 Scope。
  • 每位销售在这组 AccessKey 绑定的 GenAuth 用户池里有账号,你的应用能拿到他的用户 ID。静默委托用它识别 Agent 代表哪位销售办事。
  • 每位销售有自己的 Airtable 账号(能访问团队的客户 Base)和 Gmail。第一次运行时,Agent 会请销售在实时浏览器里登录。
  • Airtable 客户表里有下面这些字段:
字段用途
Owner负责这位客户的销售邮箱。Agent 只处理销售自己的客户
Contact email客户联系人的邮箱,Agent 用它在 Gmail 里找往来
Stage销售阶段,例如 Proposal、Negotiation
Follow-up status、Last contacted跟进状态和最近联系时间。邮件发出后,Agent 把它们改为 Followed up 和当天
  • 一个能把请求推送到销售面前的界面,比如网页弹窗或手机推送。第 ② 步会用它展示实时浏览器和发信前的确认。

安装 SDK,并在应用服务端初始化客户端:

bash
npm install @qoniai/qoni
ts
import { Qoni, type InteractionHandle } from '@qoniai/qoni'

// 只在应用服务端初始化;AccessKey 不能下发到浏览器
const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
})

接入指南 ​

① 委托 Agent ​

这一步以 Alice 的身份为 Agent 取得当天的委托 Token。企业应用是受信的服务端,用静默委托直接取得,不经过授权页、也不打扰 Alice。每个工作日早上,由你的调度器为每位销售执行一次。

ts
// 这个场景用到的全部 Scope
const scopes = [
  'webagent.do_anything:manage', // 启动跟进任务、提交交互响应
  'webagent.site_login:request', // 在受控浏览器里打开 Airtable 和 Gmail 的登录页
  'webagent.site_login:confirm', // 保存销售的登录状态,下次免登
  'gumem.memory:read', // DoAnything 召回客户记忆
  'gumem.memory:write', // DoAnything 写入往来邮件和双方承诺
]

// 静默委托:以销售本人的身份取得当天的 Token;交互要等销售处理,有效期留足一个工作日
const grant = await qoni.genauth.delegateAgent({
  mode: 'silent',
  userId: '<rep-user-id>', // 销售的 GenAuth 用户 ID
  agent: 'sales-follow-up', // Agent 的审计标签,审计记录里都用它
  scopes,
  expiresIn: '8h',
})
console.log(grant.grantId, grant.auditId)
  • Airtable 和 Gmail 没有对应的 Qoni Scope:Agent 在受控浏览器里用 Alice 本人的账号操作它们,能看到、能改的范围,就是 Alice 在 Airtable 和 Gmail 里的权限。
  • webagent.site_login:* 让 Agent 能在受控浏览器里请 Alice 登录,并保存登录状态;gumem.memory:* 让 DoAnything 能读写 Alice 名下的客户记忆。全部可申请的 Scope 见 Qoni SDK:可申请的 Scopes。
  • 静默委托不经过授权页:Agent 最多能申请哪些 Scope,由这组 AccessKey 的权限策略决定;userId 决定 Agent 代表哪位销售办事。只为当前要处理的销售取 Token,不要拿别人的用户 ID 代为委托。

检查点:服务端记录了 grant.grantId 和 grant.auditId,用于之后的审计。

② 每天:用 DoAnything 发起跟进任务并处理交互 ​

这一步把 Alice 的那段话交给 Web Agent 执行。DoAnything 在受控浏览器里用 Alice 的账号打开 Airtable 和 Gmail,逐位核对客户、起草跟进邮件;遇到需要 Alice 出手的时刻,它会暂停并发起一次交互,等你的应用把 Alice 的决定提交回来。这个场景会依次遇到四次交互,其中登录和两步验证只在第一次运行时出现,发信前确认每封邮件一次:

  1. Agent在受控浏览器中打开 Airtable 客户表
  2. site_login等待销售仅首次运行
    登录 Airtable
    销售在实时浏览器里用自己的账号登录,Agent 和你的应用都看不到密码。
    应用调用handle.openLogin()handle.confirmSignedIn()
  3. Agent读取 Alice 负责的客户,再打开 Gmail
  4. site_login等待销售仅首次运行
    登录 Gmail
    同样由销售本人登录。之后的任务里,DoAnything 会自动复用仍然有效的登录状态。
    应用调用handle.openLogin()handle.confirmSignedIn()
  5. AgentGoogle 发现是新设备,要求再次验证
  6. take_control等待销售仅首次运行
    两步验证
    销售在自己的手机上确认或输入验证码,再把浏览器交还给 Agent。
    应用调用handle.connectControl()handle.releaseControl()
  7. Agent逐位核对客户与 Gmail 往来,召回客户记忆,跳过今天已联系的客户,为 Northwind 起草回复
  8. confirmation发信闸门 · 每封邮件一次
    发信前确认
    确认内容写明客户、跟进原因和邮件草稿。销售确认后,Agent 才点「发送」。
    应用调用handle.confirm()handle.reject()
  9. Agent在原线程发出邮件,回写 Airtable,把这封邮件记进 GUMem,接着处理下一位客户

拿到第 ① 步当天的 Token 后,启动跟进任务:

ts
const run = await qoni.doAnything.run({
  token: grant.token,
  memory: { namespace: 'sales-follow-up', citeSources: true }, // 往来邮件和承诺自动写进 GUMem,需要时自动召回
  prompt: `
    打开 Airtable 客户表 https://airtable.com/appAcmeSales ,只看 Owner 是我(alice@acme.com)的客户。
    逐位到 Gmail 里核对和客户的往来,满足任一条件就需要跟进:
    客户来信超过 2 个工作日没有回复;我方承诺的报价或资料已经到期;Proposal 阶段的客户 14 天没有往来。
    Last contacted 是今天、或线程里最新一封是我方 2 个工作日内发出的客户,一律跳过,避免重复发送。
    为每位需要跟进的客户起草一封回复原线程的邮件,只引用邮件里出现过的事实,发送前请我确认。
    确认后在原线程发出,把 Airtable 里的 Follow-up status 改为 Followed up、Last contacted 改为今天,再处理下一位。
    最后只返回 JSON:{ followedUp: [{ company, subject, threadUrl }], skipped: [{ company, reason }] }。
  `,
})

再在 run.wait() 中按类型处理交互。交互按"需要销售做什么"分成五种类型,和具体业务无关:登录(site_login)、人类接管(take_control)、问询用户(ask_user)、确认(confirmation)和信息补全(fill_form),每种类型的含义和可用方法见 Personal Agent:交互类型。处理函数的骨架和 Personal Agent 相同,下面的注释换成了这个场景的例子:本场景 说明这一次的 payload 长什么样、会出现几次,→ 标出你的应用要做的事。每日跟进和第 ③ 步的「上次聊到哪了」都用它。

ts
const handled = new Set<string>() // 已经处理过的交互 ID

async function handleInteraction(handle: InteractionHandle) {
  const request = handle.interaction
  // 同一个交互在创建、状态更新和事件回放时都会触发回调:只处理待处理的,并按 ID 去重
  if (request.status !== 'pending' || handled.has(request.id)) return
  handled.add(request.id)

  // 分支按本场景出现的顺序排列
  switch (request.type) {
    // 登录:请销售亲自登录 request.payload.sites 里的网站
    // 本场景:第一次运行时出现两次,每个网站一次;登录状态保存后,之后的任务不再出现。sites 先是
    //   [{ siteId: 'airtable', displayName: 'Airtable', loginUrl: 'https://airtable.com/login' }],再是
    //   [{ siteId: 'gmail', displayName: 'Gmail', loginUrl: 'https://accounts.google.com' }]
    case 'site_login': {
      const login = await handle.openLogin() // 打开受控登录浏览器,返回实时画面地址 login.liveUrl
      // → 把 login.liveUrl 推送给 Alice,她在实时画面里输入自己的密码,完成后点「我已登录」
      await handle.confirmSignedIn() // Alice 点「我已登录」后调用:DoAnything 保存登录状态,复查后继续核对客户
      break
    }

    // 人类接管:登录之外需要销售亲手操作浏览器,request.payload.reason 说明原因
    // 本场景:Google 每次要求两步验证时一次,通常是新设备或长时间未登录
    case 'take_control': {
      await handle.connectControl() // 接管浏览器
      // → 把 request.payload.liveUrl 推送给 Alice,她在实时画面里完成两步验证
      // await handle.refreshControl() // 实时画面过期时刷新
      await handle.releaseControl() // Alice 点「已完成」后调用:把浏览器交还给 Agent
      break
    }

    // 确认:request.payload.summary 写明需要销售同意的操作,结果只有允许或拒绝
    // 本场景:每封跟进邮件发出前一次。今天要跟进 5 位客户,就会依次出现 5 次。summary 写明
    //   客户、跟进原因和邮件草稿,例如「Northwind Labs · 承诺 10 月 3 日发的报价已到期 ·
    //   草稿:Hi Dana, following up on the quote…」
    case 'confirmation': {
      // → 你的应用展示 summary 里的客户和草稿,以及「发送」「不发」两个按钮
      await handle.confirm() // Alice 允许:Agent 在原线程发出,回写 Airtable,再处理下一位
      // await handle.reject() // Alice 拒绝:这封不发,直接处理下一位
      break
    }

    // 问询用户:request.payload.question 是问题,answerType 和 options 描述答案的结构
    // 本场景:Agent 每遇到一位拿不准的客户就问一次,例如同一家公司有两位联系人:
    //   question 是「Northwind Labs 跟进哪位联系人?」,answerType 是 'single_choice',options 是
    //   [{ value: 'dana@northwind.io', label: 'Dana Lee(采购)' },
    //    { value: 'sam@northwind.io', label: 'Sam Park(技术负责人)' }]
    case 'ask_user': {
      // → 你的应用按 answerType 画成单选,Alice 选中 Dana 后,提交这个选项的 value
      await handle.answer('dana@northwind.io')
      // await handle.skip() // Alice 不回答:Agent 跳过这位客户,第二天再核对
      break
    }

    // 信息补全:按 request.payload.fields 渲染表单,提交时以 field.name 为键
    // 本场景不会出现。如果你的任务需要销售补充资料,例如报价金额,Agent 会发起它,fields 是
    //   [{ name: 'quoteAmount', label: '报价金额', type: 'number', required: true }]
    case 'fill_form': {
      // → 你的应用按 fields 渲染表单,销售填好后提交
      await handle.submit({ quoteAmount: 18000 })
      // await handle.skip() // 销售不填:Agent 跳过需要这项资料的步骤
      break
    }
  }
}
ts
const result = await run.wait({ onInteraction: handleInteraction })
console.log(result.status, result.output)

同一类交互会出现多次:

  • 每一次交互都有自己的 request.id 和 payload。这个场景里重复最多的是 confirmation:每封跟进邮件一次,今天要跟进几位客户就出现几次;第一次运行时 site_login 也会出现两次,一次 Airtable、一次 Gmail。
  • 交互按顺序一个一个出现:Alice 处理完一封邮件的确认,Agent 才会发出这封、回写 Airtable,再去起草下一封。你的界面不会同时收到两封待确认的邮件。
  • 同一个交互也会多次触发回调:创建、状态从 pending 变成 active 和 resolved,以及断线重连后的事件回放,都会再次调用 onInteraction。所以处理函数开头只处理 pending 状态,并记下处理过的 request.id,避免同一封邮件被推送两次或重复发送。你的界面同样应该按 request.id 更新同一张卡片,而不是每次回调新建一张。

几点说明:

  • 登录状态自动复用:Alice 点「我已登录」后,DoAnything 自动保存登录状态;之后每次运行,它自己判断能否复用,仍然有效就直接进入;失效了才会再次发起 site_login。所以通常只有第一次运行时会出现登录和两步验证,之后只剩发信前确认。
  • 避免重复发送:任务描述里写明了两条跳过规则,Agent 在浏览器里逐条核对 Airtable 的 Last contacted 和 Gmail 线程里的最新一封。开启 memory 后,发出过的邮件也会记进 GUMem,换了会话也不会重复跟进。
  • 记忆:memory 让 DoAnything 自动写入任务里读到的往来邮件、发出的邮件和双方承诺,并按客户归档;核对每位客户前自动召回。比如客户说过「下月初再联系」,Agent 就不会提前跟进。整个过程不需要你调用 GUMem 的方法。
  • 等待与过期:交互要等 Alice 处理,可能隔几个小时。交互过期时,Agent 跳过这位客户继续往下,第二天再核对。

检查点:

  • result.status 为 succeeded,result.output.followedUp 列出了今天发出的跟进,skipped 写明了每位跳过的原因。
  • 每位跟进过的客户,Airtable 里是 Followed up,Last contacted 是今天;当天再运行一次,不会重复发信。

③ 跨会话:「上次和这位客户聊到哪了?」 ​

这一步回答 Alice 随时会问的问题。启动一个开启了同一个 memory 的任务,DoAnything 会先从 GUMem 召回这位客户的记忆,需要时再打开 Gmail 补充最新往来;要继续跟进,同样会请 Alice 确认后再发。

ts
const ask = await qoni.doAnything.run({
  token: grant.token,
  memory: { namespace: 'sales-follow-up', citeSources: true },
  prompt: `
    上次和 Northwind Labs 聊到哪了?列出客户的需求和偏好、双方的承诺和下一步,每一条附上来源邮件。
    如果现在需要跟进,起草一封回复原线程的邮件,发送前请我确认。
    只返回 JSON:{ summary, items: [{ text, sourceUrl }], nextStep }。
  `,
})

const answer = await ask.wait({ onInteraction: handleInteraction })
console.log(answer.output)
  • citeSources: true 让每一条记忆都带上来源,sourceUrl 直接指向 Gmail 里的原邮件,你的应用把它显示成链接即可。
  • 「继续跟进」不需要另起流程:Agent 起草好邮件后发起 confirmation,由第 ② 步的同一个 handleInteraction 处理。
  • 记忆只装往来邮件和从中提炼的需求、偏好、承诺和下一步。跟进状态和联系时间只在 Airtable,GUMem 不复制客户状态。

检查点:换一个新会话问「上次和 Northwind 聊到哪了?」,回答里有双方承诺和下一步,每一条都能打开对应的 Gmail 邮件。

检查结果 ​

上线前,确认以下几点:

  • 第一次运行时,Alice 依次处理了登录 Airtable、登录 Gmail 和两步验证;之后的任务直接复用登录状态,只剩发信前确认。
  • 每封邮件都经过 Alice 确认;被拒绝的邮件不会发出。
  • 连续运行几天,同一封来信不会引出两封跟进,Airtable 里的跟进状态和联系时间与 Gmail 一致。
  • 服务端保存了 grant.grantId、grant.auditId 和每次任务的 runId,可以在 Qoni Console 中把每封邮件追溯到 Alice 的哪一次授权。
  • 停用 Alice 的 GenAuth 账号,或从 AccessKey 权限策略里收回 Scope 后,第二天的静默委托会失败,任务不会启动。

下一步 ​