@zoowork-ai/sdk 0.4.2 → 0.5.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 +66 -0
- package/dist/client.d.ts +162 -58
- package/dist/client.js +108 -36
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,72 @@
|
|
|
3
3
|
All notable changes to `@zoowork-ai/sdk` (formerly `@zooclaw-agents/sdk`). Dates are the
|
|
4
4
|
day the behaviour was verified, not the day it was written.
|
|
5
5
|
|
|
6
|
+
## 0.5.1 — 2026-08-31
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`ZooworkError` keeps the evidence: `contentType`, `bodySnippet`, `cfRay`, `requestId`,
|
|
11
|
+
`retryable`.** A production `postEvents` failure surfaced as `HTTP 502, type: undefined`
|
|
12
|
+
and cost the reporter a day of black-box contrast experiments (2026-08-30,
|
|
13
|
+
`notes/probes/system-message-cold-session-probe.mts`): the edge replaces an origin 502/504
|
|
14
|
+
body wholesale with a branded `text/html` page, so no JSON envelope ever reaches the SDK —
|
|
15
|
+
and 0.5.0 then dropped the only three facts that survived. Now every transport error keeps
|
|
16
|
+
the response `Content-Type`, the first 600 characters of the raw body, and the `cf-ray`
|
|
17
|
+
header (present on JSON errors too, verified 2026-08-31) — the id to quote when reporting
|
|
18
|
+
a gateway failure. `requestId` reads `request_id` from either error envelope once the
|
|
19
|
+
server starts sending one; today it is usually absent. `retryable` is a transport-class
|
|
20
|
+
hint (`408/429/502/503/504`): it says the failure class tends to pass, not that a replay
|
|
21
|
+
is safe — pair it with `idempotency_key` before looping on it.
|
|
22
|
+
SDK-synthesized wait timeouts keep `retryable: false`: they report that the caller's own
|
|
23
|
+
polling budget expired, not that an HTTP 408 came back from the service.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **The fallback error message names what actually came back.** A non-JSON error body used
|
|
28
|
+
to read a bare `HTTP 502`; it now reads
|
|
29
|
+
`HTTP 502 (text/html; charset=UTF-8) [cf-ray a338c539…]`. Messages parsed from a server
|
|
30
|
+
envelope are unchanged — keep matching on `type`/`status`, never on message text.
|
|
31
|
+
- **`streamEvents` raises the same enriched envelope** on a non-ok response instead of the
|
|
32
|
+
bare `events stream HTTP <status>` string, so SSE failures are diagnosable the same way.
|
|
33
|
+
- **A structured `detail` object no longer stringifies into the message.** The agents-family
|
|
34
|
+
envelope may carry `detail` as an object; it previously became the literal message
|
|
35
|
+
`[object Object]`, now it falls through to the status line and stays readable in
|
|
36
|
+
`bodySnippet`.
|
|
37
|
+
|
|
38
|
+
## 0.5.0 — 2026-08-28
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **The QR flow now covers WeCom and WeChat, not just Feishu** — gateway PR #3512 shipped
|
|
43
|
+
`/channels/{wecom,weixin}/{setup,poll,setup-cancel}`, and 0.4.x had no way to call them.
|
|
44
|
+
Four platform-taking methods replace the four Feishu-only ones:
|
|
45
|
+
`startChannelSetup(agentId, platform, input?)`, `pollChannelSetup`, `cancelChannelSetup`,
|
|
46
|
+
`waitForChannelSetup`. New types `ChannelSetupInput`, `ChannelSetupSession`,
|
|
47
|
+
`ChannelPollResult`, `GuidedSetupPlatform` (`'feishu' | 'wecom' | 'weixin'`) and
|
|
48
|
+
`AddChannelPlatform`.
|
|
49
|
+
- **`startFeishuSetup` / `pollFeishuSetup` / `cancelFeishuSetup` / `waitForFeishuSetup` still
|
|
50
|
+
work** — they now delegate to the platform-taking versions. `FeishuSetupInput` and
|
|
51
|
+
`FeishuPollResult` are aliases of the new names; `FeishuSetupSession` narrows
|
|
52
|
+
`ChannelSetupSession` to the one platform that always answers `verification_uri_complete`.
|
|
53
|
+
Only the message text of a thrown timeout/abort changed (it names `waitForChannelSetup` and
|
|
54
|
+
the platform); `status` and `type` are unchanged.
|
|
55
|
+
|
|
56
|
+
### Documentation
|
|
57
|
+
|
|
58
|
+
- **`ChannelPlatform` gains `'weixin'`, and WeChat is no longer described as unbindable.**
|
|
59
|
+
0.3.2–0.4.2 said WeChat "answers `400 channel.weixin_setup_required`, naming a QR flow this
|
|
60
|
+
API does not expose". The flow exists now; that error is a signpost to it, not a dead end.
|
|
61
|
+
`addChannel` still refuses WeChat, so `AddChannelPlatform` is the type that lists what it
|
|
62
|
+
takes.
|
|
63
|
+
- **Per-platform shapes, staging-verified 2026-08-28** (`notes/probes/channels-guided-probe.mts`):
|
|
64
|
+
Feishu answers `verification_uri_complete` + `poll_interval: 5`, `expires_in: 600`; WeCom and
|
|
65
|
+
WeChat answer `qrcode_url` with no `poll_interval` and `expires_in: 300`; WeChat's
|
|
66
|
+
`qrcode_url` may be an inline `data:image/…` payload rather than a URL. WeChat reads only
|
|
67
|
+
`dm_policy` and only `'open'`/`'disabled'` — `'allowlist'` is
|
|
68
|
+
`400 channel.allowlist_unsupported` — pins the account to `'default'`, and ignores anything
|
|
69
|
+
else in the body. Cancelled sessions 404 per platform:
|
|
70
|
+
`channel.{feishu,wecom,weixin}_session_not_found`.
|
|
71
|
+
|
|
6
72
|
## 0.4.2 — 2026-08-25
|
|
7
73
|
|
|
8
74
|
### Documentation
|
package/dist/client.d.ts
CHANGED
|
@@ -55,7 +55,39 @@ export declare class ZooworkError extends Error {
|
|
|
55
55
|
* you only need the class of failure.
|
|
56
56
|
*/
|
|
57
57
|
type?: string;
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* `Content-Type` of the error response. The field that tells an edge error page apart from an
|
|
60
|
+
* API answer: both envelopes above are `application/json`, while a gateway 502/504 arrives as
|
|
61
|
+
* `text/html` — the edge replaces the origin's body wholesale, so no JSON survives to parse
|
|
62
|
+
* (verified 2026-08-30 against both deployments;
|
|
63
|
+
* `notes/probes/system-message-cold-session-probe.mts`).
|
|
64
|
+
*/
|
|
65
|
+
contentType?: string;
|
|
66
|
+
/** First {@link BODY_SNIPPET_LIMIT} characters of the raw error body, whatever it was.
|
|
67
|
+
* Without it a non-JSON failure cannot be reconstructed from `status` alone. */
|
|
68
|
+
bodySnippet?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Cloudflare ray id (`cf-ray` response header), when the response crossed Cloudflare. It
|
|
71
|
+
* survives even the replaced-body case above — on an HTML 502 it is the only correlation id
|
|
72
|
+
* left, and the value to quote when reporting a gateway failure.
|
|
73
|
+
*/
|
|
74
|
+
cfRay?: string;
|
|
75
|
+
/** Correlation id from the error envelope (`request_id` on either vocabulary), when the
|
|
76
|
+
* server includes one. Usually absent today. */
|
|
77
|
+
requestId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Transport-class transient hint: `true` for 408, 429, 502, 503 and 504. It says the failure
|
|
80
|
+
* CLASS tends to pass, not that a replay is safe — retrying a `postEvents` without an
|
|
81
|
+
* `idempotency_key` can still deliver twice. Pair it with idempotency keys before looping.
|
|
82
|
+
*/
|
|
83
|
+
retryable: boolean;
|
|
84
|
+
constructor(status: number, message: string, type?: string, extra?: {
|
|
85
|
+
contentType?: string;
|
|
86
|
+
bodySnippet?: string;
|
|
87
|
+
cfRay?: string;
|
|
88
|
+
requestId?: string;
|
|
89
|
+
retryable?: boolean;
|
|
90
|
+
});
|
|
59
91
|
}
|
|
60
92
|
export interface Ownership {
|
|
61
93
|
owner_uid: string;
|
|
@@ -259,22 +291,47 @@ export interface AgentChannel {
|
|
|
259
291
|
[k: string]: unknown;
|
|
260
292
|
}
|
|
261
293
|
/**
|
|
262
|
-
* The chat platforms you can bind, staging-verified 2026-08-
|
|
294
|
+
* The chat platforms you can bind, staging-verified 2026-08-28.
|
|
295
|
+
*
|
|
296
|
+
* Three of them have a server-driven QR flow ({@link GuidedSetupPlatform}); Slack does not,
|
|
297
|
+
* and structurally cannot — a Slack app is created by a person and its tokens only ever exist
|
|
298
|
+
* in that person's browser, so {@link AddChannelInput} with `botToken` + `appToken` is its
|
|
299
|
+
* permanent path.
|
|
300
|
+
*
|
|
301
|
+
* WeChat is the one platform that goes the other way: `'weixin'`/`'wechat'` on
|
|
302
|
+
* {@link ZooworkClient.addChannel} answers `400 channel.weixin_setup_required`, so the QR flow
|
|
303
|
+
* is its ONLY path. See {@link AddChannelPlatform}. Any name outside this type answers
|
|
304
|
+
* `400 channel.invalid_request`.
|
|
305
|
+
*/
|
|
306
|
+
export type ChannelPlatform = 'feishu' | 'slack' | 'wecom' | 'weixin';
|
|
307
|
+
/**
|
|
308
|
+
* The platforms {@link ZooworkClient.addChannel} accepts — every {@link ChannelPlatform}
|
|
309
|
+
* except WeChat, which refuses explicit config and takes the QR flow only.
|
|
310
|
+
*/
|
|
311
|
+
export type AddChannelPlatform = 'feishu' | 'slack' | 'wecom';
|
|
312
|
+
/**
|
|
313
|
+
* The platforms with a server-driven QR flow: {@link ZooworkClient.startChannelSetup} →
|
|
314
|
+
* render the URI → poll. Slack is absent by design, not by omission.
|
|
263
315
|
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* its tokens only ever exist in that person's browser — so `addChannel` with `botToken` +
|
|
267
|
-
* `appToken` is its permanent path. WeCom's flow exists in the product but is not exposed on
|
|
268
|
-
* this API yet, so today it also binds through `addChannel`.
|
|
316
|
+
* The three differ in what the setup answer carries and in what the body may say
|
|
317
|
+
* (staging-verified 2026-08-28):
|
|
269
318
|
*
|
|
270
|
-
*
|
|
271
|
-
* `
|
|
272
|
-
*
|
|
319
|
+
* - `feishu` — answers `verification_uri_complete` and a `poll_interval`; takes `brand`,
|
|
320
|
+
* `account`, `dm_policy`, `group_policy`; `expires_in: 600`.
|
|
321
|
+
* - `wecom` — answers `qrcode_url` and NO `poll_interval` (you pick the cadence); takes
|
|
322
|
+
* `account`, `dm_policy`, `group_policy`; `expires_in: 300`.
|
|
323
|
+
* - `weixin` — answers `qrcode_url`, which may be a URL *or* an inline `data:image/…` payload;
|
|
324
|
+
* takes `dm_policy` only, `'open'` or `'disabled'` (the account is pinned to `'default'` and
|
|
325
|
+
* the group policy to `'disabled'` server-side); `expires_in: 300`.
|
|
273
326
|
*/
|
|
274
|
-
export type
|
|
327
|
+
export type GuidedSetupPlatform = 'feishu' | 'wecom' | 'weixin';
|
|
275
328
|
export interface AddChannelInput {
|
|
276
|
-
/**
|
|
277
|
-
|
|
329
|
+
/**
|
|
330
|
+
* See {@link AddChannelPlatform}. Typed loosely so a newly supported platform needs no SDK
|
|
331
|
+
* release. `'weixin'`/`'wechat'` is refused here with `400 channel.weixin_setup_required` —
|
|
332
|
+
* use {@link ZooworkClient.startChannelSetup} instead.
|
|
333
|
+
*/
|
|
334
|
+
platform: AddChannelPlatform | (string & {});
|
|
278
335
|
/**
|
|
279
336
|
* Names this binding. It is part of the record's identity, not a setting: `updateChannel`
|
|
280
337
|
* and `removeChannel` find a binding by `platform` + `account`, and nothing renames one
|
|
@@ -324,8 +381,14 @@ export interface UpdateChannelInput {
|
|
|
324
381
|
group_policy?: string;
|
|
325
382
|
enabled?: boolean;
|
|
326
383
|
}
|
|
327
|
-
|
|
328
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Body for {@link ZooworkClient.startChannelSetup}. Every field is optional, and each platform
|
|
386
|
+
* reads a different subset — see {@link GuidedSetupPlatform}. A field a platform does not read
|
|
387
|
+
* is ignored rather than rejected: `weixin` accepts an `account` in the body and still binds
|
|
388
|
+
* `'default'` (staging-verified 2026-08-28).
|
|
389
|
+
*/
|
|
390
|
+
export interface ChannelSetupInput {
|
|
391
|
+
/** Feishu only: `'feishu'` (default) or `'lark'` — the international brand of the same platform. */
|
|
329
392
|
brand?: 'feishu' | 'lark';
|
|
330
393
|
/**
|
|
331
394
|
* Names this binding. It is part of the record's identity, not a setting: `updateChannel`
|
|
@@ -351,37 +414,60 @@ export interface FeishuSetupInput {
|
|
|
351
414
|
* works as-is: it matches the pattern and is unique per agent by construction.
|
|
352
415
|
*/
|
|
353
416
|
account?: string;
|
|
354
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* Server default: `'open'`. WeChat takes only `'open'` or `'disabled'` — `'allowlist'` is
|
|
419
|
+
* `400 channel.allowlist_unsupported` there, and `'pairing'` is
|
|
420
|
+
* `400 channel.pairing_unsupported` on every platform.
|
|
421
|
+
*/
|
|
355
422
|
dm_policy?: string;
|
|
356
|
-
/** Server default: `'open'`. */
|
|
423
|
+
/** Server default: `'open'`. Ignored by WeChat, which forces `'disabled'`. */
|
|
357
424
|
group_policy?: string;
|
|
358
425
|
}
|
|
426
|
+
/** @deprecated Use {@link ChannelSetupInput}; this is the same shape under the old name. */
|
|
427
|
+
export type FeishuSetupInput = ChannelSetupInput;
|
|
359
428
|
/**
|
|
360
|
-
* A running
|
|
361
|
-
*
|
|
362
|
-
* `
|
|
363
|
-
*
|
|
429
|
+
* A running QR registration. Show the person doing the binding whichever URI the platform
|
|
430
|
+
* answered — `verification_uri_complete` for Feishu, `qrcode_url` for WeCom and WeChat, so
|
|
431
|
+
* `session.verification_uri_complete ?? session.qrcode_url` is the value to render — then poll
|
|
432
|
+
* with {@link ZooworkClient.pollChannelSetup} / {@link ZooworkClient.waitForChannelSetup} until
|
|
433
|
+
* it leaves `pending`. The session expires after `expires_in` seconds.
|
|
434
|
+
*
|
|
435
|
+
* Two of the three are QR-only: WeCom's `qrcode_url` is a URL you encode yourself, and WeChat's
|
|
436
|
+
* may be a URL *or* an inline `data:image/…` payload you render directly, so check the prefix
|
|
437
|
+
* before you feed it to a QR encoder.
|
|
364
438
|
*/
|
|
365
|
-
export interface
|
|
439
|
+
export interface ChannelSetupSession {
|
|
366
440
|
session_id: string;
|
|
367
|
-
|
|
441
|
+
/** Feishu only — the URI to encode into a QR code. */
|
|
442
|
+
verification_uri_complete?: string;
|
|
443
|
+
/** WeCom and WeChat — a URL to encode, or (WeChat) an inline `data:image/…` image. */
|
|
444
|
+
qrcode_url?: string;
|
|
368
445
|
expires_in: number;
|
|
369
|
-
/** Suggested seconds between polls;
|
|
446
|
+
/** Suggested seconds between polls. Feishu sends it; WeCom and WeChat never do. */
|
|
370
447
|
poll_interval?: number | null;
|
|
371
448
|
[k: string]: unknown;
|
|
372
449
|
}
|
|
373
450
|
/**
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* still-in-flight rather than throwing.
|
|
451
|
+
* A running Feishu QR registration — {@link ChannelSetupSession} narrowed to the one platform
|
|
452
|
+
* that always answers `verification_uri_complete`.
|
|
377
453
|
*/
|
|
378
|
-
export interface
|
|
454
|
+
export interface FeishuSetupSession extends ChannelSetupSession {
|
|
455
|
+
verification_uri_complete: string;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* One poll of a setup session. The gateway's own vocabulary for `status` is
|
|
459
|
+
* `pending | success | expired | denied | error` (`denied` is Feishu-only); treat anything
|
|
460
|
+
* unknown as still-in-flight rather than throwing.
|
|
461
|
+
*/
|
|
462
|
+
export interface ChannelPollResult {
|
|
379
463
|
status: string;
|
|
380
464
|
channel_configured?: boolean;
|
|
381
465
|
message?: string | null;
|
|
382
466
|
poll_interval?: number | null;
|
|
383
467
|
[k: string]: unknown;
|
|
384
468
|
}
|
|
469
|
+
/** @deprecated Use {@link ChannelPollResult}; this is the same shape under the old name. */
|
|
470
|
+
export type FeishuPollResult = ChannelPollResult;
|
|
385
471
|
export interface AgentRecord {
|
|
386
472
|
agent_id: string;
|
|
387
473
|
computer_id?: string;
|
|
@@ -1053,8 +1139,8 @@ export interface ZooworkClient {
|
|
|
1053
1139
|
/**
|
|
1054
1140
|
* Bind a channel from explicit platform config (the non-QR path) — `config` carries the
|
|
1055
1141
|
* platform's own credential keys. Answers the created channel (HTTP 201). This is the ONLY
|
|
1056
|
-
* path for Slack
|
|
1057
|
-
*
|
|
1142
|
+
* path for Slack, an alternative to the QR flow for Feishu and WeCom, and refused outright
|
|
1143
|
+
* for WeChat (`400 channel.weixin_setup_required`). See {@link AddChannelPlatform}.
|
|
1058
1144
|
*
|
|
1059
1145
|
* **It is idempotent, not an upsert.** Re-posting an identical body for the same
|
|
1060
1146
|
* `platform` + `account` answers `201` again and replays the binding you already have — it
|
|
@@ -1090,52 +1176,70 @@ export interface ZooworkClient {
|
|
|
1090
1176
|
account?: string;
|
|
1091
1177
|
}): Promise<void>;
|
|
1092
1178
|
/**
|
|
1093
|
-
* Start the
|
|
1094
|
-
* `verification_uri_complete`
|
|
1095
|
-
*
|
|
1179
|
+
* Start a QR registration on one of the three guided platforms. YOU own the UI: render the
|
|
1180
|
+
* URI the session answers (`verification_uri_complete` for Feishu, `qrcode_url` for WeCom and
|
|
1181
|
+
* WeChat) and drive the poll loop — {@link waitForChannelSetup} does the loop part for you.
|
|
1096
1182
|
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1183
|
+
* What each platform reads from `input`, and what it answers, is in
|
|
1184
|
+
* {@link GuidedSetupPlatform}; the short version is that only Feishu takes `brand`, only
|
|
1185
|
+
* Feishu and WeCom take `account`, and only Feishu sends back a `poll_interval`.
|
|
1100
1186
|
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1187
|
+
* For Feishu, `brand` picks the real host — `'feishu'` answers an `open.feishu.cn` URI,
|
|
1188
|
+
* `'lark'` an `open.larksuite.com` one — so it has to match the workspace the person will
|
|
1189
|
+
* approve it in.
|
|
1190
|
+
*
|
|
1191
|
+
* Pick `account` before you show the QR. On Feishu, approving the scan registers a NEW app in
|
|
1192
|
+
* that workspace, and only then does the binding get written — so a name clash surfaces as
|
|
1103
1193
|
* `409 channel.conflict` AFTER someone has scanned, with the freshly registered app left
|
|
1104
1194
|
* behind in their workspace. Retrying under the same name repeats both. See
|
|
1105
1195
|
* {@link AddChannelInput.account} for how names are scoped.
|
|
1106
1196
|
*/
|
|
1107
|
-
|
|
1197
|
+
startChannelSetup(agentId: string, platform: GuidedSetupPlatform, input?: ChannelSetupInput): Promise<ChannelSetupSession>;
|
|
1108
1198
|
/**
|
|
1109
|
-
* One poll of a setup session. `status: 'pending'` means keep going; a cancelled
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1199
|
+
* One poll of a setup session. `status: 'pending'` means keep going; a cancelled session
|
|
1200
|
+
* answers `404 channel.{platform}_session_not_found` rather than a terminal status, so a
|
|
1201
|
+
* hand-rolled loop must treat that 404 as an end condition, not as a transport error.
|
|
1112
1202
|
*/
|
|
1113
|
-
|
|
1114
|
-
/** Abandon a setup session. Afterwards polling it answers `404 channel.
|
|
1115
|
-
|
|
1203
|
+
pollChannelSetup(agentId: string, platform: GuidedSetupPlatform, sessionId: string): Promise<ChannelPollResult>;
|
|
1204
|
+
/** Abandon a setup session. Afterwards polling it answers `404 channel.{platform}_session_not_found`. */
|
|
1205
|
+
cancelChannelSetup(agentId: string, platform: GuidedSetupPlatform, sessionId: string): Promise<void>;
|
|
1116
1206
|
/**
|
|
1117
|
-
* Poll a
|
|
1207
|
+
* Poll a setup session until it leaves `pending`, then hand back that terminal poll.
|
|
1118
1208
|
* A status the server reports in the body — `success` / `expired` / `denied` / `error` — is
|
|
1119
1209
|
* RETURNED, not thrown: "the person rejected it" is an outcome, not an exception.
|
|
1120
1210
|
*
|
|
1121
1211
|
* But a session can also stop existing, and then polling answers
|
|
1122
|
-
* `404 channel.
|
|
1123
|
-
* {@link ZooworkError} carrying that `type`. Confirmed for a cancelled session
|
|
1124
|
-
* (staging 2026-08-
|
|
1125
|
-
* `status: 'expired'` in a 200 or disappears into this 404 was NOT observed — handle
|
|
1212
|
+
* `404 channel.{platform}_session_not_found`, which surfaces here as a thrown
|
|
1213
|
+
* {@link ZooworkError} carrying that `type`. Confirmed for a cancelled session on all three
|
|
1214
|
+
* platforms (staging 2026-08-28); whether a session that simply runs past `expires_in`
|
|
1215
|
+
* reports `status: 'expired'` in a 200 or disappears into this 404 was NOT observed — handle
|
|
1216
|
+
* both.
|
|
1126
1217
|
*
|
|
1127
|
-
* Pacing follows the server's `poll_interval` when present (
|
|
1128
|
-
*
|
|
1129
|
-
* pass the session's own value when you
|
|
1130
|
-
*
|
|
1131
|
-
* and every in-flight poll is bounded the way
|
|
1132
|
-
* `onPoll` fires after every poll, terminal one
|
|
1218
|
+
* Pacing follows the server's `poll_interval` when present (Feishu sends 5s; WeCom and WeChat
|
|
1219
|
+
* send none and fall back to the same 5s). The default budget is 600s, which matches Feishu's
|
|
1220
|
+
* `expires_in` but is twice WeCom's and WeChat's 300s — pass the session's own value when you
|
|
1221
|
+
* have it. On timeout it throws `status: 408` / `type: 'timeout'`; on abort, `status: 0` /
|
|
1222
|
+
* `type: 'aborted'` — both synthesized locally, and every in-flight poll is bounded the way
|
|
1223
|
+
* {@link waitUntilRunning} bounds its polls. `onPoll` fires after every poll, terminal one
|
|
1224
|
+
* included, for progress UI.
|
|
1133
1225
|
*/
|
|
1226
|
+
waitForChannelSetup(agentId: string, platform: GuidedSetupPlatform, sessionId: string, opts?: {
|
|
1227
|
+
timeoutMs?: number;
|
|
1228
|
+
signal?: AbortSignal;
|
|
1229
|
+
onPoll?: (poll: ChannelPollResult) => void;
|
|
1230
|
+
}): Promise<ChannelPollResult>;
|
|
1231
|
+
/** Feishu-only spelling of {@link startChannelSetup}, kept for callers written against 0.3.x–0.4.x. */
|
|
1232
|
+
startFeishuSetup(agentId: string, input?: ChannelSetupInput): Promise<FeishuSetupSession>;
|
|
1233
|
+
/** Feishu-only spelling of {@link pollChannelSetup}. */
|
|
1234
|
+
pollFeishuSetup(agentId: string, sessionId: string): Promise<ChannelPollResult>;
|
|
1235
|
+
/** Feishu-only spelling of {@link cancelChannelSetup}. */
|
|
1236
|
+
cancelFeishuSetup(agentId: string, sessionId: string): Promise<void>;
|
|
1237
|
+
/** Feishu-only spelling of {@link waitForChannelSetup}. */
|
|
1134
1238
|
waitForFeishuSetup(agentId: string, sessionId: string, opts?: {
|
|
1135
1239
|
timeoutMs?: number;
|
|
1136
1240
|
signal?: AbortSignal;
|
|
1137
|
-
onPoll?: (poll:
|
|
1138
|
-
}): Promise<
|
|
1241
|
+
onPoll?: (poll: ChannelPollResult) => void;
|
|
1242
|
+
}): Promise<ChannelPollResult>;
|
|
1139
1243
|
/**
|
|
1140
1244
|
* The agent's system-prompt pin and the rendered template in effect. Staging-verified
|
|
1141
1245
|
* 2026-08-14 — a fresh agent answers a real `declaration` (`{source:'platform',version:1}`),
|
package/dist/client.js
CHANGED
|
@@ -39,6 +39,11 @@ function stripTrailingSlashes(url) {
|
|
|
39
39
|
end--;
|
|
40
40
|
return end === url.length ? url : url.slice(0, end);
|
|
41
41
|
}
|
|
42
|
+
/** Statuses whose failure class tends to pass on its own: timeout, throttle, gateway. */
|
|
43
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
44
|
+
/** Bound on `ZooworkError.bodySnippet` — enough to keep a whole error envelope or the
|
|
45
|
+
* opening of an HTML error page, small enough to log unconditionally. */
|
|
46
|
+
const BODY_SNIPPET_LIMIT = 600;
|
|
42
47
|
export class ZooworkError extends Error {
|
|
43
48
|
status;
|
|
44
49
|
/**
|
|
@@ -52,13 +57,87 @@ export class ZooworkError extends Error {
|
|
|
52
57
|
* you only need the class of failure.
|
|
53
58
|
*/
|
|
54
59
|
type;
|
|
55
|
-
|
|
60
|
+
/**
|
|
61
|
+
* `Content-Type` of the error response. The field that tells an edge error page apart from an
|
|
62
|
+
* API answer: both envelopes above are `application/json`, while a gateway 502/504 arrives as
|
|
63
|
+
* `text/html` — the edge replaces the origin's body wholesale, so no JSON survives to parse
|
|
64
|
+
* (verified 2026-08-30 against both deployments;
|
|
65
|
+
* `notes/probes/system-message-cold-session-probe.mts`).
|
|
66
|
+
*/
|
|
67
|
+
contentType;
|
|
68
|
+
/** First {@link BODY_SNIPPET_LIMIT} characters of the raw error body, whatever it was.
|
|
69
|
+
* Without it a non-JSON failure cannot be reconstructed from `status` alone. */
|
|
70
|
+
bodySnippet;
|
|
71
|
+
/**
|
|
72
|
+
* Cloudflare ray id (`cf-ray` response header), when the response crossed Cloudflare. It
|
|
73
|
+
* survives even the replaced-body case above — on an HTML 502 it is the only correlation id
|
|
74
|
+
* left, and the value to quote when reporting a gateway failure.
|
|
75
|
+
*/
|
|
76
|
+
cfRay;
|
|
77
|
+
/** Correlation id from the error envelope (`request_id` on either vocabulary), when the
|
|
78
|
+
* server includes one. Usually absent today. */
|
|
79
|
+
requestId;
|
|
80
|
+
/**
|
|
81
|
+
* Transport-class transient hint: `true` for 408, 429, 502, 503 and 504. It says the failure
|
|
82
|
+
* CLASS tends to pass, not that a replay is safe — retrying a `postEvents` without an
|
|
83
|
+
* `idempotency_key` can still deliver twice. Pair it with idempotency keys before looping.
|
|
84
|
+
*/
|
|
85
|
+
retryable;
|
|
86
|
+
constructor(status, message, type, extra) {
|
|
56
87
|
super(message);
|
|
57
88
|
this.name = 'ZooworkError';
|
|
58
89
|
this.status = status;
|
|
59
90
|
if (type)
|
|
60
91
|
this.type = type;
|
|
92
|
+
if (extra?.contentType)
|
|
93
|
+
this.contentType = extra.contentType;
|
|
94
|
+
if (extra?.bodySnippet)
|
|
95
|
+
this.bodySnippet = extra.bodySnippet;
|
|
96
|
+
if (extra?.cfRay)
|
|
97
|
+
this.cfRay = extra.cfRay;
|
|
98
|
+
if (extra?.requestId)
|
|
99
|
+
this.requestId = extra.requestId;
|
|
100
|
+
this.retryable = extra?.retryable ?? RETRYABLE_STATUSES.has(status);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The response facts worth keeping on every transport-level ZooworkError, whatever the body. */
|
|
104
|
+
function responseForensics(res, text) {
|
|
105
|
+
return {
|
|
106
|
+
contentType: res.headers.get('content-type') ?? undefined,
|
|
107
|
+
cfRay: res.headers.get('cf-ray') ?? undefined,
|
|
108
|
+
bodySnippet: text ? text.slice(0, BODY_SNIPPET_LIMIT) : undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the ZooworkError for a non-2xx response. Unpacks BOTH envelope vocabularies (see
|
|
113
|
+
* {@link ZooworkError.type}); when neither matches — typically an edge error page whose body
|
|
114
|
+
* replaced the origin's JSON — the message names status, content-type and ray id instead of a
|
|
115
|
+
* bare `HTTP 502`. Those three are what turn an "HTTP 502, type: undefined" report into an
|
|
116
|
+
* answerable one (2026-08-30). A structured `detail` object is deliberately NOT stringified
|
|
117
|
+
* into the message (`[object Object]`); it stays readable in `bodySnippet`.
|
|
118
|
+
*/
|
|
119
|
+
function httpError(res, text) {
|
|
120
|
+
const forensics = responseForensics(res, text);
|
|
121
|
+
let msg;
|
|
122
|
+
let type;
|
|
123
|
+
let requestId;
|
|
124
|
+
try {
|
|
125
|
+
const j = JSON.parse(text);
|
|
126
|
+
const detail = typeof j?.detail === 'string' ? j.detail : undefined;
|
|
127
|
+
msg = j?.error?.message || j?.message || detail || undefined;
|
|
128
|
+
type = j?.error?.type ?? j?.code;
|
|
129
|
+
requestId = j?.error?.request_id ?? j?.request_id;
|
|
61
130
|
}
|
|
131
|
+
catch {
|
|
132
|
+
/* non-JSON error body — usually the edge speaking, not the API */
|
|
133
|
+
}
|
|
134
|
+
if (!msg) {
|
|
135
|
+
msg =
|
|
136
|
+
`HTTP ${res.status}` +
|
|
137
|
+
(forensics.contentType ? ` (${forensics.contentType})` : '') +
|
|
138
|
+
(forensics.cfRay ? ` [cf-ray ${forensics.cfRay}]` : '');
|
|
139
|
+
}
|
|
140
|
+
return new ZooworkError(res.status, msg, type, { ...forensics, requestId });
|
|
62
141
|
}
|
|
63
142
|
/**
|
|
64
143
|
* Create a client.
|
|
@@ -90,36 +169,22 @@ export function createZooworkClient(cfg = {}) {
|
|
|
90
169
|
}
|
|
91
170
|
const bearer = 'serviceToken' in auth ? auth.serviceToken : auth.apiKey;
|
|
92
171
|
/**
|
|
93
|
-
* TWO error envelopes, one ZooworkError shape, for every helper below
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* Reading only the first left every agent 404 with `type: undefined` and the message `HTTP 404`,
|
|
98
|
-
* so both are unpacked here. The codes stay verbatim (`not_found` vs `service_api.not_found`) —
|
|
99
|
-
* inventing a shared vocabulary would be this SDK guessing, which is what it exists not to do.
|
|
172
|
+
* TWO error envelopes, one ZooworkError shape, for every helper below — the unpacking (and
|
|
173
|
+
* why both vocabularies exist) lives in module-level {@link httpError}. The codes stay
|
|
174
|
+
* verbatim (`not_found` vs `service_api.not_found`) — inventing a shared vocabulary would be
|
|
175
|
+
* this SDK guessing, which is what it exists not to do.
|
|
100
176
|
*/
|
|
101
177
|
const readResponse = async (res, path) => {
|
|
102
178
|
const text = await res.text();
|
|
103
|
-
if (!res.ok)
|
|
104
|
-
|
|
105
|
-
let type;
|
|
106
|
-
try {
|
|
107
|
-
const j = JSON.parse(text);
|
|
108
|
-
msg = j?.error?.message || j?.message || j?.detail || msg;
|
|
109
|
-
type = j?.error?.type ?? j?.code;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
/* non-JSON error body → keep clean status */
|
|
113
|
-
}
|
|
114
|
-
throw new ZooworkError(res.status, msg, type);
|
|
115
|
-
}
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
throw httpError(res, text);
|
|
116
181
|
if (!text)
|
|
117
182
|
return {};
|
|
118
183
|
try {
|
|
119
184
|
return JSON.parse(text);
|
|
120
185
|
}
|
|
121
186
|
catch {
|
|
122
|
-
throw new ZooworkError(res.status, `non-JSON response: ${path}
|
|
187
|
+
throw new ZooworkError(res.status, `non-JSON response: ${path}`, undefined, responseForensics(res, text));
|
|
123
188
|
}
|
|
124
189
|
};
|
|
125
190
|
/**
|
|
@@ -270,7 +335,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
270
335
|
let lastSeen = 'unknown';
|
|
271
336
|
const abortedError = () => new ZooworkError(0, `waitUntilRunning(${agentId}) aborted`, 'aborted');
|
|
272
337
|
const timeoutError = () => new ZooworkError(408, `agent ${agentId} did not reach status.desired_state=running within ${timeoutMs}ms ` +
|
|
273
|
-
`(last seen: ${lastSeen})`, 'timeout');
|
|
338
|
+
`(last seen: ${lastSeen})`, 'timeout', { retryable: false });
|
|
274
339
|
for (;;) {
|
|
275
340
|
if (opts.signal?.aborted)
|
|
276
341
|
throw abortedError();
|
|
@@ -336,20 +401,21 @@ export function createZooworkClient(cfg = {}) {
|
|
|
336
401
|
body: JSON.stringify({ account: opts.account ?? 'default' }),
|
|
337
402
|
});
|
|
338
403
|
},
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
404
|
+
startChannelSetup: (agentId, platform, input = {}) => json(`${agents(agentId)}/channels/${encodeURIComponent(platform)}/setup`, {
|
|
405
|
+
method: 'POST',
|
|
406
|
+
body: JSON.stringify(input),
|
|
407
|
+
}),
|
|
408
|
+
pollChannelSetup: (agentId, platform, sessionId) => json(`${agents(agentId)}/channels/${encodeURIComponent(platform)}/poll${query({ session_id: sessionId })}`),
|
|
409
|
+
cancelChannelSetup: async (agentId, platform, sessionId) => {
|
|
410
|
+
await json(`${agents(agentId)}/channels/${encodeURIComponent(platform)}/setup/cancel${query({ session_id: sessionId })}`, { method: 'POST' });
|
|
345
411
|
},
|
|
346
|
-
|
|
412
|
+
waitForChannelSetup: async (agentId, platform, sessionId, opts = {}) => {
|
|
347
413
|
const timeoutMs = opts.timeoutMs ?? 600_000;
|
|
348
414
|
const deadline = Date.now() + timeoutMs;
|
|
349
415
|
let lastStatus = 'unknown';
|
|
350
|
-
const abortedError = () => new ZooworkError(0, `
|
|
351
|
-
const timeoutError = () => new ZooworkError(408,
|
|
352
|
-
'the QR may simply not have been scanned yet; the session itself expires server-side', 'timeout');
|
|
416
|
+
const abortedError = () => new ZooworkError(0, `waitForChannelSetup(${agentId}, ${platform}, ${sessionId}) aborted`, 'aborted');
|
|
417
|
+
const timeoutError = () => new ZooworkError(408, `${platform} setup session ${sessionId} still '${lastStatus}' after ${timeoutMs}ms — ` +
|
|
418
|
+
'the QR may simply not have been scanned yet; the session itself expires server-side', 'timeout', { retryable: false });
|
|
353
419
|
for (;;) {
|
|
354
420
|
if (opts.signal?.aborted)
|
|
355
421
|
throw abortedError();
|
|
@@ -364,7 +430,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
364
430
|
const budget = setTimeout(cancelPoll, remaining);
|
|
365
431
|
let result;
|
|
366
432
|
try {
|
|
367
|
-
result = await json(`${agents(agentId)}/channels/
|
|
433
|
+
result = await json(`${agents(agentId)}/channels/${encodeURIComponent(platform)}/poll${query({ session_id: sessionId })}`, { signal: poll.signal });
|
|
368
434
|
}
|
|
369
435
|
catch (e) {
|
|
370
436
|
if (poll.signal.aborted)
|
|
@@ -378,7 +444,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
378
444
|
opts.onPoll?.(result);
|
|
379
445
|
lastStatus = result.status ?? 'unknown';
|
|
380
446
|
// Only a literal 'pending' keeps the loop alive… except that an UNKNOWN status is
|
|
381
|
-
// treated as still-in-flight too (see
|
|
447
|
+
// treated as still-in-flight too (see ChannelPollResult): a new intermediate state on
|
|
382
448
|
// the server should stretch the wait, not end it with a fake terminal result.
|
|
383
449
|
const terminal = ['success', 'expired', 'denied', 'error'].includes(lastStatus);
|
|
384
450
|
if (terminal)
|
|
@@ -391,6 +457,12 @@ export function createZooworkClient(cfg = {}) {
|
|
|
391
457
|
await sleep(intervalMs, opts.signal);
|
|
392
458
|
}
|
|
393
459
|
},
|
|
460
|
+
// The Feishu-only spellings, kept for callers written against 0.3.x–0.4.x. `startFeishuSetup`
|
|
461
|
+
// narrows the return type only — Feishu always answers `verification_uri_complete`.
|
|
462
|
+
startFeishuSetup: (agentId, input = {}) => client.startChannelSetup(agentId, 'feishu', input),
|
|
463
|
+
pollFeishuSetup: (agentId, sessionId) => client.pollChannelSetup(agentId, 'feishu', sessionId),
|
|
464
|
+
cancelFeishuSetup: (agentId, sessionId) => client.cancelChannelSetup(agentId, 'feishu', sessionId),
|
|
465
|
+
waitForFeishuSetup: (agentId, sessionId, opts = {}) => client.waitForChannelSetup(agentId, 'feishu', sessionId, opts),
|
|
394
466
|
uploadSkill: (zip, opts) => {
|
|
395
467
|
const form = skillForm(zip, opts);
|
|
396
468
|
form.append('scope', opts.scope);
|
|
@@ -510,7 +582,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
510
582
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
511
583
|
});
|
|
512
584
|
if (!res.ok)
|
|
513
|
-
throw
|
|
585
|
+
throw httpError(res, await res.text().catch(() => ''));
|
|
514
586
|
if (!res.body)
|
|
515
587
|
return;
|
|
516
588
|
for await (const msg of parseSSE(res.body)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createZooworkClient, DEFAULT_BASE_URL, ZooworkError, type ZooworkClient, type ZooworkConfig, type ZooworkAuth, type Ownership, type ModelInfo, type AgentResource, type AgentRecord, type AgentStatus, type AgentSkill, type AgentChannel, type ChannelPlatform, type AddChannelInput, type UpdateChannelInput, type FeishuSetupInput, type FeishuSetupSession, type FeishuPollResult, type McpServerDeclaration, type SkillRecord, type SessionRecord, type SessionHistoryEntry, type SessionEvent, type SessionEventPage, type OutboundEvent, type PostEventReceipt, type ApprovalDecision, type ApprovalRecord, type ArtifactPage, type ArtifactRecord, type ArtifactStatus, type OutcomeConfig, type OutcomeEvaluator, type SystemPromptDeclaration, type SystemPromptInfo, type SystemPromptPreview, type SystemPromptPreviewInput, type SystemPromptUpgrade, type ScheduleSpec, type SchedulePayload, type ScheduleInput, type ScheduleUpdate, type ScheduleRecord, type ScheduleRun, type WakeResult, type ExecResult, type EnvironmentConfig, type EnvironmentResource, type EnvironmentRecord, type EnvironmentVersionRecord, } from './client.js';
|
|
1
|
+
export { createZooworkClient, DEFAULT_BASE_URL, ZooworkError, type ZooworkClient, type ZooworkConfig, type ZooworkAuth, type Ownership, type ModelInfo, type AgentResource, type AgentRecord, type AgentStatus, type AgentSkill, type AgentChannel, type ChannelPlatform, type AddChannelPlatform, type GuidedSetupPlatform, type AddChannelInput, type UpdateChannelInput, type ChannelSetupInput, type ChannelSetupSession, type ChannelPollResult, type FeishuSetupInput, type FeishuSetupSession, type FeishuPollResult, type McpServerDeclaration, type SkillRecord, type SessionRecord, type SessionHistoryEntry, type SessionEvent, type SessionEventPage, type OutboundEvent, type PostEventReceipt, type ApprovalDecision, type ApprovalRecord, type ArtifactPage, type ArtifactRecord, type ArtifactStatus, type OutcomeConfig, type OutcomeEvaluator, type SystemPromptDeclaration, type SystemPromptInfo, type SystemPromptPreview, type SystemPromptPreviewInput, type SystemPromptUpgrade, type ScheduleSpec, type SchedulePayload, type ScheduleInput, type ScheduleUpdate, type ScheduleRecord, type ScheduleRun, type WakeResult, type ExecResult, type EnvironmentConfig, type EnvironmentResource, type EnvironmentRecord, type EnvironmentVersionRecord, } from './client.js';
|
|
2
2
|
export { SESSION_EVENT_TYPES, type SessionEventType, PUBLIC_INPUT_EVENT_TYPES, type PublicInputEventType, normalizeEvent, isRunFinished, runOutcome, messageText, assistantText, thinkingText, toolCall, type ToolCall, } from './events.js';
|
|
3
3
|
export { parseSSE, type SSEMessage } from './sse.js';
|