@wu529778790/open-im 1.0.2-beta.2 → 1.0.2-beta.3
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/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/feishu/event-handler.js +83 -3
- package/dist/feishu/message-sender.d.ts +9 -0
- package/dist/feishu/message-sender.js +53 -0
- package/dist/hook/permission-server.d.ts +36 -4
- package/dist/hook/permission-server.js +286 -8
- package/dist/index.js +5 -0
- package/dist/shared/ai-task.js +1 -0
- package/dist/telegram/event-handler.js +1 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -87,6 +87,9 @@ export function loadConfig() {
|
|
|
87
87
|
const claudeTimeoutMs = process.env.CLAUDE_TIMEOUT_MS !== undefined
|
|
88
88
|
? parseInt(process.env.CLAUDE_TIMEOUT_MS, 10) || 600000
|
|
89
89
|
: file.claudeTimeoutMs ?? 600000;
|
|
90
|
+
const hookPort = process.env.HOOK_PORT !== undefined
|
|
91
|
+
? parseInt(process.env.HOOK_PORT, 10) || 35801
|
|
92
|
+
: file.hookPort ?? 35801;
|
|
90
93
|
// 6. 校验 Claude CLI
|
|
91
94
|
if (aiCommand === 'claude') {
|
|
92
95
|
if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
|
|
@@ -166,6 +169,7 @@ export function loadConfig() {
|
|
|
166
169
|
claudeSkipPermissions,
|
|
167
170
|
claudeTimeoutMs,
|
|
168
171
|
claudeModel: process.env.CLAUDE_MODEL ?? file.claudeModel,
|
|
172
|
+
hookPort,
|
|
169
173
|
logDir,
|
|
170
174
|
logLevel,
|
|
171
175
|
platforms,
|
|
@@ -2,8 +2,8 @@ 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, startTypingLoop, sendImageReply, createFeishuButtonCard, } from './message-sender.js';
|
|
6
|
+
import { registerPermissionSender, resolvePermissionById } from '../hook/permission-server.js';
|
|
7
7
|
import { CommandHandler } from '../commands/handler.js';
|
|
8
8
|
import { getAdapter } from '../adapters/registry.js';
|
|
9
9
|
import { runAITask } from '../shared/ai-task.js';
|
|
@@ -56,6 +56,48 @@ async function downloadFeishuImage(client, imageKey) {
|
|
|
56
56
|
await writeFile(imagePath, buffer);
|
|
57
57
|
return imagePath;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Send permission prompt card with interactive buttons
|
|
61
|
+
*/
|
|
62
|
+
async function sendPermissionCard(chatId, requestId, toolName, toolInput) {
|
|
63
|
+
const { getClient } = await import('./client.js');
|
|
64
|
+
const client = getClient();
|
|
65
|
+
// Format tool input for display
|
|
66
|
+
let formattedInput;
|
|
67
|
+
if (toolInput.length > 300) {
|
|
68
|
+
formattedInput = toolInput.slice(0, 300) + '...';
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
formattedInput = toolInput;
|
|
72
|
+
}
|
|
73
|
+
const content = `**工具:** \`${toolName}\`
|
|
74
|
+
|
|
75
|
+
**参数:**
|
|
76
|
+
\`\`\`
|
|
77
|
+
${formattedInput}
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
**请求 ID:** \`${requestId.slice(-8)}\``;
|
|
81
|
+
const cardContent = createFeishuButtonCard('权限请求', content, [
|
|
82
|
+
{ label: '✅ 允许', value: `allow_${requestId}`, type: 'primary' },
|
|
83
|
+
{ label: '❌ 拒绝', value: `deny_${requestId}`, type: 'default' },
|
|
84
|
+
]);
|
|
85
|
+
try {
|
|
86
|
+
await client.im.message.create({
|
|
87
|
+
data: {
|
|
88
|
+
receive_id: chatId,
|
|
89
|
+
msg_type: 'interactive',
|
|
90
|
+
content: cardContent,
|
|
91
|
+
},
|
|
92
|
+
params: { receive_id_type: 'chat_id' },
|
|
93
|
+
});
|
|
94
|
+
log.info(`Permission card sent for request ${requestId}`);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
log.error('Failed to send permission card:', err);
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
59
101
|
export function setupFeishuHandlers(config, sessionManager) {
|
|
60
102
|
const accessControl = new AccessControl(config.feishuAllowedUserIds);
|
|
61
103
|
const requestQueue = new RequestQueue();
|
|
@@ -68,7 +110,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
68
110
|
sender: { sendTextReply },
|
|
69
111
|
getRunningTasksSize: () => runningTasks.size,
|
|
70
112
|
});
|
|
71
|
-
registerPermissionSender('feishu', {});
|
|
113
|
+
registerPermissionSender('feishu', { sendTextReply, sendPermissionCard });
|
|
72
114
|
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
73
115
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
74
116
|
log.info(`[AI_REQUEST] Full prompt: "${prompt}"`);
|
|
@@ -133,6 +175,10 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
133
175
|
// "app_id": "...",
|
|
134
176
|
// "message": { "chat_id": "...", "content": "...", ... },
|
|
135
177
|
// "sender": { "sender_id": { "open_id": "..." } }
|
|
178
|
+
// "action": { // For card button clicks
|
|
179
|
+
// "action_id": "...",
|
|
180
|
+
// "value": { "action": "permission", "value": "allow_xxx" }
|
|
181
|
+
// }
|
|
136
182
|
// }
|
|
137
183
|
const event = data;
|
|
138
184
|
const eventType = event?.event_type;
|
|
@@ -140,6 +186,40 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
140
186
|
// Handle message received events
|
|
141
187
|
if (eventType === 'im.message.receive_v1') {
|
|
142
188
|
log.info('[handleEvent] Processing im.message.receive_v1 event');
|
|
189
|
+
// Check if this is a card button click event
|
|
190
|
+
// For interactive cards, the action is in a different location
|
|
191
|
+
if (event?.action) {
|
|
192
|
+
const action = event.action;
|
|
193
|
+
log.info('[handleEvent] Card action detected:', action);
|
|
194
|
+
if (action?.value) {
|
|
195
|
+
const actionValue = action.value;
|
|
196
|
+
if (actionValue.action === 'permission' && actionValue.value) {
|
|
197
|
+
const buttonValue = actionValue.value;
|
|
198
|
+
let decision = null;
|
|
199
|
+
let requestId = null;
|
|
200
|
+
if (buttonValue.startsWith('allow_')) {
|
|
201
|
+
decision = 'allow';
|
|
202
|
+
requestId = buttonValue.slice(6);
|
|
203
|
+
}
|
|
204
|
+
else if (buttonValue.startsWith('deny_')) {
|
|
205
|
+
decision = 'deny';
|
|
206
|
+
requestId = buttonValue.slice(5);
|
|
207
|
+
}
|
|
208
|
+
if (decision && requestId) {
|
|
209
|
+
log.info(`[handleEvent] Permission button clicked: ${decision} for ${requestId}`);
|
|
210
|
+
const resolved = resolvePermissionById(requestId, decision);
|
|
211
|
+
const chatId = event.message?.chat_id ?? '';
|
|
212
|
+
if (resolved) {
|
|
213
|
+
await sendTextReply(chatId, decision === 'allow' ? '✅ 权限已允许' : '❌ 权限已拒绝');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
await sendTextReply(chatId, '⚠️ 权限请求已过期或不存在');
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
143
223
|
const message = event?.message;
|
|
144
224
|
if (!message) {
|
|
145
225
|
log.warn('No message data in event');
|
|
@@ -1,4 +1,13 @@
|
|
|
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;
|
|
2
11
|
export declare function sendThinkingMessage(chatId: string, replyToMessageId: string | undefined, toolId?: string): Promise<string>;
|
|
3
12
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
4
13
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
@@ -61,6 +61,59 @@ 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
|
+
}
|
|
64
117
|
async function getTenantAccessToken() {
|
|
65
118
|
const client = getClient();
|
|
66
119
|
const resp = await client.auth.tenantAccessToken.internal({
|
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Permission Server - Handles tool permission requests from Claude CLI
|
|
3
|
+
*
|
|
4
|
+
* When claudeSkipPermissions is false, Claude CLI will make HTTP requests
|
|
5
|
+
* to this server to ask for user approval before running tools.
|
|
6
|
+
*/
|
|
7
|
+
interface MessageSender {
|
|
8
|
+
sendTextReply(chatId: string, text: string): Promise<void>;
|
|
9
|
+
sendPermissionCard?(chatId: string, requestId: string, toolName: string, toolInput: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Start the permission HTTP server
|
|
13
|
+
*/
|
|
14
|
+
export declare function startPermissionServer(port?: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* Stop the permission HTTP server
|
|
17
|
+
*/
|
|
18
|
+
export declare function stopPermissionServer(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Register the message sender for sending permission prompts
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerPermissionSender(_platform: string, sender: MessageSender): void;
|
|
23
|
+
/**
|
|
24
|
+
* Get the number of pending permission requests for a chat
|
|
25
|
+
*/
|
|
26
|
+
export declare function getPendingCount(chatId: string): number;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the latest pending permission request for a chat
|
|
29
|
+
* Returns the requestId if found, null otherwise
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveLatestPermission(chatId: string, decision: 'allow' | 'deny'): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a specific permission request by ID
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvePermissionById(requestId: string, decision: 'allow' | 'deny'): boolean;
|
|
36
|
+
export {};
|
|
@@ -1,12 +1,290 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Permission Server - Handles tool permission requests from Claude CLI
|
|
3
|
+
*
|
|
4
|
+
* When claudeSkipPermissions is false, Claude CLI will make HTTP requests
|
|
5
|
+
* to this server to ask for user approval before running tools.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from 'node:http';
|
|
8
|
+
import { createLogger } from '../logger.js';
|
|
9
|
+
const log = createLogger('PermissionServer');
|
|
10
|
+
const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default timeout
|
|
11
|
+
const DEFAULT_PORT = 35801;
|
|
12
|
+
// Global state
|
|
13
|
+
let server = null;
|
|
14
|
+
let serverPort = DEFAULT_PORT;
|
|
15
|
+
const pendingRequests = new Map();
|
|
16
|
+
const requestsByChat = new Map();
|
|
17
|
+
let messageSender = null;
|
|
18
|
+
/**
|
|
19
|
+
* Start the permission HTTP server
|
|
20
|
+
*/
|
|
21
|
+
export function startPermissionServer(port = DEFAULT_PORT) {
|
|
22
|
+
if (server) {
|
|
23
|
+
log.warn('Permission server already running');
|
|
24
|
+
return serverPort;
|
|
25
|
+
}
|
|
26
|
+
serverPort = port;
|
|
27
|
+
server = createServer(handleRequest);
|
|
28
|
+
server.listen(port, () => {
|
|
29
|
+
log.info(`Permission server listening on port ${port}`);
|
|
30
|
+
});
|
|
31
|
+
server.on('error', (err) => {
|
|
32
|
+
log.error('Permission server error:', err);
|
|
33
|
+
});
|
|
34
|
+
// Cleanup expired permissions every minute
|
|
35
|
+
setInterval(() => {
|
|
36
|
+
cleanupExpiredPermissions();
|
|
37
|
+
}, 60 * 1000).unref();
|
|
38
|
+
return port;
|
|
3
39
|
}
|
|
4
|
-
|
|
5
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Stop the permission HTTP server
|
|
42
|
+
*/
|
|
43
|
+
export function stopPermissionServer() {
|
|
44
|
+
if (server) {
|
|
45
|
+
server.close(() => {
|
|
46
|
+
log.info('Permission server stopped');
|
|
47
|
+
});
|
|
48
|
+
server = null;
|
|
49
|
+
// Reject all pending requests
|
|
50
|
+
for (const req of pendingRequests.values()) {
|
|
51
|
+
clearTimeout(req.timeout);
|
|
52
|
+
req.resolve(false);
|
|
53
|
+
}
|
|
54
|
+
pendingRequests.clear();
|
|
55
|
+
requestsByChat.clear();
|
|
56
|
+
}
|
|
6
57
|
}
|
|
7
|
-
|
|
8
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Register the message sender for sending permission prompts
|
|
60
|
+
*/
|
|
61
|
+
export function registerPermissionSender(_platform, sender) {
|
|
62
|
+
messageSender = sender;
|
|
63
|
+
log.info('Message sender registered for permission prompts');
|
|
9
64
|
}
|
|
10
|
-
|
|
11
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Get the number of pending permission requests for a chat
|
|
67
|
+
*/
|
|
68
|
+
export function getPendingCount(chatId) {
|
|
69
|
+
const requests = requestsByChat.get(chatId);
|
|
70
|
+
return requests ? requests.length : 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the latest pending permission request for a chat
|
|
74
|
+
* Returns the requestId if found, null otherwise
|
|
75
|
+
*/
|
|
76
|
+
export function resolveLatestPermission(chatId, decision) {
|
|
77
|
+
const requests = requestsByChat.get(chatId);
|
|
78
|
+
if (!requests || requests.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// Get the oldest (first) pending request
|
|
82
|
+
const request = requests.shift();
|
|
83
|
+
if (requests.length === 0) {
|
|
84
|
+
requestsByChat.delete(chatId);
|
|
85
|
+
}
|
|
86
|
+
// Remove from global map
|
|
87
|
+
pendingRequests.delete(request.requestId);
|
|
88
|
+
// Clear timeout and resolve
|
|
89
|
+
clearTimeout(request.timeout);
|
|
90
|
+
request.resolve(decision === 'allow');
|
|
91
|
+
log.info(`Resolved permission ${request.requestId}: ${decision} for tool ${request.toolName}`);
|
|
92
|
+
return request.requestId;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a specific permission request by ID
|
|
96
|
+
*/
|
|
97
|
+
export function resolvePermissionById(requestId, decision) {
|
|
98
|
+
const request = pendingRequests.get(requestId);
|
|
99
|
+
if (!request) {
|
|
100
|
+
log.warn(`Permission request not found: ${requestId}`);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
// Remove from chat's list
|
|
104
|
+
const chatRequests = requestsByChat.get(request.chatId);
|
|
105
|
+
if (chatRequests) {
|
|
106
|
+
const index = chatRequests.findIndex(r => r.requestId === requestId);
|
|
107
|
+
if (index !== -1) {
|
|
108
|
+
chatRequests.splice(index, 1);
|
|
109
|
+
}
|
|
110
|
+
if (chatRequests.length === 0) {
|
|
111
|
+
requestsByChat.delete(request.chatId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Remove from global map
|
|
115
|
+
pendingRequests.delete(requestId);
|
|
116
|
+
// Clear timeout and resolve
|
|
117
|
+
clearTimeout(request.timeout);
|
|
118
|
+
request.resolve(decision === 'allow');
|
|
119
|
+
log.info(`Resolved permission ${requestId}: ${decision} for tool ${request.toolName}`);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Handle incoming HTTP requests from Claude CLI
|
|
124
|
+
*/
|
|
125
|
+
function handleRequest(req, res) {
|
|
126
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
127
|
+
log.debug(`Permission request: ${req.method} ${url.pathname}`);
|
|
128
|
+
if (url.pathname === '/permission' && req.method === 'POST') {
|
|
129
|
+
handlePermissionRequest(req, res);
|
|
130
|
+
}
|
|
131
|
+
else if (url.pathname === '/health' && req.method === 'GET') {
|
|
132
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
133
|
+
res.end(JSON.stringify({ status: 'ok', port: serverPort }));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
res.writeHead(404);
|
|
137
|
+
res.end('Not found');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Handle a permission request from Claude CLI
|
|
142
|
+
*/
|
|
143
|
+
function handlePermissionRequest(req, res) {
|
|
144
|
+
let body = '';
|
|
145
|
+
req.on('data', (chunk) => {
|
|
146
|
+
body += chunk.toString();
|
|
147
|
+
});
|
|
148
|
+
req.on('end', async () => {
|
|
149
|
+
try {
|
|
150
|
+
const data = JSON.parse(body);
|
|
151
|
+
const { requestId, toolName, toolInput } = data;
|
|
152
|
+
if (!requestId || !toolName) {
|
|
153
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
154
|
+
res.end(JSON.stringify({ error: 'Missing required fields' }));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Get chatId from environment (set by CC_IM_CHAT_ID)
|
|
158
|
+
const chatId = process.env.CC_IM_CHAT_ID;
|
|
159
|
+
if (!chatId) {
|
|
160
|
+
log.warn('No chatId in environment, auto-allowing permission');
|
|
161
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
|
+
res.end(JSON.stringify({ allowed: true }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Check if skip permissions is enabled
|
|
166
|
+
if (process.env.CC_SKIP_PERMISSIONS === 'true') {
|
|
167
|
+
log.info(`Skip permissions enabled, auto-allowing ${toolName}`);
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
169
|
+
res.end(JSON.stringify({ allowed: true }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Check if already resolved (race condition)
|
|
173
|
+
if (pendingRequests.has(requestId)) {
|
|
174
|
+
log.warn(`Duplicate permission request: ${requestId}`);
|
|
175
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
176
|
+
res.end(JSON.stringify({ error: 'Duplicate request' }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Send pending response
|
|
180
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
181
|
+
res.end(JSON.stringify({ status: 'pending' }));
|
|
182
|
+
// Create permission request
|
|
183
|
+
const permissionRequest = {
|
|
184
|
+
requestId,
|
|
185
|
+
chatId,
|
|
186
|
+
toolName,
|
|
187
|
+
toolInput: formatToolInput(toolInput),
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
resolve: null, // Will set below
|
|
190
|
+
timeout: null,
|
|
191
|
+
};
|
|
192
|
+
// Create promise for waiting for user response
|
|
193
|
+
const permissionPromise = new Promise((resolve) => {
|
|
194
|
+
permissionRequest.resolve = resolve;
|
|
195
|
+
});
|
|
196
|
+
// Set timeout
|
|
197
|
+
permissionRequest.timeout = setTimeout(() => {
|
|
198
|
+
log.info(`Permission request ${requestId} timed out`);
|
|
199
|
+
resolvePermissionById(requestId, 'deny');
|
|
200
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
201
|
+
// Store request
|
|
202
|
+
pendingRequests.set(requestId, permissionRequest);
|
|
203
|
+
// Add to chat's list
|
|
204
|
+
let chatRequests = requestsByChat.get(chatId);
|
|
205
|
+
if (!chatRequests) {
|
|
206
|
+
chatRequests = [];
|
|
207
|
+
requestsByChat.set(chatId, chatRequests);
|
|
208
|
+
}
|
|
209
|
+
chatRequests.push(permissionRequest);
|
|
210
|
+
// Send permission prompt to user
|
|
211
|
+
await sendPermissionPrompt(permissionRequest);
|
|
212
|
+
// Wait for user response
|
|
213
|
+
const allowed = await permissionPromise;
|
|
214
|
+
log.info(`Permission ${requestId} for ${toolName}: ${allowed ? 'ALLOWED' : 'DENIED'}`);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
log.error('Error handling permission request:', err);
|
|
218
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
219
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Send permission prompt to the user
|
|
225
|
+
*/
|
|
226
|
+
async function sendPermissionPrompt(request) {
|
|
227
|
+
if (!messageSender) {
|
|
228
|
+
log.warn('No message sender registered, cannot send permission prompt');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Try to use interactive button card if available (Feishu)
|
|
232
|
+
if (messageSender.sendPermissionCard) {
|
|
233
|
+
try {
|
|
234
|
+
await messageSender.sendPermissionCard(request.chatId, request.requestId, request.toolName, request.toolInput);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
log.debug('Failed to send permission card, falling back to text:', err);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Fallback to text-based prompt
|
|
242
|
+
const prompt = `🔐 **权限请求**
|
|
243
|
+
|
|
244
|
+
工具: \`${request.toolName}\`
|
|
245
|
+
|
|
246
|
+
参数:
|
|
247
|
+
\`\`\`
|
|
248
|
+
${request.toolInput}
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
回复 \`/allow\` 允许,\`/deny\` 拒绝
|
|
252
|
+
|
|
253
|
+
请求 ID: ${request.requestId}`;
|
|
254
|
+
try {
|
|
255
|
+
await messageSender.sendTextReply(request.chatId, prompt);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
log.error('Failed to send permission prompt:', err);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Format tool input for display
|
|
263
|
+
*/
|
|
264
|
+
function formatToolInput(toolInput) {
|
|
265
|
+
if (typeof toolInput === 'string') {
|
|
266
|
+
return toolInput;
|
|
267
|
+
}
|
|
268
|
+
if (typeof toolInput === 'object' && toolInput !== null) {
|
|
269
|
+
try {
|
|
270
|
+
const str = JSON.stringify(toolInput, null, 2);
|
|
271
|
+
return str.length > 500 ? str.slice(0, 500) + '...' : str;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return String(toolInput);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return String(toolInput);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Clean up expired permission requests
|
|
281
|
+
*/
|
|
282
|
+
function cleanupExpiredPermissions() {
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
for (const [requestId, request] of pendingRequests.entries()) {
|
|
285
|
+
if (now - request.timestamp > PERMISSION_TIMEOUT_MS) {
|
|
286
|
+
log.info(`Cleaning up expired permission: ${requestId}`);
|
|
287
|
+
resolvePermissionById(requestId, 'deny');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
12
290
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { SessionManager } from "./session/session-manager.js";
|
|
|
16
16
|
import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
|
|
17
17
|
import { initLogger, createLogger, closeLogger } from "./logger.js";
|
|
18
18
|
import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
|
|
19
|
+
import { startPermissionServer, stopPermissionServer } from "./hook/permission-server.js";
|
|
19
20
|
import { createRequire } from "node:module";
|
|
20
21
|
const require = createRequire(import.meta.url);
|
|
21
22
|
const { version: APP_VERSION } = require("../package.json");
|
|
@@ -46,6 +47,9 @@ export async function main() {
|
|
|
46
47
|
initLogger(config.logDir, config.logLevel);
|
|
47
48
|
loadActiveChats();
|
|
48
49
|
initAdapters(config);
|
|
50
|
+
// Start permission server for tool approval
|
|
51
|
+
const actualPort = startPermissionServer(config.hookPort);
|
|
52
|
+
log.info(`Permission server listening on port ${actualPort}`);
|
|
49
53
|
log.info("Starting open-im bridge...");
|
|
50
54
|
log.info(`AI 工具: ${config.aiCommand}`);
|
|
51
55
|
log.info(`工作目录: ${config.claudeWorkDir}`);
|
|
@@ -99,6 +103,7 @@ export async function main() {
|
|
|
99
103
|
stopTelegram();
|
|
100
104
|
feishuHandle?.stop();
|
|
101
105
|
stopFeishu();
|
|
106
|
+
stopPermissionServer();
|
|
102
107
|
sessionManager.destroy();
|
|
103
108
|
cleanupAdapters();
|
|
104
109
|
flushActiveChats();
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -153,6 +153,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
153
153
|
timeoutMs: config.claudeTimeoutMs,
|
|
154
154
|
model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
|
|
155
155
|
chatId: ctx.chatId,
|
|
156
|
+
hookPort: config.hookPort,
|
|
156
157
|
});
|
|
157
158
|
taskState = { handle, latestContent: '', settle, startedAt: Date.now() };
|
|
158
159
|
platformAdapter.onTaskReady(taskState);
|
|
@@ -77,7 +77,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
77
77
|
sender: { sendTextReply },
|
|
78
78
|
getRunningTasksSize: () => runningTasks.size,
|
|
79
79
|
});
|
|
80
|
-
registerPermissionSender("telegram", {});
|
|
80
|
+
registerPermissionSender("telegram", { sendTextReply });
|
|
81
81
|
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
82
82
|
// 在用户每次发送消息时就累加计数,确保提示能轮换显示
|
|
83
83
|
const currentTurns = sessionManager.addTurns(userId, 1);
|