Quickstart:销售跟进助手
本页用一个企业销售团队的场景,说明企业应用如何按顺序调用 Qoni SDK:销售把权限委托给 Agent,Agent 通过 Web Agent 每天核对客户和邮件、起草跟进邮件。登录 Airtable 和 Gmail、两步验证、发信这些关键时刻,Agent 会把决定交还给销售;往来邮件和双方承诺由 DoAnything 自动记进 GUMem。
迈向智能体 Web
Qoni 的目标是让 Agent 成为 Web 世界的一等公民。模型负责思考,Web 是 Agent 行动的地方,Qoni 是两者之间的那一层。下图是这个分层:人把有范围、有期限的权限委托给 Agent,每一步都可审计;Agent 在云端托管浏览器里替人操作网页,登录状态在任务之间复用;做完之后,把结果交还给人。
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;下半部分是每天的跟进任务。
委托不打扰销售:每个工作日早上,你的应用以销售本人的身份静默取得当天的委托 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,并在应用服务端初始化客户端:
npm install @qoniai/qoniimport { 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。每个工作日早上,由你的调度器为每位销售执行一次。
// 这个场景用到的全部 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 的决定提交回来。这个场景会依次遇到四次交互,其中登录和两步验证只在第一次运行时出现,发信前确认每封邮件一次:
- Agent在受控浏览器中打开 Airtable 客户表
- 登录 Airtable销售在实时浏览器里用自己的账号登录,Agent 和你的应用都看不到密码。应用调用handle.openLogin()handle.confirmSignedIn()
- Agent读取 Alice 负责的客户,再打开 Gmail
- 登录 Gmail同样由销售本人登录。之后的任务里,DoAnything 会自动复用仍然有效的登录状态。应用调用handle.openLogin()handle.confirmSignedIn()
- AgentGoogle 发现是新设备,要求再次验证
- 两步验证销售在自己的手机上确认或输入验证码,再把浏览器交还给 Agent。应用调用handle.connectControl()handle.releaseControl()
- Agent逐位核对客户与 Gmail 往来,召回客户记忆,跳过今天已联系的客户,为 Northwind 起草回复
- 发信前确认确认内容写明客户、跟进原因和邮件草稿。销售确认后,Agent 才点「发送」。应用调用handle.confirm()handle.reject()
- Agent在原线程发出邮件,回写 Airtable,把这封邮件记进 GUMem,接着处理下一位客户
拿到第 ① 步当天的 Token 后,启动跟进任务:
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 长什么样、会出现几次,→ 标出你的应用要做的事。每日跟进和第 ③ 步的「上次聊到哪了」都用它。
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
}
}
}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 确认后再发。
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 后,第二天的静默委托会失败,任务不会启动。
下一步
- 阅读 Personal Agent,了解面向个人用户的 Agent 如何创建 Identity、绑定,并用同一套交互类型与用户协作完成任务。
- 阅读 Qoni SDK:交互请求与业务函数,了解每类交互的字段、动作与过期处理。
- 阅读 Memory 如何形成,了解 DoAnything 写进 GUMem 的记忆如何被提炼和召回。