@blade-hq/agent-client 1.1.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/README.md +351 -0
- package/dist/auth-login.d.ts +29 -0
- package/dist/auth.d.ts +8 -0
- package/dist/blade-client.d.ts +113 -0
- package/dist/commands/embedded.d.ts +32 -0
- package/dist/commands/protocol.d.ts +38 -0
- package/dist/commands/registry.d.ts +21 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +4182 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/auth.d.ts +34 -0
- package/dist/resources/headless.d.ts +33 -0
- package/dist/resources/sessions.d.ts +263 -0
- package/dist/rest.d.ts +21 -0
- package/dist/schemas/background.d.ts +16 -0
- package/dist/schemas/event.d.ts +51 -0
- package/dist/schemas/message-utils.d.ts +48 -0
- package/dist/schemas/message.d.ts +83 -0
- package/dist/schemas/projection.d.ts +87 -0
- package/dist/schemas/session.d.ts +126 -0
- package/dist/schemas/solution.d.ts +101 -0
- package/dist/schemas/task.d.ts +12 -0
- package/dist/session/agent-session.d.ts +179 -0
- package/dist/session/events.d.ts +143 -0
- package/dist/session/hub.d.ts +44 -0
- package/dist/session/state.d.ts +57 -0
- package/dist/shared/projection/builder.d.ts +59 -0
- package/dist/shared/projection/helpers.d.ts +34 -0
- package/dist/shared/projection/history.d.ts +12 -0
- package/dist/shared/projection/index.d.ts +3 -0
- package/dist/shared/projection/state.d.ts +22 -0
- package/dist/socket.d.ts +9 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/rest.d.ts +17315 -0
- package/dist/types/sdk-profile.d.ts +47 -0
- package/dist/types/socket-events.d.ts +401 -0
- package/dist/version.d.ts +45 -0
- package/package.json +29 -0
- package/public-api.md +1103 -0
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# @blade-hq/agent-client
|
|
2
|
+
|
|
3
|
+
Blade Agent 的框架无关客户端。浏览器和 Node.js 都能用;用 Vue、Svelte 或自建 UI 的团队直接用这个包,React 团队一般用上层的 `@blade-hq/agent-react`。
|
|
4
|
+
|
|
5
|
+
它做三件事:
|
|
6
|
+
|
|
7
|
+
1. **实时会话**(`AgentSession`):把 Socket.IO 协议、历史加载、流式合流、断线重连全部封装掉,你只面对"状态快照 + 动作 + 事件"。
|
|
8
|
+
2. **登录**:`client.auth.login()` 弹窗授权,用户点一下"许可授权"就拿到访问令牌,不用手工复制粘贴。
|
|
9
|
+
3. **REST**:只类型化会话相关的少数接口(`client.sessions.*`)。长尾接口 SDK 不封装,对照 Swagger 用原生 `fetch` + `client.token` 自行调用。
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @blade-hq/agent-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<button id="login">登录 Blade</button>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { BladeClient, getTextContent } from "@blade-hq/agent-client"
|
|
23
|
+
|
|
24
|
+
const client = new BladeClient({ baseUrl: "https://blade.example.com" })
|
|
25
|
+
|
|
26
|
+
// 登录必须由用户点击触发:浏览器会拦截非用户操作弹出的窗口
|
|
27
|
+
document.querySelector("#login")!.addEventListener("click", async () => {
|
|
28
|
+
await client.auth.login()
|
|
29
|
+
|
|
30
|
+
const chat = await client.sessions.create()
|
|
31
|
+
chat.subscribe(() => {
|
|
32
|
+
const { messages, isStreaming } = chat.getState()
|
|
33
|
+
// 换成你自己的渲染。content 可能是字符串也可能是内容块数组(多模态消息),
|
|
34
|
+
// 用 getTextContent 两种都能处理,不要自己写 typeof 判断
|
|
35
|
+
console.log(messages.map((m) => getTextContent(m.content)), isStreaming)
|
|
36
|
+
})
|
|
37
|
+
await chat.send("帮我分析一下这份数据")
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Node.js / 自动化脚本用 token(PAT 在 Blade 设置页创建):
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const client = new BladeClient({
|
|
45
|
+
baseUrl: "https://blade.example.com",
|
|
46
|
+
token: process.env.BLADE_TOKEN,
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## BladeClient
|
|
51
|
+
|
|
52
|
+
### 构造
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
new BladeClient({
|
|
56
|
+
baseUrl: "https://blade.example.com", // 后端地址;同域部署可传 ""
|
|
57
|
+
token: "sk-blade-xxx", // 可选:PAT。不传则用 cookie 或 login()
|
|
58
|
+
tokenStorage: "local", // 可选:login() 的令牌存哪("local" 默认 / "memory")
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> **`baseUrl` 填哪个地址?** 必须是 Blade Agent 后端的地址(形如 `http://<主机>:8020`),只要域名和端口、不带路径。
|
|
63
|
+
> 注意别填成你平时打开的 Blade OS 地址(同主机的 `:80`)—— 那是另一套接口,SDK 连不上。
|
|
64
|
+
|
|
65
|
+
### 登录相关
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await client.auth.login() // 弹窗授权登录,成功后 REST 与实时连接立即生效
|
|
69
|
+
client.auth.logoutToken() // 清除 login() 存的令牌
|
|
70
|
+
client.setToken("sk-blade-x") // 手动热挂载令牌(null 表示清除)
|
|
71
|
+
client.hasToken() // 是否持有令牌
|
|
72
|
+
client.token // 当前令牌(长尾接口自行调用时填进 Authorization 头)
|
|
73
|
+
await client.auth.getMe() // 当前用户信息(未登录时抛 401)
|
|
74
|
+
await client.auth.getProviders() // 服务端支持的登录方式
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`login()` 失败时的错误都带中文原因:弹窗被拦截、窗口被关闭、超时。
|
|
78
|
+
|
|
79
|
+
## 会话:client.sessions
|
|
80
|
+
|
|
81
|
+
### 实时对话(推荐入口)
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// 连接已有会话
|
|
85
|
+
const chat = await client.sessions.connect("会话id")
|
|
86
|
+
|
|
87
|
+
// 或:创建新会话并直接连接
|
|
88
|
+
const newChat = await client.sessions.create()
|
|
89
|
+
const configured = await client.sessions.create({ intent: "数据分析", model: "gpt-4o" })
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
两者都返回 [`AgentSession`](#agentsession)。
|
|
93
|
+
|
|
94
|
+
> **`sessionId` 从哪来?** 三种,按需选:
|
|
95
|
+
> 1. **不用管它** —— 首次接入最省事:`client.sessions.create()` 直接建一个新会话,用不着 ID。React 的 `<ChatView />` 不传 `sessionId` 也一样。
|
|
96
|
+
> 2. **自己建、自己存**:`const { session_id } = await client.sessions.createSessionWithRequest({ intent: "季度报表分析" })`。把返回的 `session_id`(后端是蛇形命名)存进你的数据库或 URL,用户下次进来传给 `connect(sessionId)`(前端参数是驼峰),就能接着上次的对话。
|
|
97
|
+
> 3. **列出用户已有的**:`await client.sessions.listSessions()`,让用户自己挑。
|
|
98
|
+
|
|
99
|
+
### 会话管理(REST)
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await client.sessions.listSessions() // 会话列表
|
|
103
|
+
await client.sessions.getSession(id) // 会话详情
|
|
104
|
+
await client.sessions.updateSession(id, { intent: "新标题" })
|
|
105
|
+
await client.sessions.deleteSession(id)
|
|
106
|
+
await client.sessions.getSessionTurns(id) // 历史消息(投影格式,与实时流同构)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 工作区文件
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await client.sessions.listDir(id, "") // 列目录
|
|
113
|
+
await client.sessions.uploadFiles(id, "", fileInput.files) // 上传(支持进度回调)
|
|
114
|
+
await client.sessions.writeFile(id, "notes.md", "# 内容")
|
|
115
|
+
await client.sessions.deleteFile(id, "notes.md")
|
|
116
|
+
client.buildAuthedUrl(`/api/sessions/${id}/files/report.pdf`) // 带鉴权的下载/预览地址
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## AgentSession
|
|
120
|
+
|
|
121
|
+
一个会话的实时状态机。**状态归属实例**:同一页面建多个会话互不干扰。
|
|
122
|
+
|
|
123
|
+
### 状态
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
chat.getState()
|
|
127
|
+
// {
|
|
128
|
+
// messages, 聊天消息列表(直接渲染)
|
|
129
|
+
// isStreaming, 是否正在回复
|
|
130
|
+
// status, 会话状态:created / running / completed / failed /
|
|
131
|
+
// interrupted / waiting_for_input(未加载时为 null)
|
|
132
|
+
// mode, "planning" | "executing" | null
|
|
133
|
+
// connection, 连接状态:"connected" | "connecting" | "reconnecting" | "disconnected"
|
|
134
|
+
// errorMessage, 最近一次运行错误
|
|
135
|
+
// turns, askAnswers, agentLoops, activeCompaction 进阶字段
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
const unsubscribe = chat.subscribe(() => rerender(chat.getState()))
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
快照不可变:状态一变就换新对象引用,可直接接 React `useSyncExternalStore` 或 Vue `shallowRef`(见 public-skills 的 Vue 接入文档)。
|
|
142
|
+
|
|
143
|
+
### 动作
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
await chat.send("你好") // 发消息(字符串或多模态内容)
|
|
147
|
+
await chat.send("换个方案", { mode: "planning" }) // 指定模式/模型等选项
|
|
148
|
+
chat.append("补充:预算不超过 5 万") // 智能体运行中追加说明
|
|
149
|
+
await chat.stop() // 停止当前回复
|
|
150
|
+
await chat.compact() // 手动压缩上下文
|
|
151
|
+
chat.dispose() // 彻底释放(什么时候该调见下方说明)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
> **什么时候调 `dispose()`?** 组件卸载时**不要**调 —— 会话实例由 client 缓存着,用户切走再切回来能直接复用,历史消息不用重新加载。只有这两种情况才调:
|
|
155
|
+
> 1. 用户明确关闭或删除了这个会话,之后不会再回来;
|
|
156
|
+
> 2. 页面长期开着并且会不断创建一次性会话(比如批量任务面板),需要主动回收。
|
|
157
|
+
|
|
158
|
+
### 页面协作:让智能体和你的页面互动
|
|
159
|
+
|
|
160
|
+
**智能体 → 页面**(例如智能体让地图高亮某个点):
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
chat.onCommand("map.highlight", (payload) => {
|
|
164
|
+
// payload 类型是 unknown(内容由技能作者决定),用前自行断言或校验
|
|
165
|
+
const { points } = payload as { points: Array<{ lng: number; lat: number }> }
|
|
166
|
+
map.highlight(points)
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`"map.highlight"` 这个 action 字符串从哪来?——它是**技能作者**在工具实现的返回值里写的。技能侧(Python 工具)长这样:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
def highlight_points(points):
|
|
174
|
+
return {
|
|
175
|
+
"ok": True,
|
|
176
|
+
"message": f"已定位 {len(points)} 个点位",
|
|
177
|
+
"_meta": {"bridge": {"action": "map.highlight", "payload": {"points": points}}},
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
后端会把 `_meta.bridge` 从对话历史里剥离(LLM 看不到它),单独送到前端。所以 action 是**技能作者和页面开发者之间约定的暗号**,SDK 不限制取值。在 `onCommand` 注册之前到达的指令会被缓冲,注册时立刻补投,不存在"注册晚了漏消息"。
|
|
182
|
+
|
|
183
|
+
**页面 → 智能体**(例如用户在地图上选了个点):
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
chat.attach("选中点位", { lng: 116.4, lat: 39.9 }) // 变成聊天输入框里的附件
|
|
187
|
+
chat.insertText("请分析这个区域") // 往输入框追加文字(不发送)
|
|
188
|
+
await chat.send("这里适合开店吗?") // 直接发消息
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`attach` / `insertText` 需要有渲染层(ChatView 或你自己监听 `attachRequested` / `insertTextRequested` 事件)才有可见效果;纯脚本场景直接用 `send`。
|
|
192
|
+
|
|
193
|
+
### 语义事件(进阶)
|
|
194
|
+
|
|
195
|
+
大多数场景用不到——React 用 hook、指令用 `onCommand`。需要精确监听时:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
chat.on("toolCall", (e) => console.log("调用工具", e.toolCall.name))
|
|
199
|
+
chat.on("toolResult", (e) => console.log("工具结果", e.toolCall.result))
|
|
200
|
+
chat.on("message", (e) => console.log("新消息", e.message))
|
|
201
|
+
chat.on("chatEnd", (e) => console.log("回复结束", e.status))
|
|
202
|
+
chat.on("error", (e) => console.error(e.message))
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
完整事件表见 `AgentSessionEvents` 类型定义(含 `modeChange` / `workspaceChanged` / `artifact` / `notification` / `backgroundTask` / `taskListUpdated` / `rewind` / `replayMismatch` 等)。`on()` 返回取消函数;handler 抛异常只告警,不影响会话。
|
|
206
|
+
|
|
207
|
+
## iframe 嵌入形态:connectEmbedded
|
|
208
|
+
|
|
209
|
+
如果你不是嵌组件,而是把 **Blade 的聊天页面整个用 iframe 嵌进自己系统**,宿主页面用 `connectEmbedded` 拿到同构的协作 API:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { connectEmbedded } from "@blade-hq/agent-client"
|
|
213
|
+
|
|
214
|
+
const chat = connectEmbedded({
|
|
215
|
+
iframe: document.querySelector<HTMLIFrameElement>("#blade")!,
|
|
216
|
+
allowedOrigins: ["https://blade.example.com"], // 必填:Blade 页面的来源,防伪造
|
|
217
|
+
})
|
|
218
|
+
chat.onCommand("map.highlight", (payload) => {
|
|
219
|
+
const { points } = payload as { points: Array<{ lng: number; lat: number }> }
|
|
220
|
+
map.highlight(points)
|
|
221
|
+
})
|
|
222
|
+
chat.attach("选中点位", { lng: 116.4, lat: 39.9 })
|
|
223
|
+
chat.send("这里适合开店吗?")
|
|
224
|
+
chat.dispose() // 页面卸载时
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
同页组件形态和 iframe 形态的协作 API 完全一致,业务代码可以原样复用。
|
|
228
|
+
|
|
229
|
+
## 长尾 REST:用原生 fetch
|
|
230
|
+
|
|
231
|
+
SDK 只类型化会话、登录、headless 三类接口。其余几百个后端接口一律不封装——
|
|
232
|
+
对照 Swagger(`<后端地址>/docs`)用原生 `fetch` 调用,把 `client.token` 填进
|
|
233
|
+
`Authorization` 头即可:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const resp = await fetch(`${baseUrl}/api/memories/search?q=xxx`, {
|
|
237
|
+
headers: { Authorization: `Bearer ${client.token}` },
|
|
238
|
+
})
|
|
239
|
+
const memories = await resp.json()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
不做通用包装是有意的:包装唯一的增值是自动带令牌,代价却是要补齐
|
|
243
|
+
PATCH / query / FormData / AbortSignal 等完整 HTTP 语义(否则不够用),
|
|
244
|
+
而且 SDK 隐式携带凭据存在误把令牌发往第三方域的风险。显式暴露 `client.token`
|
|
245
|
+
让责任边界回到标准 `fetch`,两个问题都不存在。
|
|
246
|
+
|
|
247
|
+
> **安全约束**:`client.token` 只应发往 `baseUrl` 同源的接口。不要把它附加到
|
|
248
|
+
> 第三方域名的请求上——那等于把用户的访问凭据交给别人。
|
|
249
|
+
|
|
250
|
+
## headless:一次性问答
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// 无头模式:直接拿结构化结果,不需要自己处理流式事件
|
|
254
|
+
await client.headless.run("统计上月订单量", { schema })
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 版本握手
|
|
258
|
+
|
|
259
|
+
本 SDK 与 Server 是配套的:`@blade-hq/agent-client` 2.x 需要 Server 1.1.1 及以上。
|
|
260
|
+
|
|
261
|
+
SDK 在首次建立会话前会查询 `GET /api/version`。版本不匹配时抛出
|
|
262
|
+
`VersionMismatchError`,报错文案里写清楚双方版本号和该升级哪一端,
|
|
263
|
+
而不是让你对着一个莫名其妙的运行时异常发呆:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { VersionMismatchError } from "@blade-hq/agent-client"
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const chat = await client.sessions.create()
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof VersionMismatchError) {
|
|
272
|
+
console.error(error.message) // 含双方版本号与建议动作
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
旧接入方的选择:继续用 `@blade-hq/agent-kit` 1.1.0 对接 1.1.0 及以下的 Server,
|
|
278
|
+
或升级到本包对接 1.1.1 及以上的 Server。**不支持交叉组合**,两个方向都会被明确拦截。
|
|
279
|
+
|
|
280
|
+
开发环境不受影响:预发布版本号(如 `1.1.1-dev.3`)一律放行。
|
|
281
|
+
|
|
282
|
+
## 自己渲染消息
|
|
283
|
+
|
|
284
|
+
`message.content` 有两种形态:纯文字时是字符串;带图片、文件的消息则是内容块数组。别自己写 `typeof` 判断,用这几个函数,两种形态都能处理:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import {
|
|
288
|
+
getTextContent,
|
|
289
|
+
getImageParts,
|
|
290
|
+
getFileParts,
|
|
291
|
+
groupMessagesByLoop,
|
|
292
|
+
contentPreview,
|
|
293
|
+
} from "@blade-hq/agent-client"
|
|
294
|
+
|
|
295
|
+
// 文字部分
|
|
296
|
+
getTextContent(message.content) // "帮我看看这张图"
|
|
297
|
+
|
|
298
|
+
// 图片部分:返回 [{ type: "image_url", image_url: { url } }, ...]
|
|
299
|
+
getImageParts(message.content).map((part) => part.image_url.url)
|
|
300
|
+
|
|
301
|
+
// 文件部分
|
|
302
|
+
getFileParts(message.content)
|
|
303
|
+
|
|
304
|
+
// 其他
|
|
305
|
+
groupMessagesByLoop(messages) // 按主/子智能体分组(智能体会派生子智能体干活)
|
|
306
|
+
contentPreview(message.content, 80) // 截断预览
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
一个完整的渲染示例:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
function Message({ message }: { message: ChatMessage }) {
|
|
313
|
+
const text = getTextContent(message.content)
|
|
314
|
+
const images = getImageParts(message.content)
|
|
315
|
+
return (
|
|
316
|
+
<div>
|
|
317
|
+
{text && <p>{text}</p>}
|
|
318
|
+
{images.map((part) => (
|
|
319
|
+
<img key={part.image_url.url} src={part.image_url.url} alt="" />
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## 常见问题
|
|
327
|
+
|
|
328
|
+
| 现象 | 原因与解法 |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| 401,提示未登录 | 浏览器调 `client.auth.login()`;脚本检查 token 是否有效 |
|
|
331
|
+
| login() 报"弹窗被拦截" | 浏览器设置里允许该站点弹窗,或改用 PAT |
|
|
332
|
+
| login() 报 403 来源未允许 | 让管理员把你的页面 origin 加进后端 `BLADE_SDK_AUTH_ALLOWED_ORIGINS` |
|
|
333
|
+
| socket 一直 connecting | 检查 baseUrl 是否可达、跨域时后端是否允许你的 origin |
|
|
334
|
+
| VersionMismatchError | SDK 与 Server 版本不配套,按报错提示升级其中一端,见「版本握手」 |
|
|
335
|
+
| onCommand 收不到指令 | 确认技能工具返回值里写了 `_meta.bridge`,action 拼写与注册一致 |
|
|
336
|
+
|
|
337
|
+
更多接入教程(Vue 完整示例、GIS 协作闭环、AI 助手接入指南)见 [public-skills 文档站](https://github.com/blade-hq/public-skills)。
|
|
338
|
+
|
|
339
|
+
## 附录:公开类型索引
|
|
340
|
+
|
|
341
|
+
完整签名见 [public-api.md](./public-api.md)(由 `scripts/public-api-report.mjs` 生成并在 CI 校验)。
|
|
342
|
+
|
|
343
|
+
- **客户端与登录**:`BladeClientOptions`、`LoginOptions`、`LoginResult`、`TokenStorageMode`、`UploadProgress`、`BladeApiError`、`AuthResource`、`ProvidersResponse`、`UserInfo`
|
|
344
|
+
- **版本握手**:`ServerVersionInfo`、`SDK_NAME`、`SDK_VERSION`、`MIN_SERVER_VERSION`
|
|
345
|
+
- **会话资源(REST)**:`SessionsResource`、`CreateSessionRequest`、`PaginatedSessionsResult`、`SessionHistory`、`SessionContextStats`、`ShareLinkResult`、`FileEntry`、`UploadFileEntry`、`UploadFilesOptions`、`SessionProfile`、`SessionDetail`、`SessionInfo`、`SessionStatus`、`SessionPortMapping`、`ModeId`、`TemplateId`、`PrimarySkillSnapshot`、`PrimarySkillParallelMode`
|
|
346
|
+
- **会话状态机**:`SessionHub`、`SessionState`、`SendOptions`、`ConnectionStatus`、`AskUserAnswerData`、`AgentLoopInfo`、`ActiveCompactionState`、`createInitialSessionState`、`AgentSessionEventName`
|
|
347
|
+
- **页面协作**:`EmbeddedChat`、`EmbeddedChatOptions`、`CommandHandler`、`CommandEnvelope`、`InboundAction`、`InboundEnvelope`、`isCommandEnvelope`、`isInboundEnvelope`
|
|
348
|
+
- **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
|
|
349
|
+
- **Solution / 任务协议**:`Solution`、`SolutionAppField`、`SolutionAppState`、`SolutionAppUiConfig`、`SolutionRef`、`ExistingSolutionRef`、`PreparedSolution`、`PreparedSolutionAsset`、`LayoutType`、`BizRole`、`TaskStatus`、`BackgroundTask`
|
|
350
|
+
- **Headless**:`HeadlessResource`、`RunOptions`、`RunResult`、`RunTrace`
|
|
351
|
+
- **低层通道(apps/web 等高级集成)**:`createSocket`、`CreateSocketOptions`、`TypedSocket`、`AsrAudioPayload`、`ClientProjectionBuilder`、`RawEvent`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BladeClient } from "./blade-client";
|
|
2
|
+
/**
|
|
3
|
+
* 弹窗授权登录,与 Chrome 扩展共用同一套后端授权内核(见 server/routes/sdk_auth.py):
|
|
4
|
+
*
|
|
5
|
+
* 1. 弹窗打开 `GET /api/auth/sdk/authorize?client_origin=<本页 origin>&state=<随机值>`
|
|
6
|
+
* 2. 用户点击「许可授权」→ 回调页把 `{ type: "blade-agent:sdk-auth", state, code }`
|
|
7
|
+
* postMessage 定向发回本页(code 一次性、5 分钟有效)
|
|
8
|
+
* 3. SDK 用 `POST /api/auth/sdk/token`(code + state + client_origin)换取 PAT,
|
|
9
|
+
* 存储并热挂载(REST 与 Socket.IO 同时生效)
|
|
10
|
+
*/
|
|
11
|
+
export interface LoginOptions {
|
|
12
|
+
/** 弹窗尺寸,默认 480x640。 */
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
/** 超时毫秒数,默认 5 分钟(与后端授权码有效期一致)。 */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface LoginResult {
|
|
19
|
+
token: string;
|
|
20
|
+
user?: {
|
|
21
|
+
id?: string;
|
|
22
|
+
username?: string;
|
|
23
|
+
display_name?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export type TokenStorageMode = "local" | "memory";
|
|
27
|
+
export declare function readStoredToken(baseUrl: string): string | null;
|
|
28
|
+
export declare function writeStoredToken(baseUrl: string, token: string | null, mode: TokenStorageMode): void;
|
|
29
|
+
export declare function loginWithPopup(client: BladeClient, options?: LoginOptions): Promise<LoginResult>;
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface AuthOptions {
|
|
2
|
+
token?: string | (() => string | null | undefined);
|
|
3
|
+
}
|
|
4
|
+
export declare function buildAuthHeaders(options: AuthOptions): Record<string, string>;
|
|
5
|
+
export declare function buildSocketAuth(options: AuthOptions): {
|
|
6
|
+
token: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
export declare function resolveAuthToken(options: AuthOptions): string | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type LoginOptions, type LoginResult, type TokenStorageMode } from "./auth-login";
|
|
2
|
+
import { type BladeFetchInit, type HttpMethod } from "./rest";
|
|
3
|
+
import { AuthResource } from "./resources/auth";
|
|
4
|
+
import { HeadlessResource } from "./resources/headless";
|
|
5
|
+
import { SessionsResource } from "./resources/sessions";
|
|
6
|
+
import { SessionHub } from "./session/hub";
|
|
7
|
+
import { type TypedSocket } from "./socket";
|
|
8
|
+
export interface BladeClientOptions {
|
|
9
|
+
/** 后端地址,如 "https://blade.example.com"。浏览器里传空字符串则使用当前域名。 */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/**
|
|
12
|
+
* 访问令牌(PAT)。不传时按 cookie 会话模式工作;
|
|
13
|
+
* 浏览器场景也可以不传,改用 client.auth.login() 弹窗登录。
|
|
14
|
+
*/
|
|
15
|
+
token?: string | (() => string | null | undefined);
|
|
16
|
+
/**
|
|
17
|
+
* Socket.IO 专用令牌。第一方应用可在 REST 继续使用 cookie 刷新的同时,
|
|
18
|
+
* 把 cookie 换得的短期令牌只用于实时连接;不传时复用 token。
|
|
19
|
+
*/
|
|
20
|
+
socketToken?: string | (() => string | null | undefined);
|
|
21
|
+
/** login() 取得的令牌存放位置,默认 "local"(localStorage,刷新页面不掉登录)。 */
|
|
22
|
+
tokenStorage?: TokenStorageMode;
|
|
23
|
+
fetchImpl?: typeof fetch;
|
|
24
|
+
onRefreshSuccess?: () => void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare class BladeClient {
|
|
27
|
+
private refreshPromise;
|
|
28
|
+
private socketInstance;
|
|
29
|
+
private readonly socketReplacementListeners;
|
|
30
|
+
/** login() / setToken() 热挂载的令牌,优先级高于构造参数。 */
|
|
31
|
+
private runtimeToken;
|
|
32
|
+
readonly options: BladeClientOptions;
|
|
33
|
+
readonly auth: AuthResource;
|
|
34
|
+
readonly headless: HeadlessResource;
|
|
35
|
+
readonly sessions: SessionsResource;
|
|
36
|
+
/** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
|
|
37
|
+
readonly hub: SessionHub;
|
|
38
|
+
/** 版本握手结果缓存:每个 client 实例只探测一次,不逐请求重复。 */
|
|
39
|
+
private versionCheck;
|
|
40
|
+
constructor(options: BladeClientOptions);
|
|
41
|
+
/**
|
|
42
|
+
* 弹窗授权登录:用户在弹出的授权页点击"许可授权"后自动取得 PAT,
|
|
43
|
+
* REST 与 Socket.IO 立即生效。只在浏览器可用。
|
|
44
|
+
*/
|
|
45
|
+
login(options?: LoginOptions): Promise<LoginResult>;
|
|
46
|
+
/** 退出登录:清除 login() 存储的令牌并断开实时连接。 */
|
|
47
|
+
logoutToken(): void;
|
|
48
|
+
/** 热挂载访问令牌(null 表示清除)。会重建 Socket.IO 连接以携带新凭证。 */
|
|
49
|
+
setToken(token: string | null): void;
|
|
50
|
+
/** 当前是否持有访问令牌(不代表令牌一定有效)。 */
|
|
51
|
+
hasToken(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 当前访问令牌(PAT),未登录时为 null。
|
|
54
|
+
*
|
|
55
|
+
* SDK 只封装会话相关接口;其余长尾接口对照后端 Swagger(`<baseUrl>/docs`)
|
|
56
|
+
* 用原生 fetch 自行调用,把本令牌填入 Authorization 头即可:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* fetch(`${baseUrl}/api/memories/search?q=xxx`, {
|
|
60
|
+
* headers: { Authorization: `Bearer ${client.token}` },
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* 安全约束:该令牌只应发往 baseUrl 同源接口,不要附加到第三方域名的请求上。
|
|
65
|
+
*/
|
|
66
|
+
get token(): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* 版本握手:首次连接前查询 Server 版本,不匹配则抛 VersionMismatchError。
|
|
69
|
+
* 结果缓存在实例上,失败的探测不缓存以便重试。
|
|
70
|
+
*/
|
|
71
|
+
ensureVersionCompatible(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* 读取 /api/version。接口存在但 404/响应不合法 → null(按旧 Server 处理);
|
|
74
|
+
* 网络层不可达(后端没启动、端口不通、代理配错)→ 抛连接错误。
|
|
75
|
+
* 两者必须区分:把「后端没起」误报成「版本不匹配,请改用 agent-kit」
|
|
76
|
+
* 会把开发者引向完全错误的排查方向。
|
|
77
|
+
*/
|
|
78
|
+
private fetchServerVersion;
|
|
79
|
+
setBaseUrl(baseUrl: string): void;
|
|
80
|
+
/** 底层 Socket.IO 实例因后端地址变化而被替换时通知高级集成层。 */
|
|
81
|
+
onSocketReplaced(listener: () => void): () => void;
|
|
82
|
+
socket(): TypedSocket;
|
|
83
|
+
json<T>(method: HttpMethod, path: string, body?: unknown): Promise<T>;
|
|
84
|
+
jsonFromInit<T>(path: string, init?: RequestInit): Promise<T>;
|
|
85
|
+
text(method: HttpMethod, path: string): Promise<string>;
|
|
86
|
+
textFromInit(path: string, init?: RequestInit): Promise<string>;
|
|
87
|
+
responseFromInit(path: string, init?: RequestInit): Promise<Response>;
|
|
88
|
+
blob(method: HttpMethod, path: string): Promise<Blob>;
|
|
89
|
+
formData(method: HttpMethod, path: string, form: FormData, options?: {
|
|
90
|
+
expectOk?: boolean;
|
|
91
|
+
onUploadProgress?: (progress: UploadProgress) => void;
|
|
92
|
+
}): Promise<Response>;
|
|
93
|
+
fetch(method: HttpMethod, path: string, init?: BladeFetchInit, isRetry?: boolean): Promise<Response>;
|
|
94
|
+
buildAuthedUrl(path: string): string;
|
|
95
|
+
private buildHeaders;
|
|
96
|
+
private buildUrl;
|
|
97
|
+
private baseUrlWithTrailingSlash;
|
|
98
|
+
private toBaseRelativePath;
|
|
99
|
+
private isSameBackendUrl;
|
|
100
|
+
private shouldRefreshFor401;
|
|
101
|
+
private hasExplicitBearerToken;
|
|
102
|
+
private tryRefresh;
|
|
103
|
+
private resolveTokenForUrl;
|
|
104
|
+
private resolveRestToken;
|
|
105
|
+
private resolveSocketToken;
|
|
106
|
+
private resolveToken;
|
|
107
|
+
private formDataWithUploadProgress;
|
|
108
|
+
}
|
|
109
|
+
export interface UploadProgress {
|
|
110
|
+
loaded: number;
|
|
111
|
+
total?: number;
|
|
112
|
+
percent?: number;
|
|
113
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CommandHandler } from "./registry";
|
|
2
|
+
/**
|
|
3
|
+
* iframe 嵌入形态的宿主端协作对象:当你把 Blade 的聊天页面用 iframe 嵌进
|
|
4
|
+
* 自己的系统时,用它与 iframe 里的智能体互通——API 与同页嵌入的
|
|
5
|
+
* AgentSession 完全一致(onCommand / attach / insertText / send)。
|
|
6
|
+
*
|
|
7
|
+
* action 的来源与同页形态相同:技能作者在工具返回值 JSON 的 `_meta.bridge`
|
|
8
|
+
* 字段里写 `{ action, payload }`,Blade 页面检测到自己处于 iframe 中时会把
|
|
9
|
+
* 它 postMessage 给宿主页面(见 docs/specs/260417-embed-bridge)。
|
|
10
|
+
*/
|
|
11
|
+
export interface EmbeddedChatOptions {
|
|
12
|
+
/** 嵌入 Blade 页面的 iframe 元素。不传则监听来自任意子窗口的消息。 */
|
|
13
|
+
iframe?: HTMLIFrameElement;
|
|
14
|
+
/**
|
|
15
|
+
* 允许接收消息的来源(Blade 页面的 origin,如 "https://blade.example.com")。
|
|
16
|
+
* 必填:不校验来源会让任意第三方页面伪造智能体指令。
|
|
17
|
+
*/
|
|
18
|
+
allowedOrigins: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface EmbeddedChat {
|
|
21
|
+
/** 监听智能体下发的指令(注册前到达的会缓冲补投)。返回取消函数。 */
|
|
22
|
+
onCommand(action: string, handler: CommandHandler): () => void;
|
|
23
|
+
/** 把业务数据作为附件放进 iframe 里的聊天输入框。 */
|
|
24
|
+
attach(label: string, data: unknown): void;
|
|
25
|
+
/** 往 iframe 里的聊天输入框追加文字。 */
|
|
26
|
+
insertText(text: string): void;
|
|
27
|
+
/** 直接代用户发送一条消息。 */
|
|
28
|
+
send(text: string): void;
|
|
29
|
+
/** 停止监听并释放资源。 */
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function connectEmbedded(options: EmbeddedChatOptions): EmbeddedChat;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页面协作协议(对应后端 `_meta.bridge` 通道,见 docs/specs/260417-embed-bridge)。
|
|
3
|
+
*
|
|
4
|
+
* ## action 从哪里来
|
|
5
|
+
*
|
|
6
|
+
* 智能体 → 宿主方向的每一条指令,都源自**技能作者写的工具代码**:工具返回值
|
|
7
|
+
* JSON 里带 `_meta.bridge: { action, payload }`,后端在持久化前把它剥离成独立
|
|
8
|
+
* 事件送达前端(LLM 的对话历史里读不到它)。因此:
|
|
9
|
+
*
|
|
10
|
+
* - action 字符串(如 "map.highlight")是**技能作者与宿主页面开发者之间的业务
|
|
11
|
+
* 契约**,LLM 不参与协议、SDK 不做枚举校验;
|
|
12
|
+
* - 宿主页面用 `session.onCommand("map.highlight", fn)` 消费;
|
|
13
|
+
* - 未注册 handler 的 action 会进入有限缓冲等待注册,超限后丢弃并 console 告警。
|
|
14
|
+
*
|
|
15
|
+
* 宿主 → 智能体方向只有三个白名单动作:attach(附件入输入框)/ insertText
|
|
16
|
+
* (追加输入文本)/ send(直接发消息),对应协议层的 addContext / appendInput /
|
|
17
|
+
* sendMessage。
|
|
18
|
+
*/
|
|
19
|
+
export interface CommandEnvelope {
|
|
20
|
+
__bladeBridge: true;
|
|
21
|
+
direction: "agent-to-host";
|
|
22
|
+
action: string;
|
|
23
|
+
payload: unknown;
|
|
24
|
+
meta: {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
toolCallId?: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export type InboundAction = "addContext" | "appendInput" | "sendMessage";
|
|
31
|
+
export interface InboundEnvelope {
|
|
32
|
+
__bladeBridge: true;
|
|
33
|
+
direction: "host-to-agent";
|
|
34
|
+
action: InboundAction;
|
|
35
|
+
payload: unknown;
|
|
36
|
+
}
|
|
37
|
+
export declare function isCommandEnvelope(value: unknown): value is CommandEnvelope;
|
|
38
|
+
export declare function isInboundEnvelope(value: unknown): value is InboundEnvelope;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CommandHandler = (payload: unknown, meta: {
|
|
2
|
+
toolCallId?: string;
|
|
3
|
+
}) => void;
|
|
4
|
+
/**
|
|
5
|
+
* 指令注册表:onCommand 事后注册 + 注册前缓冲补投。
|
|
6
|
+
*
|
|
7
|
+
* action 的来源见 ./protocol.ts 顶部说明——它由技能作者在工具返回值的
|
|
8
|
+
* `_meta.bridge` 字段里写入,是技能作者与宿主页面开发者的业务契约。
|
|
9
|
+
* 缓冲的意义:指令可能在页面还没来得及调用 onCommand 时就到达(比如
|
|
10
|
+
* 历史回放),先存起来、注册时立刻补投,消除"注册晚了漏事件"的坑。
|
|
11
|
+
*/
|
|
12
|
+
export declare class CommandRegistry {
|
|
13
|
+
private handlers;
|
|
14
|
+
private buffered;
|
|
15
|
+
onCommand(action: string, handler: CommandHandler): () => void;
|
|
16
|
+
dispatch(action: string, payload: unknown, meta?: {
|
|
17
|
+
toolCallId?: string;
|
|
18
|
+
}): void;
|
|
19
|
+
private invoke;
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { BladeClient } from "./blade-client";
|
|
2
|
+
export type { BladeClientOptions, UploadProgress } from "./blade-client";
|
|
3
|
+
export { BladeApiError } from "./rest";
|
|
4
|
+
export type { LoginOptions, LoginResult, TokenStorageMode } from "./auth-login";
|
|
5
|
+
export { VersionMismatchError, SDK_NAME, SDK_VERSION, MIN_SERVER_VERSION } from "./version";
|
|
6
|
+
export type { ServerVersionInfo } from "./version";
|
|
7
|
+
export { AgentSession } from "./session/agent-session";
|
|
8
|
+
export type { SendOptions } from "./session/agent-session";
|
|
9
|
+
export { SessionHub } from "./session/hub";
|
|
10
|
+
export type { AgentSessionEvents, AgentSessionEventName } from "./session/events";
|
|
11
|
+
export { createInitialSessionState } from "./session/state";
|
|
12
|
+
export type { AskUserAnswerData, AgentLoopInfo, ActiveCompactionState, ConnectionStatus, SessionState, } from "./session/state";
|
|
13
|
+
export { connectEmbedded } from "./commands/embedded";
|
|
14
|
+
export type { EmbeddedChat, EmbeddedChatOptions } from "./commands/embedded";
|
|
15
|
+
export type { CommandHandler } from "./commands/registry";
|
|
16
|
+
export type { CommandEnvelope, InboundAction, InboundEnvelope } from "./commands/protocol";
|
|
17
|
+
export { isCommandEnvelope, isInboundEnvelope } from "./commands/protocol";
|
|
18
|
+
export type { AuthResource, ProvidersResponse, UserInfo } from "./resources/auth";
|
|
19
|
+
export type { HeadlessResource } from "./resources/headless";
|
|
20
|
+
export type { SessionsResource } from "./resources/sessions";
|
|
21
|
+
export type { CreateSessionRequest, FileEntry, PaginatedSessionsResult, SessionContextStats, SessionHistory, ShareLinkResult, UploadFileEntry, UploadFilesOptions, } from "./resources/sessions";
|
|
22
|
+
export type { ArchivedFileInfo, ArchivedToolCallInfo, ChatMessage, CompactionInfo, FileContentPart, ImageUrlContentPart, MemoryRefInfo, MessageContent, MessageContentPart, TextContentPart, ToolBridgeContent, ToolCallInfo, } from "./schemas/message";
|
|
23
|
+
export { buildMessageContent, contentPreview, extractTextAttachments, getFileParts, getImageParts, getTextContent, groupMessagesByLoop, isHiddenInternalMessage, normalizeMessageContent, transformSlashCommand, } from "./schemas/message-utils";
|
|
24
|
+
export type { ParsedTextAttachment, ParsedTextContext } from "./schemas/message-utils";
|
|
25
|
+
export type { ContentBlock, MemoryRef, PatchEnvelope, TurnProjection } from "./schemas/projection";
|
|
26
|
+
export { SessionInfo, SessionStatus } from "./schemas/session";
|
|
27
|
+
export type { ModeId, PrimarySkillParallelMode, PrimarySkillSnapshot, SessionDetail, SessionPortMapping, TemplateId, } from "./schemas/session";
|
|
28
|
+
export { LayoutType } from "./schemas/solution";
|
|
29
|
+
export type { BizRole, Solution, SolutionAppField, SolutionAppState, SolutionAppUiConfig, } from "./schemas/solution";
|
|
30
|
+
export { Task, TaskStatus } from "./schemas/task";
|
|
31
|
+
export type { BackgroundTask } from "./schemas/background";
|
|
32
|
+
export { ClientProjectionBuilder } from "./shared/projection";
|
|
33
|
+
export type { RawEvent } from "./shared/projection";
|
|
34
|
+
export { createSocket } from "./socket";
|
|
35
|
+
export type { CreateSocketOptions, TypedSocket } from "./socket";
|
|
36
|
+
export type { AsrAudioPayload } from "./types/socket-events";
|
|
37
|
+
export type * from "./types/sdk-profile";
|