# 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