@my-life-buddies/buddy-runtime 0.15.0 → 0.17.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.
Files changed (46) hide show
  1. package/README.md +205 -183
  2. package/dist/conversation/agent.d.ts.map +1 -1
  3. package/dist/conversation/agent.js +22 -7
  4. package/dist/conversation/agent.js.map +1 -1
  5. package/dist/conversation/index.d.ts +3 -1
  6. package/dist/conversation/index.d.ts.map +1 -1
  7. package/dist/conversation/index.js +13 -5
  8. package/dist/conversation/index.js.map +1 -1
  9. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +1 -0
  10. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
  11. package/dist/conversation/mlbMessageAgentMessageConverter.js +5 -5
  12. package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
  13. package/dist/conversation/writeMLBMessage.d.ts +1 -0
  14. package/dist/conversation/writeMLBMessage.d.ts.map +1 -1
  15. package/dist/conversation/writeMLBMessage.js.map +1 -1
  16. package/dist/dataAccessRequirements.d.ts +5 -0
  17. package/dist/dataAccessRequirements.d.ts.map +1 -0
  18. package/dist/dataAccessRequirements.js +53 -0
  19. package/dist/dataAccessRequirements.js.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/mlbClient.d.ts +10 -8
  23. package/dist/mlbClient.d.ts.map +1 -1
  24. package/dist/mlbClient.js +34 -6
  25. package/dist/mlbClient.js.map +1 -1
  26. package/dist/server.d.ts +2 -2
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +22 -32
  29. package/dist/server.js.map +1 -1
  30. package/dist/tools/askQuestion.d.ts +2 -1
  31. package/dist/tools/askQuestion.d.ts.map +1 -1
  32. package/dist/tools/askQuestion.js +4 -1
  33. package/dist/tools/askQuestion.js.map +1 -1
  34. package/dist/tools/dataAccess.d.ts +14 -4
  35. package/dist/tools/dataAccess.d.ts.map +1 -1
  36. package/dist/tools/dataAccess.js +60 -10
  37. package/dist/tools/dataAccess.js.map +1 -1
  38. package/dist/tools/dataAccessRequest.js +1 -1
  39. package/dist/tools/dataAccessRequest.js.map +1 -1
  40. package/dist/tools/proactive.d.ts +5 -26
  41. package/dist/tools/proactive.d.ts.map +1 -1
  42. package/dist/tools/proactive.js +82 -65
  43. package/dist/tools/proactive.js.map +1 -1
  44. package/dist/types.d.ts +35 -25
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,7 @@ npm install @my-life-buddies/buddy-runtime typebox
20
20
 
21
21
  ### 1.2 创建搭子
22
22
 
23
- 通过 `@my-life-buddies/cli` 完成搭搭账号注册,并创建一个新的搭子,获取 buddyId & buddyToken
23
+ 通过 `@my-life-buddies/cli` 完成搭搭账号注册,并创建一个新的搭子,获取 buddyId 与开发令牌(平台另有一把正式令牌,只在托管运行时注入,不经 CLI 下发)
24
24
 
25
25
  ### 1.3 搭子工程框架
26
26
 
@@ -49,48 +49,27 @@ const planMenu: AgentTool<typeof PLAN_MENU_PARAMS, undefined> = {
49
49
  }),
50
50
  };
51
51
 
52
- await BuddyServer.start({
53
- buddyId: process.env.BUDDY_ID!,
54
- token: process.env.BUDDY_TOKEN!,
55
- dataRequirements: [],
56
- buddy: () => ({
57
- initialState: {
58
- model: "kimi-k3",
59
- systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
60
- tools: [planMenu],
61
- },
62
- }),
63
- });
52
+ await BuddyServer.start(() => ({
53
+ initialState: {
54
+ model: "kimi-k3",
55
+ systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
56
+ tools: [planMenu],
57
+ },
58
+ }));
64
59
  ```
65
60
 
66
- 完整示例:[examples/aunt-tu/](examples/aunt-tu/)(提问、记忆、小挂件)、[examples/cardio/](examples/cardio/)(授权数据、主动服务、提问、记忆、小挂件)。
67
-
68
61
  ## 2. 配置
69
62
 
70
- ### 2.1 `BuddyServer.start(options)`
63
+ ### 2.1 `BuddyServer.start(buddy)`
71
64
 
72
65
  连上平台开始处理消息,返回 `{ stop(): Promise<void> }`。
73
66
 
74
- ```ts
75
- interface StartOptions {
76
- // 必填。搭子 id 与令牌,注册时平台分配,见 1.2。平台托管时以环境变量 BUDDY_ID、BUDDY_TOKEN 注入
77
- buddyId: string;
78
- token: string;
79
-
80
- // 必填。这只搭子需要的用户数据及用途,见 3.4;不需要时传 []
81
- dataRequirements: { id: DataAccessDatasetId; purpose: string }[];
82
-
83
- // 必填。每场会话建起来时调一次,返回这场会话的配置,见 2.2、2.3
84
- buddy: (conversation: Conversation) => BuddyOptions | Promise<BuddyOptions>;
85
- }
86
- ```
67
+ `buddy` 是每场会话建起来时调用一次的工厂函数,返回这场会话的配置,见 2.2、2.3。DataAccess 声明不属于会话工厂,单独写在工程根目录的 `data-access.json`,见 3.4。
87
68
 
88
- Runtime 启动时只在本地校验 `dataRequirements`,不会额外调用接口注册声明。Runtime 根据自身导出的能力目录补齐标题、来源和 Schema 版本;实际调用 `data_access_query`、发送 HealthKit 权限卡或申请使用数据的主动服务时,会把完整声明随业务请求携带给平台。平台在该次请求中更新用途版本并校验 Grant。
69
+ **buddyId 与 token**:Runtime 会从环境变量 `BUDDY_ID`、`BUDDY_TOKEN` 读。创建搭子时会下发 buddyId 与开发 token,本地开发时,启动命令携带环境变量,正式上线时平台托管会注入同名变量,token 会切换成正式 token,对开发者无感。
89
70
 
90
71
  **buddy 的生命周期**:`buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,需要存储会话状态可以根据场景使用 3.3 记忆、3.5 小挂件实现。
91
72
 
92
- **同一时间只有一个进程**:同一个搭子身份同一时间只有一个进程连着平台,本地调试用的 `<buddyId>-dev` 也一样。新进程连上平台后,原来那个进程会打一条 ERROR 日志说明被顶掉,然后退出、不再重连。本地不小心起了多个进程时,留下的是最后连上平台的那个。
93
-
94
73
  **buddy 的会话压缩**:会话历史长到一定程度时,运行时在会话被回收后把早先的对话压成一份摘要写回平台,下次重建时模型看到的是「摘要 + 最近的原文」。开发者不用做任何事,也没有开关。
95
74
 
96
75
  ### 2.2 `BuddyOptions`
@@ -209,16 +188,17 @@ interface Conversation {
209
188
  send(id: string, options: { idempotencyKey: string }): void;
210
189
  };
211
190
 
212
- // 主动服务,见 3.6:propose 提订阅,平台建成即生效;isRunning 判断现在是不是主动服务的回合
191
+ // 主动服务,见 3.6:按稳定 key 订阅、查询和关闭;isRunning 判断现在是不是主动服务的回合
213
192
  proactive: {
214
- propose(request: ProactiveSubscriptionRequest): Promise<{ id: string; status: "active" }>;
193
+ subscribe(key: string, request: ProactiveSubscriptionRequest): Promise<ProactiveSubscription>;
194
+ list(): Promise<ProactiveSubscription[]>;
195
+ unsubscribe(key: string): Promise<boolean>;
196
+ unsubscribeAll(): Promise<number>;
215
197
  isRunning(): boolean;
216
198
  };
217
199
  }
218
200
  ```
219
201
 
220
- `tools`、`memory`、`dataAccess`、`widget`、`proactive` 已经绑好这场会话,直接用,不用传令牌或拼接口路径。
221
-
222
202
  ## 3. 平台能力
223
203
 
224
204
  | 能力 | `conversation` 对象方法 | 模型工具(`conversation.tools` 上) |
@@ -228,7 +208,7 @@ interface Conversation {
228
208
  | 记忆 | `conversation.memory` | 无,开发者可以根据具体场景,自己组装成适合的模型工具 |
229
209
  | 用户授权数据 | `conversation.dataAccess.query` / `conversation.dataAccess.request` / `conversation.dataAccess.readResult` | `data_access_query` / `data_access_request` / `data_access_read_result` |
230
210
  | 小挂件 | `conversation.widget.list` / `conversation.widget.create` / `conversation.widget.read` / `conversation.widget.readModule` / `conversation.widget.rename` / `conversation.widget.writeRecord` / `conversation.widget.deleteRecord` / `conversation.widget.delete` / `conversation.widget.setPresentation` / `conversation.widget.send` | `widget_create` / `widget_read` / `widget_read_module` / `widget_rename` / `widget_write_record` / `widget_delete_record` / `widget_delete` / `widget_send` |
231
- | 主动服务 | `conversation.proactive.propose` / `conversation.proactive.isRunning` | `proactive_propose` |
211
+ | 主动服务 | `conversation.proactive.subscribe` / `list` / `unsubscribe` / `unsubscribeAll` / `isRunning` | `proactive_subscribe` / `proactive_list` / `proactive_unsubscribe` / `proactive_unsubscribe_all` |
232
212
 
233
213
  ```ts
234
214
  buddy: (conversation) => ({
@@ -242,11 +222,11 @@ buddy: (conversation) => ({
242
222
 
243
223
  ### 3.1 资料
244
224
 
245
- `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,把 `conversation.tools.resource_list` 和 `conversation.tools.resource_read` 放进 `initialState.tools`,模型先列出有哪些资料,再按需读。
225
+ 搭子随身带的只读资料:把 `.md` 放进工程目录下的 `resources/`,模型先列出有哪些资料,再按需读一篇。资料在进程启动时一次读好,整场会话、每个回合读到的都是同一份。
226
+
227
+ **准备工作**
246
228
 
247
- - 工程目录从入口脚本往上找,与启动时的工作目录无关。
248
- - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 与点开头的文件和目录;资料改了要重启进程。
249
- - `resource_list` 列出全部资料的相对路径,`resource_read` 按路径读一篇。没有资料时 `resource_list` 回「没有资料」。
229
+ 资料放在工程目录(`package.json` 所在目录)下的 `resources/`:
250
230
 
251
231
  ```
252
232
  my-buddy/ 工程目录
@@ -257,41 +237,63 @@ my-buddy/ 工程目录
257
237
  └─ src/index.ts 入口脚本
258
238
  ```
259
239
 
260
- **在自己的代码里用**
240
+ - 资料更改要重启进程,建议 watch `resources/` 实时重启。
261
241
 
262
- 调用 `conversation.resource`,不需要把这两个工具放进 `initialState.tools`:
242
+ **`conversation` 对象方法**
263
243
 
264
244
  | 方法 | 做什么 |
265
245
  |---|---|
266
246
  | `list()` | 全部资料的路径,相对 `resources/`,按路径排序 |
267
- | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填;没有这篇时抛异常 |
247
+ | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填 |
268
248
 
269
249
  ```ts
270
250
  const notes = conversation.resource.read("research/01-writings.md");
271
251
  ```
272
252
 
253
+ **模型工具**
254
+
255
+ 把 `conversation.tools.resource_list` 和 `conversation.tools.resource_read` 放进 `initialState.tools`,模型就能先列出有哪些资料,再按路径读一篇。`systemPrompt` 里交代清楚这只搭子带了哪类资料、什么时候该去翻。
256
+
257
+ **限制与异常**
258
+
273
259
  - 两个方法都是同步的,读的是进程启动时读好的那份。
260
+ - 没有资料时 `resource_list` 回「没有资料」。
261
+ - `read(file)` 读不存在的那篇时抛异常。
274
262
  - 主动服务的回合内也能调。
275
263
 
276
264
  ### 3.2 提问
277
265
 
278
- 让模型发一张题卡请用户选:一个问题加 2 到 4 个可以直接点的选项。私聊、群聊都能用。
266
+ 让模型发一张题卡请用户选:一个问题加 2 到 4 个可以直接点的选项。私聊、群聊都能用。题卡发出,这一轮就到此结束,等用户操作。
267
+
268
+ **`conversation` 对象方法**
269
+
270
+ 没有。`conversation` 不提供发题卡的方法,题卡只由模型发。
271
+
272
+ **模型工具**
279
273
 
280
274
  把 `conversation.tools.ask_question` 放进 `initialState.tools`,模型就能发题卡。
281
275
 
282
- - 参数是 `question`(题目,不超过 500 字)和 `options`(2 到 4 个,每个不超过 30 字,不能重复);不合规时模型会看到错误,改了再调。
283
- - 用户点选项,就是用选项原文发一条消息,跟自己打字一样;也可以不点,直接打字。
284
- - `conversation` 对象不提供发题卡的方法,题卡只由模型发。
276
+ - 参数是 `question`(题目,不超过 500 字)和 `options`(2 到 4 个,每个不超过 30 字,不能重复)。
277
+ - 题目和选项只写在参数里,回复正文里不再说一遍。
278
+ - 用户点选项,就是用选项原文发一条消息,跟自己打字一样;也可以不点,直接打字回答,或者说别的。
279
+
280
+ **限制与异常**
281
+
282
+ - 参数不合规时模型会看到错误,改了再调。
283
+ - 这一轮读过用户授权数据之后不能再发题卡,调用抛异常,异常信息模型能看到;原因见 3.4 的「数据只在当轮可见」。
284
+ - 主动服务的回合里照常能发,运行时不拦。
285
285
 
286
286
  ### 3.3 记忆
287
287
 
288
- 这场会话的长期记忆,存在平台上,只在本会话内可见,会话删除时一并清除。
288
+ 这场会话的长期记忆,搭子写给自己的印象,存在平台上,平台只存不读。只在本会话内可见,会话删除时一并清除。
289
289
 
290
- - `list()`:列出全部,按 `key` 字典序。
291
- - `write(key, text)`:覆盖写一条。
292
- - `delete(key)`:删一条,不存在也算成功。
290
+ **`conversation` 对象方法**
293
291
 
294
- `key` 非空、不含空白与 `/`、最长 64 字;`text` 单条不超过 16 KB;每场会话最多 64 条、合计不超过 256 KB。超限、`key` 不合法或网络出错时抛异常,异常信息是平台给出的原因,在工具里调用时模型能看到。
292
+ | 方法 | 做什么 |
293
+ |---|---|
294
+ | `list()` | 列出全部,按 `key` 字典序 |
295
+ | `write(key, text)` | 覆盖写一条 |
296
+ | `delete(key)` | 删一条,不存在也算成功 |
295
297
 
296
298
  ```ts
297
299
  interface MemoryEntry {
@@ -301,46 +303,50 @@ interface MemoryEntry {
301
303
  }
302
304
  ```
303
305
 
304
- 运行时不提供记忆工具,要让模型记东西,自己包一个:
306
+ **模型工具**
307
+
308
+ 运行时不提供记忆工具。要让模型自己记东西,按这只搭子要记什么,自己包一个:
305
309
 
306
310
  ```ts
307
311
  const REMEMBER_PARAMS = Type.Object({ topic: Type.String(), text: Type.String() });
308
312
 
309
- await BuddyServer.start({
310
- dataRequirements: [],
311
- buddy: (conversation) => {
312
- const remember: AgentTool<typeof REMEMBER_PARAMS, undefined> = {
313
- name: "remember",
314
- label: "正在记下",
315
- description: "把一条关于对方的印象记下来,同一主题覆盖旧的。",
316
- parameters: REMEMBER_PARAMS,
317
- execute: async (_id, { topic, text }) => {
318
- await conversation.memory.write(topic, text);
319
- return { content: [{ type: "text", text: "记下了。" }], details: undefined };
320
- },
321
- };
322
- return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [remember] } };
323
- },
313
+ await BuddyServer.start((conversation) => {
314
+ const remember: AgentTool<typeof REMEMBER_PARAMS, undefined> = {
315
+ name: "remember",
316
+ label: "正在记下",
317
+ description: "把一条关于对方的印象记下来,同一主题覆盖旧的。",
318
+ parameters: REMEMBER_PARAMS,
319
+ execute: async (_id, { topic, text }) => {
320
+ await conversation.memory.write(topic, text);
321
+ return { content: [{ type: "text", text: "记下了。" }], details: undefined };
322
+ },
323
+ };
324
+ return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [remember] } };
324
325
  });
325
326
  ```
326
327
 
328
+ **限制与异常**
329
+
330
+ - `key` 非空、不含空白与 `/`、最长 64 字;`text` 单条不超过 16 KB;每场会话最多 64 条、合计不超过 256 KB。
331
+ - 超限、`key` 不合法或网络出错时抛异常,异常信息是平台给出的原因,在工具里调用时模型能看到。
332
+ - 这一轮读过用户授权数据之后,运行时拒绝本回合的记忆写入,见 3.4 的「数据只在当轮可见」。
333
+ - 主动服务的回合里跟普通回合一样能读能写,运行时不拦。
334
+
327
335
  ### 3.4 用户授权数据
328
336
 
329
- 用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id
337
+ 用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id。查到的数据只在当轮可见,见本节末尾。
330
338
 
331
339
  这里有三层不同概念:
332
340
 
333
341
  1. **Runtime 能力目录**:当前 SDK 支持哪些 Dataset,由包导出的 `DATA_ACCESS_CAPABILITIES` 提供。
334
- 2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在 `BuddyServer.start.dataRequirements`。
342
+ 2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在工程根目录的 `data-access.json`。
335
343
  3. **用户 Grant**:用户是否同意这只搭子读取某个已声明 Dataset,由客户端授权流程管理,搭子不能代替用户授予。
336
344
 
337
- `data_access_query`、`data_access_request` 等平台工具放进 `initialState.tools`,只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权。
338
-
339
- 位置和日历是当前会话的一次性授权结果,通过 `data_access_request` 请求,不属于 Dataset,也不写进 `dataRequirements`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在 `dataRequirements` 中声明。
345
+ 位置和日历是当前会话的一次性授权结果,通过 `data_access_request` 请求,不属于 Dataset,也不写进 `data-access.json`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在配置中声明。
340
346
 
341
- **读取 Runtime 能力目录**
347
+ **准备工作**
342
348
 
343
- 开发者工具和项目代码直接从 Runtime 包导入,不需要网络请求:
349
+ Runtime 能力目录直接从 Runtime 包导入,开发者工具和项目代码都不需要网络请求:
344
350
 
345
351
  ```ts
346
352
  import {
@@ -358,24 +364,25 @@ const sleep = dataAccessCapability("health.sleep");
358
364
 
359
365
  这份导出是开发者可选 `id` 的权威来源。新增 Dataset 随 Runtime 版本发布;developer-platform、脚手架和预检都应读取该导出,不再维护一份手写枚举。
360
366
 
361
- **声明需要哪些数据**
362
-
363
- 声明写在 `BuddyServer.start` 的初始化参数里,Runtime 在本地展开成带标题、来源和 Schema 版本的完整声明:
367
+ 声明写在工程根目录(与 `package.json` 同级)的 `data-access.json`:
364
368
 
365
- ```ts
366
- await BuddyServer.start({
367
- dataRequirements: [
368
- { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
369
- ],
370
- buddy: () => ({ /* ... */ }),
371
- });
369
+ ```json
370
+ {
371
+ "requirements": [
372
+ { "id": "health.sleep", "purpose": "根据你最近的睡眠调整作息建议" }
373
+ ]
374
+ }
372
375
  ```
373
376
 
374
- - `id`:从 `DATA_ACCESS_CAPABILITIES` 中选,TypeScript 会收窄为 `DataAccessDatasetId`。
377
+ - `id`:从 `DATA_ACCESS_CAPABILITIES` 中选。
375
378
  - `purpose`:用途,1~200 字。用户看到这句话再决定是否同意。
376
- - Runtime 不在启动时注册声明;查询、HealthKit 权限卡和主动服务请求会携带当前完整声明。
377
- - Server 在实际请求时按声明用途和 Schema 计算版本;`id`、`purpose` Schema 都没变化时保留已有 Grant,变化后要求重新授权。
378
- - 本地进程连接线上网关时使用 `<buddyId>-dev` 身份,实际使用形成的授权上下文与正式搭子隔离。
379
+ - `requirements` 最多 32 项,不允许重复;配置对象和每一项都不接受额外字段。
380
+ - 文件缺失等价于 `{ "requirements": [] }`。Runtime 启动时先严格校验,再把整份声明上传平台;即使为空也会上传,以清除旧声明。上传失败则启动失败。
381
+ - Server 在注册时按声明用途和 Schema 计算版本;`id`、`purpose` 与 Schema 都没变化时保留已有 Grant,变化后要求重新授权。
382
+ - 后续查询、HealthKit 权限卡和主动服务请求只发送 Dataset ID,不再重复携带用途声明。
383
+ - 本地 `.env` 里放的是 CLI 发的开发令牌,进程连上平台后身份是 `<buddyId>-dev`,实际使用形成的授权上下文与正式搭子隔离。
384
+
385
+ 可以声明的 Dataset:
379
386
 
380
387
  | Dataset | 内容 | `data.items` 每条的字段 |
381
388
  |---|---|---|
@@ -390,34 +397,45 @@ await BuddyServer.start({
390
397
 
391
398
  部分字段可能缺失,取值前先判断。
392
399
 
393
- **模型工具与代码接口**
400
+ **`conversation` 对象方法**
394
401
 
395
- - `data_access_query` / `conversation.dataAccess.query`:查询已授权 Dataset。
396
- - `data_access_request` / `conversation.dataAccess.request`:在私聊中发数据授权卡;发卡后当前回合结束,等待用户操作。
397
- - `data_access_read_result` / `conversation.dataAccess.readResult`:按授权卡的 `requestId` 读取一次性位置或日历结果;HealthKit 不走这个接口。
402
+ 开发者自己的工具直接调用 `conversation.dataAccess`,不用把 `data_access_*` 放进 `initialState.tools`:
398
403
 
399
- 模型工具要从 `conversation.tools` 取来放进 `initialState.tools` 才能调用;没声明 Dataset 时照常注册,查询的每一项都回 `notDeclared`。开发者自己的工具直接调用 `conversation.dataAccess`,不用把这几个工具放进去。
404
+ | 方法 | 做什么 |
405
+ |---|---|
406
+ | `query({ datasets, memberId? })` | 查询已授权 Dataset,每个 Dataset 各自一个状态 |
407
+ | `request({ kind, reason, datasets?, calendarDays?, idempotencyKey })` | 在私聊里发数据授权卡,`kind` 取 `healthkit`、`location` 或 `calendar` |
408
+ | `readResult(requestId)` | 按授权卡的 `requestId` 读一次性的位置或日历结果;HealthKit 不走这个接口 |
400
409
 
401
- **HealthKit:先查询,缺授权再发卡**
410
+ ```ts
411
+ const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
412
+ if (sleep.status === "available") {
413
+ // sleep.data.items 是每天一条的睡眠记录
414
+ }
415
+ ```
416
+
417
+ - `datasets`:一次最多 8 个。
418
+ - `memberId`:私聊不传;群聊必传,取群消息开头 `[名字 #m_…]` 里的 `m_…`,查这位成员的数据,传错时抛异常。
419
+
420
+ **模型工具**
421
+
422
+ 把 `conversation.tools.data_access_query`、`data_access_request`、`data_access_read_result` 放进 `initialState.tools`,模型就能自己查数据、发授权卡。放进去只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权;没声明 Dataset 时照常注册,查询的每一项都回 `notDeclared`。
423
+
424
+ HealthKit 先查询,缺授权再发卡:
402
425
 
403
426
  ```ts
404
- await BuddyServer.start({
405
- dataRequirements: [
406
- { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
407
- ],
408
- buddy: (conversation) => ({
409
- initialState: {
410
- model: "kimi-k3",
411
- systemPrompt: [
412
- "聊到作息时,先用 data_access_query 查询 health.sleep。",
413
- "返回 notGranted 时,说明当前问题为什么需要数据,再用 data_access_request 发 HealthKit 授权卡。",
414
- "用户处理授权卡后会开启新回合;再次调用 data_access_query,不要调用 data_access_read_result。",
415
- "用户拒绝后不要在同一任务里反复申请。",
416
- ].join("\n"),
417
- tools: [conversation.tools.data_access_query, conversation.tools.data_access_request],
418
- },
419
- }),
420
- });
427
+ await BuddyServer.start((conversation) => ({
428
+ initialState: {
429
+ model: "kimi-k3",
430
+ systemPrompt: [
431
+ "聊到作息时,先用 data_access_query 查询 health.sleep。",
432
+ "返回 notGranted 时,说明当前问题为什么需要数据,再用 data_access_request 发 HealthKit 授权卡。",
433
+ "用户处理授权卡后会开启新回合;再次调用 data_access_query,不要调用 data_access_read_result。",
434
+ "用户拒绝后不要在同一任务里反复申请。",
435
+ ].join("\n"),
436
+ tools: [conversation.tools.data_access_query, conversation.tools.data_access_request],
437
+ },
438
+ }));
421
439
  ```
422
440
 
423
441
  完整链路:
@@ -432,11 +450,7 @@ data_access_query
432
450
  → available + data
433
451
  ```
434
452
 
435
- 查询接口不会把“从未授权”和“用途变化后需重新确认”分成两种模型状态,都会返回 `notGranted`;重新确认原因由用户侧授权页面展示。
436
-
437
- **位置和日历:读取一次性结果**
438
-
439
- 位置和日历不属于 Dataset,不写进 `dataRequirements`。模型先用 `data_access_request` 发卡,用户处理后,隐藏结果会带回这张卡的 `requestId`;再用 `data_access_read_result` 读取。结果只在平台短期保存,读不到时返回 `unavailable`。
453
+ 位置和日历不属于 Dataset,不写进 `data-access.json`。模型先用 `data_access_request` 发卡,用户处理后,隐藏结果会带回这张卡的 `requestId`;再用 `data_access_read_result` 读取。结果只在平台短期保存,读不到时返回 `unavailable`。
440
454
 
441
455
  ```text
442
456
  data_access_request { kind: "location", reason: "查找你附近的地点" }
@@ -446,19 +460,9 @@ data_access_request { kind: "location", reason: "查找你附近的地点" }
446
460
  → available + data,或 unavailable
447
461
  ```
448
462
 
449
- `calendar` 还可传 `calendarDays`,范围为 1~31,默认 7。授权请求第一期只支持私聊;群聊需要引导目标成员去私聊操作。
463
+ `calendar` 还可传 `calendarDays`,范围为 1~31,默认 7
450
464
 
451
- **开发者代码查询 Dataset**
452
-
453
- ```ts
454
- const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
455
- if (sleep.status === "available") {
456
- // sleep.data.items 是每天一条的睡眠记录
457
- }
458
- ```
459
-
460
- - `datasets`:一次最多 8 个;响应超过 512 KiB 时抛异常,减少 Dataset 再查。
461
- - `memberId`:私聊不传;群聊必传,取群消息开头 `[名字 #m_…]` 里的 `m_…`,查这位成员的数据,传错时抛异常。
465
+ **限制与异常**
462
466
 
463
467
  每个 Dataset 各自一个状态,只有 `available` 带 `data`:
464
468
 
@@ -470,22 +474,27 @@ if (sleep.status === "available") {
470
474
  | `disabledInGroup` | 这位成员在这个群里关掉了这项数据 |
471
475
  | `unavailable` | 平台上还没有这位用户的这项数据,或数据已过期 |
472
476
 
477
+ - 查询接口不会把「从未授权」和「用途变化后需重新确认」分成两种模型状态,都会返回 `notGranted`;重新确认原因由用户侧授权页面展示。
478
+ - 一次查询的响应超过 512 KiB 时抛异常,减少 Dataset 再查。
479
+ - 发出授权卡后当前回合结束,等待用户操作。
480
+ - 授权请求第一期只支持私聊;群聊需要引导目标成员去私聊操作。
481
+
473
482
  **数据只在当轮可见**
474
483
 
475
- 查到的数据只用于这一轮回复:平台不存原文,搭子进程里的那份也在这一轮结束时被抹掉,下一轮读不到。凡是可能把它带出去的地方——工具结果、工具参数、那一步的正文和心声——这一轮统统不写进平台。
484
+ 查到的数据只用于这一轮回复:平台不存原文,搭子进程里的那份也在这一轮结束时被抹掉,下一轮读不到。Runtime 按步骤追踪数据流:读取数据之前已经生成的工具步骤正常保留;DataAccess 结果只保留 Dataset 状态;模型看到数据后生成的工具参数、工具结果、工具步骤正文和心声不写进平台,最终给用户看的普通回复只保留正文、不保留心声。
476
485
 
477
- 这么设计是为了**用户撤销授权时能真的收回**:数据没有留在任何地方,不需要事后去清。代价是这一轮的记录不完整,搭子重启后读不到自己当时的推理和工具参数。
486
+ 这么设计是为了**用户撤销授权时能真的收回**:确保模型不会记住授权的具体数据。
478
487
 
479
488
  由此有两条实操结论:
480
489
 
481
490
  - **想让某个结论以后还在场,就在回复里说出来**(用户也会看到)。模型对这些数据的记忆只能活在它说过的话里——下次它读到的是「查了 health.sleep,可用」,不是具体数字。
482
- - **别把查到的数据写进记忆、小挂件或别的工具的参数**。那些地方不脱敏,写进去就永久留下了。
491
+ - **不要把查到的数据写进记忆、题卡、小挂件或别的工具参数**。Runtime 会直接拒绝当前回合的记忆写入和题卡发送,并脱敏后续工具调用;其他开发者自定义持久化能力仍需自行遵守这条边界。
483
492
 
484
493
  ### 3.5 小挂件
485
494
 
486
- 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widget`,都能建小挂件、读整个或一个模块、改标题、写记录、删记录、删整个小挂件,并把它发进聊天。
495
+ 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。建小挂件、读整个或一个模块、改标题、写记录、删记录、删整个小挂件,并把它发进聊天,模型和开发者代码都能做。
487
496
 
488
- **放进工程目录**
497
+ **准备工作**
489
498
 
490
499
  一个小挂件是工程目录下 `widgets/` 里的一个子目录,目录名就是小挂件类型 id。
491
500
 
@@ -499,18 +508,22 @@ my-buddy/ 工程目录
499
508
  └─ src/index.ts
500
509
  ```
501
510
 
502
- **让模型使用**
503
-
504
- 把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`。
505
-
506
- - 工具说明里附上各类型的模块与字段。改了 `widgets/` 要重启进程。
507
- - 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
508
- - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
509
- - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
511
+ 资料小挂件要重启进程,建议 watch `widgets/` 实时重启。
510
512
 
511
- **在自己的代码里用**
513
+ **`conversation` 对象方法**
512
514
 
513
- 调用 `conversation.widget`,不需要把 `widget_*` 放进工具:
515
+ | 方法 | 做什么 |
516
+ |---|---|
517
+ | `list()` | 列出这场会话里的小挂件,不带记录 |
518
+ | `create({ type, title, idempotencyKey? })` | 新建一个,回 `{ id }`;同一个 `idempotencyKey` 重复建回同一个 id |
519
+ | `read(id)` | 读整个小挂件,含各模块的记录 |
520
+ | `readModule(id, module)` | 读一个模块,回 `{ records }` |
521
+ | `rename(id, title)` | 改标题,id 不变,聊天里发过的卡片跟着显示新标题 |
522
+ | `writeRecord(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖,回 `{ revision }`;带 `expectedRevision` 时版本号对不上,平台回 409 |
523
+ | `deleteRecord(id, module, recordId)` | 删一条记录 |
524
+ | `delete(id)` | 删整个小挂件连记录;聊天里发过的卡片留着,点开显示已删除 |
525
+ | `setPresentation(id, { summary, imageMediaId })` | 设聊天里卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 |
526
+ | `send(id, { idempotencyKey })` | 发进聊天:放进发送队列就返回,不等平台确认送达;同一个 `idempotencyKey` 重复发只落一条 |
514
527
 
515
528
  ```ts
516
529
  execute: async (toolCallId, params) => {
@@ -521,18 +534,16 @@ execute: async (toolCallId, params) => {
521
534
  },
522
535
  ```
523
536
 
524
- | 方法 | 做什么 | 平台接口 |
525
- |---|---|---|
526
- | `list()` | 列出这场会话里的小挂件,不带记录 | `GET /internal/v1/widgets` |
527
- | `create({ type, title, idempotencyKey? })` | 新建一个,回 `{ id }`;同一个 `idempotencyKey` 重复建回同一个 id | `POST /internal/v1/widgets` |
528
- | `read(id)` | 读整个小挂件,含各模块的记录 | `GET /internal/v1/widgets/:widgetId` |
529
- | `readModule(id, module)` | 读一个模块,回 `{ records }` | `GET /internal/v1/widgets/:widgetId/:module` |
530
- | `rename(id, title)` | 改标题,id 不变,聊天里发过的卡片跟着显示新标题 | `PUT /internal/v1/widgets/:widgetId` |
531
- | `writeRecord(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖,回 `{ revision }`;带 `expectedRevision` 时版本号对不上,平台回 409 | `PUT /internal/v1/widgets/:widgetId/:module/:recordId` |
532
- | `deleteRecord(id, module, recordId)` | 删一条记录 | `DELETE /internal/v1/widgets/:widgetId/:module/:recordId` |
533
- | `delete(id)` | 删整个小挂件连记录;聊天里发过的卡片留着,点开显示已删除 | `DELETE /internal/v1/widgets/:widgetId` |
534
- | `setPresentation(id, { summary, imageMediaId })` | 设聊天里卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 | `PUT /internal/v1/widgets/:widgetId/presentation` |
535
- | `send(id, { idempotencyKey })` | 发进聊天:放进发送队列就返回,不等平台确认送达;同一个 `idempotencyKey` 重复发只落一条 | `POST /internal/v1/messages` |
537
+ **模型工具**
538
+
539
+ 把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`,模型就能自己建、读、改、发小挂件。
540
+
541
+ - 工具说明里附上各类型的模块与字段,模型不用另外交代形状。
542
+ - 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
543
+ - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
544
+ - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
545
+
546
+ **限制与异常**
536
547
 
537
548
  - `list` 和 `read` 回的 `canEdit` 为 `false` 的,是别的会话转发来的引用,只能读;改名、写记录、删除、设卡片时平台回 403。
538
549
  - `data` 要符合 `schema.json` 里这个模块的 `record`,不符合时平台回 400。
@@ -543,53 +554,64 @@ execute: async (toolCallId, params) => {
543
554
 
544
555
  让搭子在约定的时机主动找用户说话,比如每周一次复盘。目前主动服务仅支持私聊。
545
556
 
546
- 1. 搭子向平台提一条主动服务的订阅记录,其中包含触发主动服务的条件。
557
+ 1. 搭子按稳定 `key` 向平台创建或更新一条主动服务订阅,其中包含触发条件;相同 `key` 不会重复创建。
547
558
  2. 触发条件满足,且没被免打扰时段、冷却时间、每日上限拦下时,平台向搭子发起一次主动服务的回合。
548
559
  3. 模型判断要不要开口:要就直接写出发给用户的消息,平台在落库那一刻再过一遍闸门,过了才发给用户,拦下就不发;不要就输出跳过标记,这一轮到此结束,用户什么也看不到。
549
560
 
550
- 提订阅有两种方式,二选一或并用。字段一样(见下表),产物都是一条订阅,只是谁决定何时提不同:
561
+ **`conversation` 对象方法**
551
562
 
552
- - 你的代码显式调 `conversation.proactive.propose`。它挂在这场会话的 `conversation` 上,凡是拿得到 `conversation` 的地方都能调:自己工具的 `execute` 里、`beforeToolCall` / `afterToolCall` 里、配置工厂函数体里:
563
+ | 方法 | 做什么 |
564
+ |---|---|
565
+ | `subscribe(key, request)` | 按稳定 `key` 创建或更新一条订阅,相同 `key` 更新原订阅并重新启用 |
566
+ | `list()` | 当前会话的全部订阅,包括已关闭项 |
567
+ | `unsubscribe(key)` | 关闭指定订阅,回是否找到 |
568
+ | `unsubscribeAll()` | 关闭全部 active 订阅,回关掉几条 |
569
+ | `isRunning()` | 现在是不是主动服务的回合 |
570
+
571
+ 它挂在这场会话的 `conversation` 上,凡是拿得到 `conversation` 的地方都能调:自己工具的 `execute` 里、`beforeToolCall` / `afterToolCall` 里、配置工厂函数体里。
553
572
 
554
573
  ```ts
555
- await conversation.proactive.propose({
574
+ await conversation.proactive.subscribe("weekly-review", {
556
575
  title: "每周复盘",
557
576
  reason: "在约定时间结合最近聊天做一次简短复盘",
558
- sources: ["schedule"],
559
- eventTypes: ["schedule.due"],
560
- schedule: { at: firstReviewAt, intervalMinutes: 7 * 24 * 60 },
577
+ trigger: { type: "schedule", at: firstReviewAt, intervalMinutes: 7 * 24 * 60 },
561
578
  });
562
579
  ```
563
580
 
564
- - 模型决定要不要提,把 `conversation.tools.proactive_propose` 放进 `initialState.tools`。什么场景该提,在 `systemPrompt` 里说清楚,注册进模型工具意味着用户也可以开口要求搭子主动服务:
565
-
566
- ```ts
567
- await BuddyServer.start({
568
- dataRequirements: [{ id: "health.workouts", purpose: "跑完后帮你复盘这次训练" }],
569
- buddy: (conversation) => ({
570
- initialState: {
571
- model: "kimi-k3",
572
- systemPrompt: "你是跑步搭子。用户表达想坚持训练时,可以用 proactive_propose 提议跑完步帮他复盘。",
573
- tools: [conversation.tools.proactive_propose, conversation.tools.data_access_query],
574
- },
575
- }),
576
- });
577
- ```
581
+ `subscribe` 的字段:
578
582
 
579
583
  | 字段 | 说明 |
580
584
  |---|---|
585
+ | `key` | 必填,订阅的稳定标识:1~64 位小写字母、数字、点、下划线或横线,首位为字母或数字;相同 `key` 更新原订阅并重新启用 |
581
586
  | `title` | 必填,订阅名称,最长 100 字 |
582
587
  | `reason` | 必填,为什么要主动找用户,最长 500 字 |
583
- | `sources` | 必填,触发来源,`[]` 表示不限 |
584
- | `eventTypes` | 必填,至少一个,触发事件类型 |
585
- | `schedule` | 可选,定时触发:`at` 为首次时刻(UTC 毫秒),`intervalMinutes` 为重复间隔(60~525600),按固定间隔算,不随夏令时调整。用它时 `eventTypes` 要含 `schedule.due`,`sources` 为 `[]` 或含 `schedule` |
588
+ | `trigger` | 必填,什么时候触发:`workout.completed`、`sleep.ready`、`{ type: "data.updated", dataset? }`,或 `{ type: "schedule", at, intervalMinutes? }`。一条订阅只对应一种触发 |
586
589
  | `quietHours` | 可选,免打扰时段,缺省 `{ start: "22:30", end: "08:00", timezone: "Asia/Shanghai" }` |
587
590
  | `cooldownMinutes` | 可选,冷却时间,缺省 120,取 0~10080 |
588
591
  | `dailyLimit` | 可选,每天最多几次,缺省 3,取 1~20 |
589
592
 
590
- 字段不合法、在群聊里调用,或同一用户对这只搭子已有 20 个未撤销的订阅时抛异常。
593
+ **模型工具**
594
+
595
+ 让模型自己决定是否订阅或关闭时,把需要的 `conversation.tools.proactive_*` 放进 `initialState.tools`,字段与上面那张表一样。什么场景可以订阅、何时只能查询或关闭,要在 `systemPrompt` 里说清楚:
596
+
597
+ ```ts
598
+ await BuddyServer.start((conversation) => ({
599
+ initialState: {
600
+ model: "kimi-k3",
601
+ systemPrompt: "你是跑步搭子。用户表达想坚持训练时,可以用 proactive_subscribe 订阅跑后复盘;用户要求关闭时先查询再关闭。",
602
+ tools: [conversation.tools.proactive_subscribe, conversation.tools.proactive_list, conversation.tools.proactive_unsubscribe, conversation.tools.data_access_query],
603
+ },
604
+ }));
605
+ ```
606
+
607
+ 代码订阅和模型订阅二选一或并用,产物都是一条订阅,只是谁决定何时订阅不同。
608
+
609
+ **限制与异常**
610
+
611
+ - 字段不合法、在群聊里调用,或同一用户对这只搭子已有 20 个 active 订阅时抛异常。
612
+ - 关闭订阅时,尚未完成的主动服务会同步取消。
591
613
 
592
- #### 主动服务的回合,不允许使用部分模型工具
614
+ **主动服务的回合,不允许使用部分模型工具**
593
615
 
594
616
  主动服务的回合里工具清单一个字都不动,平台工具和你自己的工具都照常执行,运行时不按回合性质拦任何一个。有时我们想禁用部分模型工具,比如一些写操作,可以使用 `beforeToolCall` 的钩子函数,用 `conversation.proactive.isRunning()` 能判断当前是不是主动服务的回合:
595
617
 
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/conversation/agent.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAExF,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7D,OAAO,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAiB,MAAM,aAAa,CAAC;AAE5I,MAAM,WAAW,SAAS;IACzB,mCAAmC;IACnC,cAAc,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,uDAAuD;IACvD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,WAAW,CAAC;IACjB,oCAAoC;IACpC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACnD;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,EAAE,sBAAsB,CAAC;IACxC,iFAAiF;IACjF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,kFAAkF;AAClF,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAyLxE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iHAAuB,CAAC;AAEtD,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAErF;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQxD"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/conversation/agent.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAExF,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7D,OAAO,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAiB,MAAM,aAAa,CAAC;AAE5I,MAAM,WAAW,SAAS;IACzB,mCAAmC;IACnC,cAAc,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,uDAAuD;IACvD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,WAAW,CAAC;IACjB,oCAAoC;IACpC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACnD;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,EAAE,sBAAsB,CAAC;IACxC,iFAAiF;IACjF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,kFAAkF;AAClF,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAwMxE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iHAAuB,CAAC;AAEtD,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAErF;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQxD"}