@qoniai/qoni 0.6.0 → 0.8.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 +70 -2
- package/README.md +74 -7
- package/README.zh-CN.md +59 -7
- package/dist/index.cjs +331 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -10
- package/dist/index.d.ts +101 -10
- package/dist/index.js +331 -149
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `qoni.genauth.delegateAgent(input)` returns the grant directly (`grant.token`),
|
|
8
|
+
or an interactive authorization request, through the existing signed endpoint.
|
|
9
|
+
- `agentKey` is an alias for `agent`. Existing root and `qoni.qoni` entries keep
|
|
10
|
+
their `QoniResponse` envelopes. Conflicting Agent labels are rejected;
|
|
11
|
+
the existing top-level `userId` precedence is unchanged.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Expiry strings such as `"15m"` are converted to integer seconds; valid
|
|
16
|
+
durations are 60–86400 seconds, matching the GenAuth DTO.
|
|
17
|
+
- Unsupported `deniedScopes` is rejected rather than forwarding an unenforced
|
|
18
|
+
deny policy. Legacy `mailbox.*`, `web.search`, and `web.extract` scopes are rejected without mapping to product scopes.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Expiry validation now happens locally before signing. Invalid formats and
|
|
23
|
+
values outside 60–86400 seconds throw `QoniValidationError`; numeric seconds
|
|
24
|
+
strings are sent as integers.
|
|
25
|
+
|
|
26
|
+
## 0.7.0 - 2026-10-02
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `InteractionHandle.openLogin()` now resolves to an `OpenLoginResult`
|
|
31
|
+
(`{ profileId, siteId, sessionId, liveUrl, raw, confirm() }`) instead of
|
|
32
|
+
`void`. TypeScript code that stores the method as `() => Promise<void>` needs
|
|
33
|
+
to widen that type; runtime callers that ignore the result are unaffected.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `OpenLoginResult.confirm()` persists the login into the profile
|
|
38
|
+
(`POST /profiles/{profileId}/confirm`) and reports `active`. Call
|
|
39
|
+
`confirmSignedIn()` once it is active (or when `probeResult` is
|
|
40
|
+
`no_session`), otherwise reopen the session with
|
|
41
|
+
`openLogin({ siteId, profileId })`.
|
|
42
|
+
- `QoniScopes.SITE_LOGIN_REQUEST` (`webagent.site_login:request`) and
|
|
43
|
+
`QoniScopes.SITE_LOGIN_CONFIRM` (`webagent.site_login:confirm`). They are not
|
|
44
|
+
part of any product bundle: delegate them explicitly and allow them in the
|
|
45
|
+
AccessKey policy.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- `openLogin()` now starts a working login session. It sends the `site_id` the
|
|
50
|
+
backend's `/profiles/login` route requires (the only site listed on the
|
|
51
|
+
`site_login` interaction, or `openLogin({ siteId })`; pass `profileId` to
|
|
52
|
+
re-login an existing profile) and requests the site-login scope instead of the
|
|
53
|
+
product's manage scope. Previously the backend always rejected it. This
|
|
54
|
+
covers `site_login` interactions on Do Anything and Deep Research runs; Track
|
|
55
|
+
monitor re-login (`monitor.needs_login`, backend `/track/monitors/{id}/re_auth`)
|
|
56
|
+
is not wrapped by the SDK yet.
|
|
57
|
+
- `run.wait()` retries a transient failure of the reads it makes after the run
|
|
58
|
+
finished (run detail, artifact list) up to twice. A dropped connection or a
|
|
59
|
+
timed-out token exchange no longer fails a run that already succeeded. Only
|
|
60
|
+
`retryable` errors are retried; aborts and the `timeoutMs` deadline stop it.
|
|
61
|
+
- `track.create()` sends `{ kind: "interval", intervalSeconds }` as
|
|
62
|
+
`interval_seconds`, as `refine()` already did. The backend previously ignored
|
|
63
|
+
the camelCase field and fell back to its default 60-second interval. Other
|
|
64
|
+
schedule fields such as `tz` pass through unchanged, for `create()` and
|
|
65
|
+
`refine()` alike. Malformed interval or cron schedules are rejected locally.
|
|
66
|
+
- Monitor and tick reads keep user-defined keys verbatim: `extractionSchema`,
|
|
67
|
+
`baselineExtracted`, a tick's `extracted`, and the `before` / `after` /
|
|
68
|
+
`added` / `removed` / `sampleUnchanged` values in `diffs` no longer rename
|
|
69
|
+
`source_url` to `sourceUrl`. Protocol fields are still camelCased.
|
|
70
|
+
|
|
3
71
|
## 0.6.0 - 2026-09-23
|
|
4
72
|
|
|
5
73
|
### Added
|
|
@@ -91,8 +159,8 @@ compatible fixes.
|
|
|
91
159
|
|
|
92
160
|
### Changed
|
|
93
161
|
|
|
94
|
-
- The default Qoni host is now `https://dashboard.qoni.ai
|
|
95
|
-
|
|
162
|
+
- The default Qoni host is now `https://dashboard.qoni.ai`.
|
|
163
|
+
Callers that never passed an explicit `host`
|
|
96
164
|
reach a different control plane after upgrading. Note that the new default has
|
|
97
165
|
no `/dashboard` path segment — pass `host` explicitly for private or local
|
|
98
166
|
deployments.
|
package/README.md
CHANGED
|
@@ -251,7 +251,7 @@ Silent product calls receive `token: delegation.data.token`. If that token is an
|
|
|
251
251
|
|
|
252
252
|
- **Silent** (`mode: "silent"`, the default) — acts for one end user, so a user
|
|
253
253
|
is **required**: pass `user: { id: <genauthUserId> }` (preferred) or the
|
|
254
|
-
|
|
254
|
+
top-level `userId`. Omitting it throws `QoniValidationError`
|
|
255
255
|
locally before any request. Authorize with the `products` sugar and/or
|
|
256
256
|
explicit `scopes`.
|
|
257
257
|
- **Interactive** (`mode: "interactive"`) — pass `redirectUri`, `state`,
|
|
@@ -306,11 +306,39 @@ await qoni.gumem.recall({
|
|
|
306
306
|
});
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
`completeDelegateToken` is the recommended server callback helper. `delegateAgent` and `completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
|
|
309
|
+
`completeDelegateToken` is the recommended server callback helper. Root `qoni.delegateAgent` and `qoni.completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
|
|
310
|
+
|
|
311
|
+
## Original delegation style (0.8.0, unreleased)
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const grant = await qoni.genauth.delegateAgent({
|
|
315
|
+
userId: process.env.QONI_USER_ID!, // The current user's GenAuth ID
|
|
316
|
+
agentKey: "email-assistant", // Agent audit label; alias for agent
|
|
317
|
+
scopes: [
|
|
318
|
+
"webagent.do_anything:read", // Read status, events, and artifacts
|
|
319
|
+
"webagent.do_anything:manage", // Start/cancel tasks and respond to interactions
|
|
320
|
+
],
|
|
321
|
+
expiresIn: "15m", // Converted to 900 seconds before signing
|
|
322
|
+
});
|
|
323
|
+
console.log(grant.grantId, grant.auditId);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
This entry returns the grant directly; root `delegateToken` still returns
|
|
327
|
+
`QoniResponse`, with the token in `response.data.token`. Interactive mode returns
|
|
328
|
+
the authorization request directly; use `completeDelegateToken()` in the server
|
|
329
|
+
callback after consent. `agentKey` aliases `agent`; both labels must agree when
|
|
330
|
+
supplied together. Top-level `userId` retains precedence over `user.id`.
|
|
331
|
+
Expiry accepts integer seconds or whole-number
|
|
332
|
+
`s/m/h/d` durations, between 60 and 86400 seconds. Until published, use the 0.8.0
|
|
333
|
+
source build for these additions.
|
|
334
|
+
|
|
335
|
+
Legacy `mailbox.*`, `web.search`, and `web.extract` scopes, and `deniedScopes`, are unsupported and rejected.
|
|
336
|
+
They are never expanded into broader product permissions. Product scopes do not
|
|
337
|
+
prohibit email sending or deletion; a prompt is not an enforced deny policy.
|
|
310
338
|
|
|
311
339
|
## Choosing Scopes
|
|
312
340
|
|
|
313
|
-
Each WebAgent product exposes exactly two scopes: `read` (observe — status, events, artifacts) and `manage` (anything that changes execution state — run, cancel, respond, messages, monitor CRUD). The
|
|
341
|
+
Each WebAgent product exposes exactly two scopes: `read` (observe — status, events, artifacts) and `manage` (anything that changes execution state — run, cancel, respond, messages, monitor CRUD). The 8 product scopes are `webagent.{do_anything, web_search, deep_research, track}:{read, manage}`. Interactive site login (`openLogin()`) additionally needs `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, which are not part of any product and must be requested explicitly in `scopes`. The simplest authorization is the `products` sugar — each product name expands to its `read` + `manage` pair:
|
|
314
342
|
|
|
315
343
|
```ts
|
|
316
344
|
await qoni.delegateToken({
|
|
@@ -457,7 +485,7 @@ const monitor = await qoni.track.create({
|
|
|
457
485
|
// runNow()/refine() require `active`, so wait for it before acting:
|
|
458
486
|
for await (const event of monitor.events()) {
|
|
459
487
|
if (event.type === "interaction") {
|
|
460
|
-
const i = monitor.interactionHandle(event.data); // e.g.
|
|
488
|
+
const i = monitor.interactionHandle(event.data); // e.g. an intent clarification
|
|
461
489
|
if (i.can("confirm")) await i.confirm();
|
|
462
490
|
continue;
|
|
463
491
|
}
|
|
@@ -470,7 +498,9 @@ const ticks = await monitor.runs({ limit: 10 }); // read-only tick run views
|
|
|
470
498
|
|
|
471
499
|
// Change the definition with refine() (any subset of: schedule / targetUrls /
|
|
472
500
|
// extractionSchema / triggerDsl / stopConditionDsl / notifyChannel). `schedule`
|
|
473
|
-
// is { kind: "cron", expr } or { kind: "interval", intervalSeconds }
|
|
501
|
+
// is { kind: "cron", expr } or { kind: "interval", intervalSeconds } (same for
|
|
502
|
+
// track.create()). Extraction field names and extracted values keep their
|
|
503
|
+
// original keys in get()/run() reads. The
|
|
474
504
|
// natural-language intent isn't refinable — create a new monitor to change it:
|
|
475
505
|
await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
|
|
476
506
|
await monitor.pause(); // stop scheduled ticks without deleting
|
|
@@ -623,12 +653,49 @@ for await (const event of run.events()) {
|
|
|
623
653
|
}
|
|
624
654
|
```
|
|
625
655
|
|
|
626
|
-
Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin()`,
|
|
656
|
+
Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin({ siteId?, profileId? })`,
|
|
627
657
|
`confirmSignedIn()`, `connectControl()`, `refreshControl()`, `releaseControl()`,
|
|
628
658
|
`retry()`, `switchProfile()`. `i.can(kind)` reports whether the interaction
|
|
629
659
|
currently offers that action; calling a method the backend did NOT declare
|
|
630
660
|
throws (so a "recovery" button only exists when it actually works).
|
|
631
661
|
|
|
662
|
+
`openLogin()` starts an interactive login session for a `site_login`
|
|
663
|
+
interaction of a run. Pass `siteId` (one of
|
|
664
|
+
`i.interaction.payload.sites[].siteId`), or omit it when exactly one site is
|
|
665
|
+
listed. Show the returned `liveUrl` to the user; after they sign in, persist the
|
|
666
|
+
login with `login.confirm()`, then tell the agent with `confirmSignedIn()`:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import { QoniError } from "@qoniai/qoni";
|
|
670
|
+
|
|
671
|
+
const login = await i.openLogin(); // { profileId, siteId, sessionId, liveUrl }
|
|
672
|
+
showToUser(login.liveUrl); // the user signs in there
|
|
673
|
+
await waitForUserToFinish();
|
|
674
|
+
let result;
|
|
675
|
+
try {
|
|
676
|
+
result = await login.confirm(); // ends the session; saves the login if found
|
|
677
|
+
} catch (error) {
|
|
678
|
+
// 409: no pending login for this profile (session already ended, or profile gone).
|
|
679
|
+
if (!(error instanceof QoniError && error.code === "no_pending_login")) throw error;
|
|
680
|
+
}
|
|
681
|
+
if (!result || result.active || result.probeResult === "no_session") {
|
|
682
|
+
await i.confirmSignedIn(); // the agent rechecks; it asks again if still signed out
|
|
683
|
+
} else {
|
|
684
|
+
// The user was not signed in, and confirm() closed that session's browser.
|
|
685
|
+
const retry = await i.openLogin({ siteId: login.siteId, profileId: login.profileId });
|
|
686
|
+
showToUser(retry.liveUrl); // a new session for the same profile
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
`probeResult: "no_session"` means the backend no longer holds the login session —
|
|
691
|
+
it may already have saved the login on its own once it detected the sign-in, or
|
|
692
|
+
it restarted. Some browser providers instead reject `confirm()` with a
|
|
693
|
+
`QoniError` coded `no_pending_login` (HTTP 409), which the backend also uses when
|
|
694
|
+
the profile no longer exists for this user; the example handles it the same way,
|
|
695
|
+
because the agent rechecks and asks again if the site is still signed out. Delegate
|
|
696
|
+
`QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, and allow
|
|
697
|
+
both in the AccessKey policy.
|
|
698
|
+
|
|
632
699
|
The discriminant strings used above are also exported as constants so you can
|
|
633
700
|
avoid bare literals: `InteractionTypes` (`site_login` / `clarification` /
|
|
634
701
|
`confirmation` / `take_control` / `wait`), `InteractionStatuses`, and
|
|
@@ -651,7 +718,7 @@ The 1:1 low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnythi
|
|
|
651
718
|
|
|
652
719
|
`event.raw.event` is the **raw wire event type** — an internal transport name (`run.action.started`, `run.section_completed`, …) whose naming is historical and inconsistent, exposed as a plain string. This is an implementation detail, not the product API: developers match on the curated `event.type` above. The full wire-type catalog is intentionally NOT exported — reach for `event.raw.event` (a string) only when you need an exact wire match the curated layer folds into `progress` (e.g. telling a specific browser action apart).
|
|
653
720
|
|
|
654
|
-
Browser Use
|
|
721
|
+
Browser Use scopes are reserved until their product runtime SDK methods are exported. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirm()` on run interactions.
|
|
655
722
|
|
|
656
723
|
### Response Shape
|
|
657
724
|
|
package/README.zh-CN.md
CHANGED
|
@@ -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"`。
|
|
@@ -277,11 +277,36 @@ await qoni.gumem.recall({
|
|
|
277
277
|
});
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
`completeDelegateToken` 是推荐的服务端 callback helper
|
|
280
|
+
`completeDelegateToken` 是推荐的服务端 callback helper。顶层 `qoni.delegateAgent` 和 `qoni.completeDelegateAgent` 仍保留为 deprecated 兼容命名,但当前 callback 契约是 `{ grantId, code, state }`;旧的 `{ code, state }` complete 签名不再兼容。
|
|
281
|
+
|
|
282
|
+
## 原始委托写法(0.8.0,尚未发布)
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const grant = await qoni.genauth.delegateAgent({
|
|
286
|
+
userId: process.env.QONI_USER_ID!, // 当前用户的 GenAuth ID
|
|
287
|
+
agentKey: "email-assistant", // Agent 审计标签,兼容 agent 参数
|
|
288
|
+
scopes: [
|
|
289
|
+
"webagent.do_anything:read", // 读取任务状态、事件和产物
|
|
290
|
+
"webagent.do_anything:manage", // 启动、取消任务和提交交互响应
|
|
291
|
+
],
|
|
292
|
+
expiresIn: "15m", // 签名前转换为 900 秒
|
|
293
|
+
});
|
|
294
|
+
console.log(grant.grantId, grant.auditId);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
此入口直接返回 grant;顶层 `delegateToken` 仍返回 `QoniResponse`,token 在
|
|
298
|
+
`response.data.token` 中。交互式模式直接返回授权请求;用户同意后,在服务端回调
|
|
299
|
+
用 `completeDelegateToken()` 兑换 token。`agentKey`、`userId` 分别兼容 `agent`、
|
|
300
|
+
`user.id`;同时提供两个 Agent 标签时必须一致,顶层 `userId` 保持原有的优先级。
|
|
301
|
+
有效期支持整数秒或整数 `s/m/h/d` 时长,
|
|
302
|
+
范围为 60–86400 秒。这些新增能力在发布前需要使用 0.8.0 源码构建。
|
|
303
|
+
|
|
304
|
+
旧版 `mailbox.*`、`web.search`、`web.extract` scope 和 `deniedScopes` 不受支持,SDK 会拒绝,不会将它们
|
|
305
|
+
扩展成更宽的产品权限。产品 scope 不限制发送、删除邮件;任务描述不能代替禁止策略。
|
|
281
306
|
|
|
282
307
|
## Scope 怎么选
|
|
283
308
|
|
|
284
|
-
每个 WebAgent 产品只有两个 scope:`read`(只读观察——状态、事件、产物)与 `manage
|
|
309
|
+
每个 WebAgent 产品只有两个 scope:`read`(只读观察——状态、事件、产物)与 `manage`(一切改变执行状态的操作——发起、取消、应答、消息、监控的增删改)。产品 scope 共 8 个:`webagent.{do_anything, web_search, deep_research, track}:{read, manage}`。交互式站点登录(`openLogin()`)另外需要 `QoniScopes.SITE_LOGIN_REQUEST` 和 `QoniScopes.SITE_LOGIN_CONFIRM`,它们不属于任何产品,必须在 `scopes` 中显式申请。最简单的授权方式是 `products` 糖——每个产品名展开为该产品的 `read` + `manage` 一对:
|
|
285
310
|
|
|
286
311
|
```ts
|
|
287
312
|
await qoni.delegateToken({
|
|
@@ -426,7 +451,7 @@ const monitor = await qoni.track.create({
|
|
|
426
451
|
// 都要求 `active`,所以先等它就绪再操作:
|
|
427
452
|
for await (const event of monitor.events()) {
|
|
428
453
|
if (event.type === "interaction") {
|
|
429
|
-
const i = monitor.interactionHandle(event.data); //
|
|
454
|
+
const i = monitor.interactionHandle(event.data); // 如意图澄清
|
|
430
455
|
if (i.can("confirm")) await i.confirm();
|
|
431
456
|
continue;
|
|
432
457
|
}
|
|
@@ -439,7 +464,8 @@ const ticks = await monitor.runs({ limit: 10 }); // tick 产生的 run 只读列
|
|
|
439
464
|
|
|
440
465
|
// 用 refine() 改定义(任意字段子集:schedule / targetUrls / extractionSchema /
|
|
441
466
|
// triggerDsl / stopConditionDsl / notifyChannel)。`schedule` 是
|
|
442
|
-
// { kind: "cron", expr } 或 { kind: "interval", intervalSeconds }
|
|
467
|
+
// { kind: "cron", expr } 或 { kind: "interval", intervalSeconds }(track.create() 相同)。
|
|
468
|
+
// get()/run() 读回时,抽取字段名与抽取结果保留原始键名。
|
|
443
469
|
// 自然语言意图不可 refine——要改描述请新建 monitor:
|
|
444
470
|
await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
|
|
445
471
|
await monitor.pause(); // 停掉定时 tick 但不删除
|
|
@@ -568,7 +594,33 @@ for await (const event of run.events()) {
|
|
|
568
594
|
}
|
|
569
595
|
```
|
|
570
596
|
|
|
571
|
-
方法:`answer(text)`、`skip()`、`confirm()`、`reject()`、`openLogin()`、`confirmSignedIn()`、`connectControl()`、`refreshControl()`、`releaseControl()`、`retry()`、`switchProfile()`。`i.can(kind)` 判断该交互当前是否提供这个动作;调用后端没声明的方法会抛错(所以「恢复」按钮只有在真能用时才出现)。
|
|
597
|
+
方法:`answer(text)`、`skip()`、`confirm()`、`reject()`、`openLogin({ siteId?, profileId? })`、`confirmSignedIn()`、`connectControl()`、`refreshControl()`、`releaseControl()`、`retry()`、`switchProfile()`。`i.can(kind)` 判断该交互当前是否提供这个动作;调用后端没声明的方法会抛错(所以「恢复」按钮只有在真能用时才出现)。
|
|
598
|
+
|
|
599
|
+
`openLogin()` 为运行中的 `site_login` 交互开启一次交互式登录会话。传入 `siteId`(取自 `i.interaction.payload.sites[].siteId`),交互只列出一个站点时可以省略。把返回的 `liveUrl` 展示给用户;用户登录完成后,先用 `login.confirm()` 把登录态写入 Profile,再调用 `confirmSignedIn()` 通知 Agent:
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
import { QoniError } from "@qoniai/qoni";
|
|
603
|
+
|
|
604
|
+
const login = await i.openLogin(); // { profileId, siteId, sessionId, liveUrl }
|
|
605
|
+
showToUser(login.liveUrl); // 用户在这里登录
|
|
606
|
+
await waitForUserToFinish();
|
|
607
|
+
let result;
|
|
608
|
+
try {
|
|
609
|
+
result = await login.confirm(); // 结束这次会话;检测到登录态就写入 Profile
|
|
610
|
+
} catch (error) {
|
|
611
|
+
// 409:该 Profile 没有待确认的登录(会话已结束,或 Profile 已不存在)。
|
|
612
|
+
if (!(error instanceof QoniError && error.code === "no_pending_login")) throw error;
|
|
613
|
+
}
|
|
614
|
+
if (!result || result.active || result.probeResult === "no_session") {
|
|
615
|
+
await i.confirmSignedIn(); // Agent 重新检查;仍未登录会再次请求
|
|
616
|
+
} else {
|
|
617
|
+
// 用户没有登录成功,而 confirm() 已关闭这次会话的浏览器。
|
|
618
|
+
const retry = await i.openLogin({ siteId: login.siteId, profileId: login.profileId });
|
|
619
|
+
showToUser(retry.liveUrl); // 同一个 Profile 的新登录会话
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
`probeResult: "no_session"` 表示服务端已不再持有这次登录会话:它可能在检测到用户登录后已经自动保存了登录态,也可能已经重启。部分浏览器提供方会改为让 `confirm()` 抛出 `code` 为 `no_pending_login` 的 `QoniError`(HTTP 409),服务端在 Profile 已不属于当前用户或已删除时也会返回这个错误码;示例按同样方式处理,因为 Agent 会重新检查,如果站点仍未登录会再次请求。委托时需要申请 `QoniScopes.SITE_LOGIN_REQUEST` 和 `QoniScopes.SITE_LOGIN_CONFIRM`,并在 AccessKey 权限策略中允许这两个 scope。
|
|
572
624
|
|
|
573
625
|
上面用到的判别字符串也都导出成了常量,避免裸写字面量:`InteractionTypes`(`site_login` / `clarification` / `confirmation` / `take_control` / `wait`)、`InteractionStatuses`、`ActionKinds`(`i.can(...)` 的动作名)。授权 scope 同样有:`QoniScopes`(每个单独 scope)、`QoniProductScopes`(`products` 语法糖展开成的 per-product `read`+`manage` 对)、`QoniScopeBundles`(精选 bundle)。用它们代替手写字符串:
|
|
574
626
|
|
|
@@ -586,7 +638,7 @@ if (i.type === InteractionTypes.Clarification && i.can(ActionKinds.answer)) {
|
|
|
586
638
|
|
|
587
639
|
原始 wire 事件类型(`run.action.started` 这种内部传输名,命名是历史遗留、也不一致)**不作为公开导出**——开发者按上面那套精选 `event.type` 匹配即可;极少数要精确 wire 的场景,`event.raw.event` 是个原始字符串,直接比对。
|
|
588
640
|
|
|
589
|
-
Browser Use
|
|
641
|
+
Browser Use 的 scope 已预留,但产品运行时 SDK 方法导出前不作为公开方法说明。Site Login 的 scope(`QoniScopes.SITE_LOGIN_REQUEST`、`QoniScopes.SITE_LOGIN_CONFIRM`)用于运行交互上的 `InteractionHandle.openLogin()` 和 `confirm()`。
|
|
590
642
|
|
|
591
643
|
### 返回结构
|
|
592
644
|
|