@qoniai/qoni 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -3
- package/README.md +146 -71
- package/README.zh-CN.md +98 -44
- package/dist/index.cjs +395 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -44
- package/dist/index.d.ts +176 -44
- package/dist/index.js +395 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/qoni-sdk/SKILL.md +42 -20
package/README.zh-CN.md
CHANGED
|
@@ -144,7 +144,7 @@ const run = await qoni.doAnything.run({
|
|
|
144
144
|
|
|
145
145
|
const result = await run.wait({
|
|
146
146
|
onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
|
|
147
|
-
onInteraction: (i) => { //
|
|
147
|
+
onInteraction: (i) => { // site_login / take_control / ask_user / confirmation
|
|
148
148
|
if (i.can("confirm_signed_in")) return i.confirmSignedIn();
|
|
149
149
|
if (i.can("confirm")) return i.confirm();
|
|
150
150
|
},
|
|
@@ -227,7 +227,7 @@ silent 产品能力方法传入 `token: delegation.data.token`。如果这个 to
|
|
|
227
227
|
|
|
228
228
|
**入参形态。** `delegateToken` 用 `mode` 判别:
|
|
229
229
|
|
|
230
|
-
- **Silent**(`mode: "silent"`,默认)——代表某个终端用户,所以 user **必填**:传 `user: { id: <genauthUserId> }
|
|
230
|
+
- **Silent**(`mode: "silent"`,默认)——代表某个终端用户,所以 user **必填**:传 `user: { id: <genauthUserId> }`(推荐)或顶层 `userId`。不传会在请求发出前本地抛 `QoniValidationError`。授权用 `products` 语法糖和/或显式 `scopes`。
|
|
231
231
|
- **Interactive**(`mode: "interactive"`)——传 `redirectUri`、`state`、`agent`、`scopes`,**不传** user(Qoni Console 在授权时解析登录)。返回 `data.authorizationUrl` 而非 token;在服务端用 `completeDelegateToken({ grantId, code, state })` 完成。
|
|
232
232
|
|
|
233
233
|
`agent` 可选,默认 `"sdk"`。
|
|
@@ -257,18 +257,27 @@ const grant = await qoni.delegateToken({
|
|
|
257
257
|
scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
// 回调不会带回 grantId:按 grantState 保存 grantId 和业务 state
|
|
261
|
+
await savePendingGrant(grant.data.grantState, {
|
|
262
|
+
grantId: grant.data.grantId,
|
|
263
|
+
businessState: "business-csrf-state",
|
|
264
|
+
});
|
|
260
265
|
redirectUserTo(grant.data.authorizationUrl);
|
|
261
266
|
```
|
|
262
267
|
|
|
263
|
-
用户访问 `authorizationUrl` 进入 Qoni Console/BFF。若用户未登录,Console/BFF 会先走 GenAuth 登录;授权确认后由 Console/BFF 统一 complete,再回跳业务 `redirectUri`,只带一次性 `code`、业务 `state
|
|
268
|
+
用户访问 `authorizationUrl` 进入 Qoni Console/BFF。若用户未登录,Console/BFF 会先走 GenAuth 登录;授权确认后由 Console/BFF 统一 complete,再回跳业务 `redirectUri`,只带一次性 `code`、业务 `state` 和 `grant_state`,**不带 `grantId`**,需要按 `grant_state` 找回创建请求时保存的记录。浏览器不会拿到 delegation token。
|
|
264
269
|
|
|
265
270
|
业务 callback handler 在服务端用 AK/SK 兑换最终 token:
|
|
266
271
|
|
|
267
272
|
```ts
|
|
273
|
+
const pending = await loadPendingGrant(String(req.query.grant_state));
|
|
274
|
+
// 业务 state 原样回到回调里,和保存的值比对,防止回调被伪造或串用
|
|
275
|
+
if (!pending || req.query.state !== pending.businessState) throw new Error("Invalid delegation callback");
|
|
276
|
+
|
|
268
277
|
const completed = await qoni.completeDelegateToken({
|
|
269
|
-
grantId:
|
|
278
|
+
grantId: pending.grantId,
|
|
270
279
|
code: String(req.query.code),
|
|
271
|
-
state: String(req.query.
|
|
280
|
+
state: String(req.query.grant_state), // 传 grant_state,不是业务 state
|
|
272
281
|
});
|
|
273
282
|
|
|
274
283
|
await qoni.gumem.recall({
|
|
@@ -277,7 +286,32 @@ await qoni.gumem.recall({
|
|
|
277
286
|
});
|
|
278
287
|
```
|
|
279
288
|
|
|
280
|
-
`completeDelegateToken` 是推荐的服务端 callback helper
|
|
289
|
+
`completeDelegateToken` 是推荐的服务端 callback helper。顶层 `qoni.delegateAgent` 和 `qoni.completeDelegateAgent` 仍保留为 deprecated 兼容命名,但当前 callback 契约是 `{ grantId, code, state }`;旧的 `{ code, state }` complete 签名不再兼容。
|
|
290
|
+
|
|
291
|
+
## 原始委托写法(0.8.0 起)
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const grant = await qoni.genauth.delegateAgent({
|
|
295
|
+
userId: process.env.QONI_USER_ID!, // 当前用户的 GenAuth ID
|
|
296
|
+
agentKey: "email-assistant", // Agent 审计标签,兼容 agent 参数
|
|
297
|
+
scopes: [
|
|
298
|
+
"webagent.do_anything:read", // 读取任务状态、事件和产物
|
|
299
|
+
"webagent.do_anything:manage", // 启动、取消任务和提交交互响应
|
|
300
|
+
],
|
|
301
|
+
expiresIn: "15m", // 签名前转换为 900 秒
|
|
302
|
+
});
|
|
303
|
+
console.log(grant.grantId, grant.auditId);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
此入口直接返回 grant;顶层 `delegateToken` 仍返回 `QoniResponse`,token 在
|
|
307
|
+
`response.data.token` 中。交互式模式直接返回授权请求;用户同意后,在服务端回调
|
|
308
|
+
用 `completeDelegateToken()` 兑换 token。`agentKey`、`userId` 分别兼容 `agent`、
|
|
309
|
+
`user.id`;同时提供两个 Agent 标签时必须一致,顶层 `userId` 保持原有的优先级。
|
|
310
|
+
有效期支持整数秒或整数 `s/m/h/d` 时长,
|
|
311
|
+
范围为 60–86400 秒。这些新增能力已随 `@qoniai/qoni@0.8.0` 发布到 npm。
|
|
312
|
+
|
|
313
|
+
旧版 `mailbox.*`、`web.search`、`web.extract` scope 和 `deniedScopes` 不受支持,SDK 会拒绝,不会将它们
|
|
314
|
+
扩展成更宽的产品权限。产品 scope 不限制发送、删除邮件;任务描述不能代替禁止策略。
|
|
281
315
|
|
|
282
316
|
## Scope 怎么选
|
|
283
317
|
|
|
@@ -291,7 +325,9 @@ await qoni.delegateToken({
|
|
|
291
325
|
});
|
|
292
326
|
```
|
|
293
327
|
|
|
294
|
-
scope 字符串在发请求前就会本地预检:漏掉服务前缀(比如写成 `do_anything:manage` 而不是 `webagent.do_anything:manage`)会立即抛出 `QoniValidationError
|
|
328
|
+
scope 字符串在发请求前就会本地预检:漏掉服务前缀(比如写成 `do_anything:manage` 而不是 `webagent.do_anything:manage`)会立即抛出 `QoniValidationError`,错误消息里带正确写法;短写法有歧义时列出全部候选(`profile:read` → `gumem.profile:read` 或 `user.profile:read`)。
|
|
329
|
+
|
|
330
|
+
站点登录、用户 Profile、支付和 Agent 邮箱的 scope(`QoniScopes.SITE_LOGIN_*`、`USER_PROFILE_READ`、`USER_PROFILE_WRITE`、`USER_PAYMENT_USE`、`AGENT_MAIL_READ`、`AGENT_MAIL_SEND`)不属于任何产品:需要在 `scopes` 中显式申请,并在 AccessKey 权限策略中允许。用户 Profile、支付和 Agent 邮箱这几个常量只是 scope 名称:申请它们本身不会让任务获得 Profile、支付或邮箱能力。
|
|
295
331
|
|
|
296
332
|
需要让授权边界直接出现在业务代码里时,使用显式 scope 字符串。
|
|
297
333
|
|
|
@@ -303,6 +339,10 @@ scope 字符串在发请求前就会本地预检:漏掉服务前缀(比如
|
|
|
303
339
|
| 搜索公开网页 | `webagent.web_search:read`, `webagent.web_search:manage` | Agent 可以搜索公开网页并读取结果。 |
|
|
304
340
|
| 执行网页任务 | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent 可以执行有边界的网页任务、读取进度、停止任务,并在需要时介入控制。 |
|
|
305
341
|
| 创建长期监控 | `webagent.track:read`, `webagent.track:manage` | Agent 可以配置监控、读取事件、立即运行检查,并停止监控。 |
|
|
342
|
+
| 在任务中登录网站 | `webagent.site_login:request`, `webagent.site_login:confirm` | 用户在实时浏览器里亲自登录,登录状态保存后供之后的任务复用。 |
|
|
343
|
+
| 使用用户 Profile | `user.profile:read`, `user.profile:write` | Agent 可以读取用户保存的资料(姓名、手机号、收货地址、卡组织和后四位),并保存用户新提供的资料。 |
|
|
344
|
+
| 用用户保存的卡支付 | `user.payment:use` | 用户允许一笔支付后,替用户填入已保存的信用卡。 |
|
|
345
|
+
| 使用 Agent 自己的邮箱 | `agent.mail:read`, `agent.mail:send` | Agent 可以读取发到自己邮箱的邮件(例如验证码),并用它发送邮件。 |
|
|
306
346
|
|
|
307
347
|
低风险能力通常可以静默授权,例如读取当前用户自己的 GUMem、写入本次会话摘要、搜索公开网页、读取普通任务结果。
|
|
308
348
|
|
|
@@ -555,55 +595,69 @@ interface RunResult {
|
|
|
555
595
|
|
|
556
596
|
### 交互(HITL)
|
|
557
597
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
```ts
|
|
561
|
-
for await (const event of run.events()) {
|
|
562
|
-
if (event.type === "interaction") {
|
|
563
|
-
const i = run.interactionHandle(event.data); // event.data 就是 Interaction
|
|
564
|
-
if (i.type === "clarification" && i.can("answer")) await i.answer("蓝色那个按钮");
|
|
565
|
-
else if (i.can("confirm")) await i.confirm();
|
|
566
|
-
else if (i.can("confirm_signed_in")) await i.confirmSignedIn();
|
|
567
|
-
else if (i.can("release_control")) await i.releaseControl();
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
```
|
|
598
|
+
run 需要用户出手时会暂停,并发出一个 `interaction` 事件,`event.data` 是带类型的 `Interaction`。交互按「需要用户做什么」分类,和具体业务无关,所以一个处理函数就能覆盖所有任务:
|
|
571
599
|
|
|
572
|
-
|
|
600
|
+
| `type` | 用户要做的事 | 方法 |
|
|
601
|
+
| --- | --- | --- |
|
|
602
|
+
| `site_login` | 在实时浏览器里亲自登录某个网站 | `openLogin()`、`confirmSignedIn()` |
|
|
603
|
+
| `take_control` | 亲手操作浏览器(验证码、两步验证等) | `connectControl()`、`refreshControl()`、`releaseControl()` |
|
|
604
|
+
| `ask_user` | 回答一个问题,答案结构由 `answerType` / `options` 描述 | `answer()`、`skip()` |
|
|
605
|
+
| `confirmation` | 允许或拒绝一个操作 | `confirm()`、`reject()` |
|
|
606
|
+
| `wait` | 不需要做决定:系统在等待(限流、外部事件) | `retry()` |
|
|
573
607
|
|
|
574
|
-
`
|
|
608
|
+
`Interaction` 是按 `type` 分布的联合类型,`switch (request.type)` 之后每个分支里的 `request.payload` 会自动收窄。每个方法都 POST 到交互 `actions[]` 里后端声明的 endpoint,不需要写死路由:
|
|
575
609
|
|
|
576
610
|
```ts
|
|
577
|
-
import {
|
|
611
|
+
import type { InteractionHandle } from "@qoniai/qoni";
|
|
578
612
|
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
613
|
+
const handled = new Set<string>();
|
|
614
|
+
|
|
615
|
+
async function handleInteraction(handle: InteractionHandle) {
|
|
616
|
+
const request = handle.interaction;
|
|
617
|
+
// 创建、状态更新和断线回放都会回调:每个待处理的交互只处理一次
|
|
618
|
+
if (request.status !== "pending" || handled.has(request.id)) return;
|
|
619
|
+
handled.add(request.id);
|
|
620
|
+
|
|
621
|
+
switch (request.type) {
|
|
622
|
+
case "site_login": {
|
|
623
|
+
const login = await handle.openLogin(); // 打开受控登录浏览器,返回实时画面 login.liveUrl
|
|
624
|
+
await showToUser(login.liveUrl); // 用户点「我已登录」后返回
|
|
625
|
+
await handle.confirmSignedIn(); // 保存登录状态,Agent 复查后继续
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case "take_control": {
|
|
629
|
+
// 服务端提供开始接管时,connectControl() 返回签名后的控制地址
|
|
630
|
+
const control = handle.can("connect_control") ? await handle.connectControl() : undefined;
|
|
631
|
+
await showToUser(control?.liveUrl ?? request.payload.liveUrl);
|
|
632
|
+
await handle.releaseControl(); // 把浏览器交还给 Agent
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
case "ask_user": // payload:{ question, answerType, options? }
|
|
636
|
+
await handle.answer(await askUser(request.payload));
|
|
637
|
+
break;
|
|
638
|
+
case "confirmation": // payload:{ summary }
|
|
639
|
+
if (await approve(request.payload.summary)) await handle.confirm();
|
|
640
|
+
else await handle.reject();
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
595
643
|
}
|
|
644
|
+
|
|
645
|
+
const result = await run.wait({ onInteraction: handleInteraction });
|
|
596
646
|
```
|
|
597
647
|
|
|
598
|
-
`
|
|
648
|
+
交互按顺序一个一个出现:当前这个处理完,run 才会继续。同一个交互在每次状态变化和断线回放时都会再次回调,`id` 和 `type` 始终不变——界面卡片请按 `request.id` 更新。带有新内容的更新会替换卡片(例如接管变成 `active` 时带来新的 `liveUrl`);后端用空壳结束交互时,卡片保留原来的 `payload`。直接以 `active` 出现的 `take_control` 是用户自己发起的接管(例如在 Console 里),界面提供接管入口时再展示它。方法:`answer(value)`、`skip()`、`confirm()`、`reject()`、`openLogin({ siteId?, profileId? })`、`confirmSignedIn()`、`connectControl()`、`refreshControl()`、`releaseControl()`、`retry()`、`switchProfile()`。`connectControl()` 和 `refreshControl()` 返回 `{ liveUrl, expiresAt?, status?, raw }`:用户正在控制的那个浏览器的签名地址。`handle.can(kind)` 判断该交互当前是否提供这个动作;调用后端没声明的方法会抛错(所以「恢复」按钮只有在真能用时才出现)。
|
|
649
|
+
|
|
650
|
+
**`ask_user` 的回答是结构化的。** `answer()` 接收与 `payload.answerType` 对应的值:`text` → 字符串,`number` → 数字,`boolean` → 布尔,`single_choice` → 一个 `options[].value`,`multiple_choice` → 选项 value 数组;不匹配时在发送前就抛出 `QoniValidationError`。线上协议里这个类型仍叫 `clarification`,SDK 统一呈现为 `ask_user`(候选项是 `{ value, label }`)。
|
|
651
|
+
|
|
652
|
+
**登录只需两步。** `openLogin()` 为 `site_login` 交互打开一个实时登录浏览器(`siteId` 取自 `payload.sites[].siteId`,只列出一个站点时可以省略)。用户登录完成后调用 `confirmSignedIn()`:SDK 先保存这次登录,之后的任务直接复用,再通知 Agent 复查站点并继续;如果用户其实没有登录成功,Agent 会再发起一次 `site_login`。无论调用几次,同一个登录会话只保存一次、只通知 Agent 复查一次。后端暂时没能保存登录时,它抛出 `code` 为 `site_login.save_failed` 的可重试 `QoniError`,不会让 Agent 继续;再调用一次即可。保存期间用户又重开了登录时,抛出的是 `site_login.session_replaced`,处理方式相同。同一个 `RunHandle`(或 `MonitorHandle`)发出的句柄共享登录会话,所以在之后回调拿到的句柄上调用 `confirmSignedIn()` 也有效;重新 `attach()` 得到的 RunHandle 不共享。委托时需要申请 `QoniScopes.SITE_LOGIN_REQUEST` 和 `QoniScopes.SITE_LOGIN_CONFIRM`,并在 AccessKey 权限策略中允许这两个 scope。`openLogin()` 返回值上的 `confirm()` 仍然保留,供自己保存登录的调用方使用,已标记为弃用。
|
|
599
653
|
|
|
600
|
-
上面用到的判别字符串也都导出成了常量,避免裸写字面量:`InteractionTypes`(`
|
|
654
|
+
上面用到的判别字符串也都导出成了常量,避免裸写字面量:`InteractionTypes`(`SiteLogin` / `TakeControl` / `AskUser` / `Confirmation` / `Wait`;已弃用的 `Clarification` 现在等于 `AskUser`)、`InteractionStatuses`、`ActionKinds`(`handle.can(...)` 的动作名)。授权 scope 同样有:`QoniScopes`(每个单独 scope)、`QoniProductScopes`(`products` 语法糖展开成的 per-product `read`+`manage` 对)、`QoniScopeBundles`(精选 bundle)。用它们代替手写字符串:
|
|
601
655
|
|
|
602
656
|
```ts
|
|
603
657
|
import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
|
|
604
658
|
|
|
605
|
-
if (
|
|
606
|
-
await
|
|
659
|
+
if (handle.type === InteractionTypes.AskUser && handle.can(ActionKinds.answer)) {
|
|
660
|
+
await handle.answer("蓝色那个按钮");
|
|
607
661
|
}
|
|
608
662
|
```
|
|
609
663
|
|
|
@@ -613,7 +667,7 @@ if (i.type === InteractionTypes.Clarification && i.can(ActionKinds.answer)) {
|
|
|
613
667
|
|
|
614
668
|
原始 wire 事件类型(`run.action.started` 这种内部传输名,命名是历史遗留、也不一致)**不作为公开导出**——开发者按上面那套精选 `event.type` 匹配即可;极少数要精确 wire 的场景,`event.raw.event` 是个原始字符串,直接比对。
|
|
615
669
|
|
|
616
|
-
Browser Use 的 scope 已预留,但产品运行时 SDK 方法导出前不作为公开方法说明。Site Login 的 scope(`QoniScopes.SITE_LOGIN_REQUEST`、`QoniScopes.SITE_LOGIN_CONFIRM`)用于运行交互上的 `InteractionHandle.openLogin()` 和 `
|
|
670
|
+
Browser Use 的 scope 已预留,但产品运行时 SDK 方法导出前不作为公开方法说明。Site Login 的 scope(`QoniScopes.SITE_LOGIN_REQUEST`、`QoniScopes.SITE_LOGIN_CONFIRM`)用于运行交互上的 `InteractionHandle.openLogin()` 和 `confirmSignedIn()`。
|
|
617
671
|
|
|
618
672
|
### 返回结构
|
|
619
673
|
|