@hwj123weijian/pi-feishu 0.10.0 → 0.10.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/package.json +1 -1
- package/src/extension.ts +28 -15
- package/src/scopes.ts +31 -2
package/package.json
CHANGED
package/src/extension.ts
CHANGED
|
@@ -35,8 +35,13 @@ interface SharedFeishuState {
|
|
|
35
35
|
mainSessionFile: string | undefined;
|
|
36
36
|
/** Pi 进程当前所在的会话文件,随每个事件刷新。 */
|
|
37
37
|
currentSessionFile: string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 本扩展发起的 switch/newSession 进行中的标记:
|
|
40
|
+
* - 期间出现的会话文件不得记为主会话;
|
|
41
|
+
* - Pi 对旧会话发出的 session_shutdown 是我们自己引起的,
|
|
42
|
+
* 不能据此取消正在等待的飞书轮次(否则群消息必然失败)。
|
|
43
|
+
*/
|
|
44
|
+
switchingSession: boolean;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
const SHARED_STATE_KEY = "__piFeishuSharedState__";
|
|
@@ -58,7 +63,7 @@ function getSharedState(): SharedFeishuState {
|
|
|
58
63
|
sendCurrentMessage: undefined,
|
|
59
64
|
mainSessionFile: undefined,
|
|
60
65
|
currentSessionFile: undefined,
|
|
61
|
-
|
|
66
|
+
switchingSession: false,
|
|
62
67
|
controller: undefined as unknown as FeishuController,
|
|
63
68
|
};
|
|
64
69
|
const proxyAgent: AgentBridge = {
|
|
@@ -85,7 +90,7 @@ function getSharedState(): SharedFeishuState {
|
|
|
85
90
|
newChatSession: async (chatId) => {
|
|
86
91
|
const context = state.latestCommandContext;
|
|
87
92
|
if (!context) return false;
|
|
88
|
-
state.
|
|
93
|
+
state.switchingSession = true;
|
|
89
94
|
try {
|
|
90
95
|
const result = await context.newSession({
|
|
91
96
|
withSession: async (nextContext) => {
|
|
@@ -96,7 +101,7 @@ function getSharedState(): SharedFeishuState {
|
|
|
96
101
|
});
|
|
97
102
|
return !result.cancelled;
|
|
98
103
|
} finally {
|
|
99
|
-
state.
|
|
104
|
+
state.switchingSession = false;
|
|
100
105
|
}
|
|
101
106
|
},
|
|
102
107
|
setThinkingLevel: (level) => state.current?.runtime.setThinkingLevel(level) ?? Promise.resolve(false),
|
|
@@ -124,7 +129,7 @@ function observeSessionFile(state: SharedFeishuState, context: ExtensionContext)
|
|
|
124
129
|
const file = tryGetSessionFile(context);
|
|
125
130
|
if (!file) return;
|
|
126
131
|
state.currentSessionFile = file;
|
|
127
|
-
if (state.
|
|
132
|
+
if (state.switchingSession) return;
|
|
128
133
|
if (state.controller.isGroupSessionFile(file)) return;
|
|
129
134
|
state.mainSessionFile = file;
|
|
130
135
|
}
|
|
@@ -157,20 +162,25 @@ async function sendToMainSession(state: SharedFeishuState, prompt: string): Prom
|
|
|
157
162
|
// 主会话与当前会话不一致却没有命令上下文:直发会串进群会话,给出可操作的错误更安全。
|
|
158
163
|
throw new Error("Pi 会话控制尚未就绪,请先在本地执行一次 /feishu status。");
|
|
159
164
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
state.switchingSession = true;
|
|
166
|
+
try {
|
|
167
|
+
const result = await context.switchSession(main, {
|
|
168
|
+
withSession: async (nextContext) => {
|
|
169
|
+
state.latestCommandContext = nextContext;
|
|
170
|
+
await nextContext.sendUserMessage(prompt);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (result.cancelled) throw new Error("切回主 Pi 会话已取消。");
|
|
174
|
+
} finally {
|
|
175
|
+
state.switchingSession = false;
|
|
176
|
+
}
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
async function sendToChatSession(state: SharedFeishuState, chatId: string, prompt: string): Promise<void> {
|
|
170
180
|
const context = state.latestCommandContext;
|
|
171
181
|
if (!context) throw new Error("Pi 会话控制尚未就绪,请先在本地执行一次 /feishu status。");
|
|
172
182
|
|
|
173
|
-
state.
|
|
183
|
+
state.switchingSession = true;
|
|
174
184
|
try {
|
|
175
185
|
const sessionFile = state.controller.getChatSessionFile(chatId);
|
|
176
186
|
if (sessionFile) {
|
|
@@ -197,7 +207,7 @@ async function sendToChatSession(state: SharedFeishuState, chatId: string, promp
|
|
|
197
207
|
});
|
|
198
208
|
if (result.cancelled) throw new Error("创建飞书群专属 Pi 会话已取消。");
|
|
199
209
|
} finally {
|
|
200
|
-
state.
|
|
210
|
+
state.switchingSession = false;
|
|
201
211
|
}
|
|
202
212
|
}
|
|
203
213
|
|
|
@@ -489,6 +499,9 @@ export default function feishuExtension(pi: ExtensionAPI): void {
|
|
|
489
499
|
});
|
|
490
500
|
pi.on("session_shutdown", () => {
|
|
491
501
|
// 长连接归全局 controller 保管,本地 /new 切换会话后自动延续。
|
|
502
|
+
// 本扩展为群/私聊路由发起的 switch/newSession 会让 Pi 对旧会话发 shutdown;
|
|
503
|
+
// 那不是真正的会话关闭,此刻轮次刚要开始,绝不能取消,否则回复永远传不回飞书。
|
|
504
|
+
if (state.switchingSession) return;
|
|
492
505
|
state.activeBridge?.cancel("Pi 会话已关闭。");
|
|
493
506
|
});
|
|
494
507
|
|
package/src/scopes.ts
CHANGED
|
@@ -165,21 +165,50 @@ export function buildBotAddedGuidance(params: {
|
|
|
165
165
|
return lines.join("\n");
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** 每个 scope 的用途说明,用于欢迎语中的权限清单。 */
|
|
169
|
+
export const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
|
170
|
+
"im:message.p2p_msg:readonly": "接收私聊消息",
|
|
171
|
+
"im:message.group_at_msg:readonly": "接收群内 @机器人 的消息",
|
|
172
|
+
"im:message:send_as_bot": "以机器人身份发送/回复消息",
|
|
173
|
+
"im:message:update": "持续更新交互卡片(流式回复)",
|
|
174
|
+
"im:message.reactions:read": "读取表情回复(❌ 取消排队消息)",
|
|
175
|
+
"im:message.reactions:write_only": "添加表情回复(思考中已读回执)",
|
|
176
|
+
"im:chat": "创建群聊并邀请 Owner",
|
|
177
|
+
"im:chat:read": "读取群信息(群名等)",
|
|
178
|
+
"application:application:self_manage": "读取应用已开通权限,自动完成权限体检",
|
|
179
|
+
[SENSITIVE_GROUP_MSG_SCOPE]: "群内免 @ 直接响应所有消息(敏感权限,需人工审核)",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function renderScopeInventory(): string[] {
|
|
183
|
+
return [...REQUIRED_APP_SCOPES, SENSITIVE_GROUP_MSG_SCOPE].map((scope) => {
|
|
184
|
+
const description = SCOPE_DESCRIPTIONS[scope];
|
|
185
|
+
return description ? `- \`${scope}\`(${description})` : `- \`${scope}\``;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
168
189
|
/**
|
|
169
190
|
* 权限体检段落:grantedScopes 未知时返回 null(不猜);
|
|
170
|
-
* 全部就绪时返回 ✅
|
|
191
|
+
* 全部就绪时返回 ✅ 段落 + 完整依赖清单(便于逐项核对),
|
|
192
|
+
* 否则返回缺失清单 + 一键申请链接。
|
|
171
193
|
*/
|
|
172
194
|
export function buildScopeHealthSection(appId: string, grantedScopes?: readonly string[]): string | null {
|
|
173
195
|
if (!grantedScopes) return null;
|
|
174
196
|
const missing = missingScopes(grantedScopes, REQUIRED_APP_SCOPES);
|
|
175
197
|
const hasGroupMsg = hasScope(grantedScopes, SENSITIVE_GROUP_MSG_SCOPE);
|
|
176
198
|
if (missing.length === 0 && hasGroupMsg) {
|
|
177
|
-
return "✅ 应用权限配置完整,所有功能均可正常使用。"
|
|
199
|
+
return ["✅ 应用权限配置完整,所有功能均可正常使用。", "", "本 Bot 依赖以下权限(均已开通):", ...renderScopeInventory()].join(
|
|
200
|
+
"\n",
|
|
201
|
+
);
|
|
178
202
|
}
|
|
179
203
|
const lines = ["⚠️ 以下应用权限尚未开通,对应功能会受限:"];
|
|
180
204
|
if (missing.length > 0) {
|
|
181
205
|
lines.push(`📋 缺失 ${missing.length} 项基础权限,点击一键申请:`);
|
|
182
206
|
lines.push(`👉 ${buildScopeApplyUrl({ appId, scopes: missing })}`);
|
|
207
|
+
lines.push(" 缺失明细:");
|
|
208
|
+
for (const scope of missing) {
|
|
209
|
+
const description = SCOPE_DESCRIPTIONS[scope];
|
|
210
|
+
lines.push(` - \`${scope}\`${description ? `(${description})` : ""}`);
|
|
211
|
+
}
|
|
183
212
|
}
|
|
184
213
|
if (!hasGroupMsg) {
|
|
185
214
|
lines.push("💬 「免 @ 响应」权限未开:群内需 @机器人 才能触发,点击开通:");
|