@wu529778790/open-im 1.0.2-beta.1 → 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 CHANGED
@@ -16,6 +16,7 @@ export interface Config {
16
16
  claudeSkipPermissions: boolean;
17
17
  claudeTimeoutMs: number;
18
18
  claudeModel?: string;
19
+ hookPort: number;
19
20
  logDir: string;
20
21
  logLevel: LogLevel;
21
22
  platforms: {
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,13 +2,12 @@ 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';
10
10
  import { startTaskCleanup } from '../shared/task-cleanup.js';
11
- import { MessageDedup } from '../shared/message-dedup.js';
12
11
  import { THROTTLE_MS, IMAGE_DIR } from '../constants.js';
13
12
  import { setActiveChatId } from '../shared/active-chats.js';
14
13
  import { createLogger } from '../logger.js';
@@ -57,12 +56,53 @@ async function downloadFeishuImage(client, imageKey) {
57
56
  await writeFile(imagePath, buffer);
58
57
  return imagePath;
59
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
+ }
60
101
  export function setupFeishuHandlers(config, sessionManager) {
61
102
  const accessControl = new AccessControl(config.feishuAllowedUserIds);
62
103
  const requestQueue = new RequestQueue();
63
104
  const runningTasks = new Map();
64
105
  const stopTaskCleanup = startTaskCleanup(runningTasks);
65
- const dedup = new MessageDedup();
66
106
  const commandHandler = new CommandHandler({
67
107
  config,
68
108
  sessionManager,
@@ -70,7 +110,7 @@ export function setupFeishuHandlers(config, sessionManager) {
70
110
  sender: { sendTextReply },
71
111
  getRunningTasksSize: () => runningTasks.size,
72
112
  });
73
- registerPermissionSender('feishu', {});
113
+ registerPermissionSender('feishu', { sendTextReply, sendPermissionCard });
74
114
  async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
75
115
  log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
76
116
  log.info(`[AI_REQUEST] Full prompt: "${prompt}"`);
@@ -135,6 +175,10 @@ export function setupFeishuHandlers(config, sessionManager) {
135
175
  // "app_id": "...",
136
176
  // "message": { "chat_id": "...", "content": "...", ... },
137
177
  // "sender": { "sender_id": { "open_id": "..." } }
178
+ // "action": { // For card button clicks
179
+ // "action_id": "...",
180
+ // "value": { "action": "permission", "value": "allow_xxx" }
181
+ // }
138
182
  // }
139
183
  const event = data;
140
184
  const eventType = event?.event_type;
@@ -142,6 +186,40 @@ export function setupFeishuHandlers(config, sessionManager) {
142
186
  // Handle message received events
143
187
  if (eventType === 'im.message.receive_v1') {
144
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
+ }
145
223
  const message = event?.message;
146
224
  if (!message) {
147
225
  log.warn('No message data in event');
@@ -170,12 +248,6 @@ export function setupFeishuHandlers(config, sessionManager) {
170
248
  return;
171
249
  }
172
250
  log.info(`Sender ID: ${senderId}`);
173
- // Dedup check
174
- const dedupKey = `${chatId}:${messageId}`;
175
- if (dedup.isDuplicate(dedupKey)) {
176
- log.info(`Duplicate message detected: ${dedupKey}`);
177
- return;
178
- }
179
251
  // Access control check
180
252
  if (!accessControl.isAllowed(senderId)) {
181
253
  log.warn(`Access denied for sender: ${senderId}`);
@@ -220,14 +292,36 @@ export function setupFeishuHandlers(config, sessionManager) {
220
292
  const post = content?.post;
221
293
  let text = '';
222
294
  if (post?.content && Array.isArray(post.content)) {
295
+ // Log full structure for debugging
296
+ log.info(`[MSG] Post content structure:`, JSON.stringify(post.content).slice(0, 500));
223
297
  // Extract text from rich text structure
224
298
  for (const section of post.content) {
225
- if (section && typeof section === 'object' && 'text' in section) {
226
- const tag = section?.tag;
299
+ if (!section || typeof section !== 'object')
300
+ continue;
301
+ const tag = section?.tag;
302
+ // Handle different content types
303
+ if (tag === 'text' || tag === 'plain_text') {
227
304
  const t = section?.text ?? '';
228
- if (tag === 'text' || tag === 'plain_text') {
229
- text += t;
305
+ text += t;
306
+ }
307
+ else if (tag === 'heading' || tag === 'heading1' || tag === 'heading2' || tag === 'heading3') {
308
+ // Handle headings - might be nested structure
309
+ const headingText = section?.text;
310
+ if (typeof headingText === 'string') {
311
+ text += headingText;
230
312
  }
313
+ else if (Array.isArray(headingText)) {
314
+ // Nested text elements in heading
315
+ for (const item of headingText) {
316
+ if (item && typeof item === 'object' && 'text' in item) {
317
+ text += item.text ?? '';
318
+ }
319
+ }
320
+ }
321
+ }
322
+ else {
323
+ // Log unhandled tags for debugging
324
+ log.info(`[MSG] Unhandled post tag: ${tag}, section:`, JSON.stringify(section).slice(0, 200));
231
325
  }
232
326
  }
233
327
  }
@@ -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
- export declare function resolveLatestPermission(_chatId: string, _decision: 'allow' | 'deny'): string | null;
2
- export declare function getPendingCount(_chatId: string): number;
3
- export declare function resolvePermissionById(_requestId: string, _decision: 'allow' | 'deny'): boolean;
4
- export declare function registerPermissionSender(_platform: string, _sender: unknown): void;
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
- export function resolveLatestPermission(_chatId, _decision) {
2
- return null;
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
- export function getPendingCount(_chatId) {
5
- return 0;
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
- export function resolvePermissionById(_requestId, _decision) {
8
- return false;
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
- export function registerPermissionSender(_platform, _sender) {
11
- /* stub */
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();
@@ -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);
@@ -9,7 +9,6 @@ import { CommandHandler } from "../commands/handler.js";
9
9
  import { getAdapter } from "../adapters/registry.js";
10
10
  import { runAITask } from "../shared/ai-task.js";
11
11
  import { startTaskCleanup } from "../shared/task-cleanup.js";
12
- import { MessageDedup } from "../shared/message-dedup.js";
13
12
  import { TELEGRAM_THROTTLE_MS, IMAGE_DIR } from "../constants.js";
14
13
  import { setActiveChatId } from "../shared/active-chats.js";
15
14
  import { createLogger } from "../logger.js";
@@ -71,7 +70,6 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
71
70
  const requestQueue = new RequestQueue();
72
71
  const runningTasks = new Map();
73
72
  const stopTaskCleanup = startTaskCleanup(runningTasks);
74
- const dedup = new MessageDedup();
75
73
  const commandHandler = new CommandHandler({
76
74
  config,
77
75
  sessionManager,
@@ -79,7 +77,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
79
77
  sender: { sendTextReply },
80
78
  getRunningTasksSize: () => runningTasks.size,
81
79
  });
82
- registerPermissionSender("telegram", {});
80
+ registerPermissionSender("telegram", { sendTextReply });
83
81
  async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
84
82
  // 在用户每次发送消息时就累加计数,确保提示能轮换显示
85
83
  const currentTurns = sessionManager.addTurns(userId, 1);
@@ -318,8 +316,6 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
318
316
  const userId = String(ctx.from.id);
319
317
  const messageId = String(ctx.message.message_id);
320
318
  let text = ctx.message.text.trim();
321
- if (dedup.isDuplicate(`${chatId}:${messageId}`))
322
- return;
323
319
  if (!accessControl.isAllowed(userId)) {
324
320
  await sendTextReply(chatId, "抱歉,您没有访问权限。\n您的 ID: " + userId);
325
321
  return;
@@ -344,8 +340,6 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
344
340
  const chatId = String(ctx.chat.id);
345
341
  const userId = String(ctx.from.id);
346
342
  const caption = ctx.message.caption?.trim() || "";
347
- if (dedup.isDuplicate(`${chatId}:${ctx.message.message_id}`))
348
- return;
349
343
  if (!accessControl.isAllowed(userId))
350
344
  return;
351
345
  setActiveChatId("telegram", chatId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.0.2-beta.1",
3
+ "version": "1.0.2-beta.3",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",