@hwj123weijian/pi-feishu 0.8.0 → 0.10.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.
package/README.md CHANGED
@@ -1,28 +1,30 @@
1
1
  # pi-feishu
2
2
 
3
- 一个独立的 [Pi](https://github.com/earendil-works/pi-mono) 飞书扩展。它通过飞书官方 Node SDK 的 WebSocket 长连接,把飞书私聊文本转发到当前 Pi 会话,并以持续更新的交互卡片展示 Pi 的回复和执行状态。
3
+ 一个独立的 [Pi](https://github.com/earendil-works/pi-mono) 飞书扩展。它通过飞书官方 Node SDK 的 WebSocket 长连接,把飞书私聊和已绑定群聊的文本转发到对应的 Pi 会话,并以持续更新的交互卡片展示 Pi 的回复和执行状态。
4
4
 
5
5
  ## 设计边界
6
6
 
7
- 当前版本只做一条可靠的最小链路:
7
+ 当前版本聚焦一条可靠的私聊 + 项目群链路:
8
8
 
9
- - 仅处理飞书私聊(`p2p`)文本消息
10
- - 首次启动生成一次性绑定码,只允许一个 Owner
9
+ - 处理飞书私聊(`p2p`)文本消息,以及已绑定群聊中的文本消息
10
+ - 首次启动生成一次性绑定码,只允许一个 Owner;绑定码只能在私聊中使用
11
+ - Owner 可以把授权成员加入 allowlist(`/allow @成员`),授权成员在私聊和已绑定群里与 Owner 权限一致
11
12
  - 收到消息后立刻在原消息上添加“思考中”表情作为已读回执,回复送达后自动移除
12
13
  - 以 `/` 开头的消息走远程命令快速通道,不排队、不进入 LLM 上下文
13
- - 私聊消息进入当前 Pi 会话;扩展创建的每个群聊会在首次收到群消息时创建并绑定一个独立 Pi session,后续该群消息自动切回自己的 session,避免群之间和主对话串上下文
14
+ - 私聊消息进入主 Pi 会话;每个绑定的群聊拥有独立 Pi session,群之间、群与主对话之间不共享上下文
15
+ - 群消息会带上说话人上下文(如 `[飞书群聊] 张三:…`)再发给 Pi;bot 识别 @ 他人、@ 自己
14
16
  - 消息需要排队时发送“排队中(第 N 位)”提示,轮到处理时自动撤回;队列变化时提示自动改写为新位数
15
17
  - Owner 在排队中的消息上点 ❌(`CrossMark`)表情即可取消该消息
18
+ - 未绑定的群里被 @ 时,bot 会回复绑定引导而不是沉默;非授权成员的普通群消息完全不会触发回复
19
+ - Bot 被拉进新群时,会私聊 Owner 发送绑定引导和群聊权限体检结果;启动成功后也会私聊 Owner 发送欢迎语和权限体检
16
20
  - Assistant 文本通过 Pi 的 `message_update` 增量渲染到同一张飞书卡片
17
21
  - 工具执行时仅显示安全的状态摘要,不展示参数、命令、文件内容或工具输出
18
- - SDK 以 500ms / 120 字符节流卡片更新;完成、失败、中止都会收尾卡片
19
- - 缺少更新权限或飞书卡片更新失败时,自动退化为原私聊的一次性文本回复
20
- - 串行处理消息,避免多条飞书消息同时驱动 Pi
21
- - 按飞书 `message_id` 去重
22
+ - SDK 以 500ms / 120 字符节流卡片更新;完成、失败、中止都会收尾卡片,失败原因(脱敏后)会写进回复
23
+ - 消息串行处理(Pi 是单会话进程,跨群并行会互相打断),按飞书 `message_id` 去重且受理记录落盘(48 小时窗口),重启后飞书重推不会重复执行
22
24
  - 长连接由跨会话的全局控制器保管:本地 `/new`、远程 `/new` 等会话切换不会断开;`/feishu stop`、`/feishu logout` 或 Pi 进程退出时释放
23
25
  - 凭据优先从环境变量读取,也可保存到本地凭据文件
24
26
 
25
- 支持 Owner 在飞书私聊中请求创建群聊;扩展会把 Owner 拉入新群、发送欢迎消息,并允许 Owner 在该扩展创建的群里 @机器人继续协作。建群后会私聊 Owner 做一次权限体检,提示缺失的基础 scope、事件订阅页和 `im:message.group_msg` 免 @ 权限申请链接。每个扩展创建的群聊会绑定自己的 Pi session,群聊之间、群聊与本地主对话之间不会共享上下文。其他群聊不会被处理。图片、文件、卡片、语音、多用户、进程级沙箱或远程命令权限管理仍不支持。Pi 本身仍拥有当前本地进程的权限。
27
+ 支持在飞书私聊中请求创建群聊;扩展会把 Owner 拉入新群、发送欢迎消息,并在绑定群与私聊中协同使用。图片、文件、卡片、语音、多用户并发执行、进程级沙箱仍不支持。Pi 本身仍拥有当前本地进程的权限。
26
28
 
27
29
  ## 环境要求
28
30
 
@@ -37,16 +39,17 @@
37
39
  1. 在“添加应用能力”中启用机器人。
38
40
  2. 在“权限管理”中申请:
39
41
  - `im:message.p2p_msg:readonly`:接收私聊消息
42
+ - `im:message.group_at_msg:readonly`:接收群内 @机器人 的消息(**群聊必需,缺了群里 @bot 会完全没有响应**)
40
43
  - `im:message:send_as_bot`:以机器人身份回复
41
- - `im:chat`:创建群聊并邀请 Owner
44
+ - `im:chat`:创建群聊并邀请 Owner、读取群信息
42
45
  - `im:message:update`:持续更新机器人发出的交互卡片
43
46
  - `application:application:self_manage`:启动/建群后读取应用已授权 scope,用于自动提示缺失权限
44
47
  - 表情回复相关权限:在原消息上添加/移除“思考中”已读回执
45
48
  3. 在“事件与回调”中选择“使用长连接接收事件”。
46
- 4. 添加事件 `im.message.receive_v1`。
49
+ 4. 添加事件 `im.message.receive_v1`;建议同时添加 `im.chat.member.bot.added_v1`(被拉进新群时私聊你发送绑定引导)。
47
50
  5. 创建并发布一个应用版本,使权限和事件订阅在企业内生效。
48
51
 
49
- 这个扩展不需要公网回调地址,也不需要加密密钥或 Verification Token。若未授予 `im:message:update`,扩展仍可工作,但会降级为最终文本一次性回复。若未授予表情回复权限,扩展同样可工作,只是原消息上不会出现已读表情。若希望已创建项目群里无需 @ 机器人即可触发,需要额外申请敏感权限 `im:message.group_msg`;未开通时请在群里使用 `@机器人 你的问题`。
52
+ 这个扩展不需要公网回调地址,也不需要加密密钥或 Verification Token。若未授予 `im:message:update`,扩展仍可工作,但会降级为最终文本一次性回复。若未授予表情回复权限,扩展同样可工作,只是原消息上不会出现已读表情。若希望已绑定项目群里无需 @ 机器人即可触发,需要额外申请敏感权限 `im:message.group_msg`(需人工审核 1-3 天);未开通时请在群里使用 `@机器人 你的问题`。
50
53
 
51
54
  ## 安装
52
55
 
@@ -131,19 +134,33 @@ pi -e D:\ai_study\pi-feishu
131
134
 
132
135
  ### 飞书远程命令
133
136
 
134
- 在飞书私聊里直接发送以下命令,会走高优先级快速通道:不进入消息队列排队,也不会作为提问发给 Pi。
137
+ 在飞书私聊或已绑定的群里直接发送以下命令,会走高优先级快速通道:不进入消息队列排队,也不会作为提问发给 Pi。
135
138
 
136
139
  | 命令 | 作用 |
137
140
  | --- | --- |
138
141
  | `/help` | 显示远程命令帮助 |
139
- | `/new` | 新建 Pi 会话(丢弃当前对话上下文) |
142
+ | `/new` | 新建 Pi 会话;在群里发送时重置该群绑定的会话,不影响主会话 |
140
143
  | `/stop` | 中止当前任务,并跳过队列中剩余消息 |
141
144
  | `/status` | 查看连接、队列、模型与上下文占用 |
142
145
  | `/compact` | 压缩当前会话上下文 |
143
146
  | `/thinking` | 查看思考档位,或切换:`/thinking off|minimal|low|medium|high|xhigh|max` |
144
147
  | `/model` | 查看当前模型,或模糊匹配切换:`/model <模型ID或名称>` |
148
+ | `/bind` | (群聊)把当前群绑定到 Pi,创建独立会话;仅 Owner |
149
+ | `/allow` | (仅 Owner)授权成员:`/allow @成员`;无参数时列出授权列表 |
150
+ | `/deny` | (仅 Owner)移除授权:`/deny @成员` |
151
+ | `/allowlist` | (仅 Owner)查看当前授权成员 |
145
152
 
146
- 在飞书私聊中也可直接说“帮我拉个 XX 群”或“创建飞书群 XX”。机器人会创建群聊、邀请已绑定的 Owner 并发送欢迎消息;之后 Owner 在新群里 @机器人即可协作。该群首次收到消息时会创建一个独立 Pi session 并绑定到群,之后该群消息都会自动切回这个 session。已创建群聊的授权列表和 session 绑定会保存在本地凭据文件中,Pi 重启后仍可继续使用。
153
+ 在飞书私聊中也可直接说“帮我拉个 XX 群”或“创建飞书群 XX”。机器人会创建群聊、邀请已绑定的 Owner 并发送欢迎消息;建群后会私聊 Owner 做一次权限体检(含免 @ 权限申请链接)。已创建群聊和 session 绑定保存在本地凭据文件中,Pi 重启后仍可继续使用。
154
+
155
+ ### 群聊使用
156
+
157
+ - **绑定已有群**:把机器人加进现有项目群后,Owner 在群里发送 `/bind` 即可绑定;之后该群获得独立 Pi session,与其他群和主对话互不串上下文。机器人被拉进新群时也会私聊 Owner 提醒绑定。
158
+ - **触发方式**:默认在群里 `@机器人 你的问题`。飞书只向 bot 推送 @bot 的群消息;普通消息直接触发需要敏感权限 `im:message.group_msg`。
159
+ - **授权成员**:默认只有 Owner 能使用。Owner 私聊发送 `/allow @张三` 后,张三即可在私聊和已绑定群里与机器人协作;`/deny @张三` 撤销。
160
+ - **分寸感**:未绑定的群里被 @ 时回复绑定引导;非授权成员被 @ 时回复一次“未授权”;其余群内消息完全静默,不会刷屏。
161
+ - **说话人上下文**:群消息发给 Pi 前会加上 `[飞书群聊] 说话人:` 前缀,@ 其他人会被改写成 `@名字`,模型知道在和谁对话。
162
+
163
+ 已知限制:Pi 是单会话进程,多个聊天的任务仍全局串行(排队提示会如实显示位数);远程命令作用于最近活跃的会话,`/model`、`/thinking`、`/compact` 建议在对应聊天的上一条消息之后紧接着发送。
147
164
 
148
165
  未知命令会返回提示,不会发给 Pi。`/model` 的候选来自本地 Pi 的 scoped models(未配置时为全部已授权模型);`/model` 无参数时展示当前模型。注意:任何以 `/` 开头的消息都会先被当作命令解析,想发给 Pi 的提问请勿以 `/` 开头。
149
166
 
@@ -175,9 +192,19 @@ pi -e D:\ai_study\pi-feishu
175
192
 
176
193
  确认已申请并发布表情回复相关权限。该权限缺失时扩展自动降级:消息照常收发,只是原消息上不会出现“思考中”表情。
177
194
 
195
+ ### 群里 @机器人 没反应
196
+
197
+ 按顺序排查:
198
+
199
+ 1. 群聊接收权限:确认已申请并发布 `im:message.group_at_msg:readonly`(缺了这个飞书不会推送任何群消息事件)。
200
+ 2. 应用版本:权限变更后需在权限管理页发布新版本才生效。
201
+ 3. 群是否绑定:只有扩展创建的群和 `/bind` 绑定过的群会处理消息。在被 @ 而无响应的群里,让 Owner 发送 `/bind`;如果连 @ 后的引导都没出现,通常是第 1、2 步的问题。
202
+ 4. 被 @ 的是否是机器人本身(@ 错了人不会触发)。
203
+ 5. 用 `/feishu status` 查看缺失 scope 的自检结果。
204
+
178
205
  ### 群里必须 @ 才有响应
179
206
 
180
- 飞书默认只向 Bot 推送群内 @bot 消息。若希望扩展创建的项目群里普通消息也触发,需要额外申请并发布敏感权限 `im:message.group_msg`。本扩展已关闭 SDK 侧的强制 @ 过滤;权限生效后,已授权项目群内普通消息会直接进入 Pi。
207
+ 飞书默认只向 Bot 推送群内 @bot 消息。若希望绑定群内普通消息也触发,需要额外申请并发布敏感权限 `im:message.group_msg`。本扩展已关闭 SDK 侧的强制 @ 过滤;权限生效后,已绑定群内普通消息会直接进入 Pi。
181
208
 
182
209
  ### 远程 `/new` 提示暂不可用
183
210
 
@@ -189,7 +216,7 @@ pi -e D:\ai_study\pi-feishu
189
216
 
190
217
  ### 重复消息
191
218
 
192
- 飞书事件可能重投。扩展在当前进程内缓存最近 1000 个 `message_id`,重复事件不会再次驱动 Pi。重启 Pi 后缓存会清空。
219
+ 飞书事件可能重投。扩展按 `message_id` 去重,并把受理记录落盘到 `~/.pi/agent/feishu/processed-messages.json`(48 小时滚动窗口、上限 5000 条),Pi 重启后飞书重推的旧消息也不会再次驱动 Pi。
193
220
 
194
221
  ## 开发
195
222
 
@@ -199,7 +226,7 @@ npm test
199
226
  npm run build
200
227
  ```
201
228
 
202
- 测试使用 Fake Gateway 和 Fake Agent,不需要真实飞书凭据,覆盖凭据处理、Owner 绑定、私聊过滤、串行队列、排队提示改位与撤回、❌ 取消、`/stop` 按条跳过、去重、已读表情回执、远程命令快速通道、思考档位与模型切换、Pi 增量文本、工具状态、流式卡片收尾、降级、错误脱敏和清理行为。
229
+ 测试使用 Fake Gateway 和 Fake Agent,不需要真实飞书凭据,覆盖凭据处理、Owner 绑定、私聊过滤、串行队列、排队提示改位与撤回、❌ 取消、`/stop` 按条跳过、去重(含落盘恢复)、已读表情回执、远程命令快速通道、群绑定(`/bind`)与未托管群引导、allowlist 授权与撤销、群聊说话人上下文注入、mention 元数据透传、botAdded 引导、思考档位与模型切换、Pi 增量文本、工具状态、流式卡片收尾、降级、错误脱敏和清理行为。
203
230
 
204
231
  ## 许可证
205
232
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hwj123weijian/pi-feishu",
3
- "version": "0.8.0",
4
- "description": "Minimal Feishu private-chat bridge for Pi",
3
+ "version": "0.10.0",
4
+ "description": "Feishu private-chat and managed-group bridge for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "publishConfig": {
package/src/contracts.ts CHANGED
@@ -5,6 +5,10 @@ export interface FeishuCredentials {
5
5
  managedGroupIds?: string[];
6
6
  /** Per managed group Pi session file. Missing value is backfilled on the group's first message. */
7
7
  groupSessions?: Record<string, string>;
8
+ /** Open IDs allowed to drive the bot besides the Owner, e.g. teammates in a managed group. */
9
+ allowlist?: string[];
10
+ /** Display names for allowlisted open IDs, captured from @mentions when authorizing. */
11
+ allowlistNames?: Record<string, string>;
8
12
  }
9
13
 
10
14
  export interface CredentialStore {
@@ -13,6 +17,13 @@ export interface CredentialStore {
13
17
  clear(): Promise<void>;
14
18
  }
15
19
 
20
+ export interface FeishuMentionInfo {
21
+ key: string;
22
+ openId?: string;
23
+ name?: string;
24
+ isBot?: boolean;
25
+ }
26
+
16
27
  export interface FeishuIncomingMessage {
17
28
  messageId: string;
18
29
  chatId: string;
@@ -20,10 +31,21 @@ export interface FeishuIncomingMessage {
20
31
  senderOpenId: string;
21
32
  contentType: string;
22
33
  text: string;
34
+ /** Sender display name when the platform provides one. */
35
+ senderName?: string;
36
+ /** True when the message @-mentions this bot (group messages only). */
37
+ mentionedBot?: boolean;
38
+ /** All @mentions carried by the message, bot included. */
39
+ mentions?: FeishuMentionInfo[];
23
40
  }
24
41
 
25
42
  export type FeishuMessageHandler = (message: FeishuIncomingMessage) => Promise<void> | void;
26
43
 
44
+ export interface FeishuBotAddedEvent {
45
+ chatId: string;
46
+ operatorOpenId: string;
47
+ }
48
+
27
49
  export interface FeishuReactionEvent {
28
50
  messageId: string;
29
51
  operatorOpenId: string;
@@ -41,7 +63,8 @@ export interface FeishuReplySnapshot {
41
63
  export interface FeishuReply {
42
64
  update(snapshot: FeishuReplySnapshot): void;
43
65
  complete(snapshot: FeishuReplySnapshot): Promise<void>;
44
- fail(): Promise<void>;
66
+ /** Finalizes as failed; the sanitized reason is shown to the user when provided. */
67
+ fail(reason?: string): Promise<void>;
45
68
  cancel(): Promise<void>;
46
69
  }
47
70
 
@@ -62,6 +85,10 @@ export interface FeishuGateway {
62
85
  recallMessage(messageId: string): Promise<void>;
63
86
  /** Subscribes to emoji reactions on messages visible to the bot. */
64
87
  onReaction(handler: FeishuReactionHandler): () => void;
88
+ /** Subscribes to "bot added to chat" events. Optional: older gateways may not expose it. */
89
+ onBotAdded?(handler: (event: FeishuBotAddedEvent) => void): () => void;
90
+ /** Fetches basic chat metadata (e.g. group name) for guidance messages. Optional. */
91
+ getChatInfo?(chatId: string): Promise<{ name?: string } | undefined>;
65
92
  }
66
93
 
67
94
  export interface FeishuStatus {
@@ -97,6 +124,11 @@ export interface PiRuntime {
97
124
  compact(): void;
98
125
  /** Returns false when the runtime is unavailable or the switch was cancelled. */
99
126
  newSession(): Promise<boolean>;
127
+ /**
128
+ * Creates a new session for the chat-bound session of the given Feishu chat
129
+ * and re-points the binding at it. Returns false when unavailable/cancelled.
130
+ */
131
+ newChatSession?(chatId: string): Promise<boolean>;
100
132
  /** Returns false when the requested thinking level does not exist. */
101
133
  setThinkingLevel(level: string): Promise<boolean>;
102
134
  /** Models the user can currently switch to. */