@kanyun-ai-infra/agenthub 1.12.1 → 1.14.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
@@ -2,6 +2,34 @@
2
2
 
3
3
  本文件从 1.12.0 开始记录;更早的版本见 git 历史中的 `chore(agenthub-sdk): release X.Y.Z` 提交。
4
4
 
5
+ ## 1.14.0
6
+
7
+ ### 随共享契约内联的语音变化
8
+
9
+ - 回合式语音的 `voice.input.provider` 和 `voice.output.provider` 支持 `dashscope`、`modelgate-chat`,并保留 `modelgate`。`publishVersion` / `publishRevision` 的本地请求校验接受这些值。
10
+ - `voice.input` 和 `voice.output` 各自支持可选的 `modelService`,用于指定该方向使用的项目模型服务名;不填写时沿用主模型服务。显式指定的模型服务无法解析或与 provider 不匹配时拒绝使用,不回落到主模型服务。
11
+ - 随包语音错误码包含 `VOICE_INPUT_FORMAT_UNSUPPORTED`(HTTP 400):所选语音服务不支持输入音频格式。模型服务不满足语音要求时仍使用 `VOICE_MODEL_SERVICE_UNSUPPORTED`(HTTP 400),模型未获授权时使用 `VOICE_MODEL_NOT_AUTHORIZED`(HTTP 502)。这些是服务端错误,不是本地 schema 校验的错误码。
12
+
13
+ ### 迁移说明
14
+
15
+ - 使用 `dashscope`、`modelgate-chat` 或语音子段 `modelService` 的接入方须升级到 `@kanyun-ai-infra/agenthub@1.14.0`。1.13.0 的内联请求 schema 会在发送请求前拒绝这些配置并抛出 `ZodError`;仅升级服务端不能更新已安装 SDK 的 schema。
16
+
17
+ ## 1.13.0
18
+
19
+ - 浏览器实时语音支持可选 `onEnded({ callId, reason })`:识别通话终态帧 `call_ended` 或关闭码 `4000`,清理采集与播放并停止自动接手;时长上限映射为 `duration_limit`,未知原因映射为 `unknown`。回调每通至多一次,本地 `stop()` 不触发;终态到达后可以手动开始新通话。
20
+
21
+ - `NO_RESPONSE` 通知新增可选 `contextReset: true`:表示网关已切换到新的模型会话,模型不再持有此前上下文;平台回合历史不变。
22
+ - 随包类型声明移除内部包名与仓库路径,公开方法签名不因这项注释整理而改变。
23
+
24
+ ### 随共享契约内联的变化
25
+
26
+ - Slack 渠道声明新增可选 `approverUserIds`,用于指定可在线程里批准或拒绝工具调用的用户;最多 64 项且不能重复。slack 声明携带有效的 `approverUserIds` 时,SDK 1.12.1 的 `publishRevision` 会在本地校验时报 `unrecognized_keys`;1.13.0 接受该字段。
27
+
28
+ ### 迁移说明
29
+
30
+ - 使用实时语音的接入方须升级到 `@kanyun-ai-infra/agenthub@1.13.0` 才能获得 `onEnded` 和到达时长上限后停止自动接手的行为。旧 SDK 1.11.0 / 1.12.x 会忽略 `call_ended`,收到关闭码 `4000` 后仍自动重连,最终报 `RECONNECT_EXHAUSTED`;仅升级网关不能修复旧客户端。
31
+ - `onEnded` 是可选回调;无需处理结束通知的接入方也会自动停止重连。`adaptiveConnect: false` 不会关闭这项通话终态处理。
32
+
5
33
  ## 1.12.1
6
34
 
7
35
  修复 1.12.0 的已知问题,只改浏览器实时语音入口 `@kanyun-ai-infra/agenthub/realtime` 的一项检查;主入口与类型声明和 1.12.0 相同。
package/README.md CHANGED
@@ -36,6 +36,8 @@ pnpm add @kanyun-ai-infra/agenthub
36
36
 
37
37
  需要 Node 18+,因为 SDK 使用全局 `fetch`。
38
38
 
39
+ 回合式语音使用 `dashscope`、`modelgate-chat` provider 或 `voice.input.modelService` / `voice.output.modelService` 时,需要 SDK ≥ 1.14.0。
40
+
39
41
  如果调用方是 Java、Python 或 Shell(curl),请优先看
40
42
  <https://agenthub.zhenguanyu.com/v1/docs/api-and-sdk#multi-language>(Python / Java)
41
43
  或 <https://agenthub.zhenguanyu.com/v1/docs/api-and-sdk#rest-flow>(curl)。
@@ -276,7 +278,7 @@ try {
276
278
 
277
279
  ## 实时语音(浏览器)
278
280
 
279
- **开发中,暂不支持线上用户接入。**
281
+ 通话前,项目要先在门户配好实时语音模型服务和站点来源,见下文「开通前提」。
280
282
 
281
283
  从 `@kanyun-ai-infra/agenthub/realtime` 导入 `createRealtimeVoiceClient`。浏览器只持有短期通话票;接入方后端调用 `client.sessions.createVoiceTicket(sessionId, options?)` 换票,`options` 可传 `signal: AbortSignal`,返回 `{ ticket, gatewayUrl, expiresAt }`。`getTicket()` 推荐返回整个响应对象,SDK 按票里的 `gatewayUrl` 建连;也可以只返回 `ticket` 字符串,此时连初始化时传入的 `gatewayUrl`。不能把平台 token 放进前端。主入口不加载浏览器代码。转写 `role` 为 `user` 或 `assistant`。
282
284
 
@@ -295,6 +297,9 @@ const voice = createRealtimeVoiceClient({
295
297
  },
296
298
  onState: (state) => renderState(state), // listening / thinking / speaking
297
299
  onTurnEnd: ({ turnSeq, turnId }) => finishTurn({ turnSeq, turnId }),
300
+ onEnded: ({ reason }) => showVoiceEnded(
301
+ reason === "duration_limit" ? "通话已结束:达到时长上限" : "通话已结束",
302
+ ),
298
303
  onError: ({ code, retryable }) => showVoiceError({ code, retryable }),
299
304
  });
300
305
 
@@ -314,11 +319,62 @@ stopButton.onclick = () => { void voice.stop(); };
314
319
 
315
320
  `stop()` 立即停止麦克风,发送本轮最终播放回执,再发 `end` 并关闭连接;Worklet 无响应时最多等待 200 ms,使用最后确认的播放统计。页面卸载同步清理。播放回执按 `turnSeq` 归属,`playedMs` 计实际渲染样本,不计排队音频;`firstPlayedAt` 是 Unix 毫秒,尚未播放时为 `null`。`turn_end` 表示业务回合收尾,不是音频 EOF。SDK 记住提前到达的回合终态,在播放排空时发送 final;迟到尾块沿用首次播放时刻并累加 playedMs,再补累计 final。记录保留最近 128 轮,尚在播放队列中的轮不会被淘汰。网关的文本截止不受晚回执影响,晚回执用于录音统计。
316
321
 
322
+ 收到当前通话的 `call_ended` 或专用关闭码 `4000` 时,SDK 停止自动接手、清理计时器、停止麦克风并清空播放,随后调用一次可选的 `onEnded({ callId, reason })`。首期原因是 `duration_limit`,未知原因映射为 `unknown`;关闭码判定不依赖 `wasClean`。只有关闭帧先于 `ready` 到达且没有通话 ID 时,`callId` 缺省;尚未完成的 `start()` 以既有 `REALTIME_STOPPED` 拒绝,不触发 `onError`。本地 `stop()` 不触发 `onEnded`,不等服务端回声。结束后可手动 `start()` 开新通话。`turn_end` 仍只代表一轮结束。
323
+
324
+ SDK 1.13.0 起提供此行为。旧版 1.11.0 / 1.12.0 / 1.12.1 忽略 `call_ended`,收到关闭后仍按原逻辑接手,不能仅靠更新网关修复旧客户端,接入方须升级到 1.13.0。普通 `1000`、`1006` 与 `reconnecting` 保留原恢复行为;若终态 JSON 和关闭帧都未送达,仍可能按网络断线接手。
325
+
317
326
  断线或收到 `reconnecting` 时保留播放队列、暂停采集,换新票并带原 `callId` 重连。每通话最多自动接手 5 次,退避从 250 ms 增至 4 s;取票与 WebSocket upgrade 最多等待 10 s,open 后另等 ready 最多 30 s,音频初始化最多 30 s。WebSocket 握手已验票,SDK 立即处理下行消息;转写、状态回调与播放可能早于 start() 完成。ready 只控制麦克风上行和 start() 完成,interrupted 始终立即清空播放。`no_response` 仅以 `NO_RESPONSE` 可重试通知上报,不主动接手通话。无法恢复时释放资源并报不可重试错误,可由用户重新开始。主动 stop 后再次 start 是新通话。
318
- 浏览器子入口同时导出只读 `REALTIME_RECOVERY_REASONS` 和 `RealtimeRecoveryReason` 类型。实时 `NO_RESPONSE` 可重试通知可附带 `reason`:`input_not_received`(本句未获得回答)、`response_incomplete`(回答未完成)、`connection_interrupted`(空闲连接中断)。调用方应显示对应提示并允许重试;旧网关不带 reason 时沿用通用提示,未知 reason 被忽略。reason 不改变 SDK 的 stop/自动接手策略,也不表示丢失输入已被重放。已发布 1.11.0 忽略该可选字段,但仍正常通知 NO_RESPONSE。
327
+ 浏览器子入口同时导出只读 `REALTIME_RECOVERY_REASONS` 和 `RealtimeRecoveryReason` 类型。实时 `NO_RESPONSE` 可重试通知可附带 `reason`:`input_not_received`(本句未获得回答)、`response_incomplete`(回答未完成)、`connection_interrupted`(空闲连接中断)。调用方应显示对应提示并允许重试;旧网关不带 reason 时沿用通用提示,未知 reason 被忽略。reason 不改变 SDK 的 stop/自动接手策略,也不表示丢失输入已被重放。已发布 1.11.0 忽略该可选字段,但仍正常通知 NO_RESPONSE。网关在 GoAway 截止时换新模型会话、无法保留此前对话上下文时,同一通知另带 `contextReset: true`:通话继续,平台回合历史不变,调用方可提示用户模型已不记得之前的对话;已发布 1.11.0 / 1.12.0 忽略该字段。
319
328
 
320
329
  首次建连(第一次收到 ready 之前)的 WebSocket 若在 open 之前关闭,SDK 依次等待 500 ms、1 s、2 s,每次重新取票再连,最多重试 3 次,每次以可重试通知 `CONNECT_RETRYING` 上报;第 3 次重试仍被拒时报 `CONNECTION_FAILED`。浏览器不暴露握手的 HTTP 状态码,网关排空或满额时的 503 与 401 等其他拒绝无法区分,因此都按此重试;票是一次性的,网关可能已经兑票,所以每次重试都重新取票。以下情况不重试:`getTicket()` 抛错、票格式错误、open 之后的协议不符或网关的不可重试错误、open 之后 ready 之前断开、10 s 内未完成取票与 upgrade。首次建连重试与通话接手的 5 次额度分开计数。`CONNECT_RETRYING` 与 `TICKET_GATEWAY_URL_*` 都是 `retryable: true` 的通知,与 `RECONNECTING` 一样不表示通话失败。
321
330
 
322
331
  `adaptiveConnect` 缺省为 `true`;设为 `false` 时只连初始化地址,首次建连被拒立即报 `CONNECTION_FAILED`,与 1.11.0 一致。
323
332
 
324
333
  仓库内运行本地浏览器冒烟:`pnpm --filter @kanyun-ai-infra/agenthub run smoke:realtime`。该入口先按依赖顺序构建 SDK 及 contracts,再用本地假网关和无头 Chrome 验证采集、播放、打断、回执与停止;运行前需安装仓库依赖及 Chrome,不需要通话凭据。
334
+
335
+ ### 开通前提
336
+
337
+ 项目要先在门户配好下面两项。SDK 和 API 都没有这两项的入口。
338
+
339
+ 1. **实时语音模型服务。** 项目里要有且只有一个启用中的 GCP 服务账号模型服务(鉴权方式 `gcp_service_account`)。在门户「凭证与集成」→「模型服务」→「新建模型服务」里把用途选为「GCP 实时语音」,粘贴服务账号 JSON,填区域(默认 `us-central1`)。Vertex AI 的费用记在服务账号 JSON 里 `project_id` 所指的 GCP 项目上。`agent.yaml` 不引用这一项:每通通话首次接通时,平台在项目里找到这一行并冻结到这通通话上。live Agent 声明的 `modelService` 是文本模型服务,照常必填,两者互不替代。
340
+ - 项目里已有一个启用中的 GCP 模型服务时,再建第二个会被拒。更换服务账号或私钥时编辑原来那一行,粘贴完整的新 JSON。
341
+ - 归档这一行后,项目不能再开新通话;已经接通的通话不受影响,断线接手照常。
342
+ - 取票时的预检只验证 Google 能为这个服务账号换发访问令牌,不验证它在该 GCP 项目里有没有调用 Vertex AI 的权限。缺权限时取票照常成功,到网关连 Vertex AI 时才失败;网关连上游失败发生在 `ready` 之前时,SDK 报 `GATEWAY_ERROR`。
343
+ 2. **站点来源。** 浏览器从接入方自己的站点发起通话时,在门户「配置」→「项目」页的「允许发起实时通话的站点来源」里加上这个站点的来源,即发起连接的页面的 `location.origin`:协议、主机名和可选端口,例如 `https://app.example.com`,不带路径和通配符。比较前只统一大小写、去掉默认端口和末尾斜杠,`https://www.example.com` 与 `https://example.com` 是两个来源。HTTP 只允许 `localhost`、`127.0.0.1` 和 `[::1]`,最多 20 个,只有项目 owner 或管理员能修改。从 AgentHub 门户(`https://agenthub.zhenguanyu.com`、`https://agenthub-test.zhenguanyu.com`)发起的通话不受这份名单限制。
344
+
345
+ ### 拒绝码与处理
346
+
347
+ 通话可能在两处被拒:接入方后端取票时,和浏览器握手时。
348
+
349
+ **取票被拒。** 发票接口的错误体是 `{ success: false, error: { code, message?, scope? } }`。SDK 只从响应顶层取码,所以 `createVoiceTicket` 抛出的 `PilotPlatformApiError` 上,`err.code` 只是按 HTTP 状态给的通用值(409 为 `REQUEST_FAILED`,503 为 `SERVER_ERROR`)。实际的码在 `err.body.error.code`,可读原因在 `err.body.error.message`,下表注明哪些码带 `message`。
350
+
351
+ ```ts
352
+ import { PilotPlatformApiError } from "@kanyun-ai-infra/agenthub";
353
+
354
+ try {
355
+ return await client.sessions.createVoiceTicket(sessionId);
356
+ } catch (err) {
357
+ if (err instanceof PilotPlatformApiError) {
358
+ const detail = (err.body?.error ?? {}) as { code?: string; message?: string; scope?: string };
359
+ // detail.code 例如 "REALTIME_UPSTREAM_NOT_CONFIGURED";detail.message 可以直接展示给用户
360
+ }
361
+ throw err;
362
+ }
363
+ ```
364
+
365
+ 浏览器里 `getTicket()` 抛错时,SDK 只报 `CONNECTION_FAILED`,不转交错误内容。要让用户看到原因,由接入方的取票接口把 `code` 和 `message` 返回给前端,在 `getTicket()` 里自行展示。
366
+
367
+ | 码 | HTTP | 带 `message` | 含义 | 处理 |
368
+ | --- | --- | --- | --- | --- |
369
+ | `REALTIME_UPSTREAM_NOT_CONFIGURED` | 409 | 是 | 项目没有启用中的 GCP 实时语音模型服务 | 按「开通前提」第 1 项添加 |
370
+ | `REALTIME_UPSTREAM_AMBIGUOUS` | 409 | 是 | 项目里有多个启用中的 GCP 实时语音模型服务 | 归档多余的,只留一个 |
371
+ | `REALTIME_UPSTREAM_CREDENTIAL_INVALID` | 409 | 是 | Google 拒绝为这个服务账号换发访问令牌,或私钥无法使用,例如服务账号被删除或停用、密钥被撤销、JSON 填错 | 在模型服务里重新粘贴服务账号 JSON;重试不会恢复 |
372
+ | `REALTIME_UPSTREAM_CREDENTIAL_UNAVAILABLE` | 503 | 是 | 暂时换不到访问令牌:Google 令牌端点不可达,或返回 5xx、429 | 稍后重试;响应不带 `Retry-After` |
373
+ | `REALTIME_CAPACITY_EXCEEDED` | 409 | 是 | `scope` 为 `"project"`:项目的在线通话数到上限,上限值写在 `message` 里;`scope` 为 `"global"`:平台的在线通话总数到上限 | 项目上限:结束本项目的其他通话后重试;平台上限:稍后重试 |
374
+ | `REALTIME_ADMISSION_UNAVAILABLE` | 503 | 否 | 平台的通话计数服务暂时不可用 | 稍后重试;响应不带 `Retry-After` |
375
+ | `REALTIME_UPSTREAM_TENANT_UNSUPPORTED` | 409 | 是 | 会话所在租户使用独立数据库,这种部署形态还不支持项目自带的实时语音凭据 | 项目侧无法处理,联系平台 |
376
+ | `REALTIME_DISABLED` | 409 | 否 | 平台紧急停用了实时语音 | 项目侧无法处理,等平台恢复 |
377
+
378
+ 会话不是 ready(`SESSION_NOT_READY`)、Revision 未声明 `voice.realtime`(`REALTIME_NOT_DECLARED`)、会话绑定了渠道(`REALTIME_CHANNEL_SESSION`)也是 409,不带 `message`。会话不存在或不属于调用方项目回 404 `SESSION_NOT_FOUND`;平台没配好网关地址回 503 `REALTIME_NOT_CONFIGURED`,票存储不可用回 503 `REALTIME_TICKET_STORE_UNAVAILABLE`,两者都不带 `message`,项目侧无法处理。会话上已有在线通话时,这张票只用于接手那通通话,不做上表里模型服务、凭据、平台上限和租户这几项预检,也不受项目上限限制。
379
+
380
+ **握手被拒(站点来源)。** 站点来源不在取票时检查。请求没有 `Origin` 头(或超过 2048 个字符)时,网关在兑票前就回 401,票没有用掉。带了 `Origin` 的握手,网关兑票之后交平台判来源:既不是平台门户、也不在项目名单里时回 403 `REALTIME_ORIGIN_FORBIDDEN`,这时票已经用掉。另一个来源码 `REALTIME_ORIGIN_REQUIRED`(claim 没带来源)经平台网关碰不到:网关在没有 `Origin` 时已先回 401。浏览器读不到握手的 HTTP 状态码,SDK 按首次建连被拒处理:报 `CONNECT_RETRYING`,每次重新取票再连,3 次后报 `CONNECTION_FAILED`(`adaptiveConnect: false` 时直接报)。取票一直成功、`start()` 却以 `CONNECTION_FAILED` 结束时,先核对项目的站点来源;网关满额、排空等其他握手拒绝也是同样的表现。断线接手时同样检查来源,通话中途从名单里删掉的来源,接手会失败。不用 SDK、自己建 WebSocket 的接入方,被拒后要重新取票再连,同一张票不能再用。