@wu529778790/open-im 1.0.2-beta.2 → 1.0.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 +34 -2
- package/dist/adapters/claude-adapter.js +1 -0
- package/dist/adapters/tool-adapter.interface.d.ts +2 -0
- package/dist/claude/cli-runner.d.ts +1 -0
- package/dist/claude/cli-runner.js +5 -1
- package/dist/claude/process-pool.d.ts +1 -0
- package/dist/claude/process-pool.js +5 -1
- package/dist/commands/handler.d.ts +5 -0
- package/dist/commands/handler.js +46 -8
- package/dist/config.d.ts +2 -0
- package/dist/config.js +6 -0
- package/dist/feishu/client.d.ts +1 -1
- package/dist/feishu/client.js +13 -0
- package/dist/feishu/event-handler.d.ts +1 -1
- package/dist/feishu/event-handler.js +253 -47
- package/dist/feishu/message-sender.d.ts +22 -0
- package/dist/feishu/message-sender.js +174 -2
- package/dist/hook/permission-server.d.ts +37 -4
- package/dist/hook/permission-server.js +288 -8
- package/dist/index.js +11 -0
- package/dist/permission-mode/session-mode.d.ts +7 -0
- package/dist/permission-mode/session-mode.js +59 -0
- package/dist/permission-mode/types.d.ts +11 -0
- package/dist/permission-mode/types.js +29 -0
- package/dist/shared/ai-task.js +17 -1
- package/dist/shared/chat-user-map.d.ts +2 -0
- package/dist/shared/chat-user-map.js +11 -0
- package/dist/telegram/event-handler.js +18 -3
- package/dist/telegram/message-sender.d.ts +1 -0
- package/dist/telegram/message-sender.js +14 -0
- package/package.json +1 -1
|
@@ -2,14 +2,17 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { AccessControl } from '../access/access-control.js';
|
|
4
4
|
import { RequestQueue } from '../queue/request-queue.js';
|
|
5
|
-
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendImageReply, } from './message-sender.js';
|
|
6
|
-
import { registerPermissionSender } from '../hook/permission-server.js';
|
|
5
|
+
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, sendTextReplyByOpenId, startTypingLoop, sendImageReply, createFeishuButtonCard, sendModeCard, createFeishuModeCardReadOnly, delayUpdateCard, } from './message-sender.js';
|
|
6
|
+
import { registerPermissionSender, resolvePermissionById } from '../hook/permission-server.js';
|
|
7
|
+
import { setPermissionMode } from '../permission-mode/session-mode.js';
|
|
8
|
+
import { MODE_LABELS } from '../permission-mode/types.js';
|
|
7
9
|
import { CommandHandler } from '../commands/handler.js';
|
|
8
10
|
import { getAdapter } from '../adapters/registry.js';
|
|
9
11
|
import { runAITask } from '../shared/ai-task.js';
|
|
10
12
|
import { startTaskCleanup } from '../shared/task-cleanup.js';
|
|
11
13
|
import { THROTTLE_MS, IMAGE_DIR } from '../constants.js';
|
|
12
14
|
import { setActiveChatId } from '../shared/active-chats.js';
|
|
15
|
+
import { setChatUser } from '../shared/chat-user-map.js';
|
|
13
16
|
import { createLogger } from '../logger.js';
|
|
14
17
|
const log = createLogger('FeishuHandler');
|
|
15
18
|
async function downloadFeishuImage(client, imageKey) {
|
|
@@ -56,6 +59,48 @@ async function downloadFeishuImage(client, imageKey) {
|
|
|
56
59
|
await writeFile(imagePath, buffer);
|
|
57
60
|
return imagePath;
|
|
58
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Send permission prompt card with interactive buttons
|
|
64
|
+
*/
|
|
65
|
+
async function sendPermissionCard(chatId, requestId, toolName, toolInput) {
|
|
66
|
+
const { getClient } = await import('./client.js');
|
|
67
|
+
const client = getClient();
|
|
68
|
+
// Format tool input for display
|
|
69
|
+
let formattedInput;
|
|
70
|
+
if (toolInput.length > 300) {
|
|
71
|
+
formattedInput = toolInput.slice(0, 300) + '...';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
formattedInput = toolInput;
|
|
75
|
+
}
|
|
76
|
+
const content = `**工具:** \`${toolName}\`
|
|
77
|
+
|
|
78
|
+
**参数:**
|
|
79
|
+
\`\`\`
|
|
80
|
+
${formattedInput}
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
**请求 ID:** \`${requestId.slice(-8)}\``;
|
|
84
|
+
const cardContent = createFeishuButtonCard('权限请求', content, [
|
|
85
|
+
{ label: '✅ 允许', value: `allow_${requestId}`, type: 'primary' },
|
|
86
|
+
{ label: '❌ 拒绝', value: `deny_${requestId}`, type: 'default' },
|
|
87
|
+
]);
|
|
88
|
+
try {
|
|
89
|
+
await client.im.message.create({
|
|
90
|
+
data: {
|
|
91
|
+
receive_id: chatId,
|
|
92
|
+
msg_type: 'interactive',
|
|
93
|
+
content: cardContent,
|
|
94
|
+
},
|
|
95
|
+
params: { receive_id_type: 'chat_id' },
|
|
96
|
+
});
|
|
97
|
+
log.info(`Permission card sent for request ${requestId}`);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
log.error('Failed to send permission card:', err);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
59
104
|
export function setupFeishuHandlers(config, sessionManager) {
|
|
60
105
|
const accessControl = new AccessControl(config.feishuAllowedUserIds);
|
|
61
106
|
const requestQueue = new RequestQueue();
|
|
@@ -65,10 +110,10 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
65
110
|
config,
|
|
66
111
|
sessionManager,
|
|
67
112
|
requestQueue,
|
|
68
|
-
sender: { sendTextReply },
|
|
113
|
+
sender: { sendTextReply, sendModeCard },
|
|
69
114
|
getRunningTasksSize: () => runningTasks.size,
|
|
70
115
|
});
|
|
71
|
-
registerPermissionSender('feishu', {});
|
|
116
|
+
registerPermissionSender('feishu', { sendTextReply, sendPermissionCard });
|
|
72
117
|
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
73
118
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
74
119
|
log.info(`[AI_REQUEST] Full prompt: "${prompt}"`);
|
|
@@ -120,26 +165,183 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
120
165
|
sendImage: (path) => sendImageReply(chatId, path),
|
|
121
166
|
});
|
|
122
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse permission button value from card action (兼容多种格式)
|
|
170
|
+
*/
|
|
171
|
+
function parsePermissionActionValue(raw) {
|
|
172
|
+
if (!raw)
|
|
173
|
+
return null;
|
|
174
|
+
let buttonValue;
|
|
175
|
+
if (typeof raw === 'string') {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
if (parsed.action === 'permission' && parsed.value)
|
|
179
|
+
buttonValue = parsed.value;
|
|
180
|
+
else if (raw.startsWith('allow_') || raw.startsWith('deny_'))
|
|
181
|
+
buttonValue = raw;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
if (raw.startsWith('allow_') || raw.startsWith('deny_'))
|
|
185
|
+
buttonValue = raw;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (typeof raw === 'object' && raw !== null) {
|
|
189
|
+
const obj = raw;
|
|
190
|
+
if (obj.action === 'permission' && obj.value)
|
|
191
|
+
buttonValue = obj.value;
|
|
192
|
+
}
|
|
193
|
+
if (!buttonValue)
|
|
194
|
+
return null;
|
|
195
|
+
if (buttonValue.startsWith('allow_')) {
|
|
196
|
+
return { decision: 'allow', requestId: buttonValue.slice(6) };
|
|
197
|
+
}
|
|
198
|
+
if (buttonValue.startsWith('deny_')) {
|
|
199
|
+
return { decision: 'deny', requestId: buttonValue.slice(5) };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 解析 action value(兼容对象、JSON 字符串)
|
|
205
|
+
*/
|
|
206
|
+
function parseActionValue(raw) {
|
|
207
|
+
if (!raw)
|
|
208
|
+
return null;
|
|
209
|
+
let obj = null;
|
|
210
|
+
if (typeof raw === 'string') {
|
|
211
|
+
try {
|
|
212
|
+
obj = JSON.parse(raw);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (typeof raw === 'object' && raw !== null) {
|
|
219
|
+
obj = raw;
|
|
220
|
+
}
|
|
221
|
+
return obj?.action && obj?.value ? obj : null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 从卡片回调事件中提取延时更新 token(格式 c-xxxx)
|
|
225
|
+
* 飞书文档:从卡片交互返回内容获取,用于延时更新接口
|
|
226
|
+
*/
|
|
227
|
+
function extractCardToken(data) {
|
|
228
|
+
const raw = data;
|
|
229
|
+
const event = (raw?.event ?? raw);
|
|
230
|
+
const action = event?.action;
|
|
231
|
+
const context = event?.context;
|
|
232
|
+
const candidates = [
|
|
233
|
+
event?.token,
|
|
234
|
+
event?.open_api_token,
|
|
235
|
+
raw?.token,
|
|
236
|
+
action?.token,
|
|
237
|
+
context?.token,
|
|
238
|
+
].filter((t) => typeof t === 'string' && t.startsWith('c-'));
|
|
239
|
+
const token = candidates[0] ?? null;
|
|
240
|
+
if (!token) {
|
|
241
|
+
log.debug('[extractCardToken] No token found, event keys:', Object.keys(event ?? {}));
|
|
242
|
+
}
|
|
243
|
+
return token;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Handle card button click (card.action.trigger) - 需在 3 秒内返回响应
|
|
247
|
+
* 同步只返回 toast,避免 200672;用延时更新 API 异步替换为只读卡片,防止二次点击
|
|
248
|
+
*/
|
|
249
|
+
async function handleCardAction(data) {
|
|
250
|
+
// 兼容 SDK 可能嵌套的 event 结构
|
|
251
|
+
const wrapped = data;
|
|
252
|
+
const event = (wrapped?.event ?? data);
|
|
253
|
+
const actionValue = event?.action?.value;
|
|
254
|
+
const chatId = event?.context?.open_chat_id ?? event?.context?.chat_id ?? event?.context?.open_id ?? '';
|
|
255
|
+
const userId = event?.sender?.sender_id?.open_id ?? '';
|
|
256
|
+
log.info(`[handleCardAction] chatId=${chatId}, userId=${userId}, actionValue=${JSON.stringify(actionValue)}`);
|
|
257
|
+
// 处理 mode 按钮(兼容 value 为对象或 JSON 字符串)
|
|
258
|
+
const modeAv = parseActionValue(actionValue);
|
|
259
|
+
if (modeAv?.action === 'mode' && modeAv.value) {
|
|
260
|
+
const mode = modeAv.value;
|
|
261
|
+
if (['ask', 'accept-edits', 'plan', 'yolo'].includes(mode)) {
|
|
262
|
+
setPermissionMode(userId, mode);
|
|
263
|
+
const toastContent = `✅ 已切换为 ${MODE_LABELS[mode]}`;
|
|
264
|
+
const label = MODE_LABELS[mode];
|
|
265
|
+
// 异步发送文本回复,不阻塞 3 秒内返回
|
|
266
|
+
const sendReply = () => {
|
|
267
|
+
if (chatId)
|
|
268
|
+
return sendTextReply(chatId, toastContent);
|
|
269
|
+
if (userId)
|
|
270
|
+
return sendTextReplyByOpenId(userId, toastContent);
|
|
271
|
+
log.warn('[handleCardAction] No chatId/userId, cannot send text reply');
|
|
272
|
+
};
|
|
273
|
+
const p = sendReply();
|
|
274
|
+
if (p)
|
|
275
|
+
p.catch((e) => log.warn('[handleCardAction] Send reply failed:', e));
|
|
276
|
+
// 同步只返回 toast,避免 200672(同步返回 card 格式易出错)
|
|
277
|
+
const cardToken = extractCardToken(data);
|
|
278
|
+
const readOnlyCard = createFeishuModeCardReadOnly(label);
|
|
279
|
+
if (cardToken && userId) {
|
|
280
|
+
// 延时更新:异步替换为只读卡片,防止二次点击
|
|
281
|
+
delayUpdateCard(cardToken, readOnlyCard, [userId]).catch((e) => log.warn('[handleCardAction] delayUpdateCard failed:', e));
|
|
282
|
+
}
|
|
283
|
+
else if (!cardToken) {
|
|
284
|
+
log.debug('[handleCardAction] No card token in event, cannot delay-update card');
|
|
285
|
+
}
|
|
286
|
+
return { toast: { type: 'success', content: toastContent } };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const parsed = parsePermissionActionValue(actionValue);
|
|
290
|
+
if (!parsed) {
|
|
291
|
+
log.info('[handleCardAction] Unrecognized action value, returning default toast');
|
|
292
|
+
return { toast: { type: 'warning', content: '未知操作' } };
|
|
293
|
+
}
|
|
294
|
+
const { decision, requestId } = parsed;
|
|
295
|
+
log.info(`[handleCardAction] Permission button: ${decision} for ${requestId}, chatId=${chatId}`);
|
|
296
|
+
const resolved = resolvePermissionById(requestId, decision);
|
|
297
|
+
const toastContent = resolved
|
|
298
|
+
? decision === 'allow'
|
|
299
|
+
? '✅ 权限已允许'
|
|
300
|
+
: '❌ 权限已拒绝'
|
|
301
|
+
: '⚠️ 权限请求已过期或不存在';
|
|
302
|
+
const sendPermReply = () => {
|
|
303
|
+
if (chatId)
|
|
304
|
+
return sendTextReply(chatId, toastContent);
|
|
305
|
+
if (userId)
|
|
306
|
+
return sendTextReplyByOpenId(userId, toastContent);
|
|
307
|
+
};
|
|
308
|
+
const permP = sendPermReply();
|
|
309
|
+
if (permP)
|
|
310
|
+
permP.catch((err) => log.warn('Failed to send permission reply:', err));
|
|
311
|
+
return { toast: { type: resolved ? 'success' : 'warning', content: toastContent } };
|
|
312
|
+
}
|
|
123
313
|
async function handleEvent(data) {
|
|
124
|
-
log.info('[handleEvent] Called with data:', JSON.stringify(data).slice(0,
|
|
314
|
+
log.info('[handleEvent] Called with data:', JSON.stringify(data).slice(0, 800));
|
|
125
315
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// {
|
|
130
|
-
// "event_type": "im.message.receive_v1",
|
|
131
|
-
// "event_id": "...",
|
|
132
|
-
// "tenant_key": "...",
|
|
133
|
-
// "app_id": "...",
|
|
134
|
-
// "message": { "chat_id": "...", "content": "...", ... },
|
|
135
|
-
// "sender": { "sender_id": { "open_id": "..." } }
|
|
136
|
-
// }
|
|
137
|
-
const event = data;
|
|
138
|
-
const eventType = event?.event_type;
|
|
316
|
+
const raw = data;
|
|
317
|
+
const event = (raw?.event ?? raw);
|
|
318
|
+
const eventType = event?.event_type ?? event?.type;
|
|
139
319
|
log.info('Feishu event type:', eventType);
|
|
140
|
-
//
|
|
320
|
+
// 1. 卡片按钮点击 (card.action.trigger) - 需快速返回响应
|
|
321
|
+
if (eventType === 'card.action.trigger') {
|
|
322
|
+
const result = await handleCardAction(data);
|
|
323
|
+
return result ?? { toast: { type: 'success', content: '已处理' } };
|
|
324
|
+
}
|
|
325
|
+
// 2. 消息接收 (im.message.receive_v1)
|
|
141
326
|
if (eventType === 'im.message.receive_v1') {
|
|
142
327
|
log.info('[handleEvent] Processing im.message.receive_v1 event');
|
|
328
|
+
// 兼容:部分场景下卡片点击可能通过 im.message 携带 action
|
|
329
|
+
if (event?.action?.value) {
|
|
330
|
+
const parsed = parsePermissionActionValue(event.action.value);
|
|
331
|
+
if (parsed) {
|
|
332
|
+
const { decision, requestId } = parsed;
|
|
333
|
+
const chatId = event.message?.chat_id ?? '';
|
|
334
|
+
log.info(`[handleEvent] Permission (via msg): ${decision} for ${requestId}`);
|
|
335
|
+
const resolved = resolvePermissionById(requestId, decision);
|
|
336
|
+
if (resolved) {
|
|
337
|
+
await sendTextReply(chatId, decision === 'allow' ? '✅ 权限已允许' : '❌ 权限已拒绝');
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
await sendTextReply(chatId, '⚠️ 权限请求已过期或不存在');
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
143
345
|
const message = event?.message;
|
|
144
346
|
if (!message) {
|
|
145
347
|
log.warn('No message data in event');
|
|
@@ -176,6 +378,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
176
378
|
}
|
|
177
379
|
log.info(`Access granted for sender: ${senderId}`);
|
|
178
380
|
setActiveChatId('feishu', chatId);
|
|
381
|
+
setChatUser(chatId, senderId);
|
|
179
382
|
// Handle different message types
|
|
180
383
|
if (msgType === 'text') {
|
|
181
384
|
const text = content.text?.trim() ?? '';
|
|
@@ -209,39 +412,42 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
209
412
|
}
|
|
210
413
|
else if (msgType === 'post') {
|
|
211
414
|
// Feishu rich text/post messages - extract text content
|
|
212
|
-
|
|
415
|
+
// 支持 post.content 或 zh_cn.content,content 可能是二维数组(段落→元素)
|
|
416
|
+
const post = content?.post
|
|
417
|
+
?? content?.zh_cn;
|
|
418
|
+
const rawContent = post?.content;
|
|
213
419
|
let text = '';
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
420
|
+
function extractTextFromElement(el) {
|
|
421
|
+
if (!el || typeof el !== 'object')
|
|
422
|
+
return '';
|
|
423
|
+
const obj = el;
|
|
424
|
+
const tag = obj.tag;
|
|
425
|
+
if (tag === 'text' || tag === 'plain_text') {
|
|
426
|
+
return (obj.text ?? obj.content ?? '').toString();
|
|
427
|
+
}
|
|
428
|
+
if (tag === 'a')
|
|
429
|
+
return (obj.text ?? obj.content ?? '').toString();
|
|
430
|
+
if (tag === 'heading' || tag === 'heading1' || tag === 'heading2' || tag === 'heading3') {
|
|
431
|
+
const headingText = el.text;
|
|
432
|
+
if (typeof headingText === 'string')
|
|
433
|
+
return headingText;
|
|
434
|
+
if (Array.isArray(headingText)) {
|
|
435
|
+
return headingText.map(extractTextFromElement).join('');
|
|
226
436
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
text += item.text ?? '';
|
|
238
|
-
}
|
|
239
|
-
}
|
|
437
|
+
}
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
if (rawContent && Array.isArray(rawContent)) {
|
|
441
|
+
log.info(`[MSG] Post content structure:`, JSON.stringify(rawContent).slice(0, 500));
|
|
442
|
+
for (const section of rawContent) {
|
|
443
|
+
if (Array.isArray(section)) {
|
|
444
|
+
// 二维数组:段落内多个元素
|
|
445
|
+
for (const el of section) {
|
|
446
|
+
text += extractTextFromElement(el);
|
|
240
447
|
}
|
|
241
448
|
}
|
|
242
449
|
else {
|
|
243
|
-
|
|
244
|
-
log.info(`[MSG] Unhandled post tag: ${tag}, section:`, JSON.stringify(section).slice(0, 200));
|
|
450
|
+
text += extractTextFromElement(section);
|
|
245
451
|
}
|
|
246
452
|
}
|
|
247
453
|
}
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
export type MessageStatus = 'thinking' | 'streaming' | 'done' | 'error';
|
|
2
|
+
/**
|
|
3
|
+
* Create Feishu card with action buttons
|
|
4
|
+
* Used for permission prompts and other interactive requests
|
|
5
|
+
*/
|
|
6
|
+
export declare function createFeishuButtonCard(title: string, content: string, buttons: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
type?: 'primary' | 'default';
|
|
10
|
+
}>): string;
|
|
11
|
+
/** 只读模式卡片(无按钮,用于回调后替换原卡片防止二次点击) */
|
|
12
|
+
export declare function createFeishuModeCardReadOnly(currentMode: string): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* 延时更新消息卡片(POST /open-apis/im/v1/cards/update)
|
|
15
|
+
* 用于在卡片回调 3 秒内无法完成时,异步替换卡片为只读版本,防止二次点击
|
|
16
|
+
* @param token 从卡片交互事件中获取的 token(格式 c-xxxx)
|
|
17
|
+
* @param card 卡片内容 { config, header, elements }
|
|
18
|
+
* @param openIds 非共享卡片需指定更新的用户 open_id 列表
|
|
19
|
+
*/
|
|
20
|
+
export declare function delayUpdateCard(token: string, card: Record<string, unknown>, openIds?: string[]): Promise<void>;
|
|
21
|
+
export declare function sendModeCard(chatId: string, _userId: string, currentMode: string): Promise<void>;
|
|
2
22
|
export declare function sendThinkingMessage(chatId: string, replyToMessageId: string | undefined, toolId?: string): Promise<string>;
|
|
3
23
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
4
24
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
5
25
|
export declare function sendTextReply(chatId: string, text: string): Promise<void>;
|
|
26
|
+
/** 使用 open_id 发送(私聊时 context 可能只有 open_id) */
|
|
27
|
+
export declare function sendTextReplyByOpenId(openId: string, text: string): Promise<void>;
|
|
6
28
|
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
7
29
|
export declare function startTypingLoop(_chatId: string): () => void;
|
|
@@ -61,6 +61,161 @@ function createFeishuCard(title, content, status, note) {
|
|
|
61
61
|
};
|
|
62
62
|
return JSON.stringify(card);
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Create Feishu card with action buttons
|
|
66
|
+
* Used for permission prompts and other interactive requests
|
|
67
|
+
*/
|
|
68
|
+
export function createFeishuButtonCard(title, content, buttons) {
|
|
69
|
+
const elements = [];
|
|
70
|
+
// Main content
|
|
71
|
+
elements.push({
|
|
72
|
+
tag: 'div',
|
|
73
|
+
text: {
|
|
74
|
+
tag: 'lark_md',
|
|
75
|
+
content: content,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
// Add separator
|
|
79
|
+
elements.push({ tag: 'hr' });
|
|
80
|
+
// Add action buttons
|
|
81
|
+
const actionGroups = [];
|
|
82
|
+
// Split buttons into rows (max 4 buttons per row in Feishu)
|
|
83
|
+
for (let i = 0; i < buttons.length; i += 4) {
|
|
84
|
+
const row = buttons.slice(i, i + 4).map((btn) => ({
|
|
85
|
+
tag: 'button',
|
|
86
|
+
text: {
|
|
87
|
+
tag: 'plain_text',
|
|
88
|
+
content: btn.label,
|
|
89
|
+
},
|
|
90
|
+
type: btn.type || 'default',
|
|
91
|
+
value: {
|
|
92
|
+
action: 'permission',
|
|
93
|
+
value: btn.value,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
actionGroups.push({
|
|
97
|
+
tag: 'action',
|
|
98
|
+
actions: row,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
elements.push(...actionGroups);
|
|
102
|
+
const card = {
|
|
103
|
+
config: {
|
|
104
|
+
wide_screen_mode: true,
|
|
105
|
+
},
|
|
106
|
+
header: {
|
|
107
|
+
template: 'blue',
|
|
108
|
+
title: {
|
|
109
|
+
content: `🔐 ${title}`,
|
|
110
|
+
tag: 'plain_text',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
elements,
|
|
114
|
+
};
|
|
115
|
+
return JSON.stringify(card);
|
|
116
|
+
}
|
|
117
|
+
/** 只读模式卡片(无按钮,用于回调后替换原卡片防止二次点击) */
|
|
118
|
+
export function createFeishuModeCardReadOnly(currentMode) {
|
|
119
|
+
return {
|
|
120
|
+
config: { wide_screen_mode: true },
|
|
121
|
+
header: {
|
|
122
|
+
template: 'green',
|
|
123
|
+
title: { content: '🔐 权限模式', tag: 'plain_text' },
|
|
124
|
+
},
|
|
125
|
+
elements: [
|
|
126
|
+
{
|
|
127
|
+
tag: 'div',
|
|
128
|
+
text: {
|
|
129
|
+
tag: 'lark_md',
|
|
130
|
+
content: `**当前模式:** ${currentMode}\n\n✅ 已切换成功,发送 \`/mode\` 可再次切换。`,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 延时更新消息卡片(POST /open-apis/im/v1/cards/update)
|
|
138
|
+
* 用于在卡片回调 3 秒内无法完成时,异步替换卡片为只读版本,防止二次点击
|
|
139
|
+
* @param token 从卡片交互事件中获取的 token(格式 c-xxxx)
|
|
140
|
+
* @param card 卡片内容 { config, header, elements }
|
|
141
|
+
* @param openIds 非共享卡片需指定更新的用户 open_id 列表
|
|
142
|
+
*/
|
|
143
|
+
export async function delayUpdateCard(token, card, openIds) {
|
|
144
|
+
const accessToken = await getTenantAccessToken();
|
|
145
|
+
// 非共享卡片需在 card 内指定 open_ids
|
|
146
|
+
const cardBody = { ...card };
|
|
147
|
+
if (openIds && openIds.length > 0) {
|
|
148
|
+
cardBody.open_ids = openIds;
|
|
149
|
+
}
|
|
150
|
+
const body = { token, card: cardBody };
|
|
151
|
+
const resp = await fetch('https://open.feishu.cn/open-apis/interactive/v1/card/update', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
Authorization: `Bearer ${accessToken}`,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(body),
|
|
158
|
+
});
|
|
159
|
+
const data = (await resp.json());
|
|
160
|
+
if (data.code !== 0) {
|
|
161
|
+
log.warn(`[delayUpdateCard] Failed: code=${data.code}, msg=${data.msg}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
log.info('[delayUpdateCard] Card updated successfully');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create mode switch card with action type for card callback
|
|
168
|
+
*/
|
|
169
|
+
function createFeishuModeCard(currentMode, buttons) {
|
|
170
|
+
const elements = [];
|
|
171
|
+
elements.push({
|
|
172
|
+
tag: 'div',
|
|
173
|
+
text: {
|
|
174
|
+
tag: 'lark_md',
|
|
175
|
+
content: `**当前模式:** ${currentMode}\n\n点击下方按钮切换模式:\n\n_💡 若点击报错:开放平台 → 事件与回调 → 切到「回调」Tab → 添加「卡片回传交互」。或直接用 \`/mode ask\` 等命令切换。_`,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
elements.push({ tag: 'hr' });
|
|
179
|
+
for (let i = 0; i < buttons.length; i += 4) {
|
|
180
|
+
const row = buttons.slice(i, i + 4).map((btn) => ({
|
|
181
|
+
tag: 'button',
|
|
182
|
+
text: { tag: 'plain_text', content: btn.label },
|
|
183
|
+
type: btn.type || 'default',
|
|
184
|
+
value: { action: 'mode', value: btn.value },
|
|
185
|
+
}));
|
|
186
|
+
elements.push({ tag: 'action', actions: row });
|
|
187
|
+
}
|
|
188
|
+
const card = {
|
|
189
|
+
config: { wide_screen_mode: true },
|
|
190
|
+
header: {
|
|
191
|
+
template: 'blue',
|
|
192
|
+
title: { content: '🔐 权限模式', tag: 'plain_text' },
|
|
193
|
+
},
|
|
194
|
+
elements,
|
|
195
|
+
};
|
|
196
|
+
return JSON.stringify(card);
|
|
197
|
+
}
|
|
198
|
+
export async function sendModeCard(chatId, _userId, currentMode) {
|
|
199
|
+
const { getClient } = await import('./client.js');
|
|
200
|
+
const { MODE_LABELS } = await import('../permission-mode/types.js');
|
|
201
|
+
const client = getClient();
|
|
202
|
+
const MODE_BTNS = [
|
|
203
|
+
{ label: MODE_LABELS.ask, value: 'ask', type: 'default' },
|
|
204
|
+
{ label: MODE_LABELS['accept-edits'], value: 'accept-edits', type: 'default' },
|
|
205
|
+
{ label: MODE_LABELS.plan, value: 'plan', type: 'default' },
|
|
206
|
+
{ label: MODE_LABELS.yolo, value: 'yolo', type: 'default' },
|
|
207
|
+
];
|
|
208
|
+
const currentLabel = MODE_BTNS.find((b) => b.value === currentMode)?.label ?? currentMode;
|
|
209
|
+
const cardContent = createFeishuModeCard(currentLabel, MODE_BTNS);
|
|
210
|
+
await client.im.message.create({
|
|
211
|
+
data: {
|
|
212
|
+
receive_id: chatId,
|
|
213
|
+
msg_type: 'interactive',
|
|
214
|
+
content: cardContent,
|
|
215
|
+
},
|
|
216
|
+
params: { receive_id_type: 'chat_id' },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
64
219
|
async function getTenantAccessToken() {
|
|
65
220
|
const client = getClient();
|
|
66
221
|
const resp = await client.auth.tenantAccessToken.internal({
|
|
@@ -100,8 +255,7 @@ export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'cl
|
|
|
100
255
|
}
|
|
101
256
|
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
102
257
|
const client = getClient();
|
|
103
|
-
const
|
|
104
|
-
const title = `${icon} ${getToolTitle(toolId, status)}`;
|
|
258
|
+
const title = getToolTitle(toolId, status);
|
|
105
259
|
const cardContent = createFeishuCard(title, content, status, note);
|
|
106
260
|
// Try to use patch API for in-place update (streaming)
|
|
107
261
|
try {
|
|
@@ -223,6 +377,24 @@ export async function sendTextReply(chatId, text) {
|
|
|
223
377
|
log.error('Failed to send text:', err);
|
|
224
378
|
}
|
|
225
379
|
}
|
|
380
|
+
/** 使用 open_id 发送(私聊时 context 可能只有 open_id) */
|
|
381
|
+
export async function sendTextReplyByOpenId(openId, text) {
|
|
382
|
+
const client = getClient();
|
|
383
|
+
const cardContent = createFeishuCard('📢 open-im', text, 'done');
|
|
384
|
+
try {
|
|
385
|
+
await client.im.message.create({
|
|
386
|
+
data: {
|
|
387
|
+
receive_id: openId,
|
|
388
|
+
msg_type: 'interactive',
|
|
389
|
+
content: cardContent,
|
|
390
|
+
},
|
|
391
|
+
params: { receive_id_type: 'open_id' },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
log.error('Failed to send text by open_id:', err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
226
398
|
export async function sendImageReply(chatId, imagePath) {
|
|
227
399
|
const client = getClient();
|
|
228
400
|
try {
|
|
@@ -1,4 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Permission Server - Handles tool permission requests from Claude CLI
|
|
3
|
+
*
|
|
4
|
+
* When claudeSkipPermissions is false and not in yolo mode, Claude CLI will make
|
|
5
|
+
* HTTP requests to this server. We forward all requests to the user for approval;
|
|
6
|
+
* permission mode logic (ask/accept-edits/plan) is handled by Claude via --permission-mode.
|
|
7
|
+
*/
|
|
8
|
+
interface MessageSender {
|
|
9
|
+
sendTextReply(chatId: string, text: string): Promise<void>;
|
|
10
|
+
sendPermissionCard?(chatId: string, requestId: string, toolName: string, toolInput: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Start the permission HTTP server
|
|
14
|
+
*/
|
|
15
|
+
export declare function startPermissionServer(port?: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Stop the permission HTTP server
|
|
18
|
+
*/
|
|
19
|
+
export declare function stopPermissionServer(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Register the message sender for sending permission prompts
|
|
22
|
+
*/
|
|
23
|
+
export declare function registerPermissionSender(_platform: string, sender: MessageSender): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get the number of pending permission requests for a chat
|
|
26
|
+
*/
|
|
27
|
+
export declare function getPendingCount(chatId: string): number;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the latest pending permission request for a chat
|
|
30
|
+
* Returns the requestId if found, null otherwise
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveLatestPermission(chatId: string, decision: 'allow' | 'deny'): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a specific permission request by ID
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolvePermissionById(requestId: string, decision: 'allow' | 'deny'): boolean;
|
|
37
|
+
export {};
|