@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 +46 -19
- package/package.json +2 -2
- package/src/contracts.ts +33 -1
- package/src/controller.ts +363 -26
- package/src/credentials.ts +13 -1
- package/src/extension.ts +112 -41
- package/src/gateway.ts +87 -16
- package/src/message-deduplicator.ts +76 -2
- package/src/remote-commands.ts +18 -3
- package/src/scopes.ts +96 -1
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
|
|
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
|
-
-
|
|
10
|
-
- 首次启动生成一次性绑定码,只允许一个 Owner
|
|
9
|
+
- 处理飞书私聊(`p2p`)文本消息,以及已绑定群聊中的文本消息
|
|
10
|
+
- 首次启动生成一次性绑定码,只允许一个 Owner;绑定码只能在私聊中使用
|
|
11
|
+
- Owner 可以把授权成员加入 allowlist(`/allow @成员`),授权成员在私聊和已绑定群里与 Owner 权限一致
|
|
11
12
|
- 收到消息后立刻在原消息上添加“思考中”表情作为已读回执,回复送达后自动移除
|
|
12
13
|
- 以 `/` 开头的消息走远程命令快速通道,不排队、不进入 LLM 上下文
|
|
13
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
|
|
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. */
|