@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 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` (was
95
- `https://anima.eazo.ai/dashboard`). Callers that never passed an explicit `host`
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
- deprecated top-level `userId`. Omitting it throws `QoniValidationError`
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 full set is the 8 scopes `webagent.{do_anything, web_search, deep_research, track}:{read, manage}`. The simplest authorization is the `products` sugar — each product name expands to its `read` + `manage` pair:
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. a needs-reauth site_login
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 }. The
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 and Site Login scopes are reserved until their product runtime SDK methods are exported.
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> }`(推荐)或已废弃的顶层 `userId`。不传会在请求发出前本地抛 `QoniValidationError`。授权用 `products` 语法糖和/或显式 `scopes`。
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。`delegateAgent` 和 `completeDelegateAgent` 仍保留为 deprecated 兼容命名,但当前 callback 契约是 `{ grantId, code, state }`;旧的 `{ code, state }` complete 签名不再兼容。
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`(一切改变执行状态的操作——发起、取消、应答、消息、监控的增删改)。全集共 8 个:`webagent.{do_anything, web_search, deep_research, track}:{read, manage}`。最简单的授权方式是 `products` 糖——每个产品名展开为该产品的 `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); // 如 needs-reauth 的 site_login
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、Site Login 的 scope 已预留,但产品运行时 SDK 方法导出前不作为公开方法说明。
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