# WebAgent — 完整文档(中文)
---
# WebAgent
WebAgent 是 Qoni 的网页行动层,用于让智能体在开放网页上完成搜索、抽取、浏览器操作、追踪和受控任务执行。读完本组文档后,你可以为自己的应用接入 WebAgent,创建 session,提交 run,并通过事件流获取执行结果。
WebAgent 不是传统爬虫 SDK。它面向的是 LLM 智能体的任务执行链路:开发者给出 instruction,WebAgent 管理运行时资源、页面状态、重试、结构化结果和 run 生命周期。Console 和 SDK 都只是同一组 API 的客户端。
## Toward the agentic web
Qoni 的目标是**让智能体成为 Web 的一等公民**。WebAgent 不只是在后台模拟点击,也不只把页面渲染给人看;它把用户、智能体和 Web 放进同一个可审计的协作层,让智能体能在授权边界内读取、行动、产出结果,并在需要时把状态渲染回用户。
## 什么时候使用 WebAgent
- 你的智能体需要访问实时网页数据,而不是只依赖模型训练数据或固定知识库。
- 你需要把搜索、网页抽取、浏览器动作和长程任务封装成可审计的 API。
- 你希望 Console、SDK 和后端服务都走同一份 REST API 契约。
- 你需要通过 session / run 模型保存运行状态、订阅事件流,或处理需要人工确认的步骤。
## 什么时候不要使用 WebAgent
- 任务只需要调用你自己的后端 API,不需要访问开放网页。
- 你需要的是大规模离线爬取、数据仓库同步或搜索引擎索引构建。
- 目标站点的使用条款不允许自动化访问,且你没有取得必要授权。
- 你还没有定义 API key、project scope、run 预算和失败重试策略。
## 核心能力
| 能力 | 说明 |
| --- | --- |
| DoAnything API | 给出自然语言 instruction,由 WebAgent 选择工具、执行步骤并返回结果。 |
| Shaped APIs | 对 DeepResearch、WebSearch、Track 等固定产物形态提供更明确的 API 契约。 |
| Session / Run model | 用 session 持有运行时资源,用 run 表达一次任务或后续动作。 |
| Event stream | 通过 SSE 订阅 run 状态、输出片段、错误和人工确认请求。 |
| SDK and raw HTTP | Python、TypeScript 和 cURL 文档使用同一份 API 语义。 |
## 文档入口
- [什么是 WebAgent](/webagent/getting-started/what-is-webagent):理解 WebAgent 的定位、边界和 API 形态。
- [快速开始](/webagent/getting-started/quickstart):用 Python、TypeScript 或 cURL 跑通第一个 run。
- [鉴权与 API key](/webagent/getting-started/authentication):了解 `wa_` key、project scope 和轮换策略。
- [DoAnything](/webagent/features/do-anything):理解 session、run、event 和 profile 的生命周期。
- [错误与重试](/webagent/reference/errors-and-retries):查看错误码、重试策略和幂等处理。
- [API 参考](/webagent/reference/):查看 base URL、鉴权、错误、限流和分页约定。
- [Vibecoding](/webagent/guides/vibecoding):把文档和 OpenAPI 交给 IDE 中的 LLM,生成集成代码。
---
# 什么是 WebAgent
WebAgent 是浏览器环境下的通用 Agent 平台——通过 API 让程序在真实浏览器里完成任务,小到一次原子搜索,大到运行一个月的长程目标。
## 四个 API
WebAgent 对外是 **4 个并列的 API**,按你要做什么直接选其一:
- **DoAnything** —— 给一句自然语言 instruction,agent 自己挑工具、定路径,在浏览器里把任务做完。产物形态不定。
- **DeepResearch** —— 给一个研究主题,做多轮检索 + 交叉核对,产出带引用和置信度的报告(`final.md` + citations + confidence)。
- **WebSearch** —— 给一批查询,跨引擎取结果、去重、重排,返回结构化搜索结果(可选摘要)。
- **Track** —— 给一个监控意图,按计划反复取数、和基线比对,变化时通知你(snapshot 序列 + 变更通知)。
公共能力:
- **Profiles** —— 跨 session 复用登录态。不用每次重新登录
- **Workspaces** —— 持久文件系统,agent 可读可写
- **Schedules** —— cron / interval / event / autonomous(让 agent 自己决定何时再跑)
- **SSE 事件流** —— Console 用的同一套 `run.*` 事件,直接推到你代码里
## 它不是什么
- 不是 low-code 自动化平台。没有可视化画布。任务是用代码(或 Console 当作原型工具)写出来的
- 不是托管 LLM API。你把任务给 WebAgent,它替你挑模型并按 credits 计费
## 三种产品形态:Console / OpenAPI / SDK
这些 API 都通过同一组三种形态对外提供,三者能力 1:1 等价、共享同一份资源面 / 事件流 / 计费:
| 你用 …… | 来做 …… |
|---|---|
| [REST API(OpenAPI)](/webagent/reference/) | 任何事。Console 与 SDK 都只是它的客户端。`api.eak.eazo.ai/v1/...` + `Authorization: Bearer wa_...` |
| Python / TypeScript [SDK](/webagent/sdk/python) | 同样的 API,更地道的类型 + 重试 + 流式 + `wait_for_done` |
| [Console](https://dashboard.qoni.ai) | 可视化原型;非开发者也能用;*Get Code* 对话框直接给可运行片段 |
**API 开发者优先**——产品本质是一组 API;Console 是便利层,不是独立产品形态;不给 Console 特权端点。
## 资源模型
4 个 API 共享同一套事件流和计费,但各自的资源模型不同。DoAnything 用 **session + run**:
```
Session (一个容器;持有浏览器、profile、workspace)
└── Run #1 status: completed (一条 instruction;生命周期 7 态)
└── Run #2 status: running (在同 session 里追加一条 instruction)
└── events: SSE 流 (status_changed, message, action.*, screenshot, …)
```
**Session** 持有运行时资源(浏览器、profile、workspace)。每个 **run** 是一条 instruction;同一 session 里追加 run 会共享前一条 run 留下的状态。Run 有 7 个状态:`pending` / `running` / `awaiting_input` / `paused` / `done` / `failed` / `canceled`,详见 [DoAnything](/webagent/features/do-anything)。
DeepResearch / WebSearch / Track 的资源模型不同:DeepResearch / WebSearch 是 standalone run(一次性产出、不暴露 session);Track 用长寿命的 monitor。各自的功能页有详述。
## 接下来读哪
- [Quickstart](/webagent/getting-started/quickstart) —— 5 分钟从注册到看到第一个 SSE 事件
- [鉴权与 API key](/webagent/getting-started/authentication) —— `wa_` key 怎么用、怎么轮换
- [DoAnything](/webagent/features/do-anything) —— 核心资源模型
---
# 5 分钟上手
5 分钟。注册账号、装 SDK、起一个 run、看到事件流回来。
## Step 0 —— 拿到 API key(30 秒)
1. 在 [dashboard.qoni.ai](https://dashboard.qoni.ai) 注册
2. **Settings → API Keys → Create**
3. 复制 `wa_…`。**只显示一次。**丢了只能撤销重建
```bash
export WEBAGENT_API_KEY=wa_xxxxxxxxxxxxxxxxxxxxxxxx
export WEBAGENT_PROJECT_ID=proj_xxxxxxxxxxxxxxxxxxxxxxxx
```
::: tip Project ID
所有 project-scoped 路径都以 project 为隔离单位:`/v1/projects/{pid}/…`(DoAnything / WebSearch / Track)。在 Console URL 里**Project Switcher → 你的 project**就能找到。Standalone 端点(DeepResearch)的 project 由 Bearer token 解析。
## Step 1 —— 装 SDK(30 秒)
```bash
pip install web-agent-sdk
```
```bash
npm install @web-agent/sdk
```
```bash
# 不用装
```
## Step 2 —— 跑一个任务(90 秒)
WebAgent 是 **4 个并列的 API**——DoAnything、DeepResearch、WebSearch、Track,按你要做什么直接选其一。
下面用 **DoAnything** 走一遍完整流程作演示——4 个 API 共用同一套机制:`Client` → 起任务 → 订阅事件流到终态。其余 3 个见 Step 3。
`Client` 起一个 DoAnything session,订阅事件流到终态。
```python
import asyncio
from web_agent.v1 import Client
from web_agent.v1.types import CreateSessionRequest
async def main():
async with Client(
api_key="wa_demo_xxxxxxxxxxxxxxxx",
project_id="proj_demo_0001",
) as client:
session = await client.sessions.create(CreateSessionRequest(
instructions="搜 Hacker News 今天 Top 5 的故事,列成 list 返回。",
))
run = session.runs[0]
async for event in client.events.stream(session.id, run.id):
print(event.type, event.data)
if event.type == "run.completed":
break
asyncio.run(main())
```
```typescript
import { Client } from "@web-agent/sdk";
const client = new Client({
apiKey: "wa_demo_xxxxxxxxxxxxxxxx",
projectId: "proj_demo_0001",
});
const session = await client.sessions.create({
instructions: "搜 Hacker News 今天 Top 5 的故事,列成 list 返回。",
});
const run = session.runs[0]!;
for await (const event of client.events.stream(session.id, run.id)) {
console.log(event.type, event.data);
if (event.type === "run.completed") break;
}
```
```bash
curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"instructions": "搜 Hacker News 今天 Top 5 的故事,列成 list 返回。"
}'
```
::: tip 同样的代码 Console 也能给你
Console 的 **Get Code** 对话框输出的就是这段——带你的真实 API key 和当前表单值。打开 `dashboard.qoni.ai/new`,填表,点 **Get Code**,就省掉了打字。
## Step 3 —— DeepResearch / WebSearch / Track(60 秒)
这 3 个 API 跟 DoAnything 共用同一套 `Client` / 端点 / 错误信封。要研究报告 / 搜索结果 / 变更监控时,直接用对应的那个:
```python
async with Client(api_key="wa_...", project_id="proj_demo") as client:
run = await client.deep_research.run(
topic="2026 年开源向量数据库格局",
depth="deep",
)
print(run["run_id"])
```
```python
async with Client(api_key="wa_...", project_id="proj_demo") as client:
# wait=true 默认,同步阻塞 ≤30s
result = await client.web_search.run(
queries=["best Python ORM 2026"],
)
for hit in result["results"]["results"]:
print(hit["title"], hit["url"])
```
```python
async with Client(api_key="wa_...", project_id="proj_demo") as client:
mon = await client.track.create(
intent="苹果股价跌破 $200 时通知我",
schedule={"kind": "interval", "interval_seconds": 3600},
notify_channel={"kind": "callback_url", "url": "https://hooks.example.com/track"},
)
print(mon["id"])
```
这 3 个 API 的端点 / 字段 / 错误信封都跟 DoAnything 走同一份契约——见 [Python SDK](/webagent/sdk/python) / [TypeScript SDK](/webagent/sdk/typescript)。
## Step 4 —— 在 Console 看一眼(30 秒)
打开 `https://dashboard.qoni.ai/sessions/`(用 step 2 打印的 id)。你会看到刚才那个 run 的 chat + Live Preview iframe——和 SSE 流里的内容一样,可视化呈现。
## 接下来呢
- [让 run 中途问你确认](/webagent/features/do-anything#input-request) —— `run.input_request`
- [跨 session 保存登录态](/webagent/features/do-anything#profiles) —— Profiles
- [每天早上跑一次](/webagent/reference/) —— Schedules
- [翻完整 API](/webagent/reference/) —— 每个端点每个字段
## 排错
| 现象 | 原因 | 解 |
|---|---|---|
| `401 unauthorized` | key 错 / 过期 / 撤销 | **Settings → API Keys** 重建 |
| `402 insufficient_credits` | 免费额度耗尽 | **Settings → Billing → Add credits** |
| SSE 流卡 60s+ | 网络断 / proxy 缓冲 | 带 `Last-Event-ID` 重连,详见 [Events & SSE](/webagent/features/do-anything#events) |
| `429 rate_limit_exceeded` | 短时间 burst | 退避重试;Dev plan 默认 5–10 并发 |
---
# 鉴权与 API key
本页说明 WebAgent 的 API key 形态、每次请求如何携带、以及创建、撤销、轮换的流程。
每次请求带 bearer token:
```http
Authorization: Bearer wa_xxxxxxxxxxxxxxxxxxxxxxxx
```
## Key 形态
- **前缀** `wa_`,"web agent" 缩写(仿 Stripe `sk_*`)
- **长度** 前缀后 28+ 字符。当作不透明 token
- **范围** 单 project;多个 project 各创建一个 key
- **可见性** 创建时**只显示一次**;丢了只能撤销重建
- **撤销** 软删除 + 1 小时 grace,不让在跑的 run 中途 401
## Key 放哪
- **本地开发** 环境变量、`.env.local`(已在 `.gitignore` 里)
- **生产** 用你的 secret manager(Vault / AWS Secrets Manager / GCP Secret Manager / ……)
- **永远不要**提交进 git。Console 的 *Get Code* 对话框默认用占位符
## 多 project
一个用户可以有多个 project。API path 把 project 直接编进去:
```http
GET /v1/projects/{project_id}/do_anything/sessions
```
没有 `X-Project-Id` header。path 把租户写明——一条 curl 打错 project ID 就是不同的 URL,不会静默跨租户。
## 轮换
可以同时持有两个有效 key:
1. 在 **Settings → API Keys** 创建新 key
2. 用新 key 部署
3. 撤销旧 key。旧 key 还能用 1 小时;部署完成后自动 401
## 错误
| 状态码 | code | 含义 |
|---|---|---|
| 401 | `unauthorized` | 缺失 / 错误 / 过期 / 撤销超 grace |
| 403 | `forbidden` | key 有效但无权访问该 project |
| 429 | `rate_limit_exceeded` | per-key 并发或频率上限 |
## 接下来
- [计费与额度](/webagent/getting-started/pricing) —— 每个 run 多少钱
- [DoAnything](/webagent/features/do-anything) —— 一次请求实际创建什么
---
# 计费与额度
WebAgent 按 **credits**(预付 USD 余额)计费。每个 run 跑的时候余额会扣。Console 顶部 header 实时显示 `$X.XX`,hover 展开拆账。
## 双桶 credits
| 桶 | 来源 | 过期 |
|---|---|---|
| **Monthly** | 订阅自带的 monthly credits | 每个计费周期末重置 |
| **Additional** | 一次性充值 + auto-recharge | **永不过期** |
每个 run 先扣 monthly,再扣 additional——这样月底重置不会吞掉你充的钱。
## 四桶 cost 拆账
每个 run 单独记 4 个 cost 字段,方便你做仪表盘:
- `llm_cost_usd` —— LLM token 消耗
- `browser_cost_usd` —— 浏览器池秒数
- `proxy_cost_usd` —— proxy 流量(用到时)
- `total_cost_usd` —— 总和;扣余额按这个
`SessionResponse` 上能看到,每个 `run.cost_update` SSE 事件也带这四个。
## Plans
| Plan | 月费 | 包含 | 并发 |
|---|---|---|---|
| Free | $0 | 早期试用 $50–100 credits | 1–2 |
| Dev | ~$29 | $30 credits | 5–10 |
| Business | ~$299 | $400 credits + 多人席位 | 50–100 |
| Scaleup | ~$999 | $1,400 credits + 独占队列 + 区域指定 | 250+ |
::: tip 实际数字保留 ±30% 调整空间
上面是 baseline;公开发布前最终敲定,早期用户锁定折扣价。
## Auto-recharge
不想周日晚上的 cron 因为 402 挂掉?**Settings → Billing** 里开 auto-recharge:
- **阈值** 余额低于 `$X` 触发
- **额度** 一次充 `$Y`
- **月上限** 每个自然月最多 `$Z` 防失控
## 单 run 时长上限
可以给 run 设时长硬上限,cron 这种宁愿 fail-fast 不要 runaway:
```python
await client.sessions.create(CreateSessionRequest(
instructions="...",
max_duration_minutes=30,
))
```
时长上限触发后 run 会被终止。已花掉的 credits 还是会扣。
## 接下来
- [鉴权](/webagent/getting-started/authentication) —— key 怎么放
- [DoAnything](/webagent/features/do-anything) —— 一个 run 究竟跑什么
---
# DoAnything
DoAnything 是 WebAgent 的开放型 API。你给一句自然语言 instruction,agent 自己挑工具、定路径、跑步骤,在浏览器环境里尽力达成目标。
和 DeepResearch / WebSearch / Track 不同:那 3 个是定型 API,产物形态固定、给质量契约。DoAnything **产物形态不定**——连目标最终长什么样你可能都不完全知道,交给 agent 自由发挥。因此它不承诺质量契约;要契约就走对应的定型 API。4 个 API 并列,按你要什么直接选。
## 什么时候用
- 目标不定、复合,需要 agent 自主决定怎么做。
- 任务跨多步、跨页面,可能要登录态、人工确认、长程运行。
- 你接受「尽力而为」,而不是固定形态的结果契约。
需要固定形态的产物时,用 [DeepResearch](/webagent/features/deep-research) / [WebSearch](/webagent/features/web-search) / [Track](/webagent/features/track)。
## 资源模型
**Session** 是一个运行时容器——它持有浏览器、profile(cookies / 登录态)、workspace(文件系统)。**Run** 是跑在 session 里的一条 instruction。同一 session 可以接连跑多个 run,它们共享前一个 run 留下的状态。
```text
project
└── session id: sess_…
├── browser, profile, workspace
└── run id: run_…
├── instructions "搜 Hacker News..."
├── status running | done | …
└── events (SSE) run.status_changed, run.message, …
```
## 生命周期(7 态)
```mermaid
stateDiagram-v2
[*] --> pending
pending --> running
running --> awaiting_input : run.input_request
awaiting_input --> running : POST /intervene
running --> paused : POST /pause
paused --> running : POST /resume
running --> done
running --> failed
pending --> canceled
running --> canceled : POST /cancel
awaiting_input --> canceled
paused --> canceled
```
| 状态 | 含义 | 下一步 |
|---|---|---|
| `pending` | 已接收,等 agent slot | → `running`、`canceled` |
| `running` | agent 在跑 | → `done`、`failed`、`awaiting_input`、`paused`、`canceled` |
| `awaiting_input` | agent 自己暂停了,等你回应 | → `running`(通过 `intervene`) |
| `paused` | 你手动暂停 | → `running`(`resume`)、`canceled` |
| `done` | 成功完成;`output` 已落 | 终态 |
| `failed` | 报错;`error.code` `error.detail` 已落 | 终态 |
| `canceled` | 你取消(或 max_duration 触发) | 终态 |
非终态都占 session 资源。用 `max_duration_minutes` 兜底。
## 提交一个 run
创建 session 时,第一条 run 随 `CreateSessionRequest` 一起提交。请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `instructions` | string | 是 | 一段中文 / 英文。最多 10 000 字符 |
| `max_duration_minutes` | int | 否 | 1–10 080(一周) |
| `recording` | object | 否 | `{enabled, quality, capture_during_take_control}`;不传 = 关 |
| `keep_alive` | bool | 否 | run 结束后保持 session 热度,方便接 follow-up run |
| `allowed_actions` | string[] | 否 | 允许 agent 调的工具白名单。空数组 = 全允许 |
| `profile_id` | string | 否 | 复用 [Profile](/webagent/features/profiles) 的 cookies / 登录态 |
完整 schema 见 [OpenAPI spec](/openapi/v1.json)。
示例:
```python
from web_agent import Client
from web_agent.v1.types import CreateSessionRequest, RecordingConfigRequest
session = await client.sessions.create(CreateSessionRequest(
instructions="找出 24 小时内 Show HN 上 Top 5 的帖子。",
max_duration_minutes=10,
recording=RecordingConfigRequest(enabled=True),
keep_alive=True,
))
run = session.runs[0]
```
## Follow-up run
```python
from web_agent.v1.types import CreateRunRequest
followup = await client.sessions.create_run(
session.id,
CreateRunRequest(instructions="点进第一篇帖子,总结一下评论区。"),
)
```
跑在同一个浏览器里,cookies 与上一条 run 留下的 DOM 都在。
## 事件 {#events}
每个 run 有一条 SSE 流:
```http
GET /v1/projects/{pid}/do_anything/sessions/{sid}/runs/{rid}/events
Authorization: Bearer wa_…
```
11 种事件类型(envelope 一致;`data` 内容不同):
| 类型 | 何时 emit |
|---|---|
| `run.status_changed` | 状态切换 |
| `run.message` | chat 里多一条(agent 或 user) |
| `run.action.started` | agent 调用某工具 |
| `run.action.completed` | 工具返回 |
| `run.action.failed` | 工具抛错 |
| `run.screenshot` | 浏览器新 frame(`url` 短期签名) |
| `run.input_request` | agent 暂停,需要你回应 |
| `run.input_request_resolved` | 你的 intervene 已采纳 |
| `run.cost_update` | 一步的 cost 增量 |
| `run.completed` | 终态;`output` 已落 |
| `stream.heartbeat` | 约 15 s 一次;可忽略 |
断线干净续传:
```http
GET …/events
Last-Event-ID: 142
```
server 会重放 `id > 142` 的事件,不漏。
## Input request(人在回路) {#input-request}
agent 撞到 captcha、2FA、判断不准的事,会发 `run.input_request`:
```json
{
"type": "run.input_request",
"data": {
"input_request_id": "ir_01HXX…",
"prompt": "我看到一个 'Verify you're human' 挑战,你帮我点过去?",
"schema": { "type": "object", "properties": { "solved": { "type": "boolean" } } }
}
}
```
你 `POST /intervene` 回:
```python
await client.messages.intervene(
session.id, run.id,
input_request_id="ir_01HXX…",
response={"solved": True},
)
```
run 切回 `running`。整轮一次往返,无 polling。
## Take Control(接管浏览器) {#take-control}
有些步骤 agent 没法独自完成——captcha、2FA、要人亲手输的密码。这时可以**把浏览器交给人操作**:弹出一个可交互的实时浏览器,人点完、填完,再把控制权交还给 agent。
它和上面的 input request 不同:input request 是 agent 问一个问题、你用 JSON 回答;Take Control 是**人直接操作 agent 那台浏览器**。
接管走的是同一个 `intervene` 端点,用 `kind` 区分动作:
### 1. 发起接管
```http
POST /v1/projects/{pid}/do_anything/sessions/{sid}/runs/{rid}/intervene
Content-Type: application/json
{ "kind": "take_control", "reason": "captcha" }
```
常见触发点是 agent 先发了一条 `run.input_request`(captcha / 需要登录),你也可以在任意时刻主动发起。
### 2. 拿到控制 URL,交给人操作
后端签发一个 standalone 控制 URL,通过 `run.take_control_pending` 事件下发(`data` 带 `standalone_control_url` / `exp` / `reason`)。这是一个无品牌的实时浏览器页面——可以**直接转发给你的终端用户**。人在这个页面里直接对 agent 浏览器点击、输入:过验证码、登录、做需要人判断的事。
发起接管后 agent **不会立刻停**——只有真的有人打开 URL、连上浏览器,run 才切到 `paused`、agent 才暂停(事件 `run.user_paused`)。没人连上,URL 约 5 分钟后失效,发 `run.take_control_expired`。
### 3. 交还控制
人操作完,再调一次 `intervene`:
```http
{ "kind": "release_control", "trigger": "client_release" }
```
run 切回 `running`,agent 重新观察当前页面、接着跑(事件 `run.user_released`)。控制页空闲约 30s 也会自动交还。URL 过期想重发,用 `{ "kind": "refresh_control_url" }`。
### 相关事件
| 事件 | 含义 |
|---|---|
| `run.take_control_pending` | 控制 URL 已签发、等人连接;`data.standalone_control_url` 是给人操作的页面 |
| `run.user_paused` | 有人连上、开始操作,agent 已暂停 |
| `run.user_released` | 控制已交还,agent 继续 |
| `run.take_control_expired` | 没人连接,URL 超时作废 |
> SDK 现状:Python / TypeScript SDK 的 `intervene()` 目前只覆盖 `answer_input_request`。Take Control 的 `take_control` / `release_control` / `refresh_control_url` 直接调 HTTP intervene 端点。完整字段见 [OpenAPI spec](/openapi/v1.json)。
## Profiles {#profiles}
**Profile** = 一份整体的浏览器身份(一个用户一份,累积他在所有站点的登录态)。create session 时引用:
```python
await client.sessions.create(CreateSessionRequest(
instructions="打开 LinkedIn 收件箱,回复最新一条消息。",
profile_id="prof_alice",
))
```
第一次在 Console 里手动设置 Profile(把需要的站点逐个登录一遍)。后续 session 直接复用同一份。详见 [Profiles](/webagent/features/profiles)。
## Workspaces
**Workspace** = 持久文件系统。agent 可读可写;run 跑完后用签名 URL 取文件。"抓这个站,写 CSV,丢回来"这种场景常用。
## 接下来
- [API 参考](/webagent/reference/) —— 每个字段
- [鉴权](/webagent/getting-started/authentication) —— key、scope、轮换
- [Vibecoding](/webagent/guides/vibecoding) —— 怎么把这些提供给 IDE 里的 LLM
---
# DeepResearch
DeepResearch 是 WebAgent 的定型 API 之一。你给一个 topic,它做多轮检索、交叉核对,产出一份带引用和置信度的 Markdown 研究报告。
和 DoAnything 不同:DoAnything 产物形态不定,agent 自己决定返回什么;DeepResearch **产物形态固定**——永远是研究报告,因此能给字段稳定的结果契约(`final_md` + citations + confidence)。知道自己要的就是一份报告时,走这个 API。
## 什么时候用
- 你要的产物明确是一份研究报告,而不是一个开放任务的结果。
- 你需要引用可追溯、覆盖面可衡量,而不只是一段摘要。
- 任务允许跑几分钟到几十分钟——DeepResearch 是长程任务。
需要的是「查一批关键词、拿结构化命中」而不是成文报告时,用 [WebSearch](/webagent/features/web-search)。需要的是开放任务时,用 [DoAnything](/webagent/features/do-anything)。
## 跑一次 research
HTTP 端点:
```http
POST /v1/projects/{pid}/deep_research/runs
Authorization: Bearer wa_...
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `topic` | string | 是 | 研究主题 |
| `depth` | string | 否 | `light` / `standard` / `deep`——决定检索轮数和成本。默认 `standard` |
| `output_format` | string | 否 | 报告形态。默认 `report` |
| `target_audience` | string | 否 | 报告写给谁看,影响行文深浅 |
| `require_outline_approval` | bool | 否 | `true` 时在大纲阶段停下等你批准,见下文。默认 `true` |
| `max_duration_minutes` | int | 否 | 时长兜底 |
| `domain_whitelist` / `domain_blacklist` | string[] | 否 | 限定 / 排除检索域名 |
| `callback_url` | string | 否 | 终态时回调的 URL |
完整 schema 见 [OpenAPI spec](/openapi/v1.json) 的 `CreateResearchRequest`。
示例:
```python
from web_agent.v1 import Client
async with Client(api_key="wa_...", project_id="proj_demo") as client:
run = await client.deep_research.run(
topic="2026 年开源向量数据库格局",
depth="standard",
require_outline_approval=False,
)
print(run["run_id"], run["status"])
```
## 阶段(phase)
一个 research run 会依次走过这些 phase,`status` 仍是 `running` 时 `phase` 告诉你跑到哪了:
| phase | 含义 |
|---|---|
| `brief` | 解析 topic,确定研究目标 |
| `plan` | 规划检索路径 |
| `hitl_outline` | 大纲就绪,等你批准(仅 `require_outline_approval=true` 时出现) |
| `gather` | 多轮检索、抽取来源 |
| `crosscheck` | 跨来源交叉核对 |
| `synthesize` | 成稿,附引用和置信度 |
## 大纲审批(人在回路)
`require_outline_approval=true` 时,run 跑到 `hitl_outline` 阶段会停下,等你确认大纲再继续。这样可以在花掉检索成本之前先纠正方向。
```python
run = await client.deep_research.run(topic="...", require_outline_approval=True)
# run 停在 hitl_outline,通过 intervene 批准或调整大纲
await client.deep_research.intervene(run["run_id"], response={"approved": True})
```
不需要这一步时设 `require_outline_approval=False`,run 会一路跑到终态。
## 结果
run 进入终态后,`result` 字段(`ResearchResultPayload`)包含:
| 字段 | 说明 |
|---|---|
| `final_md` | 研究报告正文,Markdown |
| `final_artifact_id` | 报告作为 artifact 的 id,可用 artifacts 端点取原文件 |
| `citations_count` | 引用数量 |
| `confidence_summary` | 置信度概要 |
| `partial_sections` | 未完成时已产出的章节 |
取 artifact:
```python
artifacts = await client.deep_research.list_artifacts(run["run_id"])
blob = await client.deep_research.get_artifact(run["run_id"], artifacts[0]["id"])
```
## 异步与长程
DeepResearch 默认同步,但 `deep` 档可能跑很久。需要不阻塞时用异步模式,订阅事件流跟进:
```python
run = await client.deep_research.run_async(topic="...", depth="deep")
async for event in client.deep_research.events.stream(run["run_id"]):
print(event.type, event.data)
```
事件流端点 `…/deep_research/runs/{run_id}/events` 走 [SSE 约定](/webagent/reference/#sse-约定):带数字 `id`,断线后用 `Last-Event-ID` 续传。
其它生命周期操作:`cancel`(取消)、`feedback`(对结果打分)、`send_followup`(在已有报告上追加要求)、`refine`(按反馈重跑)。
## 接下来
- [WebSearch](/webagent/features/web-search) —— 不要成文报告、只要结构化命中时走这个
- [DoAnything](/webagent/features/do-anything) —— 开放任务用 DoAnything
- [计费与额度](/webagent/getting-started/pricing) —— `depth` 怎么影响成本
- [API 概览](/webagent/reference/) —— 共享约定;字段全集见 OpenAPI spec
---
# WebSearch
WebSearch 是 WebAgent 的定型 API 之一。你给一批查询,它跨搜索引擎取结果、去重、重排,可选地附摘要,返回结构化命中列表。
和 DeepResearch 不同:DeepResearch 产出成文报告,跑几分钟到几十分钟;WebSearch 产出**结构化结果列表**,默认同步阻塞、秒级返回。要的是「命中」而不是「报告」时走这个。
## 什么时候用
- 你要的是一批可程序处理的搜索结果(title / url / snippet),不是成文报告。
- 你想在一次调用里跑多个查询,并拿到跨引擎去重后的统一列表。
- 你需要秒级返回,能接受同步阻塞。
需要成文研究报告时用 [DeepResearch](/webagent/features/deep-research)。需要 agent 自己定检索路径、跨页操作时用 [DoAnything](/webagent/features/do-anything)。
## 跑一次搜索
HTTP 端点:
```http
POST /v1/projects/{pid}/web_search/runs
Authorization: Bearer wa_...
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `queries` | string[] | 是 | 一批查询 |
| `engines` | string[] | 否 | 限定搜索引擎;不传 = 默认引擎集 |
| `max_results_per_query` | int | 否 | 每个查询最多取多少条;不传用服务端默认 |
| `rerank` | bool | 否 | 跨引擎结果统一重排 |
| `summarize` | bool | 否 | 为结果附摘要;开启会增加成本和耗时 |
| `freshness` | string | 否 | 限定结果时效 |
| `site_whitelist` / `site_blacklist` | string[] | 否 | 限定 / 排除站点 |
| `language` | string | 否 | 结果语言偏好 |
| `objective` | string | 否 | 检索意图说明,用于重排和摘要 |
完整 schema 见 [OpenAPI spec](/openapi/v1.json) 的 `WebSearchRequest`。
示例:
```python
from web_agent.v1 import Client
async with Client(api_key="wa_...", project_id="proj_demo") as client:
run = await client.web_search.run(
queries=["best Python ORM 2026", "SQLAlchemy vs Tortoise"],
max_results_per_query=10,
rerank=True,
)
for hit in run["results"]["results"]:
print(hit["final_rank"], hit["title"], hit["url"])
```
## 结果
run 的 `results` 字段(`WebSearchResults`):
| 字段 | 说明 |
|---|---|
| `results` | 命中列表,每项是一个 `SearchResultItem` |
| `total_unique_results` | 去重后的结果总数 |
| `is_summarized` | 是否附了摘要(`summarize=true` 时为真) |
| `engine_answer` | 引擎直出的答案(部分引擎提供) |
每个 `SearchResultItem`:
| 字段 | 说明 |
|---|---|
| `title` / `url` | 标题与链接 |
| `canonical_url` | 规范化后的 URL,去重以此为准 |
| `snippet` | 引擎返回的摘要片段 |
| `summary` | WebAgent 生成的摘要(`summarize=true` 时有) |
| `final_rank` / `original_rank` | 重排后 / 重排前的排名 |
| `source_engine` | 命中来自哪个引擎 |
| `source_query` | 命中对应哪条查询 |
| `dedup_count` | 这条结果被多少个引擎/查询命中——越高越可信 |
## 异步与续期
WebSearch 默认同步返回。查询量大或开了 `summarize` 时耗时变长,可订阅事件流跟进:
```http
GET /v1/projects/{pid}/web_search/runs/{run_id}/events
```
事件流走 [SSE 约定](/webagent/reference/#sse-约定)。其它操作:`cancel`(取消)、`refine`(按新意图重跑同一批查询)、`send_followup`(追加查询)。
## 接下来
- [DeepResearch](/webagent/features/deep-research) —— 要成文报告时走这个
- [Track](/webagent/features/track) —— 要的是「持续监控某个查询的变化」
- [API 概览](/webagent/reference/) —— 共享约定;字段全集见 OpenAPI spec
---
# Track
Track 是 WebAgent 的定型 API 之一。你描述一个监控意图,它按 schedule 反复去网页取数,和基线比对,命中变化时通过你指定的渠道通知你。
和 DoAnything / DeepResearch / WebSearch 不同:那些是「跑一次、出结果」。Track 的资源是 **monitor**——一个长期存在的对象,自己按计划反复跑,每次跑产生一个 run。
## 什么时候用
- 你关心的是某个东西**随时间的变化**,而不是此刻的一次性结果。
- 你希望在变化发生时被推送,而不是自己轮询。
- 监控可以长期挂着——monitor 在你删除前一直存在。
只要一次性结果时,用 [WebSearch](/webagent/features/web-search) 或 [DoAnything](/webagent/features/do-anything)。
## 创建一个 monitor
HTTP 端点:
```http
POST /v1/projects/{pid}/track/monitors
Authorization: Bearer wa_...
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `intent` | string | 是 | 监控意图,自然语言 |
| `notify_channel` | object | 是 | 通知渠道,见下方 [notify_channel](#notify-channel) |
| `schedule` | object | 否 | 触发计划,见下方 [schedule](#schedule)。不传 = 用默认计划 |
| `target_urls` | string[] | 否 | 限定监控哪些页面;不传则 agent 自己找 |
| `extraction_schema` | object | 否 | 指定从页面抽取哪些结构化字段 |
| `trigger_dsl` | object | 否 | 触发条件——满足才算「命中变化」 |
| `stop_condition_dsl` | object | 否 | 停止条件——满足后 monitor 自动停 |
| `profile_id` | string | 否 | 复用 [Profile](/webagent/features/profiles) 的登录态 |
完整 schema 见 [OpenAPI spec](/openapi/v1.json) 的 `CreateMonitorRequest`。`schedule` 和 `notify_channel` 是嵌套对象,结构见下面两节。
## schedule
`schedule.kind` 决定 monitor 什么时候跑:
| kind | 说明 |
|---|---|
| `interval` | 固定间隔;配 `interval_seconds` |
| `cron` | cron 表达式;配 `cron` |
| `event` | 由外部事件触发;配 `event_filter` |
| `autonomous` | 由 agent 自己判断下次该什么时候跑 |
## notify_channel
`notify_channel.kind` 决定命中变化时往哪推:
| kind | 说明 |
|---|---|
| `callback_url` | POST 到你的 URL;配 `url` |
| `global_webhook` | 复用项目级 webhook;配 `webhook_id` |
| `console_inbox` | 推到 Console 的收件箱,适合人来看 |
## 示例
凑齐 `intent` + `schedule` + `notify_channel` 三者创建 monitor:
```python
from web_agent.v1 import Client
async with Client(api_key="wa_...", project_id="proj_demo") as client:
monitor = await client.track.create(
intent="苹果股价跌破 $200 时通知我",
schedule={"kind": "interval", "interval_seconds": 3600},
notify_channel={"kind": "callback_url", "url": "https://hooks.example.com/track"},
)
print(monitor["id"], monitor["status"])
```
## 基线与变化检测
monitor 第一次跑会建立**基线**(`baseline_extracted`)。之后每次跑都和基线比对,命中 `trigger_dsl` 才发通知。`last_tick_at` / `last_tick_n` 记录最近一次跑的时间和序号,`consecutive_failures` 连续失败计数——连续失败过多 monitor 会进入异常状态。
## 生命周期
monitor 创建后长期存在,直到你删除。常用操作:
| 操作 | 说明 |
|---|---|
| `pause` / `resume` | 暂停 / 恢复调度 |
| `run_now` | 不等 schedule,立刻跑一次 |
| `cancel` / `delete` | 停止 / 删除 monitor |
| `patch` | 改 intent、schedule、渠道等 |
| `refine` | 用自然语言调整监控意图 |
| `list_runs` / `get_run` | 查历史 run |
| `list_deliveries` / `retry_delivery` | 查通知投递记录;失败的可重投 |
| `intervene` / `message` | monitor 撞到登录、验证码等需要人介入时回应 |
事件流端点 `…/track/monitors/{mid}/events` 走 [SSE 约定](/webagent/reference/#sse-约定),实时跟进 monitor 的每次 tick。
## 接下来
- [Profiles](/webagent/features/profiles) —— 监控需要登录的站点时复用登录态
- [WebSearch](/webagent/features/web-search) —— 只要一次性结果时走这个
- [API 概览](/webagent/reference/) —— 共享约定;字段全集见 OpenAPI spec
---
# Profiles
**Profile** 是一份**整体的浏览器身份**:cookies、localStorage、各个站点的登录态都存在同一份 Profile 里。它**不按站点拆分**——一个用户对应一份 Profile,agent 用它打开浏览器时,这个人在所有站点的登录态都在。
创建 session 或 monitor 时用 `profile_id` 引用它,agent 就带着这份身份打开浏览器,不用每次重新登录。没有 Profile,每个 session 都是一张白纸——撞到需要登录的页面就只能停下。
::: tip 不要按站点拆 Profile
Profile 是整体的。需要哪些站点,就在**同一份** Profile 里逐个登录一遍;后续所有任务共用这一份,不要为每个站点单独建 Profile。
## 什么时候用
- 任务要访问需要登录的站点(邮箱、社交平台、内部系统)。
- 你希望多个 session / monitor 共享同一份登录态,而不是各自登录。
- 登录态需要跨任务、跨天复用。
任务只访问公开页面、不需要登录时,不用 Profile。
## 创建并登录一个 Profile
第一次给 Profile 装登录态,推荐在 [Console](https://dashboard.qoni.ai) 里做:Console 会开一个受控浏览器,你在里面正常登录、过验证码、做想做的设置,登录态就落进 Profile。这一步天然需要人操作,Console 是最顺的形态。
之后这个 Profile 就能被代码反复引用。也可以用 API 管理 Profile 资源:
```http
POST /v1/projects/{pid}/profiles 创建 profile
GET /v1/projects/{pid}/profiles 列出 profile
GET /v1/projects/{pid}/profiles/{id} 查单个
PATCH /v1/projects/{pid}/profiles/{id} 改名等
DELETE /v1/projects/{pid}/profiles/{id} 删除
```
创建时可带的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `name` | string | 给 Profile 起个好认的名字,通常对应一个用户 |
| `customer_user_id` | string | 把 Profile 关联到你业务系统里的用户 |
交互式登录走 `POST /v1/projects/{pid}/profiles/login` 和 `POST /v1/projects/{pid}/profiles/{id}/confirm`——具体请求体见 [OpenAPI spec](/openapi/v1.json)。
> API 里还有个 `cookie_domains` 字段,只服务旧的「自带 cookies」B2B 接入路径,整体 Profile 用不到,可忽略。
## 在 session / monitor 里用 Profile
拿到 `profile_id` 后,创建 session 时引用它:
```python
from web_agent.v1 import Client
from web_agent.v1.types import CreateSessionRequest
async with Client(api_key="wa_...", project_id="proj_demo") as client:
session = await client.sessions.create(CreateSessionRequest(
instructions="打开 LinkedIn 收件箱,回复最新一条消息。",
profile_id="prof_alice",
))
```
[Track](/webagent/features/track) 的 monitor 同样接受 `profile_id`,监控需要登录的站点时用它。
## Profile 状态
Profile 的 `state` 和这几个字段反映它还能不能用:
| 字段 | 说明 |
|---|---|
| `state` | Profile 当前状态 |
| `last_used_at` | 最近一次被 session / monitor 使用的时间 |
| `last_alive_at` | 最近一次确认登录态仍有效的时间 |
| `last_failure_reason` | 最近一次使用失败的原因 |
cookies 和登录态会过期。如果 run 因为登录失效而失败,`last_failure_reason` 会说明原因——这时需要回到 Console 重新登录一次该 Profile。把这一步做成可观测的,比让 run 反复在登录页失败更省成本。
## 接下来
- [DoAnything](/webagent/features/do-anything) —— `profile_id` 怎么进 session
- [Track](/webagent/features/track) —— 给长期监控复用登录态
- [API 概览](/webagent/reference/) —— 共享约定;字段全集见 OpenAPI spec
---
# 错误码与重试
本页列出 WebAgent 的错误码矩阵,说明哪些错误可以重试、哪些要直接抛给用户、哪些需要修改代码。
所有错误返回结构一致:
```json
{
"code": "rate_limit_exceeded",
"detail": "Per-key concurrency limit (10) reached.",
"extra": { "limit": 10, "active": 10 }
}
```
HTTP 状态码告诉你**类别**,`code` 字段是**稳定契约**——switch 在 `code` 上,不要在 `detail` 上(英文文案会随时间变)。
## 错误码矩阵
| 状态 | code | 重试? | 怎么处理 |
|---|---|---|---|
| 400 | `bad_request` | ❌ | 改请求体。对照 OpenAPI 检查字段。 |
| 401 | `unauthorized` | ❌ | key 缺失 / 格式错 / 过期 / 撤销超过 1 小时宽限期。新建一个。 |
| 402 | `insufficient_credits` | ❌ | **Settings → Billing** 充值,或开自动续费。 |
| 402 | `budget_exceeded` | ❌ | 项目预算上限触发。提高额度或拆分 run。 |
| 403 | `forbidden` | ❌ | key 有效,但当前 project 不授权访问该资源。 |
| 403 | `safety_boundary_violated` | ❌ | Agent 出于安全拒绝。读 `extra.reason`,改写指令。 |
| 404 | `session_not_found`、`run_not_found`、`profile_not_found` 等 | ❌ | id 错了,或资源已删除。 |
| 409 | `conflict` | ❌ | 状态不一致(如对终态 run 调 `cancel`)。重读状态再决定。 |
| 422 | `validation_error` | ❌ | schema 校验失败——`extra.errors[]` 列出有问题的字段。 |
| 429 | `rate_limit_exceeded` | ✅ | 遵守 `Retry-After`;没有就指数退避。 |
| 429 | `too_many_concurrent_sessions` | ✅ | 等手头 session 释放,或升级套餐。 |
| 5xx | `internal_error` | ✅ | 同样调用,指数退避,3–5 次封顶。 |
| (网络错) | — | ✅ | 连接重置 / 超时——幂等重试。 |
✅ = 不需要思考就能重试。❌ = 不改东西继续报。
## 推荐的重试策略
```python
import time, random
def with_retries(fn, *, attempts=4, base=0.5, cap=8.0):
for i in range(attempts):
try:
return fn()
except WebAgentError as e:
if e.code not in {"rate_limit_exceeded",
"too_many_concurrent_sessions",
"internal_error"}:
raise # 不可重试
if i == attempts - 1:
raise
sleep_s = min(cap, base * 2**i) + random.uniform(0, 0.25)
time.sleep(e.retry_after_seconds or sleep_s)
```
Python / TypeScript SDK 默认就内置了这套循环;上面的表是给你直接调 API 时用的。
## 幂等键(Idempotency-Key){#idempotency}
所有写入接口(`POST /sessions`、`POST /sessions/{sid}/runs`、`POST /messages` 等)都接受 `Idempotency-Key` 头:
```http
POST /v1/projects/proj_demo_0001/do_anything/sessions
Idempotency-Key: 9b2f7c1e-…-uuid
```
24 小时内带同一个 UUID 重发,会拿到**完全相同的响应**(同样的 `session_id`、同样的状态码)——网络抖动也安全。**每个逻辑动作生成一个 UUID,不是每次重试生成一个**。
## run 执行期间的错误
Agent 执行过程中报错不一定让 run 整个失败——很多是可恢复的:
- **工具错误**——通过 `run.action.failed` 推送;agent 自己决定重试 / 换工具 / 整体失败。
- **Captcha / 2FA**——通过 `run.input_request` 推送;你用 `POST /intervene` 回答。
- **硬上限触发**——run 转 `failed`,`error.code = budget_exceeded` 或 `duration_exceeded`,已花的 credits 仍计费。
- **安全拒绝**——run 转 `failed`,`error.code = safety_boundary_violated`,被拒绝那一步不计费。
四种都通过 [SSE 流](/webagent/features/do-anything#events) 看得见。
## SSE 专属故障
| 现象 | 原因 | 解决 |
|---|---|---|
| 流卡 60 s 以上 | 网络断 / 代理缓冲 | 带 `Last-Event-ID: <最后一个 id>` 重连。 |
| `Last-Event-ID` 不生效 | 缓冲过期(> 1 小时) | `GET /sessions/{sid}/runs/{rid}` 重读状态,从当前继续。 |
| 重连后事件重复 | at-least-once 投递 | 按 `id` 去重(每个 run 内单调递增)。 |
## 下一步
- [API 概览](/webagent/reference/)——所有接口都共享这套约定。
- [DoAnything](/webagent/features/do-anything)——上面的生命周期就是规范。
---
# Python SDK
本页说明如何安装、配置并使用官方 `web-agent-sdk` Python 包:起 session、跑 run、订阅事件。
```bash
pip install web-agent-sdk
```
需要 Python 3.10+。SDK 是 async-first(`asyncio` / `anyio`)。
## 一个入口:`Client`
用户面 API 都挂在同一个 `Client` 上:
```python
from web_agent.v1 import Client
```
构造 `Client` 需要 `api_key` 和 `project_id`。两者都可省略——省略时分别自动读环境变量 `$WEBAGENT_API_KEY` / `$WEBAGENT_PROJECT_ID`。
| 资源 | 对应产品 | 用法 |
|---|---|---|
| `client.sessions / messages / events` | DoAnything(开放型)| 自由输入,agent 自己定路径 |
| `client.deep_research` | DeepResearch(定型 — 研究→报告)| Standalone API |
| `client.web_search` | WebSearch(查询 → 结果)| 默认 `wait=true`,等结果就绪再返回 |
| `client.track` | Track(监控 → snapshot)| 长寿命 monitor + webhook 通道 |
> 包名是 `web-agent-sdk`(带连字符),但 import 名是 `web_agent`——同 `python-dateutil` → `dateutil` 的惯例。
## DoAnything —— 开放型任务
```python
import asyncio
from web_agent.v1 import Client
from web_agent.v1.types import CreateSessionRequest
async def main():
async with Client(
api_key="wa_demo_xxxxxxxxxxxxxxxx",
project_id="proj_demo_0001",
) as client:
session = await client.sessions.create(CreateSessionRequest(
instructions="找出 Hacker News 现在 Top 5 的故事,列表返回。",
))
run = session.runs[0] # session 创建会隐式排好首个 run
async for event in client.events.stream(session.id, run.id):
print(event.type, event.data)
if event.type == "run.completed":
break
asyncio.run(main())
```
### 「起新 run」 vs 「往运行中 run 塞消息」
```python
# 1. 往当前 run 的对话队列里塞一条消息
# (agent 在下个 ReAct 边界自己决定要不要消费)
await client.messages.send(
session.id, run.id,
content="顺便把每条的评论数也带上。",
)
# 2. 在同一个 session 里起一个新 run
# (复用 browser / profile / workspace;上一个 run 必须已终态)
from web_agent.v1.types import CreateRunRequest
new_run = await client.sessions.create_run(
session.id,
CreateRunRequest(instructions="点进第一条,总结评论区。"),
)
```
### 回应 input request
```python
await client.messages.intervene(
session.id, run.id,
input_request_id="ir_01HXX",
response={"solved": True},
)
```
### 取消 / 停止 / 列表
```python
await client.sessions.cancel_run(session.id, run.id, reason="user_cancelled")
await client.sessions.stop(session.id, force=False) # 软停 session
listing = await client.sessions.list(status="running", limit=20)
for s in listing.items:
print(s.id, s.status)
```
### 心跳事件
`stream()` 默认过滤心跳事件;做连接健康 UI 时传 `include_heartbeats=True`。续传支持 `Last-Event-ID`:
```python
client.events.stream(session.id, run.id, last_event_id="142")
```
## DeepResearch —— 研究 → 报告
DeepResearch 是 standalone API——路径不带 project 段(`/v1/deep_research`),项目租户从 Bearer token 解析。
```python
async with Client(api_key="wa_...", project_id="proj_demo") as client:
run = await client.deep_research.run(
topic="2026 年开源向量数据库格局",
depth="deep", # light / standard / deep
require_outline_approval=True, # 默认开 outline HITL gate
)
print(run["run_id"], run["status"])
```
订阅事件流(DR 走的是 DoAnything 的 SSE 通道)+ 响应 outline gate:
```python
# run["run_id"] / run["session_id"] 都在创建响应里
async for event in client.events.stream(
run["session_id"], run["run_id"],
):
if event.type == "run.input_request":
# outline 已生成,请求人工批准
await client.deep_research.intervene(
run["run_id"],
request_id=event.data["request_id"],
response="approve", # 或 {"action": "approve_with_edits", "edits": [...]}
)
if event.type == "run.completed":
break
# 拉取产物三件套(final.md / citations.json / confidence.json)
artifacts = await client.deep_research.list_artifacts(run["run_id"])
final = await client.deep_research.get_artifact(
run["run_id"], artifacts[0]["id"],
)
```
## WebSearch —— 查询 → 结果
WebSearch 是 project-scoped API。`run()` 默认 `wait=true`:服务器最多阻塞 30s,等搜索完成后直接返回完整结果;超过 30s 则返回 202,再调 `get(run_id)` 轮询。
```python
# 同步形态(默认)
result = await client.web_search.run(
queries=["best Python ORM 2026"],
engines=["tavily"],
summarize=True,
)
for hit in result["results"]["results"]:
print(hit["title"], hit["url"])
# 异步形态
pending = await client.web_search.run_async(queries=["best Python ORM 2026"])
detail = await client.web_search.get(pending["run_id"])
# Refine(同 run 内重搜)
await client.web_search.refine(
pending["run_id"],
text="再加 site:reddit.com 限制重搜",
)
```
## Track —— 长寿命监控
Track 会创建一个长期运行的 **monitor**。它按 schedule(cron / interval / event)反复检查目标页面,每次检查保存一份 `snapshot`;当变化满足触发条件时,通过你配置的通道(如 webhook)通知你。
```python
mon = await client.track.create(
intent="apple.com 上 iPhone 17 Pro 跌破 $999 时通知我",
schedule={"kind": "interval", "interval_seconds": 3600},
notify_channel={"kind": "callback_url", "url": "https://hooks.example.com/track"},
)
# 生命周期 —— 通过 patch 暂停 / 恢复 / refine:
await client.track.pause(mon["id"], reason="人工核对")
await client.track.resume(mon["id"])
await client.track.refine(mon["id"], trigger_dsl={"op": "lt", "field": "price", "value": 999})
# 手动触发一次检查(绕开 schedule);返回这一次检查的结果:
outcome = await client.track.run_now(mon["id"])
# 拉 snapshot 历史(最新优先):
snapshots = await client.track.list_snapshots(mon["id"])
snap = await client.track.get_snapshot(mon["id"], snapshots["items"][0]["id"])
# webhook 投递历史 + 重投失败的投递:
deliveries = await client.track.list_deliveries(mon["id"], include_payload=True)
await client.track.retry_delivery(mon["id"], deliveries["items"][0]["id"])
# 取消 monitor(终态):
await client.track.cancel(mon["id"]) # 等价 await client.track.delete(mon["id"])
```
### Alignment HITL(可选)
Supervisor 需要让你澄清意图(如 "你说的是 SKU A 还是 SKU B?")时,monitor 进入 `pending_clarification`,发出 `alignment.input_request` 事件。用 `intervene()` 回答:
```python
await client.track.intervene(
mon["id"],
request_id="req_align_1",
response="SKU A",
)
```
也可以随时往 alignment 队列推自由文本:`client.track.message(mon_id, content="…")`。
## 错误
SDK 用 typed exception,按类 catch:
```python
from web_agent.v1 import (
UnauthorizedError, InsufficientCreditsError, RateLimitedError,
)
try:
await client.sessions.create(CreateSessionRequest(instructions="…"))
except InsufficientCreditsError as e:
print("top up:", e.detail, e.extra)
```
每个 exception 都继承 `ApiError`,带 `code` / `detail` / `extra`,与 [API 错误信封](/webagent/reference/#错误) 对应。
| 异常类 | HTTP | `code` |
|---|---|---|
| `UnauthorizedError` | 401 | `unauthorized` |
| `ForbiddenError` | 403 | `forbidden`、`safety_boundary_violated` |
| `NotFoundError` | 404 | `*_not_found` |
| `ConflictError` | 409 | `conflict` |
| `ValidationError` | 422 | `validation_error` |
| `RateLimitedError` | 429 | `rate_limit_exceeded` |
| `InsufficientCreditsError` | 402 | `insufficient_credits` |
| `BudgetExceededError` | 402 | `budget_exceeded` |
## 类型
`Session`、`Run`、`Event` 等 DoAnything 资源都是 dataclass,从 `web_agent.v1` 重新导出:
```python
from web_agent.v1 import Session, Run, Event, RunStatus
```
DR / DS / WS 的响应直接是 `dict[str, Any]`(按 OpenAPI 信封原样回传),用键名访问即可(`run["run_id"]` / `run["status"]`)。`mypy --strict` 通过。
## 接下来
- [TypeScript SDK](/webagent/sdk/typescript) —— JS / TS 同样的接口
- [错误码与重试](/webagent/reference/errors-and-retries) —— 推荐重试策略 + 幂等键
- [DoAnything](/webagent/features/do-anything) —— 生命周期 / profile / workspace
---
# TypeScript SDK
本页说明如何安装、配置并使用官方 `@web-agent/sdk` Node / 浏览器包:起 session、跑 run、订阅事件。
```bash
npm install @web-agent/sdk
# 或 pnpm add @web-agent/sdk / yarn add @web-agent/sdk / bun add @web-agent/sdk
```
支持 Node 20+ 与现代浏览器。
> **不要把 server 端 `wa_` key 塞进浏览器**——key 是 project 级权限,仅限 server-side 或可信运行时。
## 一个入口:`Client`
用户面 API 都挂在同一个 `Client` 上:
```typescript
import { Client } from "@web-agent/sdk";
```
`new Client({ apiKey, projectId })` 创建客户端,`apiKey` 和 `projectId` 都是必填——下面示例里从环境变量读。
| 资源 | 对应产品 | 用法 |
|---|---|---|
| `client.sessions / messages / events` | DoAnything(开放型)| 自由输入,agent 自己定路径 |
| `client.deepResearch` | DeepResearch(定型 — 研究→报告)| Standalone API |
| `client.webSearch` | WebSearch(查询 → 结果)| 默认 `wait=true`,等结果就绪再返回 |
| `client.track` | Track(监控 → snapshot)| 长寿命 monitor + webhook 通道 |
## DoAnything —— 开放型任务
```typescript
import { Client } from "@web-agent/sdk";
const client = new Client({
apiKey: process.env.WEBAGENT_API_KEY!,
projectId: process.env.WEBAGENT_PROJECT_ID!,
});
const session = await client.sessions.create({
instructions: "找出 Hacker News 现在 Top 5 的故事。",
});
const run = session.runs[0]; // session 创建会隐式排好首个 run
for await (const event of client.events.stream(session.id, run.id)) {
console.log(event.type, event.data);
if (event.type === "run.completed") break;
}
```
wire 字段一律 snake_case 与 API 对齐;方法名 camelCase。
### 续传 + 心跳
```typescript
for await (const event of client.events.stream(session.id, run.id, {
lastEventId: "142",
includeHeartbeats: false,
})) {
if (event.type === "run.completed") break;
}
```
底层用 `fetch` + 手写 SSE parser——Node 20+ / Bun / Cloudflare Workers / 浏览器都跑得起来。
### 「起新 run」 vs 「往运行中 run 塞消息」
```typescript
// 1. 塞消息进当前 run 的对话队列
await client.messages.send(session.id, run.id, {
content: "顺便把每条的评论数也带上。",
});
// 2. 在同一个 session 里起新 run
const followup = await client.sessions.createRun(session.id, {
instructions: "点进第一条,总结评论区。",
});
```
### 回应 input request
```typescript
await client.messages.intervene(session.id, run.id, {
kind: "answer_input_request",
input_request_id: "ir_01HXX",
response: { solved: true },
});
```
`kind` 是 discriminator,同一个端点也处理 `take_control` / `release_control`——见 [Take Control](/webagent/features/do-anything#input-request)。
### 取消 / 停止 / 列表
```typescript
await client.sessions.cancelRun(session.id, run.id, { reason: "user_cancelled" });
await client.sessions.stop(session.id, { force: false });
const list = await client.sessions.list({ status: "running", limit: 20 });
list.items.forEach((s) => console.log(s.id, s.status));
```
## DeepResearch —— 研究 → 报告
DeepResearch 是 standalone API——路径不带 project 段(`/v1/deep_research`),项目租户从 Bearer token 解析。
```typescript
const run = await client.deepResearch.run({
topic: "2026 年开源向量数据库格局",
depth: "deep", // light / standard / deep
requireOutlineApproval: true, // 默认开 outline HITL gate
});
// 订阅事件流(DR 走 DoAnything 的 SSE 通道)+ 响应 outline gate
for await (const event of client.events.stream(
run.session_id as string, run.run_id as string,
)) {
if (event.type === "run.input_request") {
await client.deepResearch.intervene(run.run_id as string, {
requestId: (event.data as { request_id: string }).request_id,
response: "approve", // 或 { action: "approve_with_edits", edits: [...] }
});
}
if (event.type === "run.completed") break;
}
// 拉取产物三件套(final.md / citations.json / confidence.json)
const artifacts = await client.deepResearch.listArtifacts(run.run_id as string);
const final = await client.deepResearch.getArtifact(
run.run_id as string, artifacts[0]!.id as string,
);
```
## WebSearch —— 查询 → 结果
WebSearch 是 project-scoped API。`run()` 默认 `wait: true`:服务器最多阻塞 30s,等搜索完成后直接返回完整结果;超过 30s 则返回 202,再调 `get(runId)` 轮询。
```typescript
// 同步形态(默认)
const result = await client.webSearch.run({
queries: ["best TypeScript ORM 2026"],
engines: ["tavily"],
summarize: true,
});
// 异步形态
const pending = await client.webSearch.runAsync({
queries: ["best TypeScript ORM 2026"],
});
const detail = await client.webSearch.get(pending.run_id as string);
// Refine(同 run 内重搜)
await client.webSearch.refine(pending.run_id as string, {
text: "再加 site:reddit.com 限制重搜",
});
```
## Track —— 长寿命监控
Track 会创建一个长期运行的 **monitor**。它按 schedule(cron / interval / event)反复检查目标页面,每次检查保存一份 `snapshot`;当变化满足触发条件时,通过你配置的通道(如 webhook)通知你。
```typescript
const mon = await client.track.create({
intent: "apple.com 上 iPhone 17 Pro 跌破 $999 时通知我",
schedule: { kind: "interval", interval_seconds: 3600 },
notifyChannel: { kind: "callback_url", url: "https://hooks.example.com/track" },
});
// 生命周期 —— 通过 patch 暂停 / 恢复 / refine:
await client.track.pause(mon.id as string, { reason: "人工核对" });
await client.track.resume(mon.id as string);
await client.track.refine(mon.id as string, {
triggerDsl: { op: "lt", field: "price", value: 999 },
});
// 手动触发一次检查(绕开 schedule):
const outcome = await client.track.runNow(mon.id as string);
// snapshot 历史(最新优先):
const snaps = await client.track.listSnapshots(mon.id as string);
const snap = await client.track.getSnapshot(
mon.id as string,
snaps.items[0]!.id as string,
);
// webhook 投递历史 + 重投失败的投递:
const deliveries = await client.track.listDeliveries(mon.id as string, {
includePayload: true,
});
await client.track.retryDelivery(
mon.id as string,
deliveries.items[0]!.id as number,
);
// 取消 monitor(终态):
await client.track.cancel(mon.id as string); // 等价:client.track.delete(...)
```
### Alignment HITL(可选)
Supervisor 需要让你澄清意图时,monitor 进入 `pending_clarification`,发出 `alignment.input_request` 事件。用 `intervene()` 回答:
```typescript
await client.track.intervene(mon.id as string, {
requestId: "req_align_1",
response: "SKU A",
});
```
也可以随时往 alignment 队列推自由文本:`client.track.message(monId, { content: "…" })`。
## 错误
```typescript
import {
ApiError,
InsufficientCreditsError,
RateLimitedError,
UnauthorizedError,
} from "@web-agent/sdk";
try {
await client.sessions.create({ instructions: "…" });
} catch (err) {
if (err instanceof InsufficientCreditsError) {
console.log("top up:", err.detail, err.extra);
} else if (err instanceof ApiError) {
console.log(err.code, err.statusCode, err.detail);
} else {
throw err;
}
}
```
每个错误类都继承 `ApiError`,暴露 `code` / `statusCode` / `detail` / `extra`,对应 [API 错误信封](/webagent/reference/#错误)。
## 类型
DoAnything 的 request / response 类型都是顶层 export:
```typescript
import type {
Session, Run, Event, EventType,
CreateSessionRequest, CreateRunRequest, InterveneRequest,
RunStatus, SessionStatus, TerminalReason,
} from "@web-agent/sdk";
```
DR / DS / WS 的响应是 `Record`(按 OpenAPI 信封原样回传),用键名访问(`run.run_id` / `run.status`)。各资源也导出自己的 option 类型(`DRRunOptions` / `DSRunOptions` / `WSRunOptions`)。
## 接下来
- [Python SDK](/webagent/sdk/python) —— Python 同样的接口
- [错误码与重试](/webagent/reference/errors-and-retries) —— 推荐重试策略 + 幂等键
- [DoAnything](/webagent/features/do-anything) —— 生命周期 / profile / workspace
---
# cURL 与原生 HTTP
本页给出 WebAgent 在原生 HTTP 上的常用调用模式——任何语言只要能发 HTTPS / JSON 就能用,不依赖官方 SDK。
## 请求结构
所有调用共享同一套结构。先看下面的规范,本页后面每段 `curl` 都是它的一个实例。
### Base URL 与路径
```
https://api.eak.eazo.ai
```
project-scoped 端点的路径都以 project 为前缀:
```
/v1/projects/{project_id}/do_anything/sessions
/v1/projects/{project_id}/do_anything/sessions/{session_id}
/v1/projects/{project_id}/do_anything/sessions/{session_id}/runs/{run_id}/events
```
### 路径参数
下列参数按端点路径所需出现,出现即必填:
| 参数 | 形态 | 来源 |
|---|---|---|
| `{project_id}` | `proj_` 前缀字符串 | Console → Project Switcher;project 是 project-scoped 端点的隔离单位 |
| `{session_id}` | `sess_` 前缀字符串 | 「起 session」响应里的 `id` |
| `{run_id}` | `run_` 前缀字符串 | session 响应 `runs[]` 中的 run `id`,或 `latest_run_id` |
### 鉴权
每个请求都要带 bearer token:
```
Authorization: Bearer
```
`` 是 `wa_` 前缀的 API key,在 Console → Settings → API Keys 创建,详见[认证](/webagent/getting-started/authentication)。
### 请求体
变更类端点(`POST`)带 `Content-Type: application/json`,请求体为 JSON。每个端点的字段全集以 [OpenAPI spec](/openapi/v1.json) 为准。
## 起 session
下面的示例用占位值(`proj_demo_0001` / `wa_demo_…` / `sess_demo_0001` / `run_demo_0001`),实际调用按上面「请求结构」替换成你自己的值。
```bash
curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"instructions": "找出 Hacker News 现在 Top 5 的故事。"
}'
```
## 订阅事件流(SSE)
```bash
curl -N \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
"https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/events"
```
断线续传:
```bash
curl -N \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
-H "Last-Event-ID: 142" \
"…/events"
```
## 追加消息
```bash
curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/messages \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "content": "顺便把每条的评论数也带上。" }'
```
## 回应 input request
```bash
curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/intervene \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"kind": "answer_input_request",
"input_request_id": "ir_01HXX",
"response": { "solved": true }
}'
```
`kind` 是 discriminator——同一个端点也接受 `take_control` / `release_control`,见 [Take Control](/webagent/features/do-anything#input-request)。
## 取消 run
```bash
curl -X POST https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/cancel \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "reason": "user_cancelled" }'
```
## 列 sessions
```bash
curl "https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions?status=running&limit=20" \
-H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx"
```
## 错误
非 2xx 响应都是 JSON,带稳定 `code`:
```json
{
"code": "insufficient_credits",
"detail": "Project balance below the minimum required ($0.50).",
"extra": { "balance_usd": "0.12", "required_usd": "0.50" }
}
```
完整列表见 [API 概览 → 错误](/webagent/reference/#错误)。
## 幂等
```bash
curl … -H "Idempotency-Key: $(uuidgen)"
```
同 key 重发返回同响应——网络异常时安全 retry。
---
# API 概览
本页讲 WebAgent 所有端点共享的约定。具体每个端点字段请直接看 [OpenAPI 3.1 spec](/openapi/v1.json)——SDK 和 Console 的 *Get Code* 对话框都是从这里生成。
## Base URL
```
https://api.eak.eazo.ai
```
所有资源 path 以 project 为前缀:
```
/v1/projects/{project_id}/...
```
## 鉴权
`Authorization` header 带 bearer token,详见 [鉴权与 API key](/webagent/getting-started/authentication):
```http
Authorization: Bearer wa_xxxxxxxxxxxxxxxxxxxxxxxx
```
## 错误
JSON 响应体 + HTTP 状态 + 稳定的 `code` 字段(可程序判断):
```json
{
"code": "session_not_found",
"detail": "Session sess_demo_0001 not found.",
"extra": { "session_id": "sess_demo_0001" }
}
```
| 状态 | 常见 code |
|---|---|
| 400 | `bad_request` |
| 401 | `unauthorized` |
| 402 | `insufficient_credits`、`budget_exceeded` |
| 403 | `forbidden`、`safety_boundary_violated` |
| 404 | `session_not_found`、`run_not_found`、`profile_not_found`…… |
| 409 | `conflict` |
| 422 | `validation_error` |
| 429 | `rate_limit_exceeded`、`too_many_concurrent_sessions` |
| 5xx | `internal_error` |
## 限流
- per-key 并发 session 数——按 plan
- per-key 请求频率——滑动窗;超时返回 `429` + `Retry-After`
- per-project 月额度——80% 软警告,100% 硬停
## 分页
list 类端点全是 cursor 分页:
```http
GET /v1/projects/{pid}/do_anything/sessions?limit=50&cursor=eyJ…
```
响应里带 `next_cursor`(最后一页是 `null`)。`limit` 上限 100。
## 幂等
变更类端点(`POST /sessions`、`POST /messages`……)支持 `Idempotency-Key` header。同 UUID 重发返回同响应——retry 安全。
## SSE 约定
流式端点(`…/events`)发 JSON 编码 SSE 事件,带数字 `id`。断线后用 `Last-Event-ID: ` 续传,server 重放该 id 之后的全部事件。
## 接下来
- [OpenAPI spec](/openapi/v1.json) —— 每个端点、每个字段,机器可读
- [DoAnything](/webagent/features/do-anything) —— 资源模型
- [错误码与重试](/webagent/reference/errors-and-retries) —— 完整错误码矩阵
---
# WebAgent Vibecoding
如果你正在 IDE 里用 LLM 写代码——Cursor、Claude Code、Aider、Continue……——先把 WebAgent 完整文档和 OpenAPI schema 放进上下文,再让模型编写集成。这样可以避免模型依赖旧知识或猜测字段。
## 准备上下文
| 站内资源 | 用途 | 推荐方式 |
|---|---|---|
| [`/webagent/llms.txt`](/webagent/llms.txt) | 每页标题与一句描述的索引 | 让模型先定位相关页面 |
| [`/webagent/llms-full.txt`](/webagent/llms-full.txt) | 合并后的完整 Markdown 文档 | 下载或作为长上下文附件提供给模型 |
| [`/openapi/v1.json`](/openapi/v1.json) | OpenAPI 3.1 schema | 下载并作为接口字段、请求与响应结构的依据 |
这些资源使用当前文档站的站内路径,不依赖独立文档域名。复制下面的规则前,先打开或下载所需资源,并将内容添加到 IDE 会话或项目上下文中。
## 拿来即用 prompt
复制到你的 IDE 系统 prompt / 规则文件 / 第一条消息:
```
你正在集成 WebAgent。只以我提供的 WebAgent 完整文档和 OpenAPI 3.1 schema 为准;如果上下文缺少所需接口或字段,先说明缺口,不要猜测。
API 约定:
- Base URL:https://api.eak.eazo.ai
- Bearer 鉴权:header `Authorization: Bearer wa_…`
- path 隔离 project:/v1/projects/{project_id}/...
- wire 字段一律 snake_case;decimal 用 JSON 字符串("10.00" 不是 10.00)
- 大多数变更端点支持 Idempotency-Key
SDK 包:
- Python: `pip install web-agent-sdk`
- TypeScript: `npm install web-agent-sdk`
两边 SDK 都自带 Last-Event-ID SSE 重连。优先用 SDK,除非用户要求纯 HTTP。
写代码前先读取附加的 OpenAPI schema,并根据完整文档确认调用流程。
```
## Cursor
`.cursor/rules/webagent.md`:
```markdown
---
description: WebAgent 集成约定
globs: ["**/*.{ts,tsx,py}"]
---
写 WebAgent 代码前先读取项目提供的完整文档和 OpenAPI schema
字段名、请求和响应结构以 OpenAPI schema 为准——不允许猜
如果上下文没有定义所需接口,先询问再继续
默认用官方 SDK(`web-agent-sdk`),用户特别要求才用 raw HTTP
```
## Claude Code
加到项目 `CLAUDE.md`:
```markdown
## WebAgent 集成
先读取项目附加的 WebAgent 完整文档和 OpenAPI schema。
SDK:`web-agent-sdk`(Python 与 TypeScript)
字段名、请求和响应结构以 OpenAPI schema 为准。wire 字段全 snake_case。订阅 run 事件用 SDK 的 `.stream()`,自带 `Last-Event-ID` 重连。如果上下文没有定义所需接口,先询问,不要猜测。
```
## 更新上下文
当 WebAgent API 或 SDK 版本变化时,重新从本页的站内资源下载完整文档和 OpenAPI schema。不要在 IDE 规则文件中长期保存已经失效的外部文档域名。
## Console "Get Code" 对话框
最快拿到能跑的代码:打开 [Console](https://dashboard.qoni.ai/new),填表,点 **Get Code**。四个 tab(给 LLM agent 的 Prompt / Python / TypeScript / cURL),全部带你的真实 key 和当前配置。粘贴进编辑器即可。
## 为什么这套有效
- 文档和 schema 跟随当前项目或会话,不依赖固定的外部文档域名
- `llms-full.txt` 是 Markdown 而不是 HTML,LLM 可以直接解析
- OpenAPI spec 是 SDK / Console 共同的单一真相,没有漂移
## 接下来
- [Quickstart](/webagent/getting-started/quickstart) —— 5 分钟跑通第一个 run
- [API 参考](/webagent/reference/) —— 交互式 + Try-it