pi-remote-feishu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/ARCHITECTURE.md +837 -0
  2. package/ARCHITECTURE.zh-CN.md +704 -0
  3. package/README.md +118 -0
  4. package/dist/attachments/mime.d.ts +11 -0
  5. package/dist/attachments/mime.js +27 -0
  6. package/dist/attachments/mime.js.map +1 -0
  7. package/dist/attachments/processor.d.ts +29 -0
  8. package/dist/attachments/processor.js +48 -0
  9. package/dist/attachments/processor.js.map +1 -0
  10. package/dist/attachments/temp-files.d.ts +16 -0
  11. package/dist/attachments/temp-files.js +32 -0
  12. package/dist/attachments/temp-files.js.map +1 -0
  13. package/dist/bin/pi-remote-feishu.d.ts +2 -0
  14. package/dist/bin/pi-remote-feishu.js +359 -0
  15. package/dist/bin/pi-remote-feishu.js.map +1 -0
  16. package/dist/bridge/card-actions.d.ts +13 -0
  17. package/dist/bridge/card-actions.js +156 -0
  18. package/dist/bridge/card-actions.js.map +1 -0
  19. package/dist/bridge/conversation-router.d.ts +21 -0
  20. package/dist/bridge/conversation-router.js +65 -0
  21. package/dist/bridge/conversation-router.js.map +1 -0
  22. package/dist/bridge/message-handler.d.ts +22 -0
  23. package/dist/bridge/message-handler.js +218 -0
  24. package/dist/bridge/message-handler.js.map +1 -0
  25. package/dist/bridge/message-normalizer.d.ts +17 -0
  26. package/dist/bridge/message-normalizer.js +81 -0
  27. package/dist/bridge/message-normalizer.js.map +1 -0
  28. package/dist/bridge/prompt-queue.d.ts +20 -0
  29. package/dist/bridge/prompt-queue.js +39 -0
  30. package/dist/bridge/prompt-queue.js.map +1 -0
  31. package/dist/bridge/runtime-host.d.ts +45 -0
  32. package/dist/bridge/runtime-host.js +133 -0
  33. package/dist/bridge/runtime-host.js.map +1 -0
  34. package/dist/bridge/session-host-manager.d.ts +59 -0
  35. package/dist/bridge/session-host-manager.js +143 -0
  36. package/dist/bridge/session-host-manager.js.map +1 -0
  37. package/dist/bridge/stream-renderer.d.ts +50 -0
  38. package/dist/bridge/stream-renderer.js +108 -0
  39. package/dist/bridge/stream-renderer.js.map +1 -0
  40. package/dist/bridge/ui-context.d.ts +19 -0
  41. package/dist/bridge/ui-context.js +150 -0
  42. package/dist/bridge/ui-context.js.map +1 -0
  43. package/dist/cards/common.d.ts +33 -0
  44. package/dist/cards/common.js +84 -0
  45. package/dist/cards/common.js.map +1 -0
  46. package/dist/cards/help.d.ts +11 -0
  47. package/dist/cards/help.js +31 -0
  48. package/dist/cards/help.js.map +1 -0
  49. package/dist/cards/models.d.ts +24 -0
  50. package/dist/cards/models.js +46 -0
  51. package/dist/cards/models.js.map +1 -0
  52. package/dist/cards/permission.d.ts +9 -0
  53. package/dist/cards/permission.js +17 -0
  54. package/dist/cards/permission.js.map +1 -0
  55. package/dist/cards/sessions.d.ts +11 -0
  56. package/dist/cards/sessions.js +54 -0
  57. package/dist/cards/sessions.js.map +1 -0
  58. package/dist/cards/status.d.ts +15 -0
  59. package/dist/cards/status.js +20 -0
  60. package/dist/cards/status.js.map +1 -0
  61. package/dist/cards/stop.d.ts +16 -0
  62. package/dist/cards/stop.js +24 -0
  63. package/dist/cards/stop.js.map +1 -0
  64. package/dist/config/load-config.d.ts +19 -0
  65. package/dist/config/load-config.js +51 -0
  66. package/dist/config/load-config.js.map +1 -0
  67. package/dist/config/schema.d.ts +52 -0
  68. package/dist/config/schema.js +260 -0
  69. package/dist/config/schema.js.map +1 -0
  70. package/dist/extensions/index.d.ts +7 -0
  71. package/dist/extensions/index.js +71 -0
  72. package/dist/extensions/index.js.map +1 -0
  73. package/dist/extensions/lark-cli-guard.d.ts +12 -0
  74. package/dist/extensions/lark-cli-guard.js +75 -0
  75. package/dist/extensions/lark-cli-guard.js.map +1 -0
  76. package/dist/feishu/channel.d.ts +28 -0
  77. package/dist/feishu/channel.js +191 -0
  78. package/dist/feishu/channel.js.map +1 -0
  79. package/dist/feishu/context.d.ts +5 -0
  80. package/dist/feishu/context.js +19 -0
  81. package/dist/feishu/context.js.map +1 -0
  82. package/dist/feishu/webhook.d.ts +5 -0
  83. package/dist/feishu/webhook.js +8 -0
  84. package/dist/feishu/webhook.js.map +1 -0
  85. package/dist/index.d.ts +30 -0
  86. package/dist/index.js +29 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/store/json-store.d.ts +25 -0
  89. package/dist/store/json-store.js +105 -0
  90. package/dist/store/json-store.js.map +1 -0
  91. package/dist/store/store.d.ts +7 -0
  92. package/dist/store/store.js +15 -0
  93. package/dist/store/store.js.map +1 -0
  94. package/dist/tools/send-file-to-chat.d.ts +26 -0
  95. package/dist/tools/send-file-to-chat.js +85 -0
  96. package/dist/tools/send-file-to-chat.js.map +1 -0
  97. package/dist/types.d.ts +300 -0
  98. package/dist/types.js +2 -0
  99. package/dist/types.js.map +1 -0
  100. package/package.json +67 -0
  101. package/skills/lark-doc-cli/SKILL.md +56 -0
  102. package/skills/lark-im-readonly/SKILL.md +49 -0
@@ -0,0 +1,704 @@
1
+ # Pi 飞书扩展架构设计
2
+
3
+ ## 目标
4
+
5
+ 设计一个面向 Pi Agent 的飞书集成扩展包,让用户可以在飞书私聊和群聊里向 Pi Agent 发送消息,并把 Pi 的回复、卡片、权限确认、文件回传等能力投射回飞书。
6
+
7
+ 这不是简单复制 `pi-remote-feishu-cli`。参考包证明了飞书消息、卡片、流式输出、附件和 `AgentSessionRuntime` 可以跑通,但目标架构应该更清晰地分离“飞书长连接进程”和“Pi 扩展注册能力”,这样后续能安装、部署、测试和演进。
8
+
9
+ MVP 聚焦一个飞书应用服务一个团队内的私聊和群聊。完整多租户不是 MVP 目标。可以在数据结构中保留 `tenantKey` 字段,方便未来扩展,但第一版不要为多企业共享服务支付复杂度。
10
+
11
+ ## 核心判断
12
+
13
+ Pi 的 `AgentSessionRuntime` 是单 active session 模型:
14
+
15
+ ```text
16
+ AgentSessionRuntime
17
+ -> runtime.session
18
+ -> runtime.switchSession()
19
+ -> runtime.newSession()
20
+ ```
21
+
22
+ 这适合终端 TUI,因为终端天然只有一个当前会话。但飞书是多用户、多群、多消息入口。如果所有飞书消息共用一个 `runtime.session`,会出现:
23
+
24
+ - A 用户消息进入 B 用户上下文。
25
+ - A 正在生成时,B 的消息触发 session 切换。
26
+ - B 点击 stop 误中断 A 的任务。
27
+ - 权限确认卡片串线。
28
+ - 流式输出发错群或发错私聊。
29
+
30
+ 因此架构里要引入:
31
+
32
+ ```text
33
+ ConversationRouter
34
+ -> SessionHostManager
35
+ -> Map<sessionKey, SessionHost>
36
+ ```
37
+
38
+ 每个 `SessionHost` 拥有自己的 runtime、queue、active run 和飞书上下文。
39
+
40
+ ## 设计原则
41
+
42
+ 1. 不改 Pi core。
43
+ 飞书应该作为外部 transport 接入,尽量不侵入 `pi-main`。
44
+
45
+ 2. 把飞书作为正式 transport。
46
+ 不要把它做成“终端外挂”。它需要独立的消息归一化、会话映射、权限策略、UI 桥接和渲染层。
47
+
48
+ 3. 分离 transport host 和 Pi extension。
49
+ 接收飞书消息需要一个长期运行的进程;注册工具、命令和 prompt guidance 属于 Pi extension。
50
+
51
+ 4. 私聊和群聊分开建模。
52
+ 私聊默认按用户隔离;群聊默认一个群共享一个会话,并要求 @bot 才响应。
53
+
54
+ 5. 不提前做复杂多租户。
55
+ 保留 `tenantKey` 字段,但 MVP 不做 tenant router、tenant policy、tenant billing 这类设计。
56
+
57
+ 6. 每个会话独立并发。
58
+ 同一个私聊或群聊内部串行,不同私聊和群聊可以并行。
59
+
60
+ ## 推荐包结构
61
+
62
+ ```text
63
+ pi-remote-feishu/
64
+ package.json
65
+ README.md
66
+ ARCHITECTURE.md
67
+ ARCHITECTURE.zh-CN.md
68
+ src/
69
+ bin/
70
+ pi-remote-feishu.ts
71
+ config/
72
+ load-config.ts
73
+ schema.ts
74
+ feishu/
75
+ channel.ts
76
+ webhook.ts
77
+ verifier.ts
78
+ types.ts
79
+ bridge/
80
+ runtime-host.ts
81
+ conversation-router.ts
82
+ session-host-manager.ts
83
+ message-normalizer.ts
84
+ message-handler.ts
85
+ stream-renderer.ts
86
+ ui-context.ts
87
+ card-actions.ts
88
+ prompt-queue.ts
89
+ cards/
90
+ help.ts
91
+ models.ts
92
+ sessions.ts
93
+ stop.ts
94
+ permission.ts
95
+ result.ts
96
+ attachments/
97
+ processor.ts
98
+ mime.ts
99
+ temp-files.ts
100
+ store/
101
+ store.ts
102
+ json-store.ts
103
+ sqlite-store.ts
104
+ tools/
105
+ send-file-to-chat.ts
106
+ extensions/
107
+ index.ts
108
+ test/
109
+ suite/
110
+ ```
111
+
112
+ `package.json` 同时暴露 CLI 和 Pi package 资源:
113
+
114
+ ```json
115
+ {
116
+ "name": "pi-remote-feishu",
117
+ "type": "module",
118
+ "bin": {
119
+ "pi-remote-feishu": "./dist/bin/pi-remote-feishu.js"
120
+ },
121
+ "pi": {
122
+ "extensions": [
123
+ "./dist/extensions/index.js"
124
+ ]
125
+ }
126
+ }
127
+ ```
128
+
129
+ `bin` 用来启动飞书服务进程;`pi.extensions` 让 Pi 的包管理器发现并加载扩展。
130
+
131
+ ## 总体架构
132
+
133
+ ```text
134
+ Feishu 用户
135
+ -> 飞书 WebSocket 或 Webhook
136
+ -> 事件校验
137
+ -> 消息归一化
138
+ -> 权限和路由
139
+ -> ConversationRouter
140
+ -> SessionHostManager
141
+ -> SessionHost
142
+ -> per-session PromptQueue
143
+ -> per-session AgentSessionRuntime
144
+ -> StreamRenderer
145
+ -> 飞书文本/卡片/文件响应
146
+
147
+ Pi extension system
148
+ -> extensions/index.ts
149
+ -> 注册 send_file_to_chat
150
+ -> 注册 /feishu 命令
151
+ -> 把 ExtensionUIContext 桥接到飞书卡片
152
+ ```
153
+
154
+ 关键拆分:
155
+
156
+ ```text
157
+ Transport Host
158
+ 长期运行的进程,负责连接飞书、接收消息、分发事件。
159
+
160
+ Pi Extension
161
+ 被 Pi 加载,负责注册工具、命令、prompt guidance 和 UI 能力。
162
+ ```
163
+
164
+ ## 私聊和群聊会话模型
165
+
166
+ 推荐 MVP key:
167
+
168
+ ```text
169
+ 私聊:
170
+ dm:{userOpenId}
171
+
172
+ 群聊共享会话:
173
+ group:{chatId}
174
+
175
+ 群聊按用户隔离,可选:
176
+ group-user:{chatId}:{userOpenId}
177
+ ```
178
+
179
+ 默认策略:
180
+
181
+ ```ts
182
+ interface FeishuConversationPolicy {
183
+ privateScope: "per-user";
184
+ groupScope: "shared-chat";
185
+ requireMentionInGroup: true;
186
+ }
187
+ ```
188
+
189
+ 行为:
190
+
191
+ - 私聊:每个用户一个 Pi session。
192
+ - 群聊:每个群一个共享 Pi session。
193
+ - 群聊必须 @bot 才响应。
194
+ - 群聊 prompt 里带发送者信息。
195
+ - 未来可以让指定群开启 `group-user` 模式。
196
+
197
+ 群聊 prompt 示例:
198
+
199
+ ```text
200
+ [Feishu group message]
201
+ Sender: 张三
202
+ Message:
203
+ 帮我看一下这个报错
204
+ ```
205
+
206
+ 这样 Pi 能知道群里是谁在说话。
207
+
208
+ ## SessionHost 设计
209
+
210
+ 不要让所有用户共用一个 runtime。推荐:
211
+
212
+ ```ts
213
+ interface SessionHost {
214
+ sessionKey: string;
215
+ runtime: AgentSessionRuntime;
216
+ queue: PromptQueue;
217
+ activeRun?: ActiveRun;
218
+ lastUsedAt: Date;
219
+ }
220
+ ```
221
+
222
+ 运行时结构:
223
+
224
+ ```text
225
+ FeishuBotHost
226
+ -> ConversationRouter
227
+ -> SessionHostManager
228
+ -> dm:alice -> SessionHost A -> Runtime A
229
+ -> dm:bob -> SessionHost B -> Runtime B
230
+ -> group:chat-123 -> SessionHost C -> Runtime C
231
+ ```
232
+
233
+ 并发规则:
234
+
235
+ - 同一个 `sessionKey` 内部串行。
236
+ - 不同 `sessionKey` 可以并行。
237
+ - `/stop` 只中断当前 `sessionKey` 的 active run。
238
+ - 权限卡片只 resolve 当前 run 的 pending dialog。
239
+ - 流式输出只发送回当前 chat。
240
+
241
+ 生命周期:
242
+
243
+ - 首次消息到达时创建 `SessionHost`。
244
+ - 活跃期间复用 runtime。
245
+ - 空闲超过 TTL 后释放 runtime。
246
+ - 释放前持久化 session file 映射。
247
+ - 下次消息到达时从 session file 恢复。
248
+
249
+ ## 核心模块说明
250
+
251
+ ### `feishu/channel.ts`
252
+
253
+ 封装飞书 SDK。
254
+
255
+ 职责:
256
+
257
+ - 创建飞书 WebSocket client。
258
+ - 接收消息和卡片 action。
259
+ - 发送文本、markdown、卡片、图片、文件。
260
+ - 通过 message id 或 token 更新卡片。
261
+ - 下载消息附件资源。
262
+ - 屏蔽 SDK 的原始类型细节。
263
+
264
+ ### `feishu/webhook.ts`
265
+
266
+ Webhook transport。
267
+
268
+ 职责:
269
+
270
+ - 启动 HTTP server。
271
+ - 处理飞书 URL challenge。
272
+ - 校验 timestamp、signature、verification token。
273
+ - 解密加密事件。
274
+ - 把 webhook payload 转成统一事件模型。
275
+
276
+ MVP 可以先不做 webhook,先做 WebSocket。
277
+
278
+ ### `bridge/conversation-router.ts`
279
+
280
+ 把飞书消息映射成 `sessionKey`。
281
+
282
+ 职责:
283
+
284
+ - 判断私聊还是群聊。
285
+ - 群聊检查是否 @bot。
286
+ - 生成 `dm:*`、`group:*` 或 `group-user:*`。
287
+ - 提供可配置 group scope。
288
+ - 保留 `tenantKey` 作为未来扩展字段。
289
+
290
+ ### `bridge/session-host-manager.ts`
291
+
292
+ 管理所有活跃 `SessionHost`。
293
+
294
+ 职责:
295
+
296
+ - 根据 `sessionKey` 查找或创建 host。
297
+ - 从 store 恢复 session file。
298
+ - 空闲回收 runtime。
299
+ - 管理 active run。
300
+ - 保证 stop、stream、permission、context 不串线。
301
+
302
+ ### `bridge/runtime-host.ts`
303
+
304
+ 管理单个 Pi runtime。
305
+
306
+ 职责:
307
+
308
+ - 创建 `AgentSessionRuntime`。
309
+ - 打开指定 session file。
310
+ - 绑定 extensions。
311
+ - 暴露 `prompt()`、`abort()`、`setModel()`、`listSessions()` 等能力。
312
+ - 在一次飞书请求执行期间设置 Feishu context 和 Feishu UI context。
313
+
314
+ ### `bridge/prompt-queue.ts`
315
+
316
+ 每个 `SessionHost` 一个 queue。
317
+
318
+ 职责:
319
+
320
+ - 同会话消息串行。
321
+ - 不同会话不互相阻塞。
322
+ - 保存当前 active run。
323
+ - 支持 abort。
324
+
325
+ 不要使用全局锁。全局锁会导致一个群生成很久时,所有私聊和其他群都被阻塞。
326
+
327
+ ### `bridge/message-normalizer.ts`
328
+
329
+ 把飞书消息转换成 Pi 输入。
330
+
331
+ 归一化后的结构:
332
+
333
+ ```ts
334
+ interface NormalizedPiInput {
335
+ sessionKey: string;
336
+ chatId: string;
337
+ messageId: string;
338
+ userId: string;
339
+ tenantKey?: string;
340
+ chatType: "private" | "group";
341
+ senderName?: string;
342
+ text: string;
343
+ images: Array<{ type: "image"; data: string; mimeType: string }>;
344
+ attachmentNotes: string[];
345
+ command?: FeishuCommand;
346
+ }
347
+ ```
348
+
349
+ 职责:
350
+
351
+ - 去掉群聊中的 bot mention。
352
+ - 识别 `/help`、`/sessions`、`/models`、`/stop`。
353
+ - 忽略不支持的消息类型。
354
+ - 保留 sender 信息。
355
+ - 合并附件文本。
356
+
357
+ ### `bridge/stream-renderer.ts`
358
+
359
+ 把 Pi 事件渲染到飞书。
360
+
361
+ 职责:
362
+
363
+ - 流式输出 assistant 文本。
364
+ - 渲染 thinking。
365
+ - 渲染 tool start/update/end。
366
+ - 渲染 retry、compaction、error 状态。
367
+ - 完成后更新最终卡片。
368
+
369
+ 策略:
370
+
371
+ - 文本 delta 流式展示。
372
+ - thinking 默认折叠或引用展示。
373
+ - tool 输出默认简短。
374
+ - 大输出可以转文件发送。
375
+
376
+ ### `bridge/ui-context.ts`
377
+
378
+ 把 Pi 的 `ExtensionUIContext` 映射到飞书卡片。
379
+
380
+ 这是一处亮点,因为它让现有 Pi extension 的交互能力可以远程使用。
381
+
382
+ 映射:
383
+
384
+ ```text
385
+ ctx.ui.confirm()
386
+ -> 飞书确认卡片
387
+ -> 用户点击
388
+ -> resolve Promise
389
+ -> 工具继续执行或被拒绝
390
+
391
+ ctx.ui.select()
392
+ -> 飞书选择卡片
393
+
394
+ ctx.ui.notify()
395
+ -> 飞书文本或提示卡片
396
+ ```
397
+
398
+ 注意:
399
+
400
+ - 卡片 action 必须能在 prompt 等待期间被处理。
401
+ - 如果飞书 SDK 对同一个 chat 串行化 message 和 card action,要关闭 SDK 内部 chat queue,用自己的 per-session queue。
402
+
403
+ ### `bridge/card-actions.ts`
404
+
405
+ 处理飞书卡片按钮。
406
+
407
+ 支持:
408
+
409
+ - `session`: 新建、切换、删除。
410
+ - `model`: 选择 provider、model、thinking level。
411
+ - `stop`: 中断当前 run。
412
+ - `permission`: resolve confirm/select。
413
+ - `help`: 卡片内导航。
414
+
415
+ 卡片 action handler 应该快速返回,耗时操作放到异步任务里。
416
+
417
+ ### `tools/send-file-to-chat.ts`
418
+
419
+ 注册 Pi 工具,让模型把生成的文件发回当前飞书聊天。
420
+
421
+ 行为:
422
+
423
+ - 参数:`filePath`、可选 `fileName`。
424
+ - 校验文件存在。
425
+ - 校验路径在允许目录内。
426
+ - 校验文件大小。
427
+ - 读取当前 Feishu context。
428
+ - 调用 `FeishuChannel.sendFile()`。
429
+ - 返回标准 Pi tool result。
430
+
431
+ 这个工具不应该依赖全局飞书状态,只读取当前 run 绑定的上下文。
432
+
433
+ ### `extensions/index.ts`
434
+
435
+ Pi extension 入口。
436
+
437
+ 职责:
438
+
439
+ - 注册 `send_file_to_chat`。
440
+ - 注册 `/feishu status`。
441
+ - 注册 `/feishu sessions` 或其他 TUI 内命令。
442
+ - 添加 prompt guidelines,告诉模型何时主动发文件。
443
+
444
+ 它不应该自己打开飞书连接。飞书连接属于 transport host。
445
+
446
+ ## 配置设计
447
+
448
+ 配置优先级:
449
+
450
+ 1. CLI flags。
451
+ 2. 项目配置:`.pi/feishu.json`。
452
+ 3. 用户配置:`~/.pi/agent/feishu.json`。
453
+ 4. 环境变量。
454
+
455
+ 推荐 schema:
456
+
457
+ ```ts
458
+ interface FeishuConfig {
459
+ appId: string;
460
+ appSecret: string;
461
+ encryptKey?: string;
462
+ verificationToken?: string;
463
+ botName?: string;
464
+ transport: "websocket" | "webhook";
465
+ webhook?: {
466
+ host?: string;
467
+ port: number;
468
+ path: string;
469
+ };
470
+ policy: {
471
+ requireMention: boolean;
472
+ dmEnabled: boolean;
473
+ groupEnabled: boolean;
474
+ allowUsers?: string[];
475
+ allowChats?: string[];
476
+ };
477
+ sessions: {
478
+ privateScope: "per-user";
479
+ groupScope: "shared-chat" | "per-user";
480
+ defaultCwd?: string;
481
+ store: "json" | "sqlite";
482
+ idleTtlMs: number;
483
+ };
484
+ rendering: {
485
+ mode: "stream-card" | "markdown" | "text";
486
+ showThinking: "hide" | "quote" | "plain";
487
+ showToolEvents: boolean;
488
+ };
489
+ files: {
490
+ allowedOutputDirs: string[];
491
+ maxUploadBytes: number;
492
+ tempDir?: string;
493
+ };
494
+ }
495
+ ```
496
+
497
+ ## 飞书命令设计
498
+
499
+ ```text
500
+ /help
501
+ /sessions
502
+ /models
503
+ /new
504
+ /stop
505
+ /reset
506
+ /status
507
+ ```
508
+
509
+ 行为:
510
+
511
+ - `/help`: 显示帮助卡片。
512
+ - `/sessions`: 显示当前会话和可切换会话。
513
+ - `/models`: 显示模型选择卡片。
514
+ - `/new`: 当前私聊或群聊新建 Pi session。
515
+ - `/stop`: 中断当前会话正在生成的任务。
516
+ - `/reset`: 清除当前映射并新建 session。
517
+ - `/status`: 显示当前模型、session id、队列状态、连接状态。
518
+
519
+ 未知 slash command:
520
+
521
+ - MVP 不转发。
522
+ - 后续可以在安全策略允许时转发给 Pi slash command。
523
+
524
+ ## 数据流
525
+
526
+ ### 普通消息
527
+
528
+ ```text
529
+ 飞书文本/图片/文件
530
+ -> channel.onMessage
531
+ -> policy 检查
532
+ -> ConversationRouter 生成 sessionKey
533
+ -> SessionHostManager 获取 host
534
+ -> message-normalizer
535
+ -> attachments processor
536
+ -> host.queue.enqueue()
537
+ -> 设置 Feishu context
538
+ -> 设置 Feishu UI context
539
+ -> stream renderer 订阅 session events
540
+ -> runtime.session.prompt()
541
+ -> renderer 更新飞书卡片
542
+ -> 清理 context/temp files
543
+ -> 释放队列
544
+ ```
545
+
546
+ ### 权限确认
547
+
548
+ ```text
549
+ Pi 工具调用需要确认
550
+ -> extension 调用 ctx.ui.confirm()
551
+ -> Feishu UI context 发送确认卡片
552
+ -> 用户点击按钮
553
+ -> card action resolve pending dialog
554
+ -> 工具继续或被拒绝
555
+ ```
556
+
557
+ ### 中断生成
558
+
559
+ ```text
560
+ 用户点击 stop 或发送 /stop
561
+ -> card action 或 command 定位 sessionKey
562
+ -> SessionHostManager 找到 active run
563
+ -> runtime.session.abort()
564
+ -> stop 卡片更新为 cancelled
565
+ ```
566
+
567
+ ### 文件回传
568
+
569
+ ```text
570
+ Pi 创建本地文件
571
+ -> 模型调用 send_file_to_chat
572
+ -> 工具校验路径和大小
573
+ -> 读取当前 Feishu context
574
+ -> FeishuChannel.sendFile()
575
+ -> 工具返回成功或失败
576
+ ```
577
+
578
+ ## 存储设计
579
+
580
+ MVP 用 JSON 足够,接口预留 SQLite。
581
+
582
+ ```ts
583
+ interface FeishuStore {
584
+ getSessionMapping(sessionKey: string): Promise<SessionMapping | undefined>;
585
+ setSessionMapping(mapping: SessionMapping): Promise<void>;
586
+ deleteSessionMapping(sessionKey: string): Promise<void>;
587
+ listSessionMappings(filter?: SessionFilter): Promise<SessionMapping[]>;
588
+ }
589
+ ```
590
+
591
+ 映射结构:
592
+
593
+ ```ts
594
+ interface SessionMapping {
595
+ sessionKey: string;
596
+ appId: string;
597
+ tenantKey?: string;
598
+ chatType: "private" | "group";
599
+ chatId: string;
600
+ userId?: string;
601
+ cwd: string;
602
+ sessionFile: string;
603
+ createdAt: string;
604
+ updatedAt: string;
605
+ }
606
+ ```
607
+
608
+ SQLite 适合后续场景:
609
+
610
+ - 活跃群和用户很多。
611
+ - 需要审计。
612
+ - 多 worker 进程。
613
+ - 需要 run history 查询。
614
+
615
+ ## 安全策略
616
+
617
+ MVP 最小策略:
618
+
619
+ - 群聊默认必须 @bot。
620
+ - 支持用户和群 allowlist。
621
+ - 不在日志里打印 app secret。
622
+ - webhook 模式必须验签。
623
+ - 凭证优先放用户配置,不默认放项目配置。
624
+ - 文件发送限制在 allowed output dirs。
625
+ - 飞书附件作为不可信输入处理。
626
+
627
+ 生产建议:
628
+
629
+ - 私聊默认开启。
630
+ - 群聊可配置开启。
631
+ - 项目 cwd 需要显式绑定。
632
+ - 危险工具继续依赖 Pi 的权限系统。
633
+
634
+ ## MVP 分期
635
+
636
+ ### Phase 1: 文本可用
637
+
638
+ - WebSocket channel。
639
+ - 配置加载。
640
+ - ConversationRouter。
641
+ - SessionHostManager。
642
+ - 每个活跃会话一个 runtime。
643
+ - per-session queue。
644
+ - 私聊文本。
645
+ - 群聊 @bot 文本。
646
+ - markdown 回复。
647
+
648
+ ### Phase 2: Pi 扩展包
649
+
650
+ - `extensions/index.ts`。
651
+ - 注册 `send_file_to_chat`。
652
+ - 添加文件回传 prompt guidance。
653
+ - package manifest 支持 `pi.extensions`。
654
+ - 验证 Pi 包管理器能发现扩展。
655
+
656
+ ### Phase 3: 飞书卡片体验
657
+
658
+ - streaming card。
659
+ - `/help`。
660
+ - `/sessions`。
661
+ - `/models`。
662
+ - stop card。
663
+ - model/thinking level card action。
664
+ - `ExtensionUIContext` 的 confirm/select 桥接。
665
+
666
+ ### Phase 4: 附件和文件
667
+
668
+ - 图片输入。
669
+ - 小文本文件展开进 prompt。
670
+ - 大文件保存到临时目录。
671
+ - 生成文件通过 tool 发回飞书。
672
+ - 清理和大小限制。
673
+
674
+ ### Phase 5: 生产能力
675
+
676
+ - webhook transport。
677
+ - 签名和加密校验。
678
+ - SQLite store。
679
+ - 结构化日志。
680
+ - allowlist policy。
681
+ - 部署文档。
682
+ - 完整多租户放到明确有需求后再做。
683
+
684
+ ## 面试亮点
685
+
686
+ 1. 把飞书抽象成 transport,而不是硬编码 bot。
687
+ 2. 分离 transport host 和 Pi extension,边界清楚。
688
+ 3. 识别 Pi runtime 是单 active session 模型,因此引入 `SessionHostManager`。
689
+ 4. 私聊按用户隔离,群聊默认共享上下文,符合真实协作场景。
690
+ 5. per-session queue 解决并发和状态一致性。
691
+ 6. stop、stream、permission 都绑定 active run,避免串线。
692
+ 7. `ExtensionUIContext -> Feishu Card` 让 Pi 原有权限确认能力远程可用。
693
+ 8. `send_file_to_chat` 作为工具注册,模型可以主动交付文件。
694
+ 9. 多租户不提前复杂化,只保留字段,体现工程取舍。
695
+ 10. 从 JSON store 到 SQLite store 的演进路径清晰。
696
+
697
+ ## 待确认问题
698
+
699
+ 1. 群聊是否默认永远共享 session,还是允许某些群配置成按用户隔离?
700
+ 2. 未知 slash command 是否允许转发给 Pi?
701
+ 3. 文件是否由模型主动调用工具发送,还是检测到文件生成后自动发送?
702
+ 4. 飞书消息是否允许默认使用项目 cwd,还是必须先绑定?
703
+ 5. 第一版是否只需要单进程,还是要预留多 worker?
704
+