@blade-hq/agent-client 2612.0.0-beta.13 → 2612.0.0-beta.14

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/README.md CHANGED
@@ -788,3 +788,64 @@ Solution 的 `skill_tools_enabled`、是否为 `solution-app`、是否采用 `ni
788
788
  已应用的版本 ID 持久保留:当前版本相同请求幂等返回;被后续升级覆盖的版本返回 409,禁止延迟重试回滚配置。主动回退配置应提交新的版本 ID。
789
789
 
790
790
  分组会话复用现有 validate_group_session 约束,只允许通用助手;切换到业务 Solution 返回 422,普通运行设置可升级,分组绑定保留。
791
+
792
+
793
+ ### 匿名会话
794
+
795
+ 创建匿名会话需要已认证的客户端。每个匿名会话使用独立持久沙盒,不继承创建者的文件、记忆、个人环境变量或登录凭据。UUID 链接授予该会话的查看、聊天及文件访问权限,请妥善保管。当前不自动回收。
796
+
797
+ ```ts
798
+ const account = new BladeClient({ baseUrl, token: userPat })
799
+ const { session_id } = await account.sessions.createSessionWithRequest({ anonymous: true })
800
+ const link = `${baseUrl}/anonymous/${session_id}`
801
+
802
+ // 访客只有 UUID 即可连接,不需要账号令牌。
803
+ const visitor = account.forAnonymousSession(session_id)
804
+ const session = await visitor.sessions.connect(session_id)
805
+ await session.send("你好")
806
+ // 用完后释放;匿名客户端在最后一个会话引用释放时断开连接。
807
+ session.dispose()
808
+ ```
809
+
810
+ 也可以使用 `new BladeClient({ baseUrl, anonymousSessionId: session_id })`,并把它交给 `BladeProvider` / `ChatView`。`account.sessions.create({ anonymous: true })` 会自动返回已连接的匿名 `AgentSession`;`ChatView createOptions={{ anonymous: true }}` 同样先认证创建,再使用会话凭证连接。账号客户端与匿名客户端分别持有连接,匿名客户端不能创建会话或访问用户级资源。匿名会话只支持 sandbox,不能绑定已有聊天分组、父会话或启用个人记忆。
811
+
812
+ 每个账号最多保留 6 个匿名会话;超过时创建返回 409。使用已认证账号客户端管理和删除旧会话:
813
+
814
+ ```ts
815
+ const retained = await account.sessions.listAnonymousSessions()
816
+ await account.sessions.deleteAnonymousSession(retained[0].id)
817
+ ```
818
+
819
+ 删除会永久移除该会话的沙盒、文件与历史,原链接失效。会话凭证不能调用管理接口。
820
+
821
+ ## 后端应用工具连接
822
+
823
+ `client.connectToolProvider({ providerId, capability, tools })` 返回 `ToolProvider`。仅用于 Node 后端;BA 用户 token 由 BladeClient 提供。`await provider.ready()` 等待已鉴权注册,再将 `provider.url` 和 capability 配入会话 `mcp_sources`。providerId 与 capability 由应用持久化,重连复用。
824
+
825
+ `ToolProviderOptions` 定义注册参数,`ProviderTool` 定义工具与处理函数,`ProviderToolResult` 定义调用结果。
826
+
827
+ 工具包含 `name`、`description`、`inputSchema` 与异步 `handler(arguments, { sessionId, signal })`;结果为 `content`、可选 `structuredContent` 与 `isError`。sessionId 来自 BA 已授权调用,应用还应校验它对应自身业务会话。AbortSignal 表示取消请求,不保证业务回滚。连接恢复不重放调用;业务动作应使用应用自己的幂等 ID。`provider.dispose()` 关闭连接。SDK 退出登录/更换后端也会关闭来源。
828
+
829
+ 连接使用 `/api/app-tools/connect`,HTTP 后端对应 ws,HTTPS 对应 wss;不要求应用暴露回调地址。需要支持此入口的 BA,未加入 SDK/Server 版本校验。
830
+
831
+ ### 可恢复的文本发送
832
+
833
+ 应用需要在回执丢失或进程重启后确认输入时,使用 SDK 准备请求,并在联网前保存:
834
+
835
+ ```ts
836
+ const request = session.prepareSend("制作报告", { mode: "executing" })
837
+ await savePendingRequest(request) // 应用自己的持久化存储
838
+ const receipt = await session.sendPrepared(request)
839
+ // unknown 时恢复保存的 request 再发送,或查询:
840
+ const delivery = await session.getDelivery(request.requestId)
841
+ ```
842
+
843
+ 同一保存请求可重试,BA 不重复入队;同 ID 改变正文或参数会拒绝。接收结果 `accepted` 表示输入已持久化,不表示任务完成。`unknown` 表示无法确认,`not_found` 是查询未找到;不得因此生成新 ID。查询可返回队列项、队列状态和执行 ID;记录保留至会话删除。
844
+
845
+ 此接口复用后端等待队列:空闲时自动开始,运行中等待,暂停时保留。可靠请求不能编辑或作为立即补充,但可以取消、重排和继续。支持普通文本、模式、模型、思考与推理强度;AskUser 作答、多模态、whatif 和 replay 继续使用 `send()`。需要支持 `prepared_options` 及 `chat:queue:request` 的 BA;旧服务的普通队列确认不算可靠请求回执。
846
+
847
+ 公开类型:`PreparedSend` 是可持久化的请求快照,`PreparedSendOptions` 是普通输入的执行参数,`DeliveryReceipt` 是接受或查询结果。
848
+
849
+ 匿名客户端不能读取 owner-only `configuration`,详情不返回 `env`。持链接者可以在共享沙盒执行代码,因此显式提供的环境变量会对该会话参与者可见;不要用它保存创建者的私人凭据。第一版不支持匿名 `app-dev`(软件工厂),也不提供同源 HTML 应用展示;文件可以下载查看。
850
+
851
+ 匿名子客户端保留 `commandTransport`,并为匿名命令额外提供第四个可选参数 `{ anonymousSessionId }`。中间件应使用本次调用的 `send()` 保持匿名凭证,或按该范围使用专属通道;不能把命令转发到另一个账号连接。普通命令仍提供原来的三个参数。
@@ -1,3 +1,4 @@
1
+ import { ToolProvider, type ToolProviderOptions } from "./tool-provider";
1
2
  import { type LoginOptions, type LoginResult, type TokenStorageMode } from "./auth-login";
2
3
  import { type BladeFetchInit, type HttpMethod } from "./rest";
3
4
  import { AuthResource } from "./resources/auth";
@@ -18,6 +19,8 @@ export interface BladeClientOptions {
18
19
  * 浏览器场景也可以不传,改用 client.auth.login() 弹窗登录。
19
20
  */
20
21
  token?: string | (() => string | null | undefined);
22
+ /** 只访问此匿名会话;UUID 链接本身是访问凭证,不使用账号令牌。 */
23
+ anonymousSessionId?: string;
21
24
  /**
22
25
  * Socket.IO 专用令牌。第一方应用可在 REST 继续使用 cookie 刷新的同时,
23
26
  * 把 cookie 换得的短期令牌只用于实时连接;不传时复用 token。
@@ -26,11 +29,15 @@ export interface BladeClientOptions {
26
29
  /** login() 取得的令牌存放位置,默认 "local"(localStorage,刷新页面不掉登录)。 */
27
30
  tokenStorage?: TokenStorageMode;
28
31
  /** Host-owned command transport; event subscriptions stay on the normal socket. */
29
- commandTransport?: (event: string, payload: Record<string, unknown>, send: () => Promise<Record<string, unknown>>) => Promise<Record<string, unknown>>;
32
+ commandTransport?: (event: string, payload: Record<string, unknown>, send: () => Promise<Record<string, unknown>>, scope?: {
33
+ anonymousSessionId: string;
34
+ }) => Promise<Record<string, unknown>>;
30
35
  fetchImpl?: typeof fetch;
31
36
  onRefreshSuccess?: () => void | Promise<void>;
32
37
  }
33
38
  export declare class BladeClient {
39
+ private readonly anonymousClients;
40
+ private readonly toolProviders;
34
41
  private refreshPromise;
35
42
  private socketInstance;
36
43
  private readonly socketReplacementListeners;
@@ -53,6 +60,8 @@ export declare class BladeClient {
53
60
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
54
61
  readonly hub: SessionHub;
55
62
  constructor(options: BladeClientOptions);
63
+ /** Backend application tools, callable over an outbound WebSocket. */
64
+ connectToolProvider(options: ToolProviderOptions): ToolProvider;
56
65
  /**
57
66
  * 弹窗授权登录:用户在弹出的授权页点击"许可授权"后自动取得 PAT,
58
67
  * REST 与 Socket.IO 立即生效。只在浏览器可用。
@@ -79,6 +88,8 @@ export declare class BladeClient {
79
88
  * 安全约束:该令牌只应发往 baseUrl 同源接口,不要附加到第三方域名的请求上。
80
89
  */
81
90
  get token(): string | null;
91
+ /** 获取只访问指定匿名会话的客户端;创建仍由已认证的原客户端完成。 */
92
+ forAnonymousSession(sessionId: string): BladeClient;
82
93
  setBaseUrl(baseUrl: string): void;
83
94
  /** 底层 Socket.IO 实例因后端地址变化而被替换时通知高级集成层。 */
84
95
  onSocketReplaced(listener: () => void): () => void;
@@ -1,6 +1,219 @@
1
+ // src/rest.ts
2
+ var DOCS_URL = "https://blade-hq.github.io/public-skills/";
3
+ var BladeApiError = class extends Error {
4
+ response;
5
+ status;
6
+ statusText;
7
+ constructor(response, message) {
8
+ super(message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08${response.status} ${response.statusText}\uFF09`);
9
+ this.name = "BladeApiError";
10
+ this.response = response;
11
+ this.status = response.status;
12
+ this.statusText = response.statusText;
13
+ }
14
+ };
15
+ async function extractErrorDetail(response) {
16
+ try {
17
+ const cloned = response.clone();
18
+ const contentType = cloned.headers.get("content-type") ?? "";
19
+ if (contentType.includes("application/json")) {
20
+ const data = await cloned.json();
21
+ const detail = data?.detail;
22
+ if (typeof detail === "string" && detail.trim()) return detail;
23
+ if (Array.isArray(detail) && detail.length > 0) {
24
+ const first = detail[0];
25
+ if (first && typeof first.msg === "string") return first.msg;
26
+ }
27
+ } else {
28
+ const text = (await cloned.text()).trim();
29
+ if (text) return text;
30
+ }
31
+ } catch {
32
+ }
33
+ return void 0;
34
+ }
35
+ function decorateAuthError(error) {
36
+ if (!(error instanceof BladeApiError)) return error;
37
+ if (error.status === 401) {
38
+ error.message = `${error.message}\uFF08\u672A\u767B\u5F55\u6216\u51ED\u8BC1\u5DF2\u5931\u6548\u3002\u6D4F\u89C8\u5668\u573A\u666F\u8C03\u7528 client.auth.login() \u5F39\u7A97\u767B\u5F55\uFF0C\u81EA\u52A8\u5316\u573A\u666F\u5728 new BladeClient({ token }) \u4F20\u5165 PAT\u3002\u63A5\u5165\u6307\u5357\uFF1A${DOCS_URL}\uFF09`;
39
+ } else if (error.status === 403) {
40
+ error.message = `${error.message}\uFF08\u5F53\u524D\u8D26\u53F7\u65E0\u6743\u8BBF\u95EE\u8BE5\u8D44\u6E90\u3002\u63A5\u5165\u6307\u5357\uFF1A${DOCS_URL}\uFF09`;
41
+ }
42
+ return error;
43
+ }
44
+ function buildBackendUrl(baseUrl, path) {
45
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
46
+ return new URL(path.startsWith("/") ? path.slice(1) : path, base);
47
+ }
48
+
49
+ // src/tool-provider.ts
50
+ var ToolProvider = class {
51
+ constructor(client, options, onDispose) {
52
+ this.client = client;
53
+ this.options = options;
54
+ this.onDispose = onDispose;
55
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(options.providerId) || options.capability.length < 16 || options.capability.length > 4096) throw new Error("Invalid tool provider identity");
56
+ if (new Set(options.tools.map((t) => t.name)).size !== options.tools.length || options.tools.length > 256) throw new Error("Duplicate or excessive provider tools");
57
+ this.url = `blade-rpc://${options.providerId}`;
58
+ this.open();
59
+ }
60
+ client;
61
+ options;
62
+ onDispose;
63
+ url;
64
+ socket;
65
+ timer;
66
+ registrationTimer;
67
+ disposed = false;
68
+ registered = false;
69
+ attempts = 0;
70
+ calls = /* @__PURE__ */ new Map();
71
+ waiters = /* @__PURE__ */ new Set();
72
+ /** Resolves only after BA has authenticated and registered this connection. */
73
+ ready() {
74
+ if (this.disposed) return Promise.reject(new Error("Tool provider disposed"));
75
+ if (this.registered) return Promise.resolve();
76
+ return new Promise((resolve, reject) => {
77
+ const timer = setTimeout(() => {
78
+ this.waiters.delete(waiter);
79
+ reject(new Error("Tool provider registration timed out"));
80
+ }, 25e3);
81
+ const waiter = { resolve: () => {
82
+ clearTimeout(timer);
83
+ resolve();
84
+ }, reject: (e) => {
85
+ clearTimeout(timer);
86
+ reject(e);
87
+ } };
88
+ this.waiters.add(waiter);
89
+ });
90
+ }
91
+ reconnect() {
92
+ if (this.disposed) return;
93
+ clearTimeout(this.timer);
94
+ this.reset();
95
+ this.open();
96
+ }
97
+ reset() {
98
+ clearTimeout(this.registrationTimer);
99
+ this.registered = false;
100
+ const socket = this.socket;
101
+ this.socket = void 0;
102
+ socket?.close();
103
+ for (const controller of this.calls.values()) controller.abort();
104
+ this.calls.clear();
105
+ }
106
+ fail(error) {
107
+ for (const waiter of this.waiters) waiter.reject(error);
108
+ this.waiters.clear();
109
+ try {
110
+ this.options.onError?.(error);
111
+ } catch {
112
+ }
113
+ }
114
+ open() {
115
+ if (this.disposed) return;
116
+ const endpoint = buildBackendUrl(this.client.options.baseUrl, "/api/app-tools/connect");
117
+ endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:";
118
+ let socket;
119
+ try {
120
+ const token = this.client.token;
121
+ if (!token) throw new Error("Tool provider requires a token");
122
+ const encoded = btoa(Array.from(new TextEncoder().encode(token), (byte) => String.fromCharCode(byte)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
123
+ socket = new WebSocket(endpoint, ["blade-rpc", `blade-bearer.${encoded}`]);
124
+ } catch {
125
+ this.fail(new Error("Tool provider WebSocket unavailable"));
126
+ this.schedule();
127
+ return;
128
+ }
129
+ this.socket = socket;
130
+ this.registrationTimer = setTimeout(() => {
131
+ if (this.socket === socket) {
132
+ this.fail(new Error("Tool provider registration timed out"));
133
+ socket.close();
134
+ }
135
+ }, 15e3);
136
+ socket.addEventListener("open", () => {
137
+ if (this.socket !== socket) return;
138
+ socket.send(JSON.stringify({ jsonrpc: "2.0", id: "register", method: "provider/register", params: { providerId: this.options.providerId, capability: this.options.capability } }));
139
+ });
140
+ socket.addEventListener("message", (event) => {
141
+ if (this.socket !== socket || typeof event.data !== "string") return;
142
+ void this.receive(socket, event.data).catch(() => {
143
+ this.fail(new Error("Invalid application tool RPC message"));
144
+ socket.close();
145
+ });
146
+ });
147
+ socket.addEventListener("error", () => {
148
+ if (this.socket === socket) this.fail(new Error("Application tool connection failed"));
149
+ });
150
+ socket.addEventListener("close", () => {
151
+ if (this.socket !== socket) return;
152
+ this.reset();
153
+ this.fail(new Error("Application tool connection closed; unfinished calls will not be replayed"));
154
+ this.schedule();
155
+ });
156
+ }
157
+ schedule() {
158
+ if (this.disposed) return;
159
+ this.timer = setTimeout(() => this.open(), Math.min(1e4, 500 * 2 ** Math.min(this.attempts++, 5)));
160
+ }
161
+ async receive(socket, text) {
162
+ const message = JSON.parse(text);
163
+ if (message.jsonrpc !== "2.0") throw new Error("Invalid JSON-RPC envelope");
164
+ if (message.id === "register" && !message.method) {
165
+ clearTimeout(this.registrationTimer);
166
+ if (message.error || !message.result?.registered) {
167
+ this.fail(new Error(message.error?.message ?? "Tool provider registration rejected"));
168
+ socket.close();
169
+ return;
170
+ }
171
+ this.registered = true;
172
+ this.attempts = 0;
173
+ for (const waiter of this.waiters) waiter.resolve();
174
+ this.waiters.clear();
175
+ return;
176
+ }
177
+ if (!this.registered) throw new Error("Provider not registered");
178
+ if (message.method === "notifications/cancelled") {
179
+ this.calls.get(String(message.params?.requestId))?.abort();
180
+ return;
181
+ }
182
+ if (!message.id || typeof message.id !== "string" || this.calls.has(message.id)) throw new Error("Invalid RPC request ID");
183
+ const controller = new AbortController();
184
+ this.calls.set(message.id, controller);
185
+ let response = { jsonrpc: "2.0", id: message.id };
186
+ try {
187
+ if (message.method === "tools/list") {
188
+ response.result = { tools: this.options.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) };
189
+ } else if (message.method === "tools/call") {
190
+ const params = message.params;
191
+ const tool = this.options.tools.find((t) => t.name === params?.name);
192
+ if (!tool || typeof params?.sessionId !== "string" || !params.sessionId || !params.arguments || typeof params.arguments !== "object" || Array.isArray(params.arguments)) throw new Error("Invalid or unauthorized tool call");
193
+ response.result = await tool.handler(params.arguments, { sessionId: params.sessionId, signal: controller.signal });
194
+ } else {
195
+ response.error = { code: -32601, message: "Method not found" };
196
+ }
197
+ } catch {
198
+ response = { jsonrpc: "2.0", id: message.id, error: { code: -32e3, message: "Application tool execution failed" } };
199
+ } finally {
200
+ this.calls.delete(message.id);
201
+ }
202
+ if (this.socket === socket && socket.readyState === WebSocket.OPEN && !controller.signal.aborted) socket.send(JSON.stringify(response));
203
+ }
204
+ dispose() {
205
+ if (this.disposed) return;
206
+ this.disposed = true;
207
+ clearTimeout(this.timer);
208
+ this.reset();
209
+ this.fail(new Error("Tool provider disposed"));
210
+ this.onDispose();
211
+ }
212
+ };
213
+
1
214
  // src/version.ts
2
215
  var SDK_NAME = "agent-client";
3
- var SDK_VERSION = true ? "2612.0.0-beta.13" : "1.1.1";
216
+ var SDK_VERSION = true ? "2612.0.0-beta.14" : "1.1.1";
4
217
 
5
218
  // src/auth-login.ts
6
219
  function isAuthCallbackMessage(value) {
@@ -128,54 +341,6 @@ async function loginWithPopup(client, options = {}) {
128
341
  });
129
342
  }
130
343
 
131
- // src/rest.ts
132
- var DOCS_URL = "https://blade-hq.github.io/public-skills/";
133
- var BladeApiError = class extends Error {
134
- response;
135
- status;
136
- statusText;
137
- constructor(response, message) {
138
- super(message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08${response.status} ${response.statusText}\uFF09`);
139
- this.name = "BladeApiError";
140
- this.response = response;
141
- this.status = response.status;
142
- this.statusText = response.statusText;
143
- }
144
- };
145
- async function extractErrorDetail(response) {
146
- try {
147
- const cloned = response.clone();
148
- const contentType = cloned.headers.get("content-type") ?? "";
149
- if (contentType.includes("application/json")) {
150
- const data = await cloned.json();
151
- const detail = data?.detail;
152
- if (typeof detail === "string" && detail.trim()) return detail;
153
- if (Array.isArray(detail) && detail.length > 0) {
154
- const first = detail[0];
155
- if (first && typeof first.msg === "string") return first.msg;
156
- }
157
- } else {
158
- const text = (await cloned.text()).trim();
159
- if (text) return text;
160
- }
161
- } catch {
162
- }
163
- return void 0;
164
- }
165
- function decorateAuthError(error) {
166
- if (!(error instanceof BladeApiError)) return error;
167
- if (error.status === 401) {
168
- error.message = `${error.message}\uFF08\u672A\u767B\u5F55\u6216\u51ED\u8BC1\u5DF2\u5931\u6548\u3002\u6D4F\u89C8\u5668\u573A\u666F\u8C03\u7528 client.auth.login() \u5F39\u7A97\u767B\u5F55\uFF0C\u81EA\u52A8\u5316\u573A\u666F\u5728 new BladeClient({ token }) \u4F20\u5165 PAT\u3002\u63A5\u5165\u6307\u5357\uFF1A${DOCS_URL}\uFF09`;
169
- } else if (error.status === 403) {
170
- error.message = `${error.message}\uFF08\u5F53\u524D\u8D26\u53F7\u65E0\u6743\u8BBF\u95EE\u8BE5\u8D44\u6E90\u3002\u63A5\u5165\u6307\u5357\uFF1A${DOCS_URL}\uFF09`;
171
- }
172
- return error;
173
- }
174
- function buildBackendUrl(baseUrl, path) {
175
- const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
176
- return new URL(path.startsWith("/") ? path.slice(1) : path, base);
177
- }
178
-
179
344
  // src/resources/auth.ts
180
345
  var AuthResource = class {
181
346
  constructor(client) {
@@ -2241,6 +2406,7 @@ function projectHistory(entries) {
2241
2406
  loop_id: loopId,
2242
2407
  kind: "message",
2243
2408
  role,
2409
+ client_request_id: stringOrNull(entry.client_request_id),
2244
2410
  status: message._error ? "failed" : message._interrupted ? "interrupted" : "completed",
2245
2411
  blocks,
2246
2412
  tool_calls: toolCalls,
@@ -3109,6 +3275,7 @@ var SessionInfo = type({
3109
3275
  created_at: "string",
3110
3276
  updated_at: "string",
3111
3277
  "shared?": "boolean",
3278
+ "anonymous?": "boolean",
3112
3279
  "memory_enabled?": "boolean",
3113
3280
  "is_persistent?": "boolean",
3114
3281
  "initiator?": "'ui' | 'sdk' | 'unknown' | null",
@@ -3160,6 +3327,7 @@ function toCreateSessionPayload(request) {
3160
3327
  const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
3161
3328
  return {
3162
3329
  intent: request.intent ?? "",
3330
+ anonymous: request.anonymous,
3163
3331
  solution_id: solutionRef ? void 0 : request.solution_id,
3164
3332
  solution_ref: solutionRef,
3165
3333
  biz_role_id: request.biz_role_id ?? null,
@@ -3318,7 +3486,7 @@ var SessionsResource = class {
3318
3486
  return this.createDeclared(request);
3319
3487
  }
3320
3488
  const { session_id } = await this.createSessionWithRequest(request);
3321
- return this.connect(session_id);
3489
+ return request.anonymous ? this.client.forAnonymousSession(session_id).sessions.connect(session_id) : this.connect(session_id);
3322
3490
  }
3323
3491
  async createDeclared(definition) {
3324
3492
  const { intent, solution, skills = [], config } = definition;
@@ -3621,6 +3789,12 @@ var SessionsResource = class {
3621
3789
  checkpoint_id: checkpointId
3622
3790
  });
3623
3791
  }
3792
+ listAnonymousSessions() {
3793
+ return this.client.json("GET", "/api/anonymous-sessions");
3794
+ }
3795
+ deleteAnonymousSession(sessionId) {
3796
+ return this.client.json("DELETE", `/api/anonymous-sessions/${encodeURIComponent(sessionId)}`);
3797
+ }
3624
3798
  deleteSession(sessionId, options) {
3625
3799
  const query = options?.deleteWorkspace ? "?delete_workspace=true" : "";
3626
3800
  return this.client.json("DELETE", `/api/sessions/${sessionId}${query}`);
@@ -4964,6 +5138,7 @@ function projectionToMessage(turn) {
4964
5138
  ...toolCalls ? { tool_calls: toolCalls } : {},
4965
5139
  loop_name: turn.loop_id,
4966
5140
  entry_id: turn.turn_id,
5141
+ ...turn.client_request_id ? { client_request_id: turn.client_request_id } : {},
4967
5142
  status: turn.status,
4968
5143
  ...typeof turn.duration_ms === "number" ? { duration_ms: turn.duration_ms } : {},
4969
5144
  ...turn.started_at ? { timestamp: turn.started_at } : {},
@@ -5768,6 +5943,71 @@ var AgentSession = class _AgentSession {
5768
5943
  this.getAppContext = options.getContext;
5769
5944
  return attachment;
5770
5945
  }
5946
+ /** Prepare a durable text input without sending any network request. */
5947
+ prepareSend(content, options = {}) {
5948
+ if (!content.trim()) throw new Error("\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A");
5949
+ if (Object.keys(options).some((key) => !["mode", "model", "thinkingOverride", "reasoningLevel"].includes(key))) {
5950
+ throw new Error("\u53EF\u9760\u6587\u672C\u53D1\u9001\u4E0D\u652F\u6301\u8FD9\u4E9B\u53C2\u6570\uFF0C\u8BF7\u4F7F\u7528 send()");
5951
+ }
5952
+ return { sessionId: this.sessionId, requestId: createClientRequestId(), content, options: JSON.parse(JSON.stringify(options)) };
5953
+ }
5954
+ /** Durably enqueue an ordinary text input. Retries must reuse the saved request. */
5955
+ async sendPrepared(request) {
5956
+ if (request.sessionId !== this.sessionId) throw new Error("\u53D1\u9001\u8BF7\u6C42\u4E0D\u5C5E\u4E8E\u5F53\u524D\u4F1A\u8BDD");
5957
+ try {
5958
+ this.runtime.ensureConnected();
5959
+ const response = await this.runtime.emitWithAck("chat:append", {
5960
+ session_id: this.sessionId,
5961
+ client_request_id: request.requestId,
5962
+ message: request.content,
5963
+ prepared_options: {
5964
+ mode: request.options.mode,
5965
+ model: request.options.model,
5966
+ thinking_override: request.options.thinkingOverride,
5967
+ reasoning_level: request.options.reasoningLevel
5968
+ }
5969
+ }, QUEUE_ACK_TIMEOUT_MS);
5970
+ const result = parseQueueAck(response, this.sessionId);
5971
+ if (result.snapshot) this.update((state) => withQueue(state, result.snapshot));
5972
+ if (!result.ok) return {
5973
+ requestId: request.requestId,
5974
+ status: response?.status === "conflict" ? "rejected" : "unknown",
5975
+ message: result.message ?? void 0
5976
+ };
5977
+ const delivery = response?.delivery;
5978
+ if (!isRecord(delivery) || delivery.status !== "accepted") return { requestId: request.requestId, status: "unknown" };
5979
+ return {
5980
+ requestId: request.requestId,
5981
+ status: "accepted",
5982
+ itemId: typeof delivery.item_id === "string" ? delivery.item_id : void 0,
5983
+ queueStatus: typeof delivery.queue_status === "string" ? delivery.queue_status : void 0,
5984
+ executionId: typeof delivery.execution_id === "string" ? delivery.execution_id : null
5985
+ };
5986
+ } catch {
5987
+ return { requestId: request.requestId, status: "unknown" };
5988
+ }
5989
+ }
5990
+ /** Owner-authorized lookup; receipts remain until the session is deleted. */
5991
+ async getDelivery(requestId) {
5992
+ try {
5993
+ this.runtime.ensureConnected();
5994
+ const response = await this.runtime.emitWithAck("chat:queue:request", {
5995
+ session_id: this.sessionId,
5996
+ client_request_id: requestId
5997
+ }, QUEUE_ACK_TIMEOUT_MS);
5998
+ if (response?.status === "accepted") return {
5999
+ requestId,
6000
+ status: "accepted",
6001
+ itemId: typeof response.item_id === "string" ? response.item_id : void 0,
6002
+ queueStatus: typeof response.queue_status === "string" ? response.queue_status : void 0,
6003
+ executionId: typeof response.execution_id === "string" ? response.execution_id : null
6004
+ };
6005
+ if (response?.status === "not_found") return { requestId, status: "not_found" };
6006
+ return { requestId, status: "unknown", message: typeof response?.message === "string" ? response.message : void 0 };
6007
+ } catch {
6008
+ return { requestId, status: "unknown" };
6009
+ }
6010
+ }
5771
6011
  /** 发送一条消息。字符串或多模态内容均可。 */
5772
6012
  async send(content, options = {}) {
5773
6013
  this.runtime.ensureConnected();
@@ -7440,6 +7680,7 @@ var SessionHub = class {
7440
7680
  this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
7441
7681
  if (this.statusWatchCounts.has(session.sessionId)) this.emitStatusSubscribe([session.sessionId]);
7442
7682
  else this.statusLogCursors.delete(session.sessionId);
7683
+ if (this.client.options.anonymousSessionId && !this.needsConnection()) this.disconnect();
7443
7684
  return true;
7444
7685
  }
7445
7686
  statusCursor(sessionId) {
@@ -7761,12 +8002,14 @@ function createSocket(options) {
7761
8002
 
7762
8003
  // src/blade-client.ts
7763
8004
  var REFRESH_PATH = "/api/auth/refresh";
7764
- var BladeClient = class {
8005
+ var BladeClient = class _BladeClient {
8006
+ anonymousClients = /* @__PURE__ */ new Map();
8007
+ toolProviders = /* @__PURE__ */ new Set();
7765
8008
  refreshPromise = null;
7766
8009
  socketInstance = null;
7767
8010
  socketReplacementListeners = /* @__PURE__ */ new Set();
7768
8011
  /** login() / setToken() 热挂载的令牌,优先级高于构造参数。 */
7769
- runtimeToken = null;
8012
+ runtimeToken = void 0;
7770
8013
  options;
7771
8014
  auth;
7772
8015
  headless;
@@ -7784,6 +8027,9 @@ var BladeClient = class {
7784
8027
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
7785
8028
  hub;
7786
8029
  constructor(options) {
8030
+ if (options.anonymousSessionId !== void 0 && !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(options.anonymousSessionId)) {
8031
+ throw new Error("\u533F\u540D\u4F1A\u8BDD ID \u5FC5\u987B\u662F UUID v4");
8032
+ }
7787
8033
  this.options = {
7788
8034
  ...options,
7789
8035
  baseUrl: normalizeBaseUrl(options.baseUrl)
@@ -7797,10 +8043,16 @@ var BladeClient = class {
7797
8043
  this.chatGroups = new ChatGroupsResource(this);
7798
8044
  this.siteVisits = new SiteVisitsResource(this);
7799
8045
  this.hub = new SessionHub(this);
7800
- if (this.options.token === void 0) {
7801
- this.runtimeToken = readStoredToken(this.options.baseUrl);
8046
+ if (this.options.token === void 0 && !this.options.anonymousSessionId) {
8047
+ this.runtimeToken = readStoredToken(this.options.baseUrl) ?? void 0;
7802
8048
  }
7803
8049
  }
8050
+ /** Backend application tools, callable over an outbound WebSocket. */
8051
+ connectToolProvider(options) {
8052
+ const provider = new ToolProvider(this, options, () => this.toolProviders.delete(provider));
8053
+ this.toolProviders.add(provider);
8054
+ return provider;
8055
+ }
7804
8056
  /**
7805
8057
  * 弹窗授权登录:用户在弹出的授权页点击"许可授权"后自动取得 PAT,
7806
8058
  * REST 与 Socket.IO 立即生效。只在浏览器可用。
@@ -7820,7 +8072,12 @@ var BladeClient = class {
7820
8072
  const previousToken = this.resolveToken();
7821
8073
  this.runtimeToken = token;
7822
8074
  const nextToken = this.resolveToken();
8075
+ if (token === null) for (const provider of this.toolProviders) provider.dispose();
7823
8076
  if (previousToken !== nextToken) {
8077
+ for (const provider of this.toolProviders) {
8078
+ if (nextToken) provider.reconnect();
8079
+ else provider.dispose();
8080
+ }
7824
8081
  this.hub.disposeAll();
7825
8082
  }
7826
8083
  writeStoredToken(this.options.baseUrl, token, this.options.tokenStorage ?? "local");
@@ -7853,12 +8110,27 @@ var BladeClient = class {
7853
8110
  get token() {
7854
8111
  return this.resolveRestToken();
7855
8112
  }
8113
+ /** 获取只访问指定匿名会话的客户端;创建仍由已认证的原客户端完成。 */
8114
+ forAnonymousSession(sessionId) {
8115
+ let client = this.anonymousClients.get(sessionId);
8116
+ if (!client) {
8117
+ client = new _BladeClient({ baseUrl: this.options.baseUrl, anonymousSessionId: sessionId, fetchImpl: this.options.fetchImpl, commandTransport: this.options.commandTransport });
8118
+ this.anonymousClients.set(sessionId, client);
8119
+ }
8120
+ return client;
8121
+ }
7856
8122
  setBaseUrl(baseUrl) {
7857
8123
  const nextBaseUrl = normalizeBaseUrl(baseUrl);
7858
8124
  if (nextBaseUrl === this.options.baseUrl) {
7859
8125
  return;
7860
8126
  }
8127
+ for (const provider of this.toolProviders) provider.dispose();
7861
8128
  this.hub.disposeAll();
8129
+ for (const client of this.anonymousClients.values()) {
8130
+ client.hub.disposeAll();
8131
+ client.hub.disconnect();
8132
+ }
8133
+ this.anonymousClients.clear();
7862
8134
  this.options.baseUrl = nextBaseUrl;
7863
8135
  if (this.socketInstance) {
7864
8136
  this.socketInstance.removeAllListeners();
@@ -7898,7 +8170,10 @@ var BladeClient = class {
7898
8170
  else resolve(ack ?? {});
7899
8171
  });
7900
8172
  });
7901
- return this.options.commandTransport?.(event, payload, send) ?? send();
8173
+ const transport = this.options.commandTransport;
8174
+ if (!transport) return send();
8175
+ const handled = this.options.anonymousSessionId ? transport(event, payload, send, { anonymousSessionId: this.options.anonymousSessionId }) : transport(event, payload, send);
8176
+ return handled ?? send();
7902
8177
  }
7903
8178
  async json(method, path, body) {
7904
8179
  const response = await this.fetch(method, path, {
@@ -8041,6 +8316,7 @@ var BladeClient = class {
8041
8316
  }
8042
8317
  }
8043
8318
  async tryRefresh() {
8319
+ if (this.options.anonymousSessionId) return false;
8044
8320
  if (this.refreshPromise) {
8045
8321
  return this.refreshPromise;
8046
8322
  }
@@ -8075,14 +8351,16 @@ var BladeClient = class {
8075
8351
  return this.resolveRestToken();
8076
8352
  }
8077
8353
  resolveRestToken() {
8354
+ if (this.options.anonymousSessionId) return `ba-anonymous-${this.options.anonymousSessionId}`;
8078
8355
  return this.resolveToken();
8079
8356
  }
8080
8357
  resolveSocketToken() {
8081
- if (this.runtimeToken) return this.runtimeToken;
8358
+ if (this.options.anonymousSessionId) return `ba-anonymous-${this.options.anonymousSessionId}`;
8359
+ if (this.runtimeToken !== void 0) return this.runtimeToken;
8082
8360
  return resolveTokenOption(this.options.socketToken ?? this.options.token);
8083
8361
  }
8084
8362
  resolveToken() {
8085
- if (this.runtimeToken) return this.runtimeToken;
8363
+ if (this.runtimeToken !== void 0) return this.runtimeToken;
8086
8364
  return resolveTokenOption(this.options.token);
8087
8365
  }
8088
8366
  formDataWithUploadProgress(method, path, form, options, isRetry = false) {
@@ -8180,9 +8458,10 @@ function parseXhrHeaders(rawHeaders) {
8180
8458
  }
8181
8459
 
8182
8460
  export {
8461
+ BladeApiError,
8462
+ ToolProvider,
8183
8463
  SDK_NAME,
8184
8464
  SDK_VERSION,
8185
- BladeApiError,
8186
8465
  isRecord,
8187
8466
  normalizePluginRefs,
8188
8467
  normalizeTopicAssetRefs,
@@ -8264,4 +8543,4 @@ export {
8264
8543
  createSocket,
8265
8544
  BladeClient
8266
8545
  };
8267
- //# sourceMappingURL=chunk-KPS6X6QX.js.map
8546
+ //# sourceMappingURL=chunk-KT5LY27X.js.map