@qoniai/qoni 0.8.0 → 0.9.1
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 +164 -2
- package/README.md +250 -109
- package/README.zh-CN.md +194 -78
- package/dist/index.cjs +14166 -519
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25619 -371
- package/dist/index.d.ts +25619 -371
- package/dist/index.js +14164 -519
- package/dist/index.js.map +1 -1
- package/docs/authing-sdk-LICENSE.txt +21 -0
- package/docs/genauth-communication.md +1410 -0
- package/docs/genauth-management-api.md +597 -0
- package/docs/genauth-management.md +285 -0
- package/package.json +14 -5
- package/skills/qoni-sdk/SKILL.md +156 -23
package/README.zh-CN.md
CHANGED
|
@@ -118,6 +118,25 @@ await qoni.genauth.users.update({
|
|
|
118
118
|
// await qoni.genauth.users.deleteBatch({ userIds: [profile.data.userId] });
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
### GenAuth 管理功能
|
|
122
|
+
|
|
123
|
+
`qoni.genauth` 现在提供 **35 个命名空间、591 个管理方法**,以 [Authing 官方 Node SDK 4.0.2](https://github.com/Authing/authing-node-sdk) 为参考,覆盖其全部 496 个 JSON 管理方法,并保留 GenAuth 扩展及兼容名称。除用户、角色、组织、应用和资源授权外,还包括岗位、公共账号、邀请、元数据、审批和风险名单。所有方法复用 Qoni AK/SK 管理 Token 交换,具备 TypeScript 参数及返回类型,并支持取消请求;实际部署的功能开关和权限仍决定接口可用性。
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await qoni.genauth.groups.create({ code: "engineering", name: "工程团队" });
|
|
127
|
+
await qoni.genauth.groups.addMembers({ code: "engineering", userIds: ["USER_ID"] });
|
|
128
|
+
|
|
129
|
+
// 沿用 Authing v3 方法名称的调用入口:
|
|
130
|
+
const roles = await qoni.genauth.management.listRoles({ namespace: "project", page: 1, limit: 20 });
|
|
131
|
+
|
|
132
|
+
// 自动逐页读取;break 后不会请求下一页:
|
|
133
|
+
for await (const user of qoni.genauth.users.iterate({ limit: 50 })) {
|
|
134
|
+
console.log(user.userId);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
[使用指南](./docs/genauth-management.md) · [完整 API 清单](./docs/genauth-management-api.md)。角色需属于已存在的权限分组;删除用户仍需遵循 GenAuth 的在职与离职规则。
|
|
139
|
+
|
|
121
140
|
### 运行时产品委托
|
|
122
141
|
|
|
123
142
|
GUMem、WebAgent、Do Anything、Web Search、Track 是代表终端用户执行的运行时产品能力。silent 委托需要一个来自 Qoni credential 绑定 userpool 的真实 GenAuth 用户 ID,再使用 `delegateToken` 返回的 token。
|
|
@@ -144,7 +163,7 @@ const run = await qoni.doAnything.run({
|
|
|
144
163
|
|
|
145
164
|
const result = await run.wait({
|
|
146
165
|
onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
|
|
147
|
-
onInteraction: (i) => { //
|
|
166
|
+
onInteraction: (i) => { // site_login / take_control / ask_user / confirmation
|
|
148
167
|
if (i.can("confirm_signed_in")) return i.confirmSignedIn();
|
|
149
168
|
if (i.can("confirm")) return i.confirm();
|
|
150
169
|
},
|
|
@@ -257,18 +276,27 @@ const grant = await qoni.delegateToken({
|
|
|
257
276
|
scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
|
|
258
277
|
});
|
|
259
278
|
|
|
279
|
+
// 回调不会带回 grantId:按 grantState 保存 grantId 和业务 state
|
|
280
|
+
await savePendingGrant(grant.data.grantState, {
|
|
281
|
+
grantId: grant.data.grantId,
|
|
282
|
+
businessState: "business-csrf-state",
|
|
283
|
+
});
|
|
260
284
|
redirectUserTo(grant.data.authorizationUrl);
|
|
261
285
|
```
|
|
262
286
|
|
|
263
|
-
用户访问 `authorizationUrl` 进入 Qoni Console/BFF。若用户未登录,Console/BFF 会先走 GenAuth 登录;授权确认后由 Console/BFF 统一 complete,再回跳业务 `redirectUri`,只带一次性 `code`、业务 `state
|
|
287
|
+
用户访问 `authorizationUrl` 进入 Qoni Console/BFF。若用户未登录,Console/BFF 会先走 GenAuth 登录;授权确认后由 Console/BFF 统一 complete,再回跳业务 `redirectUri`,只带一次性 `code`、业务 `state` 和 `grant_state`,**不带 `grantId`**,需要按 `grant_state` 找回创建请求时保存的记录。浏览器不会拿到 delegation token。
|
|
264
288
|
|
|
265
289
|
业务 callback handler 在服务端用 AK/SK 兑换最终 token:
|
|
266
290
|
|
|
267
291
|
```ts
|
|
292
|
+
const pending = await loadPendingGrant(String(req.query.grant_state));
|
|
293
|
+
// 业务 state 原样回到回调里,和保存的值比对,防止回调被伪造或串用
|
|
294
|
+
if (!pending || req.query.state !== pending.businessState) throw new Error("Invalid delegation callback");
|
|
295
|
+
|
|
268
296
|
const completed = await qoni.completeDelegateToken({
|
|
269
|
-
grantId:
|
|
297
|
+
grantId: pending.grantId,
|
|
270
298
|
code: String(req.query.code),
|
|
271
|
-
state: String(req.query.
|
|
299
|
+
state: String(req.query.grant_state), // 传 grant_state,不是业务 state
|
|
272
300
|
});
|
|
273
301
|
|
|
274
302
|
await qoni.gumem.recall({
|
|
@@ -279,7 +307,7 @@ await qoni.gumem.recall({
|
|
|
279
307
|
|
|
280
308
|
`completeDelegateToken` 是推荐的服务端 callback helper。顶层 `qoni.delegateAgent` 和 `qoni.completeDelegateAgent` 仍保留为 deprecated 兼容命名,但当前 callback 契约是 `{ grantId, code, state }`;旧的 `{ code, state }` complete 签名不再兼容。
|
|
281
309
|
|
|
282
|
-
## 原始委托写法(0.8.0
|
|
310
|
+
## 原始委托写法(0.8.0 起)
|
|
283
311
|
|
|
284
312
|
```ts
|
|
285
313
|
const grant = await qoni.genauth.delegateAgent({
|
|
@@ -299,7 +327,7 @@ console.log(grant.grantId, grant.auditId);
|
|
|
299
327
|
用 `completeDelegateToken()` 兑换 token。`agentKey`、`userId` 分别兼容 `agent`、
|
|
300
328
|
`user.id`;同时提供两个 Agent 标签时必须一致,顶层 `userId` 保持原有的优先级。
|
|
301
329
|
有效期支持整数秒或整数 `s/m/h/d` 时长,
|
|
302
|
-
范围为 60–86400
|
|
330
|
+
范围为 60–86400 秒。这些新增能力已随 `@qoniai/qoni@0.8.0` 发布到 npm。
|
|
303
331
|
|
|
304
332
|
旧版 `mailbox.*`、`web.search`、`web.extract` scope 和 `deniedScopes` 不受支持,SDK 会拒绝,不会将它们
|
|
305
333
|
扩展成更宽的产品权限。产品 scope 不限制发送、删除邮件;任务描述不能代替禁止策略。
|
|
@@ -316,7 +344,9 @@ await qoni.delegateToken({
|
|
|
316
344
|
});
|
|
317
345
|
```
|
|
318
346
|
|
|
319
|
-
scope 字符串在发请求前就会本地预检:漏掉服务前缀(比如写成 `do_anything:manage` 而不是 `webagent.do_anything:manage`)会立即抛出 `QoniValidationError
|
|
347
|
+
scope 字符串在发请求前就会本地预检:漏掉服务前缀(比如写成 `do_anything:manage` 而不是 `webagent.do_anything:manage`)会立即抛出 `QoniValidationError`,错误消息里带正确写法;短写法有歧义时列出全部候选(`profile:read` → `gumem.profile:read` 或 `user.profile:read`)。
|
|
348
|
+
|
|
349
|
+
站点登录、用户 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、支付或邮箱能力。
|
|
320
350
|
|
|
321
351
|
需要让授权边界直接出现在业务代码里时,使用显式 scope 字符串。
|
|
322
352
|
|
|
@@ -328,6 +358,10 @@ scope 字符串在发请求前就会本地预检:漏掉服务前缀(比如
|
|
|
328
358
|
| 搜索公开网页 | `webagent.web_search:read`, `webagent.web_search:manage` | Agent 可以搜索公开网页并读取结果。 |
|
|
329
359
|
| 执行网页任务 | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent 可以执行有边界的网页任务、读取进度、停止任务,并在需要时介入控制。 |
|
|
330
360
|
| 创建长期监控 | `webagent.track:read`, `webagent.track:manage` | Agent 可以配置监控、读取事件、立即运行检查,并停止监控。 |
|
|
361
|
+
| 在任务中登录网站 | `webagent.site_login:request`, `webagent.site_login:confirm` | 用户在实时浏览器里亲自登录,登录状态保存后供之后的任务复用。 |
|
|
362
|
+
| 使用用户 Profile | `user.profile:read`, `user.profile:write` | Agent 可以读取用户保存的资料(姓名、手机号、收货地址、卡组织和后四位),并保存用户新提供的资料。 |
|
|
363
|
+
| 用用户保存的卡支付 | `user.payment:use` | 用户允许一笔支付后,替用户填入已保存的信用卡。 |
|
|
364
|
+
| 使用 Agent 自己的邮箱 | `agent.mail:read`, `agent.mail:send` | Agent 可以读取发到自己邮箱的邮件(例如验证码),并用它发送邮件。 |
|
|
331
365
|
|
|
332
366
|
低风险能力通常可以静默授权,例如读取当前用户自己的 GUMem、写入本次会话摘要、搜索公开网页、读取普通任务结果。
|
|
333
367
|
|
|
@@ -438,43 +472,93 @@ const followUp = await qoni.deepResearch.run({
|
|
|
438
472
|
});
|
|
439
473
|
```
|
|
440
474
|
|
|
475
|
+
`doAnything.api.readArtifacts()` 及导出类型 `DoAnythingSnapshot`、`SnapshotImage`、
|
|
476
|
+
`SnapshotAction` 已 deprecated:该结构化快照契约从未有后端路径或字段来源。
|
|
477
|
+
方法本地抛 `QoniUnsupportedError(code: "unsupported.operation")`,零 HTTP。
|
|
478
|
+
文件请用 `qoni.doAnything.artifacts({ token, runId })` / `api.listArtifacts()`,下载用 `artifact.content()` /
|
|
479
|
+
`api.artifactDownloadUrl()`;图片请开启 `capture: { screenshots: true }` 并读取
|
|
480
|
+
screenshot events。文本文件不是 snapshot,真实文件 artifacts API 保持。
|
|
481
|
+
|
|
441
482
|
### Track
|
|
442
483
|
|
|
484
|
+
Track 使用真实 `/track/tracks`。`prompt` 原样映射到 `instructions`,目标 URL
|
|
485
|
+
请明确写在描述中。必须显式指定 schedule:整数 600–2592000 秒 interval,或带
|
|
486
|
+
时区的 daily。daily 在目标时间前 0–30 分钟启动,不保证整点启动。
|
|
487
|
+
|
|
443
488
|
```ts
|
|
489
|
+
import type { MonitorSchedule } from "@qoniai/qoni";
|
|
490
|
+
|
|
444
491
|
const monitor = await qoni.track.create({
|
|
445
492
|
token,
|
|
446
|
-
prompt: "
|
|
447
|
-
|
|
493
|
+
prompt: "监控 https://example.com/policy 并报告变化。",
|
|
494
|
+
title: "政策变化",
|
|
495
|
+
schedule: { kind: "interval", intervalSeconds: 3600 },
|
|
496
|
+
}); // create 立即启动 first_look 首次检查
|
|
497
|
+
const initial = await monitor.get();
|
|
498
|
+
const firstRunId = initial.lastTaskId; // 只捕获一次原始首检 ID
|
|
499
|
+
if (typeof firstRunId !== "string" || !firstRunId) throw new Error("缺少初始 lastTaskId");
|
|
500
|
+
const sameMonitor = await qoni.track.attach(monitor.id, { token });
|
|
448
501
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
502
|
+
async function waitForCheck(runId: string) {
|
|
503
|
+
const deadline = Date.now() + 300_000;
|
|
504
|
+
do {
|
|
505
|
+
const check = await monitor.run(runId); // 在最近 50 条中按原始 ID 精确查找
|
|
506
|
+
if (["done", "failed", "expired", "canceled"].includes(String(check.status))) {
|
|
507
|
+
if (check.status !== "done") throw new Error(`检查 ${runId} ${String(check.status)}:${String(check.failure ?? "")}`);
|
|
508
|
+
return check;
|
|
509
|
+
}
|
|
510
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - Date.now()))));
|
|
511
|
+
} while (Date.now() < deadline);
|
|
512
|
+
throw new Error(`检查 ${runId} 超时`);
|
|
460
513
|
}
|
|
461
514
|
|
|
462
|
-
|
|
463
|
-
|
|
515
|
+
try {
|
|
516
|
+
await monitor.pause(); // 停止未来调度,已经启动的首检继续
|
|
517
|
+
await waitForCheck(firstRunId);
|
|
518
|
+
await monitor.refine({ schedule: initial.schedule as MonitorSchedule }); // 本次已校验的已知输入可 roundtrip
|
|
519
|
+
await monitor.refine({ schedule: { kind: "daily", at: "09:00", tz: "Asia/Singapore" } });
|
|
520
|
+
const task = await monitor.runNow(); // 调度暂停时,显式手动执行一次
|
|
521
|
+
const check = await waitForCheck(task.runId); // 202 接受不等于完成
|
|
522
|
+
const checks = await monitor.runs({ limit: 10, offset: 0 });
|
|
523
|
+
await monitor.resume();
|
|
524
|
+
} finally {
|
|
525
|
+
await monitor.delete(); // 软删除;仍在运行的检查继续
|
|
526
|
+
}
|
|
527
|
+
```
|
|
464
528
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// get()/run() 读回时,抽取字段名与抽取结果保留原始键名。
|
|
469
|
-
// 自然语言意图不可 refine——要改描述请新建 monitor:
|
|
470
|
-
await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
|
|
471
|
-
await monitor.pause(); // 停掉定时 tick 但不删除
|
|
472
|
-
await monitor.resume();
|
|
473
|
-
await monitor.delete();
|
|
529
|
+
create 会立即启动一次 `first_look` 首检。只捕获一次初始 `lastTaskId`,等待
|
|
530
|
+
同一个首检结束,再显式调用一次 runNow。并发启动其他任务仍可能导致 409,SDK
|
|
531
|
+
原样抛出,不隐式重试。pause 只暂停未来调度,已经启动的检查继续。
|
|
474
532
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
533
|
+
Track check.status 为 `running/done/failed/expired/canceled`,成功终态是 **done**,
|
|
534
|
+
原样保留,区别于单次 RunResult.status 的 succeeded。读回已知 wire schedule 形状
|
|
535
|
+
时不重新套用 SDK 写入边界或 Intl 时区校验;未知/不完整形状原样保留。写入仍严格
|
|
536
|
+
校验,请选择支持的输入,不要把任意服务端 schedule 强制 cast 后重写。
|
|
537
|
+
|
|
538
|
+
`runs()` 查询后端最近 50 条 checks,再在本地应用 limit/offset;`run(id)` 只在
|
|
539
|
+
同一窗口按精确 run_id 查找,未找到抛本地 `QoniError(code: "not_found")`(没有 HTTP status)并说明范围;Track 本身不存在是后端 404 `track_not_found`。
|
|
540
|
+
检查保留真实 status/outcome/headline/answer/failure,不补造 extracted、diffs 或
|
|
541
|
+
snapshotId。早前任务仍在运行时,`runNow()` 会原样抛后端 409 `task_in_progress`;
|
|
542
|
+
成功只返回真实任务引用,完成与否需要再读 checks。
|
|
543
|
+
|
|
544
|
+
| 保留的方法或输入 | 当前行为 |
|
|
545
|
+
| --- | --- |
|
|
546
|
+
| `track.create`、`attach`、`MonitorHandle.get` | 创建/读取真实 Track;get 转换已知 schedule 形状,未知形状原样保留。 |
|
|
547
|
+
| `MonitorHandle.pause`、`resume` | PATCH status 为 paused / active;pause 不停已启动的检查。 |
|
|
548
|
+
| `MonitorHandle.refine` | 仅修改 title/schedule;改 prompt/instructions/intent 需新建 Track。 |
|
|
549
|
+
| `MonitorHandle.runNow` | POST run_now,返回真实任务引用。 |
|
|
550
|
+
| `MonitorHandle.runs`、`run` | 仅以 Track read scope 读取最近检查,不回退到 DoAnything。 |
|
|
551
|
+
| `MonitorHandle.delete` | DELETE204 软删除停止调度,不取消正在运行的任务。 |
|
|
552
|
+
| `track.api.createMonitor`、`getMonitor` | 保留旧方法名;真实字段为 instructions/title,wire schedule 为 `{ every_seconds: N }` 或 `{ daily_at: "HH:MM", tz }`。 |
|
|
553
|
+
| `track.api.updateMonitor` | PATCH 真实 title/instructions/schedule/status;低层可改 instructions,高层 refine 不允许。 |
|
|
554
|
+
| `track.api.runNow`、`deleteMonitor` | 真实 run_now / DELETE;wire 任务引用 / 空 204。 |
|
|
555
|
+
| `track.api.listRuns`、`getRun` | 原始 checks,本地分页,精确查找最近 50 条。 |
|
|
556
|
+
| `MonitorHandle.events`、`interactionHandle`、`track.api.events`、`intervene` | deprecated,无后端路径,本地 QoniUnsupportedError,零 HTTP/零 token exchange。 |
|
|
557
|
+
| `targetUrls`、`extractionSchema`、`triggerDsl`、`stopConditionDsl`、`notifyChannel`(含 snake_case 别名)、旧 `url` | 无对应字段,本地 QoniUnsupportedError;URL 需明确写入 prompt。 |
|
|
558
|
+
| cron schedule | deprecated,本地明确拒绝并给出 daily 替代,不默默转换任意 cron。 |
|
|
559
|
+
|
|
560
|
+
无 schedule、非法 interval/时间/时区/分页等输入在 HTTP 前抛 `QoniValidationError`。
|
|
561
|
+
旧类型导出与 cron 分支保留并标记 deprecated;值为 undefined 的可选键保持省略行为,有值的不支持字段仍拒绝;unsupported 负例不算能力正例。
|
|
478
562
|
|
|
479
563
|
### GenAuth 用户上下文与用户管理
|
|
480
564
|
|
|
@@ -567,7 +651,7 @@ interface RunResult {
|
|
|
567
651
|
runId: string;
|
|
568
652
|
status: "succeeded" | "failed" | "canceled"; // run 的交付状态
|
|
569
653
|
output?: unknown; // 最终答案 / 载荷(形状随任务而定)
|
|
570
|
-
artifacts: Artifact[]; //
|
|
654
|
+
artifacts: Artifact[]; // 持久化 DoAnything 文件 / deepResearch 报告
|
|
571
655
|
isTaskSuccessful?: boolean; // 任务是否达成目标(与 status 不同)
|
|
572
656
|
terminalReason?: string;
|
|
573
657
|
raw: JsonObject; // 完整 wire envelope——成本、步数、token 用量
|
|
@@ -576,69 +660,83 @@ interface RunResult {
|
|
|
576
660
|
|
|
577
661
|
每个 `Artifact` 暴露 `id`、`name?`、`mime?`、`sizeBytes?`、`createdAt?` 和 `content(): Promise<Uint8Array>`(懒加载字节)。其余方法都返回下文的 `QoniResponse<T>` 信封,`T` 是该调用的具体载荷类型(例如 `genauth.users.list` → `.data` 上的分页用户列表)。`T` 的精确形状见随包的 `.d.ts`,与后端契约一致。
|
|
578
662
|
|
|
579
|
-
`MonitorHandle`(Track)提供 `id`、`get()`、`pause()`、`resume()`、`refine(patch)`、`runNow()`、`
|
|
663
|
+
`MonitorHandle`(Track)提供 `id`、`get()`、`pause()`、`resume()`、`refine(patch)`、`runNow()`、`runs()`、`run(runId)`、`delete()`。deprecated 的 `events()` / `interactionHandle()` 本地拒绝,完整逐项映射见上方 Track。
|
|
580
664
|
|
|
581
665
|
### 交互(HITL)
|
|
582
666
|
|
|
583
|
-
|
|
667
|
+
run 需要用户出手时会暂停,并发出一个 `interaction` 事件,`event.data` 是带类型的 `Interaction`。交互按「需要用户做什么」分类,和具体业务无关,所以一个处理函数就能覆盖所有任务:
|
|
584
668
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
else if (i.can("release_control")) await i.releaseControl();
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
方法:`answer(text)`、`skip()`、`confirm()`、`reject()`、`openLogin({ siteId?, profileId? })`、`confirmSignedIn()`、`connectControl()`、`refreshControl()`、`releaseControl()`、`retry()`、`switchProfile()`。`i.can(kind)` 判断该交互当前是否提供这个动作;调用后端没声明的方法会抛错(所以「恢复」按钮只有在真能用时才出现)。
|
|
669
|
+
| `type` | 用户要做的事 | 方法 |
|
|
670
|
+
| --- | --- | --- |
|
|
671
|
+
| `site_login` | 在实时浏览器里亲自登录某个网站 | `openLogin()`、`confirmSignedIn()` |
|
|
672
|
+
| `take_control` | 亲手操作浏览器(验证码、两步验证等) | `connectControl()`、`refreshControl()`、`releaseControl()` |
|
|
673
|
+
| `ask_user` | 回答一个问题,答案结构由 `answerType` / `options` 描述 | `answer()`、`skip()` |
|
|
674
|
+
| `confirmation` | 允许或拒绝一个操作 | `confirm()`、`reject()` |
|
|
675
|
+
| `wait` | 不需要做决定:系统在等待(限流、外部事件) | `retry()` |
|
|
598
676
|
|
|
599
|
-
`
|
|
677
|
+
`Interaction` 是按 `type` 分布的联合类型,`switch (request.type)` 之后每个分支里的 `request.payload` 会自动收窄。每个方法都 POST 到交互 `actions[]` 里后端声明的 endpoint,不需要写死路由:
|
|
600
678
|
|
|
601
679
|
```ts
|
|
602
|
-
import {
|
|
680
|
+
import type { InteractionHandle } from "@qoniai/qoni";
|
|
603
681
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
682
|
+
const handled = new Set<string>();
|
|
683
|
+
|
|
684
|
+
async function handleInteraction(handle: InteractionHandle) {
|
|
685
|
+
const request = handle.interaction;
|
|
686
|
+
// 创建、状态更新和断线回放都会回调:每个待处理的交互只处理一次
|
|
687
|
+
if (request.status !== "pending" || handled.has(request.id)) return;
|
|
688
|
+
handled.add(request.id);
|
|
689
|
+
|
|
690
|
+
switch (request.type) {
|
|
691
|
+
case "site_login": {
|
|
692
|
+
const login = await handle.openLogin(); // 打开受控登录浏览器,返回实时画面 login.liveUrl
|
|
693
|
+
await showToUser(login.liveUrl); // 用户点「我已登录」后返回
|
|
694
|
+
await handle.confirmSignedIn(); // 保存登录状态,Agent 复查后继续
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case "take_control": {
|
|
698
|
+
// 服务端提供开始接管时,connectControl() 返回签名后的控制地址
|
|
699
|
+
const control = handle.can("connect_control") ? await handle.connectControl() : undefined;
|
|
700
|
+
await showToUser(control?.liveUrl ?? request.payload.liveUrl);
|
|
701
|
+
await handle.releaseControl(); // 把浏览器交还给 Agent
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
case "ask_user": // payload:{ question, answerType, options? }
|
|
705
|
+
await handle.answer(await askUser(request.payload));
|
|
706
|
+
break;
|
|
707
|
+
case "confirmation": // payload:{ summary }
|
|
708
|
+
if (await approve(request.payload.summary)) await handle.confirm();
|
|
709
|
+
else await handle.reject();
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
620
712
|
}
|
|
713
|
+
|
|
714
|
+
const result = await run.wait({ onInteraction: handleInteraction });
|
|
621
715
|
```
|
|
622
716
|
|
|
623
|
-
`
|
|
717
|
+
交互按顺序一个一个出现:当前这个处理完,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)` 判断该交互当前是否提供这个动作;调用后端没声明的方法会抛错(所以「恢复」按钮只有在真能用时才出现)。
|
|
718
|
+
|
|
719
|
+
**`ask_user` 的回答是结构化的。** `answer()` 接收与 `payload.answerType` 对应的值:`text` → 字符串,`number` → 数字,`boolean` → 布尔,`single_choice` → 一个 `options[].value`,`multiple_choice` → 选项 value 数组;不匹配时在发送前就抛出 `QoniValidationError`。线上协议里这个类型仍叫 `clarification`,SDK 统一呈现为 `ask_user`(候选项是 `{ value, label }`)。
|
|
624
720
|
|
|
625
|
-
|
|
721
|
+
**登录只需两步。** `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()` 仍然保留,供自己保存登录的调用方使用,已标记为弃用。
|
|
722
|
+
|
|
723
|
+
上面用到的判别字符串也都导出成了常量,避免裸写字面量:`InteractionTypes`(`SiteLogin` / `TakeControl` / `AskUser` / `Confirmation` / `Wait`;已弃用的 `Clarification` 现在等于 `AskUser`)、`InteractionStatuses`、`ActionKinds`(`handle.can(...)` 的动作名)。授权 scope 同样有:`QoniScopes`(每个单独 scope)、`QoniProductScopes`(`products` 语法糖展开成的 per-product `read`+`manage` 对)、`QoniScopeBundles`(精选 bundle)。用它们代替手写字符串:
|
|
626
724
|
|
|
627
725
|
```ts
|
|
628
726
|
import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
|
|
629
727
|
|
|
630
|
-
if (
|
|
631
|
-
await
|
|
728
|
+
if (handle.type === InteractionTypes.AskUser && handle.can(ActionKinds.answer)) {
|
|
729
|
+
await handle.answer("蓝色那个按钮");
|
|
632
730
|
}
|
|
633
731
|
```
|
|
634
732
|
|
|
635
733
|
### Wire 级逃生舱
|
|
636
734
|
|
|
637
|
-
|
|
735
|
+
可支持的低层方法保留在 `qoni.<product>.api.*` 下(如 `qoni.doAnything.api.createSession`、`qoni.deepResearch.api.followUp`、`qoni.track.api.createMonitor`)。这些方法直接镜像 wire 形状——snake_case 字段、双层 envelope 事件、run 操作统一走 `/{product}/runs/{run_id}` 单 ID 寻址——形状会随 API 演进变化,不在冻结的公开契约范围内。
|
|
638
736
|
|
|
639
737
|
原始 wire 事件类型(`run.action.started` 这种内部传输名,命名是历史遗留、也不一致)**不作为公开导出**——开发者按上面那套精选 `event.type` 匹配即可;极少数要精确 wire 的场景,`event.raw.event` 是个原始字符串,直接比对。
|
|
640
738
|
|
|
641
|
-
Browser Use 的 scope 已预留,但产品运行时 SDK 方法导出前不作为公开方法说明。Site Login 的 scope(`QoniScopes.SITE_LOGIN_REQUEST`、`QoniScopes.SITE_LOGIN_CONFIRM`)用于运行交互上的 `InteractionHandle.openLogin()` 和 `
|
|
739
|
+
Browser Use 的 scope 已预留,但产品运行时 SDK 方法导出前不作为公开方法说明。Site Login 的 scope(`QoniScopes.SITE_LOGIN_REQUEST`、`QoniScopes.SITE_LOGIN_CONFIRM`)用于运行交互上的 `InteractionHandle.openLogin()` 和 `confirmSignedIn()`。
|
|
642
740
|
|
|
643
741
|
### 返回结构
|
|
644
742
|
|
|
@@ -665,9 +763,9 @@ type QoniResponse<T> = {
|
|
|
665
763
|
// "phase" string —— Deep Research 阶段名
|
|
666
764
|
// "sectionReady" string —— Deep Research 章节标题
|
|
667
765
|
// "resultsReady" number —— Web Search 结果数
|
|
668
|
-
// "monitorCreated" string ——
|
|
669
|
-
// "triggered" string ——
|
|
670
|
-
// "checkCompleted" boolean ——
|
|
766
|
+
// "monitorCreated" string —— deprecated 的旧 monitor wire 类型
|
|
767
|
+
// "triggered" string —— deprecated 的旧 monitor wire 类型
|
|
768
|
+
// "checkCompleted" boolean —— deprecated 的旧 monitor wire 类型
|
|
671
769
|
// 丰富事件 —— event.data 是个小对象:
|
|
672
770
|
// "message" { text, role }
|
|
673
771
|
// "interaction" 带类型的 Interaction 对象(HITL)—— 见「交互」一节
|
|
@@ -684,7 +782,7 @@ type QoniResponse<T> = {
|
|
|
684
782
|
| Do Anything | 核心:`progress` / `message` / `interaction` / `screenshot` / `browserLiveUrlChanged` / `done` |
|
|
685
783
|
| Web Search | 核心 + `resultsReady` |
|
|
686
784
|
| Deep Research | 核心 + `phase` / `sectionReady`(大纲审批走 `interaction`) |
|
|
687
|
-
| Track(`MonitorHandle`) |
|
|
785
|
+
| Track(`MonitorHandle`) | 无 SSE 路径,旧 MonitorEvent 类型 deprecated;用 runs()/run() 读取检查。 |
|
|
688
786
|
|
|
689
787
|
展示实时浏览器时,监听 `browserLiveUrlChanged`,用 `event.data.liveUrl` 更新预览地址。任务执行期间可能切换标签页,因此不能只使用第一次收到的地址。`forceViewerReload` 为 true 时重载已有预览。live URL 可访问浏览器画面,不要写入公开日志。
|
|
690
788
|
|
|
@@ -710,6 +808,7 @@ type QoniEvent<T = unknown> = {
|
|
|
710
808
|
|
|
711
809
|
| 类 | 触发时机 | 典型处理 |
|
|
712
810
|
| --- | --- | --- |
|
|
811
|
+
| `QoniUnsupportedError` | 保留表面没有后端操作(code: `unsupported.operation`),零 HTTP。 | 使用错误提示中的真实替代能力。 |
|
|
713
812
|
| `QoniValidationError` | 请求发出前的本地校验失败(scope 字符串格式错、silent 委托缺 user)。 | 按消息修正调用——消息里给出正确写法。 |
|
|
714
813
|
| `QoniAuthError` | AK/SK 签名被 Qoni 拒绝(凭据错误/已轮换)。 | 检查 `accessKey` / `secretKey`。 |
|
|
715
814
|
| `QoniPermissionDeniedError` | AccessKey 或 delegation token 缺少所需 scope(403)。 | 检查 AccessKey 权限策略,并在 `delegateToken` 中申请所需 scope。 |
|
|
@@ -798,3 +897,20 @@ const artifacts = await client.doAnything.artifacts({ token, runId: run.id });
|
|
|
798
897
|
```
|
|
799
898
|
|
|
800
899
|
Wire methods: `client.doAnything.api.listArtifacts(...)` and `artifactDownloadUrl(...)`.
|
|
900
|
+
|
|
901
|
+
## Agent Mail 与 Agent Phone
|
|
902
|
+
|
|
903
|
+
本地通信扩展提供 `qoni.genauth.agentMail` 和 `qoni.genauth.agentPhone` 的运行侧与管理侧 API。运行侧通过 `genauthCommunicationCredentials` 配置原生 GenAuth 凭据;管理侧复用 Qoni 的兑换流程。详见 [通信指南](docs/genauth-communication.md) 与 [方法索引](docs/genauth-communication-api.md)。
|
|
904
|
+
|
|
905
|
+
## 本地 Agent 整合扩展
|
|
906
|
+
|
|
907
|
+
整合分支增加 `qoni.genauth.auth.createLoginUrl` / `exchangeLoginCode` 与
|
|
908
|
+
`qoni.genauth.agents.create` / `get` / `bind` / `bindUser`,并保留原有管理、
|
|
909
|
+
Agent Mail 和 Agent Phone 接口。这些新增内容尚未发布到 npm。登录回调必须先核对
|
|
910
|
+
App 保存的 `state`,再兑换一次性 code。Personal 邮箱委托必须指定真实 IAM
|
|
911
|
+
`agentId`,审计标签不能证明邮箱归属。创建可能仍处于 pending;结果未知时只能使用
|
|
912
|
+
原 provisioning 引用或幂等键核对,不能重新创建资源。
|
|
913
|
+
|
|
914
|
+
Personal Quickstart 的邮箱创建流程与统一 Agent 管理的可选能力模型分别保留。
|
|
915
|
+
统一管理中身份、邮箱、手机号均可选,一个 Agent 可以拥有多个邮箱和手机号。
|
|
916
|
+
绑定 Personal 身份不会签发凭据,也不会自动打通 IAM token 调用原生 Mail/Phone API。
|