@chatu-ai/builder-sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chatu-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @chatu-ai/builder-sdk
2
+
3
+ Framework-agnostic client for the ChatU **App Builder** (AI-driven app generation with a live sandbox preview).
4
+
5
+ ```ts
6
+ import { createBuilderClient, CookieAuth } from '@chatu-ai/builder-sdk'
7
+
8
+ const client = createBuilderClient({
9
+ restBase: 'https://your-chatu-host/web/Builder',
10
+ auth: new CookieAuth(), // or new ApiKeyAuth('...')
11
+ transport, // streaming transport injected by the host (see docs)
12
+ })
13
+
14
+ const status = await client.sandbox.status(conversationId)
15
+ const { previewUrl } = await client.sandbox.previewToken(conversationId)
16
+ const versions = await client.versions.list(conversationId)
17
+ ```
18
+
19
+ - Zod event schemas (`events.ts`) are the single source of truth for the streaming protocol
20
+ - `parseBuilderEvent` + seq-based resume for reconnect-safe consumption
21
+ - No login-state assumptions: bring your own `AuthProvider`
22
+
23
+ Vue bindings: [`@chatu-ai/builder-sdk-vue`](https://www.npmjs.com/package/@chatu-ai/builder-sdk-vue). Mock client for UI dev: [`@chatu-ai/builder-sdk-mock`](https://www.npmjs.com/package/@chatu-ai/builder-sdk-mock).
24
+
25
+ MIT
@@ -0,0 +1,22 @@
1
+ /**
2
+ * agent3 SSE(BuilderController create/connect 直通流)→ BuilderEvent 翻译。
3
+ * P0 步骤一形态:服务端不做翻译,前端按 chatuse Message 形态推导事件(03 §2 语义)。
4
+ * 步骤二切 A2A 时本模块保留为 REST/SSE 传输的实现之一(同一 BuilderEvent 出口)。
5
+ *
6
+ * agent3 Message: { type: step|chunk|complete|error|status|input|dialog, content, sequenceNumber, xid }
7
+ * content = Claude Agent SDK 原始消息(assistant text/tool_use、user tool_result、result...)
8
+ */
9
+ import { BuilderEvent } from './events';
10
+ /**
11
+ * 有状态翻译器:一个 xid 一个实例(跟踪 tool_use_id → 任务卡 id,用于 done 状态回填)
12
+ */
13
+ export declare class Agent3Translator {
14
+ private readonly toolCards;
15
+ /**
16
+ * 翻译一条 SSE data 行(JSON 文本或服务端自产的 builder-ack/created-response)
17
+ * 返回 0..n 个事件(一条 assistant 消息可能含 text + 多个 tool_use)
18
+ */
19
+ translate(raw: unknown): BuilderEvent[];
20
+ private fromSdkMessage;
21
+ private resultState;
22
+ }
package/dist/agent3.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * agent3 SSE(BuilderController create/connect 直通流)→ BuilderEvent 翻译。
3
+ * P0 步骤一形态:服务端不做翻译,前端按 chatuse Message 形态推导事件(03 §2 语义)。
4
+ * 步骤二切 A2A 时本模块保留为 REST/SSE 传输的实现之一(同一 BuilderEvent 出口)。
5
+ *
6
+ * agent3 Message: { type: step|chunk|complete|error|status|input|dialog, content, sequenceNumber, xid }
7
+ * content = Claude Agent SDK 原始消息(assistant text/tool_use、user tool_result、result...)
8
+ */
9
+ import { BuilderEvent } from './events';
10
+ /** 工具名 → 任务卡标签(规则化,07 §4 taskCardMapper 的前端镜像) */
11
+ const TOOL_LABELS = {
12
+ Bash: '执行命令',
13
+ Write: '写入文件',
14
+ Edit: '修改文件',
15
+ MultiEdit: '批量修改',
16
+ Read: '读取文件',
17
+ Glob: '查找文件',
18
+ Grep: '搜索代码',
19
+ WebSearch: '搜索资料',
20
+ WebFetch: '获取网页',
21
+ Task: '子任务',
22
+ };
23
+ /**
24
+ * 有状态翻译器:一个 xid 一个实例(跟踪 tool_use_id → 任务卡 id,用于 done 状态回填)
25
+ */
26
+ export class Agent3Translator {
27
+ toolCards = new Map();
28
+ /**
29
+ * 翻译一条 SSE data 行(JSON 文本或服务端自产的 builder-ack/created-response)
30
+ * 返回 0..n 个事件(一条 assistant 消息可能含 text + 多个 tool_use)
31
+ */
32
+ translate(raw) {
33
+ if (typeof raw !== 'object' || raw === null)
34
+ return [];
35
+ const m = raw;
36
+ // 服务端流前事件(BuilderController)
37
+ if (m.type === 'builder-ack' && m.sandbox) {
38
+ const parsed = BuilderEvent.safeParse({ kind: 'ack', xid: 'pending', seq: 0, sandbox: m.sandbox });
39
+ return parsed.success ? [parsed.data] : [];
40
+ }
41
+ if (m.type === 'created-response')
42
+ return []; // xid 由调用方从此事件取出,不产出 BuilderEvent
43
+ const xid = m.xid;
44
+ const seq = m.sequenceNumber;
45
+ if (!xid || typeof seq !== 'number')
46
+ return [];
47
+ const out = [];
48
+ switch (m.type) {
49
+ case 'chunk':
50
+ case 'step':
51
+ out.push(...this.fromSdkMessage(xid, seq, m.content));
52
+ break;
53
+ case 'complete':
54
+ out.push({ kind: 'done', xid, seq, state: this.resultState(m.content) });
55
+ break;
56
+ case 'error':
57
+ out.push({ kind: 'done', xid, seq, state: 'failed', error: errorText(m.content) });
58
+ break;
59
+ case 'status':
60
+ break; // 系统状态:P0 不映射(后续可映射 task_updated → taskCard)
61
+ default:
62
+ break;
63
+ }
64
+ return out
65
+ .map(e => BuilderEvent.safeParse(e))
66
+ .filter((r) => r.success)
67
+ .map(r => r.data);
68
+ }
69
+ fromSdkMessage(xid, seq, content) {
70
+ const events = [];
71
+ const message = content?.message ?? content;
72
+ const blocks = Array.isArray(message?.content) ? message.content : [];
73
+ const role = message?.role ?? content?.type;
74
+ for (const block of blocks) {
75
+ if (block?.type === 'text' && role === 'assistant' && typeof block.text === 'string' && block.text.trim()) {
76
+ events.push({ kind: 'message', xid, seq, role: 'assistant', text: block.text });
77
+ }
78
+ else if (block?.type === 'tool_use') {
79
+ const cardId = `tc_${block.id ?? seq}`;
80
+ this.toolCards.set(block.id, cardId);
81
+ events.push({
82
+ kind: 'taskCard', xid, seq, id: cardId,
83
+ label: TOOL_LABELS[block.name] ?? block.name ?? '执行工具',
84
+ state: 'running',
85
+ detail: describeInput(block.name, block.input),
86
+ });
87
+ // 文件变更事件(Write/Edit)
88
+ const path = block.input?.file_path ?? block.input?.path;
89
+ if ((block.name === 'Write' || block.name === 'Edit' || block.name === 'MultiEdit') && typeof path === 'string') {
90
+ events.push({
91
+ kind: 'fileDiff', xid, seq, path: relPath(path),
92
+ action: block.name === 'Write' ? 'create' : 'modify',
93
+ bytes: typeof block.input?.content === 'string' ? block.input.content.length : 0,
94
+ truncated: false,
95
+ });
96
+ }
97
+ }
98
+ else if (block?.type === 'tool_result') {
99
+ const cardId = this.toolCards.get(block.tool_use_id);
100
+ if (cardId) {
101
+ events.push({
102
+ kind: 'taskCard', xid, seq, id: cardId,
103
+ label: '', // upsert 时保留原 label(reducer 以 id 合并;空 label 由 reducer 忽略)
104
+ state: block.is_error ? 'failed' : 'done',
105
+ });
106
+ }
107
+ }
108
+ }
109
+ return events;
110
+ }
111
+ resultState(content) {
112
+ const subtype = content?.subtype ?? content?.result?.subtype;
113
+ if (subtype === 'success')
114
+ return 'completed';
115
+ if (typeof subtype === 'string' && subtype.includes('cancel'))
116
+ return 'canceled';
117
+ if (content?.is_error)
118
+ return 'failed';
119
+ return 'completed';
120
+ }
121
+ }
122
+ function describeInput(tool, input) {
123
+ if (!input)
124
+ return undefined;
125
+ if (tool === 'Bash' && typeof input.command === 'string')
126
+ return input.command.slice(0, 80);
127
+ const p = input.file_path ?? input.path ?? input.pattern;
128
+ return typeof p === 'string' ? relPath(p).slice(0, 80) : undefined;
129
+ }
130
+ function relPath(p) {
131
+ return p.replace(/^\/workspace\//, '');
132
+ }
133
+ function errorText(content) {
134
+ if (typeof content === 'string')
135
+ return content;
136
+ return content?.message ?? content?.error ?? 'unknown error';
137
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Agent3Translator } from './agent3';
3
+ const X = 'xid-1';
4
+ const sdk = (seq, type, content) => ({ xid: X, sequenceNumber: seq, type, content });
5
+ describe('Agent3Translator', () => {
6
+ it('translates builder-ack into ack event with seq 0', () => {
7
+ const t = new Agent3Translator();
8
+ const evs = t.translate({ type: 'builder-ack', seq: 0, sandbox: { sandboxId: 'chat-use-abc', state: 'ready', previewUrl: 'https://chat-use-abc.n.y.com' } });
9
+ expect(evs).toHaveLength(1);
10
+ expect(evs[0]).toMatchObject({ kind: 'ack', seq: 0, sandbox: { sandboxId: 'chat-use-abc' } });
11
+ });
12
+ it('assistant text + tool_use -> message + running taskCard (+ fileDiff for Write)', () => {
13
+ const t = new Agent3Translator();
14
+ const evs = t.translate(sdk(3, 'chunk', {
15
+ type: 'assistant',
16
+ message: {
17
+ role: 'assistant',
18
+ content: [
19
+ { type: 'text', text: '开始创建首页' },
20
+ { type: 'tool_use', id: 'tu_1', name: 'Write', input: { file_path: '/workspace/src/app/page.tsx', content: 'export default 1' } },
21
+ ],
22
+ },
23
+ }));
24
+ expect(evs.map(e => e.kind)).toEqual(['message', 'taskCard', 'fileDiff']);
25
+ expect(evs[1]).toMatchObject({ kind: 'taskCard', id: 'tc_tu_1', label: '写入文件', state: 'running' });
26
+ expect(evs[2]).toMatchObject({ kind: 'fileDiff', path: 'src/app/page.tsx', action: 'create' });
27
+ });
28
+ it('tool_result marks matching card done/failed with empty label (state-only upsert)', () => {
29
+ const t = new Agent3Translator();
30
+ t.translate(sdk(1, 'chunk', { message: { role: 'assistant', content: [{ type: 'tool_use', id: 'tu_9', name: 'Bash', input: { command: 'npm i' } }] } }));
31
+ const evs = t.translate(sdk(2, 'step', { message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tu_9', is_error: true }] } }));
32
+ expect(evs).toHaveLength(1);
33
+ expect(evs[0]).toMatchObject({ kind: 'taskCard', id: 'tc_tu_9', state: 'failed', label: '' });
34
+ });
35
+ it('complete/error -> done', () => {
36
+ const t = new Agent3Translator();
37
+ expect(t.translate(sdk(9, 'complete', { subtype: 'success' }))[0]).toMatchObject({ kind: 'done', state: 'completed' });
38
+ expect(t.translate(sdk(10, 'error', { message: 'boom' }))[0]).toMatchObject({ kind: 'done', state: 'failed', error: 'boom' });
39
+ });
40
+ it('ignores status/created-response/garbage', () => {
41
+ const t = new Agent3Translator();
42
+ expect(t.translate(sdk(1, 'status', { subtype: 'init' }))).toEqual([]);
43
+ expect(t.translate({ type: 'created-response', xid: 'x' })).toEqual([]);
44
+ expect(t.translate('not json')).toEqual([]);
45
+ expect(t.translate({ type: 'chunk' })).toEqual([]); // 无 xid/seq
46
+ });
47
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Agent3 SSE 直通传输 —— A2ATransport 的 REST/SSE 实现(P0 步骤一,对接 BuilderController)。
3
+ * 产出的是**已翻译**的 BuilderEvent(而非 A2A 原始事件),因此配套 client 需用 identity 解析。
4
+ */
5
+ import type { AuthProvider } from './auth';
6
+ import type { BuilderEvent } from './events';
7
+ import type { ResilienceOptions } from './resume';
8
+ export interface Agent3TransportOptions {
9
+ /** BuilderController 前缀,如 https://api.example.com/web/Builder */
10
+ baseUrl: string;
11
+ auth: AuthProvider;
12
+ fetchImpl?: typeof fetch;
13
+ }
14
+ export interface Agent3Transport {
15
+ stream(conversationId: string, prompt: string, opts?: {
16
+ agentId?: string;
17
+ attachments?: unknown[];
18
+ }): AsyncIterable<BuilderEvent>;
19
+ resubscribe(conversationId: string, xid: string, lastSeq: number): AsyncIterable<BuilderEvent>;
20
+ cancel(conversationId: string, xid: string): Promise<void>;
21
+ }
22
+ export declare function createAgent3Transport(options: Agent3TransportOptions): Agent3Transport;
23
+ /**
24
+ * 便捷工厂:agent3 SSE 直通形态的完整 BuilderClient(P0 步骤一,chat-web USE_MOCK=false 即用此)。
25
+ * conversationId 通过闭包绑定到 transport(A2ATransport.resubscribe 只带 xid)。
26
+ */
27
+ export declare function createAgent3Client(conversationId: string, options: Agent3TransportOptions & {
28
+ restBase?: string;
29
+ agentId?: string;
30
+ resilience?: ResilienceOptions;
31
+ }): import("./client").BuilderClient;
@@ -0,0 +1,53 @@
1
+ import { Agent3Translator } from './agent3';
2
+ import { createBuilderClient } from './client';
3
+ import { readSse } from './sse';
4
+ export function createAgent3Transport(options) {
5
+ const { baseUrl, auth } = options;
6
+ async function* translated(src, translator) {
7
+ for await (const raw of src) {
8
+ for (const ev of translator.translate(raw))
9
+ yield ev;
10
+ }
11
+ }
12
+ return {
13
+ stream(conversationId, prompt, opts) {
14
+ const translator = new Agent3Translator();
15
+ const init = auth.apply({
16
+ method: 'POST',
17
+ headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
18
+ body: JSON.stringify({ conversationId, prompt, agentId: opts?.agentId }),
19
+ });
20
+ return translated(readSse(`${baseUrl}/create`, init, { fetchImpl: options.fetchImpl }), translator);
21
+ },
22
+ resubscribe(conversationId, xid, lastSeq) {
23
+ const translator = new Agent3Translator();
24
+ const init = auth.apply({ method: 'GET', headers: { accept: 'text/event-stream' } });
25
+ const url = `${baseUrl}/connect?conversationId=${conversationId}&xid=${xid}&checkpoint=${lastSeq}`;
26
+ return translated(readSse(url, init, { fetchImpl: options.fetchImpl }), translator);
27
+ },
28
+ async cancel(conversationId, xid) {
29
+ const init = auth.apply({ method: 'POST' });
30
+ await (options.fetchImpl ?? fetch)(`${baseUrl}/cancel?conversationId=${conversationId}&xid=${xid}`, init);
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * 便捷工厂:agent3 SSE 直通形态的完整 BuilderClient(P0 步骤一,chat-web USE_MOCK=false 即用此)。
36
+ * conversationId 通过闭包绑定到 transport(A2ATransport.resubscribe 只带 xid)。
37
+ */
38
+ export function createAgent3Client(conversationId, options) {
39
+ const transport = createAgent3Transport(options);
40
+ const a2aLike = {
41
+ stream: (cid, prompt, opts) => transport.stream(cid, prompt, { ...opts, agentId: opts?.agentId ?? options.agentId }),
42
+ resubscribe: (xid, lastSeq) => transport.resubscribe(conversationId, xid, lastSeq),
43
+ cancel: xid => transport.cancel(conversationId, xid),
44
+ };
45
+ return createBuilderClient({
46
+ restBase: options.restBase ?? options.baseUrl,
47
+ transport: a2aLike,
48
+ auth: options.auth,
49
+ fetchImpl: options.fetchImpl,
50
+ resilience: options.resilience,
51
+ parse: e => e, // transport 已产出 BuilderEvent
52
+ });
53
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /** auth provider 接口:内部=Cookie 会话;开放 API=API Key(P2)。(04 §1 约束 1) */
2
+ export interface AuthProvider {
3
+ /** 为请求附加认证信息(headers / credentials 模式) */
4
+ apply(init: RequestInit): RequestInit;
5
+ }
6
+ export declare class CookieAuth implements AuthProvider {
7
+ apply(init: RequestInit): RequestInit;
8
+ }
9
+ export declare class ApiKeyAuth implements AuthProvider {
10
+ private readonly apiKey;
11
+ constructor(apiKey: string);
12
+ apply(init: RequestInit): RequestInit;
13
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,17 @@
1
+ export class CookieAuth {
2
+ apply(init) {
3
+ return { ...init, credentials: 'include' };
4
+ }
5
+ }
6
+ export class ApiKeyAuth {
7
+ apiKey;
8
+ constructor(apiKey) {
9
+ this.apiKey = apiKey;
10
+ }
11
+ apply(init) {
12
+ return {
13
+ ...init,
14
+ headers: { ...(init.headers ?? {}), Authorization: `Bearer ${this.apiKey}` },
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * BuilderClient —— 08 §2 冻结 API 面的实现。
3
+ * A2A 传输层由宿主注入(chat-web 传入 libs/a2a-client 的包装),core 不重写传输。
4
+ */
5
+ import type { AuthProvider } from './auth';
6
+ import type { BuilderEvent, SandboxState } from './events';
7
+ import { type ResilienceOptions } from './resume';
8
+ export interface A2ATransport {
9
+ /** message/stream:发起生成,产出原始 A2A 事件 */
10
+ stream(conversationId: string, prompt: string, opts?: {
11
+ agentId?: string;
12
+ attachments?: unknown[];
13
+ }): AsyncIterable<unknown>;
14
+ /** tasks/resubscribe:按 task 重连(服务端回放 seq > lastSeq) */
15
+ resubscribe(xid: string, lastSeq: number): AsyncIterable<unknown>;
16
+ /** tasks/cancel */
17
+ cancel(xid: string): Promise<void>;
18
+ }
19
+ export interface BuilderClientOptions {
20
+ /** REST 前缀,如 https://api.example.com/web/builder */
21
+ restBase: string;
22
+ transport: A2ATransport;
23
+ auth: AuthProvider;
24
+ fetchImpl?: typeof fetch;
25
+ resilience?: ResilienceOptions;
26
+ /**
27
+ * 原始传输事件 → BuilderEvent 解析器。默认 A2A 解析;
28
+ * 传输层已产出 BuilderEvent 时(如 agent3 SSE 直通)传 identity:`e => e as BuilderEvent`
29
+ */
30
+ parse?: (raw: unknown) => BuilderEvent | null;
31
+ }
32
+ export interface SandboxStatus {
33
+ state: SandboxState;
34
+ previewUrl?: string;
35
+ devServer?: {
36
+ running: boolean;
37
+ lastError?: string;
38
+ };
39
+ }
40
+ export interface VersionInfo {
41
+ sha: string;
42
+ message: string;
43
+ filesChanged: number;
44
+ createdAt?: string;
45
+ }
46
+ export interface FileNode {
47
+ path: string;
48
+ type: 'file' | 'dir';
49
+ children?: FileNode[];
50
+ }
51
+ export interface BuilderClient {
52
+ chat: {
53
+ stream(conversationId: string, prompt: string, opts?: {
54
+ agentId?: string;
55
+ attachments?: unknown[];
56
+ }): AsyncIterable<BuilderEvent>;
57
+ resubscribe(conversationId: string, xid: string, lastSeq: number): AsyncIterable<BuilderEvent>;
58
+ cancel(conversationId: string, xid: string): Promise<void>;
59
+ };
60
+ sandbox: {
61
+ status(conversationId: string): Promise<SandboxStatus>;
62
+ heartbeat(conversationId: string, opts: {
63
+ visible: boolean;
64
+ }): Promise<void | {
65
+ ok?: boolean;
66
+ state?: SandboxState | string;
67
+ }>;
68
+ /** 一次性预览 token(06 §6.1):返回可直接作 iframe src 的带 ?t= 的 URL */
69
+ previewToken(conversationId: string): Promise<{
70
+ token: string;
71
+ previewUrl: string;
72
+ }>;
73
+ /** 唤醒/确保沙箱(休眠 → 恢复快照 → 起 dev server;不发起 agent 会话) */
74
+ wake(conversationId: string): Promise<SandboxStatus>;
75
+ };
76
+ versions: {
77
+ list(conversationId: string, opts?: {
78
+ limit?: number;
79
+ }): Promise<VersionInfo[]>;
80
+ restore(conversationId: string, sha: string): Promise<void>;
81
+ };
82
+ files: {
83
+ tree(conversationId: string, opts?: {
84
+ path?: string;
85
+ ref?: string;
86
+ }): Promise<FileNode[]>;
87
+ read(conversationId: string, path: string, opts?: {
88
+ ref?: string;
89
+ }): Promise<string>;
90
+ downloadUrl(conversationId: string): string;
91
+ };
92
+ }
93
+ export declare function createBuilderClient(options: BuilderClientOptions): BuilderClient;
94
+ export declare class BuilderApiError extends Error {
95
+ readonly status: number;
96
+ constructor(status: number, body: string);
97
+ }
package/dist/client.js ADDED
@@ -0,0 +1,107 @@
1
+ import { parseBuilderEvent } from './parse';
2
+ import { resilientStream } from './resume';
3
+ export function createBuilderClient(options) {
4
+ const { restBase, transport, auth } = options;
5
+ const doFetch = options.fetchImpl ?? fetch;
6
+ const parse = options.parse ?? parseBuilderEvent;
7
+ async function req(path, init = {}) {
8
+ const res = await doFetch(`${restBase}${path}`, auth.apply(init));
9
+ if (!res.ok)
10
+ throw new BuilderApiError(res.status, await res.text().catch(() => ''));
11
+ const ct = res.headers.get('content-type') ?? '';
12
+ if (!ct.includes('json'))
13
+ return (await res.text());
14
+ const json = await res.json();
15
+ return unwrapEnvelope(json);
16
+ }
17
+ /** 把原始 A2A 迭代器包装成解析后的迭代器 */
18
+ async function* parsed(src) {
19
+ for await (const raw of src)
20
+ yield parse(raw);
21
+ }
22
+ return {
23
+ chat: {
24
+ stream(conversationId, prompt, opts) {
25
+ // xid 在 ack 事件中获知,用于断线 reopen
26
+ let xid;
27
+ const base = resilientStream({
28
+ open: () => parsed(transport.stream(conversationId, prompt, opts)),
29
+ reopen: lastSeq => {
30
+ if (!xid)
31
+ throw new Error('cannot resubscribe before ack (xid unknown)');
32
+ return parsed(transport.resubscribe(xid, lastSeq));
33
+ },
34
+ }, options.resilience);
35
+ // 旁路捕获 xid
36
+ return (async function* () {
37
+ for await (const ev of base) {
38
+ if (ev.kind === 'ack')
39
+ xid = ev.xid;
40
+ yield ev;
41
+ }
42
+ })();
43
+ },
44
+ resubscribe(_conversationId, xid, lastSeq) {
45
+ return resilientStream({
46
+ open: () => parsed(transport.resubscribe(xid, lastSeq)),
47
+ reopen: seq => parsed(transport.resubscribe(xid, seq)),
48
+ }, options.resilience);
49
+ },
50
+ cancel: (_conversationId, xid) => transport.cancel(xid),
51
+ },
52
+ sandbox: {
53
+ status: id => req(`/sandbox/${id}/status`),
54
+ heartbeat: (id, opts) => req(`/sandbox/${id}/heartbeat`, {
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify(opts),
58
+ }),
59
+ previewToken: id => req(`/${id}/preview-token`),
60
+ wake: id => req(`/sandbox/${id}/wake`, { method: 'POST' }),
61
+ },
62
+ versions: {
63
+ // 服务端形状:{ versions: VersionInfo[] }(runtime 透传)
64
+ list: async (id, opts) => {
65
+ const r = await req(`/${id}/versions${opts?.limit ? `?limit=${opts.limit}` : ''}`);
66
+ return Array.isArray(r) ? r : (r.versions ?? []);
67
+ },
68
+ restore: async (id, sha) => { await req(`/${id}/versions/${sha}/restore`, { method: 'POST' }); },
69
+ },
70
+ files: {
71
+ // 服务端形状:{ tree: FileNode[] }
72
+ tree: async (id, opts) => {
73
+ const r = await req(`/${id}/files?${qs(opts)}`);
74
+ return Array.isArray(r) ? r : (r.tree ?? []);
75
+ },
76
+ read: (id, path, opts) => req(`/${id}/files/read?${qs({ path, ...opts })}`),
77
+ downloadUrl: id => `${restBase}/${id}/files/download`,
78
+ },
79
+ };
80
+ }
81
+ /**
82
+ * Dapi.Web 结果过滤器会把 object 返回包成 { code, data, message }(ContentResult 直通不包)。
83
+ * 统一拆信封:code !== 0 视为业务错误抛出;非信封形状原样返回。
84
+ */
85
+ function unwrapEnvelope(json) {
86
+ if (json && typeof json === 'object' && 'code' in json && 'data' in json) {
87
+ const env = json;
88
+ if (env.code !== 0)
89
+ throw new BuilderApiError(200, env.message ?? `api code ${env.code}`);
90
+ return env.data;
91
+ }
92
+ return json;
93
+ }
94
+ export class BuilderApiError extends Error {
95
+ status;
96
+ constructor(status, body) {
97
+ super(`builder api ${status}: ${body.slice(0, 200)}`);
98
+ this.status = status;
99
+ }
100
+ }
101
+ function qs(obj) {
102
+ const p = new URLSearchParams();
103
+ for (const [k, v] of Object.entries(obj ?? {}))
104
+ if (v !== undefined)
105
+ p.set(k, v);
106
+ return p.toString();
107
+ }