@my-life-buddies/buddy-runtime 0.25.0-dev.1 → 0.25.0-dev.2
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 +284 -105
- package/dist/mlbClient.d.ts +1 -1
- package/dist/mlbClient.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,10 +208,10 @@ interface Conversation {
|
|
|
208
208
|
|
|
209
209
|
## 3. 平台能力
|
|
210
210
|
|
|
211
|
-
| 能力 | `conversation` 对象方法 |
|
|
211
|
+
| 能力 | `conversation` 对象方法 | 内建模型工具(`conversation.tools` 上) |
|
|
212
212
|
|---|---|---|
|
|
213
213
|
| 资料 | `conversation.resource.list` / `conversation.resource.read` | `resource_list` / `resource_read` |
|
|
214
|
-
| 搜索 |
|
|
214
|
+
| 搜索 | `conversation.search.web` / `conversation.search.rednote` | `search_web` / `search_rednote` |
|
|
215
215
|
| 提问 | 无 | `ask_question` |
|
|
216
216
|
| 记忆 | `conversation.memory` | 无,开发者可以根据具体场景,自己组装成适合的模型工具 |
|
|
217
217
|
| 用户授权数据 | `conversation.dataAccess.query` / `conversation.dataAccess.request` | `data_access_query` / `data_access_request` |
|
|
@@ -228,6 +228,8 @@ buddy: (conversation) => ({
|
|
|
228
228
|
}),
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
+
`conversation` 上的方法在自己的模型工具里调,各节「`conversation` 对象方法」下都有一个完整的注册例子。
|
|
232
|
+
|
|
231
233
|
### 3.1 资料
|
|
232
234
|
|
|
233
235
|
搭子随身带的只读资料:把 `.md` 放进工程目录下的 `resources/`,模型先列出有哪些资料,再按需读一篇。资料在进程启动时一次读好,整场会话、每个回合读到的都是同一份。
|
|
@@ -251,14 +253,36 @@ my-buddy/ 工程目录
|
|
|
251
253
|
|
|
252
254
|
| 方法 | 做什么 |
|
|
253
255
|
|---|---|
|
|
254
|
-
| `list()` |
|
|
255
|
-
| `read(file)` |
|
|
256
|
+
| `resource.list()` | 列出全部资料的路径 |
|
|
257
|
+
| `resource.read(file)` | 读一篇的正文 |
|
|
256
258
|
|
|
257
259
|
```ts
|
|
258
|
-
|
|
260
|
+
resource: {
|
|
261
|
+
list(): string[]; // 相对 resources/ 的路径,按路径排序,如 research/01-writings.md
|
|
262
|
+
read(file: string): string; // file 照 list() 给的路径原样填;没有这篇抛异常
|
|
263
|
+
}
|
|
259
264
|
```
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
注册成模型工具:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
await BuddyServer.start((conversation) => {
|
|
270
|
+
const READ_NOTES_PARAMS = Type.Object({});
|
|
271
|
+
const readNotes: AgentTool<typeof READ_NOTES_PARAMS, undefined> = {
|
|
272
|
+
name: "read_notes",
|
|
273
|
+
label: "正在翻资料",
|
|
274
|
+
description: "读这只搭子带的写作研究笔记,回答写作方法的问题前先读。",
|
|
275
|
+
parameters: READ_NOTES_PARAMS,
|
|
276
|
+
execute: async () => {
|
|
277
|
+
const notes = conversation.resource.read("research/01-writings.md");
|
|
278
|
+
return { content: [{ type: "text", text: notes }], details: undefined };
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [readNotes] } };
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**内建模型工具**
|
|
262
286
|
|
|
263
287
|
把 `conversation.tools.resource_list` 和 `conversation.tools.resource_read` 放进 `initialState.tools`,模型就能先列出有哪些资料,再按路径读一篇。`systemPrompt` 里交代清楚这只搭子带了哪类资料、什么时候该去翻。
|
|
264
288
|
|
|
@@ -277,56 +301,76 @@ const notes = conversation.resource.read("research/01-writings.md");
|
|
|
277
301
|
|
|
278
302
|
| 方法 | 做什么 |
|
|
279
303
|
|---|---|
|
|
280
|
-
| `search.web(
|
|
281
|
-
| `search.rednote(
|
|
304
|
+
| `search.web(input)` | 搜站外网页,回结果数组 |
|
|
305
|
+
| `search.rednote(input)` | 搜小红书笔记,回结果数组 |
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
search: {
|
|
309
|
+
web(input: SearchWebRequest): Promise<SearchWebResult[]>;
|
|
310
|
+
rednote(input: SearchRednoteRequest): Promise<SearchRednoteResult[]>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface SearchWebRequest {
|
|
314
|
+
query: string; // 必填,1~120 字
|
|
315
|
+
topic?: "general" | "news"; // 缺省 general
|
|
316
|
+
timeRange?: "day" | "week" | "month" | "year"; // 缺省不限时间
|
|
317
|
+
maxResults?: number; // 1~20,缺省 20
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
interface SearchWebResult {
|
|
321
|
+
title: string;
|
|
322
|
+
url: string;
|
|
323
|
+
content: string; // 上游按查询词挑出的片段,不是整页
|
|
324
|
+
score: number; // 相关度,0~1
|
|
325
|
+
publishedAt?: string; // 上游给了才有
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface SearchRednoteRequest {
|
|
329
|
+
query: string; // 必填,1~120 字
|
|
330
|
+
timeRange?: "day" | "week" | "month" | "year"; // 缺省不限时间
|
|
331
|
+
maxResults?: number; // 1~40,缺省 20
|
|
332
|
+
includeComments?: boolean; // 缺省 false
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface SearchRednoteResult {
|
|
336
|
+
title: string;
|
|
337
|
+
url: string;
|
|
338
|
+
content: string; // 正文原文,不截断
|
|
339
|
+
score: number; // 相关度,0~1
|
|
340
|
+
publishedAt?: string; // 上游给了才有
|
|
341
|
+
likes?: string; // 点赞数所在的区间,如 "1000-10000",上游给了才有
|
|
342
|
+
collects?: string; // 收藏数所在的区间,同上
|
|
343
|
+
comments?: string[]; // 评论原文,每篇最多 10 条;includeComments 为 true 且有评论才有
|
|
344
|
+
}
|
|
345
|
+
```
|
|
282
346
|
|
|
283
|
-
|
|
347
|
+
注册成模型工具:
|
|
284
348
|
|
|
285
349
|
```ts
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
350
|
+
await BuddyServer.start((conversation) => {
|
|
351
|
+
const FIND_RECIPE_PARAMS = Type.Object({ dish: Type.String({ description: "菜名" }) });
|
|
352
|
+
const findRecipe: AgentTool<typeof FIND_RECIPE_PARAMS, undefined> = {
|
|
353
|
+
name: "find_recipe",
|
|
354
|
+
label: "正在找菜谱",
|
|
355
|
+
description: "按菜名找做法,只看小红书。",
|
|
356
|
+
parameters: FIND_RECIPE_PARAMS,
|
|
357
|
+
execute: async (_toolCallId, { dish }) => {
|
|
358
|
+
const notes = await conversation.search.rednote({ query: `${dish} 做法`, maxResults: 5 });
|
|
359
|
+
return { content: [{ type: "text", text: notes.map((n) => `${n.title}\n${n.url}\n${n.content}`).join("\n\n") }], details: undefined };
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [findRecipe] } };
|
|
363
|
+
});
|
|
297
364
|
```
|
|
298
365
|
|
|
299
|
-
|
|
366
|
+
**内建模型工具**
|
|
300
367
|
|
|
301
368
|
把需要的工具放进 `initialState.tools`;什么时候该搜、搜哪边、回答里怎么引用,在 `systemPrompt` 里交代。
|
|
302
369
|
|
|
303
370
|
| 工具 | 参数 | 结果 |
|
|
304
371
|
|---|---|---|
|
|
305
|
-
| `search_web` |
|
|
306
|
-
| `search_rednote` | `
|
|
307
|
-
|
|
308
|
-
`search_web` 每条结果的字段:
|
|
309
|
-
|
|
310
|
-
| 字段 | 说明 |
|
|
311
|
-
|---|---|
|
|
312
|
-
| `title` | 网页标题 |
|
|
313
|
-
| `url` | 链接 |
|
|
314
|
-
| `content` | 上游按查询词挑出的相关内容片段 |
|
|
315
|
-
| `score` | 相关度,0 到 1 |
|
|
316
|
-
| `publishedAt` | 发布时间,上游给了才有 |
|
|
317
|
-
|
|
318
|
-
`search_rednote` 每篇结果的字段:
|
|
319
|
-
|
|
320
|
-
| 字段 | 说明 |
|
|
321
|
-
|---|---|
|
|
322
|
-
| `title` | 笔记标题 |
|
|
323
|
-
| `url` | 笔记链接 |
|
|
324
|
-
| `content` | 正文原文 |
|
|
325
|
-
| `score` | 相关度,0 到 1 |
|
|
326
|
-
| `publishedAt` | 发布时间 |
|
|
327
|
-
| `likes` | 点赞数所在的区间,如 `1000-10000` |
|
|
328
|
-
| `collects` | 收藏数所在的区间,同上 |
|
|
329
|
-
| `comments` | 评论原文,每篇最多 10 条;`includeComments` 为 true 且有评论才有 |
|
|
372
|
+
| `search_web` | 同 `SearchWebRequest`,少 `maxResults`,固定 20。走哪家搜索引擎由平台定,开发者和模型都不选 | 最多 20 条 `SearchWebResult` |
|
|
373
|
+
| `search_rednote` | 同 `SearchRednoteRequest` | 最多 40 篇 `SearchRednoteResult` |
|
|
330
374
|
|
|
331
375
|
```ts
|
|
332
376
|
await BuddyServer.start((conversation) => ({
|
|
@@ -343,7 +387,7 @@ await BuddyServer.start((conversation) => ({
|
|
|
343
387
|
**限制与异常**
|
|
344
388
|
|
|
345
389
|
- `query` 超过 120 字、`maxResults` 超出 1~40 时参数校验不通过,工具不执行。
|
|
346
|
-
- 同一会话一分钟最多 12 次搜索,同一搭子全部会话一分钟最多 180
|
|
390
|
+
- 同一会话一分钟最多 12 次搜索,同一搭子全部会话一分钟最多 180 次;本地用开发令牌跑的 `<id>-dev` 身份两项都是 4 次。超出时方法与工具都抛异常(HTTP 429)。
|
|
347
391
|
- 平台搜索服务不可用或超时时工具抛异常,异常只带 HTTP 状态码,不带上游原文;模型可以告诉用户暂时搜不了,不应编造结果。
|
|
348
392
|
- 没有结果时工具正常返回「没有找到相关结果。可以换个说法再搜一次。」
|
|
349
393
|
- `search_rednote` 的正文和评论不截断,40 篇带评论的一次结果可能有几万字,会占用较多上下文;不需要那么多时让模型用缺省篇数。
|
|
@@ -356,7 +400,7 @@ await BuddyServer.start((conversation) => ({
|
|
|
356
400
|
|
|
357
401
|
没有。`conversation` 不提供发题卡的方法,题卡只由模型发。
|
|
358
402
|
|
|
359
|
-
|
|
403
|
+
**内建模型工具**
|
|
360
404
|
|
|
361
405
|
把 `conversation.tools.ask_question` 放进 `initialState.tools`,模型就能发题卡。
|
|
362
406
|
|
|
@@ -378,19 +422,45 @@ await BuddyServer.start((conversation) => ({
|
|
|
378
422
|
|
|
379
423
|
| 方法 | 做什么 |
|
|
380
424
|
|---|---|
|
|
381
|
-
| `list()` |
|
|
382
|
-
| `write(key, text)` | 覆盖写一条 |
|
|
383
|
-
| `delete(key)` |
|
|
425
|
+
| `memory.list()` | 列出全部记忆 |
|
|
426
|
+
| `memory.write(key, text)` | 覆盖写一条 |
|
|
427
|
+
| `memory.delete(key)` | 删一条 |
|
|
384
428
|
|
|
385
429
|
```ts
|
|
430
|
+
memory: {
|
|
431
|
+
list(): Promise<MemoryEntry[]>; // 按 key 字典序
|
|
432
|
+
write(key: string, text: string): Promise<void>; // 同一个 key 覆盖写;超配额、key 不合法时抛异常,文案是平台给的原因
|
|
433
|
+
delete(key: string): Promise<void>; // 不存在也算成功
|
|
434
|
+
}
|
|
435
|
+
|
|
386
436
|
interface MemoryEntry {
|
|
387
|
-
key: string;
|
|
437
|
+
key: string; // 主题名
|
|
388
438
|
text: string;
|
|
389
439
|
updatedAt: number; // 最后写入时刻,毫秒
|
|
390
440
|
}
|
|
391
441
|
```
|
|
392
442
|
|
|
393
|
-
|
|
443
|
+
注册成模型工具:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
await BuddyServer.start((conversation) => {
|
|
447
|
+
const RECALL_PARAMS = Type.Object({});
|
|
448
|
+
const recall: AgentTool<typeof RECALL_PARAMS, undefined> = {
|
|
449
|
+
name: "recall",
|
|
450
|
+
label: "正在翻印象",
|
|
451
|
+
description: "读出记过的全部印象,回答跟对方有关的问题前先读。",
|
|
452
|
+
parameters: RECALL_PARAMS,
|
|
453
|
+
execute: async () => {
|
|
454
|
+
const entries = await conversation.memory.list();
|
|
455
|
+
const text = entries.length === 0 ? "还没记过印象。" : entries.map((e) => `【${e.key}】\n${e.text}`).join("\n\n");
|
|
456
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [recall] } };
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**内建模型工具**
|
|
394
464
|
|
|
395
465
|
运行时不提供记忆工具。要让模型自己记东西,按这只搭子要记什么,自己包一个:
|
|
396
466
|
|
|
@@ -493,20 +563,53 @@ const sleep = dataAccessCapability("health.sleep");
|
|
|
493
563
|
|
|
494
564
|
| 方法 | 做什么 |
|
|
495
565
|
|---|---|
|
|
496
|
-
| `query(
|
|
497
|
-
| `request(
|
|
566
|
+
| `dataAccess.query(request)` | 查询已授权的 Dataset,每个各自一个状态 |
|
|
567
|
+
| `dataAccess.request(input)` | 发数据授权卡,这一轮到此停下 |
|
|
498
568
|
|
|
499
569
|
```ts
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
//
|
|
570
|
+
dataAccess: {
|
|
571
|
+
query(request: DataAccessQuery): Promise<DatasetAccessResult[]>;
|
|
572
|
+
request(input: DataAccessRequestInput & { idempotencyKey: string }): Promise<void>; // 用户处理授权卡后开新回合,再 query 一次
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
interface DataAccessQuery {
|
|
576
|
+
datasets: string[]; // Dataset id,如 health.sleep;一次最多 8 个
|
|
577
|
+
memberId?: string; // 私聊不传;群聊必传,取群消息开头 [名字 #m_…] 里的 m_…,查这位成员的数据,传错抛异常
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
type DatasetAccessResult =
|
|
581
|
+
| { id: string; status: "available"; schemaVersion: number; revision: number; sourceUpdatedAt: number; data: Record<string, unknown> } // data.items 的字段见上面各 Dataset 那张表
|
|
582
|
+
| { id: string; status: "notDeclared" | "notGranted" | "disabledInGroup" | "unavailable" | "systemPermissionDenied" };
|
|
583
|
+
|
|
584
|
+
interface DataAccessRequestInput {
|
|
585
|
+
reason: string; // 为什么要这些数据,用户在授权卡上看到的就是这句
|
|
586
|
+
datasets: string[]; // 只能是 data-access.json 里声明过的
|
|
587
|
+
memberId?: string; // 同 query
|
|
503
588
|
}
|
|
504
589
|
```
|
|
505
590
|
|
|
506
|
-
|
|
507
|
-
|
|
591
|
+
注册成模型工具:
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
await BuddyServer.start((conversation) => {
|
|
595
|
+
const SLEEP_PARAMS = Type.Object({});
|
|
596
|
+
const readSleep: AgentTool<typeof SLEEP_PARAMS, undefined> = {
|
|
597
|
+
name: "read_sleep",
|
|
598
|
+
label: "正在看睡眠",
|
|
599
|
+
description: "读用户最近的睡眠记录,聊作息时先调。",
|
|
600
|
+
parameters: SLEEP_PARAMS,
|
|
601
|
+
execute: async () => {
|
|
602
|
+
const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
|
|
603
|
+
if (sleep.status !== "available") return { content: [{ type: "text", text: `睡眠数据现在拿不到:${sleep.status}` }], details: undefined };
|
|
604
|
+
const items = sleep.data.items as { date: string; totalSleepMinutes: number }[];
|
|
605
|
+
return { content: [{ type: "text", text: items.map((d) => `${d.date}:睡了 ${d.totalSleepMinutes} 分钟`).join("\n") }], details: undefined };
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [readSleep] } };
|
|
609
|
+
});
|
|
610
|
+
```
|
|
508
611
|
|
|
509
|
-
|
|
612
|
+
**内建模型工具**
|
|
510
613
|
|
|
511
614
|
把 `conversation.tools.data_access_query`、`data_access_request` 放进 `initialState.tools`,模型就能自己查数据、发授权卡。放进去只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权;没声明 Dataset 时照常注册,查询的每一项都回 `notDeclared`。
|
|
512
615
|
|
|
@@ -631,33 +734,78 @@ my-buddy/ 工程目录
|
|
|
631
734
|
|
|
632
735
|
| 方法 | 做什么 |
|
|
633
736
|
|---|---|
|
|
634
|
-
| `list()` | 列出这场会话里的小挂件,不带记录 |
|
|
635
|
-
| `create(
|
|
636
|
-
| `read(id)` | 读整个小挂件,含各模块的记录 |
|
|
637
|
-
| `readModule(id, module)` |
|
|
638
|
-
| `rename(id, title)` |
|
|
639
|
-
| `writeRecord(id,
|
|
640
|
-
| `deleteRecord(id, module, recordId)` | 删一条记录 |
|
|
641
|
-
| `setPresentation(id,
|
|
642
|
-
| `send(id,
|
|
737
|
+
| `widget.list()` | 列出这场会话里的小挂件,不带记录 |
|
|
738
|
+
| `widget.create(input)` | 新建一个 |
|
|
739
|
+
| `widget.read(id)` | 读整个小挂件,含各模块的记录 |
|
|
740
|
+
| `widget.readModule(id, module)` | 读一个模块 |
|
|
741
|
+
| `widget.rename(id, title)` | 改标题 |
|
|
742
|
+
| `widget.writeRecord(id, input)` | 写一条记录 |
|
|
743
|
+
| `widget.deleteRecord(id, module, recordId)` | 删一条记录 |
|
|
744
|
+
| `widget.setPresentation(id, input)` | 设聊天里卡片的摘要和封面图 |
|
|
745
|
+
| `widget.send(id, options)` | 把小挂件发进聊天 |
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
widget: {
|
|
749
|
+
list(): Promise<WidgetSummary[]>;
|
|
750
|
+
create(input: { type: string; title: string; idempotencyKey?: string }): Promise<{ id: string }>; // 同一个 idempotencyKey 重复建回同一个 id
|
|
751
|
+
read(id: string): Promise<Widget>;
|
|
752
|
+
readModule(id: string, module: string): Promise<{ records: WidgetRecord[] }>;
|
|
753
|
+
rename(id: string, title: string): Promise<void>; // id 不变,聊天里发过的卡片跟着显示新标题
|
|
754
|
+
writeRecord(id: string, input: { module: string; recordId: string; data: Record<string, unknown>; expectedRevision?: number }): Promise<{ revision?: number }>; // 已存在就整条覆盖;带 expectedRevision 时版本号对不上平台回 409
|
|
755
|
+
deleteRecord(id: string, module: string, recordId: string): Promise<void>;
|
|
756
|
+
setPresentation(id: string, input: { summary: string; imageMediaId: string }): Promise<void>; // summary 不超过 500 字;imageMediaId 传空串去掉封面
|
|
757
|
+
send(id: string, options: { idempotencyKey: string; openTarget?: WidgetOpenTarget }): void; // 放进发送队列就返回,不等平台确认送达;同一个 idempotencyKey 重复发只落一条
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
interface WidgetSummary {
|
|
761
|
+
id: string;
|
|
762
|
+
type: string; // 开发者登记的类型 id
|
|
763
|
+
title: string;
|
|
764
|
+
updatedAt: number; // 最后改动时刻,毫秒
|
|
765
|
+
canEdit: boolean; // 这场会话能不能改:别的会话转发来的引用是 false,改名、写记录、删除、设卡片平台都回 403
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
interface Widget extends WidgetSummary {
|
|
769
|
+
modules: Record<string, WidgetRecord[]>;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
interface WidgetRecord {
|
|
773
|
+
id: string;
|
|
774
|
+
data: Record<string, unknown>;
|
|
775
|
+
revision: number; // 从 1 起,每次覆盖加一;写的时候带上它当 expectedRevision,防止互相覆盖
|
|
776
|
+
updatedAt: number;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
interface WidgetOpenTarget {
|
|
780
|
+
version: 1;
|
|
781
|
+
entry: string; // schema.json 里声明过的语义入口
|
|
782
|
+
resource?: { module: string; recordId: string }; // 点开直接落到这条记录
|
|
783
|
+
state?: Record<string, unknown>;
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
注册成模型工具:
|
|
643
788
|
|
|
644
789
|
```ts
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
790
|
+
await BuddyServer.start((conversation) => {
|
|
791
|
+
const MENU_PARAMS = Type.Object({ date: Type.String({ description: "哪天的菜单,YYYY-MM-DD" }), dishes: Type.Array(Type.String()) });
|
|
792
|
+
const writeMenu: AgentTool<typeof MENU_PARAMS, undefined> = {
|
|
793
|
+
name: "write_menu",
|
|
794
|
+
label: "正在排菜单",
|
|
795
|
+
description: "把一天的菜写进「本周菜单」小挂件并发进聊天。",
|
|
796
|
+
parameters: MENU_PARAMS,
|
|
797
|
+
execute: async (toolCallId, { date, dishes }) => {
|
|
798
|
+
const { id } = await conversation.widget.create({ type: "menu", title: "本周菜单", idempotencyKey: "menu:weekly" });
|
|
799
|
+
await conversation.widget.writeRecord(id, { module: "菜单", recordId: date, data: { dishes } });
|
|
800
|
+
conversation.widget.send(id, { idempotencyKey: `menu-send:${toolCallId}`, openTarget: { version: 1, entry: "menu-day", resource: { module: "菜单", recordId: date } } });
|
|
801
|
+
return { content: [{ type: "text", text: `${date} 的菜单排好了。` }], details: undefined };
|
|
654
802
|
},
|
|
655
|
-
}
|
|
656
|
-
return {
|
|
657
|
-
}
|
|
803
|
+
};
|
|
804
|
+
return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [writeMenu] } };
|
|
805
|
+
});
|
|
658
806
|
```
|
|
659
807
|
|
|
660
|
-
|
|
808
|
+
**内建模型工具**
|
|
661
809
|
|
|
662
810
|
把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`,模型就能自己建、读、改、发小挂件。
|
|
663
811
|
|
|
@@ -689,35 +837,66 @@ execute: async (toolCallId, params) => {
|
|
|
689
837
|
|
|
690
838
|
| 方法 | 做什么 |
|
|
691
839
|
|---|---|
|
|
692
|
-
| `subscribe(key, request)` | 按稳定 `key`
|
|
693
|
-
| `list()` |
|
|
694
|
-
| `unsubscribe(key)` |
|
|
695
|
-
| `unsubscribeAll()` |
|
|
696
|
-
| `isRunning()` | 现在是不是主动服务的回合 |
|
|
840
|
+
| `proactive.subscribe(key, request)` | 按稳定 `key` 创建或更新一条订阅 |
|
|
841
|
+
| `proactive.list()` | 当前会话的全部订阅 |
|
|
842
|
+
| `proactive.unsubscribe(key)` | 关闭一条订阅 |
|
|
843
|
+
| `proactive.unsubscribeAll()` | 关闭全部订阅 |
|
|
844
|
+
| `proactive.isRunning()` | 现在是不是主动服务的回合 |
|
|
697
845
|
|
|
698
846
|
它挂在这场会话的 `conversation` 上,凡是拿得到 `conversation` 的地方都能调:自己工具的 `execute` 里、`beforeToolCall` / `afterToolCall` 里、配置工厂函数体里。
|
|
699
847
|
|
|
700
848
|
```ts
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
849
|
+
proactive: {
|
|
850
|
+
subscribe(key: string, request: ProactiveSubscriptionRequest): Promise<ProactiveSubscription>; // key 1~64 位小写字母、数字、点、下划线或横线,首位为字母或数字;相同 key 更新原订阅并重新启用
|
|
851
|
+
list(): Promise<ProactiveSubscription[]>; // 包括已关闭的
|
|
852
|
+
unsubscribe(key: string): Promise<boolean>; // 回是否找到
|
|
853
|
+
unsubscribeAll(): Promise<number>; // 回关掉几条
|
|
854
|
+
isRunning(): boolean;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
interface ProactiveSubscriptionRequest {
|
|
858
|
+
title: string; // 订阅名称,最长 100 字
|
|
859
|
+
reason: string; // 为什么要主动找用户,最长 500 字
|
|
860
|
+
trigger: // 什么时候触发,一条订阅只对应一种
|
|
861
|
+
| { type: "workout.completed" }
|
|
862
|
+
| { type: "sleep.ready" }
|
|
863
|
+
| { type: "data.updated"; dataset?: DataAccessDatasetId }
|
|
864
|
+
| { type: "schedule"; at: number; intervalMinutes?: number }; // at 是 UTC 毫秒
|
|
865
|
+
quietHours?: { start: string; end: string; timezone: string }; // 免打扰时段,缺省 { start: "22:30", end: "08:00", timezone: "Asia/Shanghai" }
|
|
866
|
+
cooldownMinutes?: number; // 冷却时间,缺省 120,取 0~10080
|
|
867
|
+
dailyLimit?: number; // 每天最多几次,缺省 3,取 1~20
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
interface ProactiveSubscription extends Required<ProactiveSubscriptionRequest> {
|
|
871
|
+
key: string;
|
|
872
|
+
status: "active" | "revoked";
|
|
873
|
+
}
|
|
706
874
|
```
|
|
707
875
|
|
|
708
|
-
|
|
876
|
+
注册成模型工具:
|
|
709
877
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
878
|
+
```ts
|
|
879
|
+
await BuddyServer.start((conversation) => {
|
|
880
|
+
const REVIEW_PARAMS = Type.Object({ hour: Type.Integer({ minimum: 0, maximum: 23, description: "每周一几点复盘,24 小时制" }) });
|
|
881
|
+
const subscribeReview: AgentTool<typeof REVIEW_PARAMS, undefined> = {
|
|
882
|
+
name: "subscribe_review",
|
|
883
|
+
label: "正在订每周复盘",
|
|
884
|
+
description: "用户想坚持训练时,订一条每周一次的复盘。",
|
|
885
|
+
parameters: REVIEW_PARAMS,
|
|
886
|
+
execute: async (_toolCallId, { hour }) => {
|
|
887
|
+
await conversation.proactive.subscribe("weekly-review", {
|
|
888
|
+
title: "每周复盘",
|
|
889
|
+
reason: "在约定时间结合最近聊天做一次简短复盘",
|
|
890
|
+
trigger: { type: "schedule", at: nextMondayAt(hour), intervalMinutes: 7 * 24 * 60 }, // nextMondayAt 算下个周一那个点的 UTC 毫秒,自己实现
|
|
891
|
+
});
|
|
892
|
+
return { content: [{ type: "text", text: `订好了,每周一 ${hour} 点复盘。` }], details: undefined };
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [subscribeReview] } };
|
|
896
|
+
});
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**内建模型工具**
|
|
721
900
|
|
|
722
901
|
让模型自己决定是否订阅或关闭时,把需要的 `conversation.tools.proactive_*` 放进 `initialState.tools`,字段与上面那张表一样。什么场景可以订阅、何时只能查询或关闭,要在 `systemPrompt` 里说清楚:
|
|
723
902
|
|
package/dist/mlbClient.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface WidgetType {
|
|
|
45
45
|
* package.json(测试机上的 fitness-buddy 就是这样,它那份没有 version 字段)。常量会被打包器原样搬走。
|
|
46
46
|
* 发版时由 scripts/release.ts 连同 package.json 一起改,scripts/check-publish.ts 在 npm publish 前比对两处是否一致。
|
|
47
47
|
*/
|
|
48
|
-
export declare const RUNTIME_VERSION = "0.25.0-dev.
|
|
48
|
+
export declare const RUNTIME_VERSION = "0.25.0-dev.2";
|
|
49
49
|
export declare const MAX_DATASETS_PER_QUERY = 8;
|
|
50
50
|
export declare const MAX_DATA_ACCESS_RESPONSE_BYTES: number;
|
|
51
51
|
/** 不向模型暴露服务端原文、凭据或临时媒体地址。写入失败不自动重试。 */
|
package/dist/mlbClient.js
CHANGED
|
@@ -11,7 +11,7 @@ import { errorText } from "./log.js";
|
|
|
11
11
|
* package.json(测试机上的 fitness-buddy 就是这样,它那份没有 version 字段)。常量会被打包器原样搬走。
|
|
12
12
|
* 发版时由 scripts/release.ts 连同 package.json 一起改,scripts/check-publish.ts 在 npm publish 前比对两处是否一致。
|
|
13
13
|
*/
|
|
14
|
-
export const RUNTIME_VERSION = "0.25.0-dev.
|
|
14
|
+
export const RUNTIME_VERSION = "0.25.0-dev.2";
|
|
15
15
|
/** 除事件流以外的请求都限时 30 秒:卡死的请求超时后按网络错误处理,调用方决定重不重发 */
|
|
16
16
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
17
17
|
export const MAX_DATASETS_PER_QUERY = 8;
|