@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.
@@ -1,12 +1,292 @@
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 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
+ import { createServer } from 'node:http';
9
+ import { createLogger } from '../logger.js';
10
+ const log = createLogger('PermissionServer');
11
+ const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default timeout
12
+ const DEFAULT_PORT = 35801;
13
+ // Global state
14
+ let server = null;
15
+ let serverPort = DEFAULT_PORT;
16
+ const pendingRequests = new Map();
17
+ const requestsByChat = new Map();
18
+ let messageSender = null;
19
+ /**
20
+ * Start the permission HTTP server
21
+ */
22
+ export function startPermissionServer(port = DEFAULT_PORT) {
23
+ if (server) {
24
+ log.warn('Permission server already running');
25
+ return serverPort;
26
+ }
27
+ serverPort = port;
28
+ server = createServer(handleRequest);
29
+ server.listen(port, () => {
30
+ log.info(`Permission server listening on port ${port}`);
31
+ });
32
+ server.on('error', (err) => {
33
+ log.error('Permission server error:', err);
34
+ });
35
+ // Cleanup expired permissions every minute
36
+ setInterval(() => {
37
+ cleanupExpiredPermissions();
38
+ }, 60 * 1000).unref();
39
+ return port;
3
40
  }
4
- export function getPendingCount(_chatId) {
5
- return 0;
41
+ /**
42
+ * Stop the permission HTTP server
43
+ */
44
+ export function stopPermissionServer() {
45
+ if (server) {
46
+ server.close(() => {
47
+ log.info('Permission server stopped');
48
+ });
49
+ server = null;
50
+ // Reject all pending requests
51
+ for (const req of pendingRequests.values()) {
52
+ clearTimeout(req.timeout);
53
+ req.resolve(false);
54
+ }
55
+ pendingRequests.clear();
56
+ requestsByChat.clear();
57
+ }
6
58
  }
7
- export function resolvePermissionById(_requestId, _decision) {
8
- return false;
59
+ /**
60
+ * Register the message sender for sending permission prompts
61
+ */
62
+ export function registerPermissionSender(_platform, sender) {
63
+ messageSender = sender;
64
+ log.info('Message sender registered for permission prompts');
9
65
  }
10
- export function registerPermissionSender(_platform, _sender) {
11
- /* stub */
66
+ /**
67
+ * Get the number of pending permission requests for a chat
68
+ */
69
+ export function getPendingCount(chatId) {
70
+ const requests = requestsByChat.get(chatId);
71
+ return requests ? requests.length : 0;
72
+ }
73
+ /**
74
+ * Resolve the latest pending permission request for a chat
75
+ * Returns the requestId if found, null otherwise
76
+ */
77
+ export function resolveLatestPermission(chatId, decision) {
78
+ const requests = requestsByChat.get(chatId);
79
+ if (!requests || requests.length === 0) {
80
+ return null;
81
+ }
82
+ // Get the oldest (first) pending request
83
+ const request = requests.shift();
84
+ if (requests.length === 0) {
85
+ requestsByChat.delete(chatId);
86
+ }
87
+ // Remove from global map
88
+ pendingRequests.delete(request.requestId);
89
+ // Clear timeout and resolve
90
+ clearTimeout(request.timeout);
91
+ request.resolve(decision === 'allow');
92
+ log.info(`Resolved permission ${request.requestId}: ${decision} for tool ${request.toolName}`);
93
+ return request.requestId;
94
+ }
95
+ /**
96
+ * Resolve a specific permission request by ID
97
+ */
98
+ export function resolvePermissionById(requestId, decision) {
99
+ const request = pendingRequests.get(requestId);
100
+ if (!request) {
101
+ log.warn(`Permission request not found: ${requestId}`);
102
+ return false;
103
+ }
104
+ // Remove from chat's list
105
+ const chatRequests = requestsByChat.get(request.chatId);
106
+ if (chatRequests) {
107
+ const index = chatRequests.findIndex(r => r.requestId === requestId);
108
+ if (index !== -1) {
109
+ chatRequests.splice(index, 1);
110
+ }
111
+ if (chatRequests.length === 0) {
112
+ requestsByChat.delete(request.chatId);
113
+ }
114
+ }
115
+ // Remove from global map
116
+ pendingRequests.delete(requestId);
117
+ // Clear timeout and resolve
118
+ clearTimeout(request.timeout);
119
+ request.resolve(decision === 'allow');
120
+ log.info(`Resolved permission ${requestId}: ${decision} for tool ${request.toolName}`);
121
+ return true;
122
+ }
123
+ /**
124
+ * Handle incoming HTTP requests from Claude CLI
125
+ */
126
+ function handleRequest(req, res) {
127
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
128
+ log.debug(`Permission request: ${req.method} ${url.pathname}`);
129
+ if (url.pathname === '/permission' && req.method === 'POST') {
130
+ handlePermissionRequest(req, res);
131
+ }
132
+ else if (url.pathname === '/health' && req.method === 'GET') {
133
+ res.writeHead(200, { 'Content-Type': 'application/json' });
134
+ res.end(JSON.stringify({ status: 'ok', port: serverPort }));
135
+ }
136
+ else {
137
+ res.writeHead(404);
138
+ res.end('Not found');
139
+ }
140
+ }
141
+ /**
142
+ * Handle a permission request from Claude CLI
143
+ */
144
+ function handlePermissionRequest(req, res) {
145
+ let body = '';
146
+ req.on('data', (chunk) => {
147
+ body += chunk.toString();
148
+ });
149
+ req.on('end', async () => {
150
+ try {
151
+ const data = JSON.parse(body);
152
+ const requestId = String(data.requestId ?? '');
153
+ const toolName = String(data.toolName ?? '');
154
+ const toolInput = data.toolInput;
155
+ const chatId = (typeof data.chatId === 'string' ? data.chatId : null) ??
156
+ (typeof data.chat_id === 'string' ? data.chat_id : null) ??
157
+ process.env.CC_IM_CHAT_ID ??
158
+ undefined;
159
+ if (!requestId || !toolName) {
160
+ res.writeHead(400, { 'Content-Type': 'application/json' });
161
+ res.end(JSON.stringify({ error: 'Missing required fields' }));
162
+ return;
163
+ }
164
+ if (!chatId) {
165
+ log.warn('No chatId, auto-allowing permission');
166
+ res.writeHead(200, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify({ allowed: true }));
168
+ return;
169
+ }
170
+ if (process.env.CC_SKIP_PERMISSIONS === 'true') {
171
+ log.info(`Skip permissions enabled, auto-allowing ${toolName}`);
172
+ res.writeHead(200, { 'Content-Type': 'application/json' });
173
+ res.end(JSON.stringify({ allowed: true }));
174
+ return;
175
+ }
176
+ if (pendingRequests.has(requestId)) {
177
+ log.warn(`Duplicate permission request: ${requestId}`);
178
+ res.writeHead(409, { 'Content-Type': 'application/json' });
179
+ res.end(JSON.stringify({ error: 'Duplicate request' }));
180
+ return;
181
+ }
182
+ res.writeHead(202, { 'Content-Type': 'application/json' });
183
+ res.end(JSON.stringify({ status: 'pending' }));
184
+ // Create permission request
185
+ const permissionRequest = {
186
+ requestId,
187
+ chatId,
188
+ toolName,
189
+ toolInput: formatToolInput(toolInput),
190
+ timestamp: Date.now(),
191
+ resolve: null, // Will set below
192
+ timeout: null,
193
+ };
194
+ // Create promise for waiting for user response
195
+ const permissionPromise = new Promise((resolve) => {
196
+ permissionRequest.resolve = resolve;
197
+ });
198
+ // Set timeout
199
+ permissionRequest.timeout = setTimeout(() => {
200
+ log.info(`Permission request ${requestId} timed out`);
201
+ resolvePermissionById(requestId, 'deny');
202
+ }, PERMISSION_TIMEOUT_MS);
203
+ // Store request
204
+ pendingRequests.set(requestId, permissionRequest);
205
+ // Add to chat's list
206
+ let chatRequests = requestsByChat.get(chatId);
207
+ if (!chatRequests) {
208
+ chatRequests = [];
209
+ requestsByChat.set(chatId, chatRequests);
210
+ }
211
+ chatRequests.push(permissionRequest);
212
+ // Send permission prompt to user
213
+ await sendPermissionPrompt(permissionRequest);
214
+ // Wait for user response
215
+ const allowed = await permissionPromise;
216
+ log.info(`Permission ${requestId} for ${toolName}: ${allowed ? 'ALLOWED' : 'DENIED'}`);
217
+ }
218
+ catch (err) {
219
+ log.error('Error handling permission request:', err);
220
+ res.writeHead(500, { 'Content-Type': 'application/json' });
221
+ res.end(JSON.stringify({ error: 'Internal server error' }));
222
+ }
223
+ });
224
+ }
225
+ /**
226
+ * Send permission prompt to the user
227
+ */
228
+ async function sendPermissionPrompt(request) {
229
+ if (!messageSender) {
230
+ log.warn('No message sender registered, cannot send permission prompt');
231
+ return;
232
+ }
233
+ // Try to use interactive button card if available (Feishu)
234
+ if (messageSender.sendPermissionCard) {
235
+ try {
236
+ await messageSender.sendPermissionCard(request.chatId, request.requestId, request.toolName, request.toolInput);
237
+ return;
238
+ }
239
+ catch (err) {
240
+ log.debug('Failed to send permission card, falling back to text:', err);
241
+ }
242
+ }
243
+ // Fallback to text-based prompt
244
+ const prompt = `🔐 **权限请求**
245
+
246
+ 工具: \`${request.toolName}\`
247
+
248
+ 参数:
249
+ \`\`\`
250
+ ${request.toolInput}
251
+ \`\`\`
252
+
253
+ 回复 \`/allow\` 允许,\`/deny\` 拒绝
254
+
255
+ 请求 ID: ${request.requestId}`;
256
+ try {
257
+ await messageSender.sendTextReply(request.chatId, prompt);
258
+ }
259
+ catch (err) {
260
+ log.error('Failed to send permission prompt:', err);
261
+ }
262
+ }
263
+ /**
264
+ * Format tool input for display
265
+ */
266
+ function formatToolInput(toolInput) {
267
+ if (typeof toolInput === 'string') {
268
+ return toolInput;
269
+ }
270
+ if (typeof toolInput === 'object' && toolInput !== null) {
271
+ try {
272
+ const str = JSON.stringify(toolInput, null, 2);
273
+ return str.length > 500 ? str.slice(0, 500) + '...' : str;
274
+ }
275
+ catch {
276
+ return String(toolInput);
277
+ }
278
+ }
279
+ return String(toolInput);
280
+ }
281
+ /**
282
+ * Clean up expired permission requests
283
+ */
284
+ function cleanupExpiredPermissions() {
285
+ const now = Date.now();
286
+ for (const [requestId, request] of pendingRequests.entries()) {
287
+ if (now - request.timestamp > PERMISSION_TIMEOUT_MS) {
288
+ log.info(`Cleaning up expired permission: ${requestId}`);
289
+ resolvePermissionById(requestId, 'deny');
290
+ }
291
+ }
12
292
  }
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ 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";
20
+ import { initPermissionModes } from "./permission-mode/session-mode.js";
19
21
  import { createRequire } from "node:module";
20
22
  const require = createRequire(import.meta.url);
21
23
  const { version: APP_VERSION } = require("../package.json");
@@ -45,10 +47,17 @@ export async function main() {
45
47
  const config = loadConfig();
46
48
  initLogger(config.logDir, config.logLevel);
47
49
  loadActiveChats();
50
+ initPermissionModes();
48
51
  initAdapters(config);
52
+ // Start permission server for tool approval
53
+ const actualPort = startPermissionServer(config.hookPort);
54
+ log.info(`Permission server listening on port ${actualPort}`);
55
+ const { MODE_LABELS } = await import('./permission-mode/types.js');
56
+ const defaultModeLabel = MODE_LABELS[config.defaultPermissionMode];
49
57
  log.info("Starting open-im bridge...");
50
58
  log.info(`AI 工具: ${config.aiCommand}`);
51
59
  log.info(`工作目录: ${config.claudeWorkDir}`);
60
+ log.info(`默认权限模式: ${defaultModeLabel} (${config.defaultPermissionMode})`);
52
61
  log.info(`启用平台: ${config.enabledPlatforms.join(", ")}`);
53
62
  const sessionManager = new SessionManager(config.claudeWorkDir, config.allowedBaseDirs);
54
63
  let telegramHandle = null;
@@ -68,6 +77,7 @@ export async function main() {
68
77
  "",
69
78
  `AI 工具: ${config.aiCommand}`,
70
79
  `工作目录: ${config.claudeWorkDir}`,
80
+ `默认权限模式: ${defaultModeLabel} (${config.defaultPermissionMode})`,
71
81
  `启用平台: ${config.enabledPlatforms.join(", ")}`,
72
82
  ].join("\n");
73
83
  // Send notification to all enabled platforms
@@ -99,6 +109,7 @@ export async function main() {
99
109
  stopTelegram();
100
110
  feishuHandle?.stop();
101
111
  stopFeishu();
112
+ stopPermissionServer();
102
113
  sessionManager.destroy();
103
114
  cleanupAdapters();
104
115
  flushActiveChats();
@@ -0,0 +1,7 @@
1
+ import type { PermissionMode } from './types.js';
2
+ /** 初始化(启动时调用) */
3
+ export declare function initPermissionModes(): void;
4
+ /** 获取用户当前权限模式 */
5
+ export declare function getPermissionMode(userId: string, defaultMode?: PermissionMode): PermissionMode;
6
+ /** 设置用户权限模式 */
7
+ export declare function setPermissionMode(userId: string, mode: PermissionMode): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 按用户存储权限模式,支持运行时切换
3
+ */
4
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { APP_HOME } from '../constants.js';
7
+ import { createLogger } from '../logger.js';
8
+ import { PERMISSION_MODES } from './types.js';
9
+ const log = createLogger('PermissionMode');
10
+ const MODE_FILE = join(APP_HOME, 'data', 'permission-modes.json');
11
+ let modes = new Map();
12
+ let saveTimer = null;
13
+ function isValidMode(v) {
14
+ return typeof v === 'string' && PERMISSION_MODES.includes(v);
15
+ }
16
+ function load() {
17
+ try {
18
+ if (existsSync(MODE_FILE)) {
19
+ const data = JSON.parse(readFileSync(MODE_FILE, 'utf-8'));
20
+ modes = new Map(Object.entries(data).filter(([, v]) => isValidMode(v)));
21
+ }
22
+ }
23
+ catch {
24
+ modes = new Map();
25
+ }
26
+ }
27
+ function scheduleSave() {
28
+ if (saveTimer)
29
+ return;
30
+ saveTimer = setTimeout(() => {
31
+ saveTimer = null;
32
+ try {
33
+ const dir = dirname(MODE_FILE);
34
+ if (!existsSync(dir))
35
+ mkdirSync(dir, { recursive: true });
36
+ const obj = {};
37
+ for (const [k, v] of modes)
38
+ obj[k] = v;
39
+ writeFileSync(MODE_FILE, JSON.stringify(obj, null, 2), 'utf-8');
40
+ }
41
+ catch (err) {
42
+ log.error('Failed to save permission modes:', err);
43
+ }
44
+ }, 300);
45
+ }
46
+ /** 初始化(启动时调用) */
47
+ export function initPermissionModes() {
48
+ load();
49
+ }
50
+ /** 获取用户当前权限模式 */
51
+ export function getPermissionMode(userId, defaultMode = 'ask') {
52
+ return modes.get(userId) ?? defaultMode;
53
+ }
54
+ /** 设置用户权限模式 */
55
+ export function setPermissionMode(userId, mode) {
56
+ modes.set(userId, mode);
57
+ scheduleSave();
58
+ log.info(`Permission mode for ${userId}: ${mode}`);
59
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 权限模式类型定义
3
+ * 与 Claude Code 官方命名一致: default | acceptEdits | plan | bypassPermissions
4
+ * 见 https://code.claude.com/docs/en/permissions
5
+ */
6
+ export type PermissionMode = 'ask' | 'accept-edits' | 'plan' | 'yolo';
7
+ export declare const PERMISSION_MODES: PermissionMode[];
8
+ /** Claude Code 官方模式名(用于显示) */
9
+ export declare const MODE_LABELS: Record<PermissionMode, string>;
10
+ export declare const MODE_DESCRIPTIONS: Record<PermissionMode, string>;
11
+ export declare function parsePermissionMode(raw: string): PermissionMode | null;
@@ -0,0 +1,29 @@
1
+ export const PERMISSION_MODES = ['ask', 'accept-edits', 'plan', 'yolo'];
2
+ /** Claude Code 官方模式名(用于显示) */
3
+ export const MODE_LABELS = {
4
+ ask: 'default',
5
+ 'accept-edits': 'acceptEdits',
6
+ plan: 'plan',
7
+ yolo: 'bypassPermissions',
8
+ };
9
+ export const MODE_DESCRIPTIONS = {
10
+ ask: '首次使用每个工具时提示确认',
11
+ 'accept-edits': '编辑权限自动通过',
12
+ plan: '仅分析,不修改文件不执行命令',
13
+ yolo: '跳过所有权限确认',
14
+ };
15
+ export function parsePermissionMode(raw) {
16
+ const s = raw.trim().toLowerCase();
17
+ if (PERMISSION_MODES.includes(s))
18
+ return s;
19
+ const aliases = {
20
+ safe: 'ask',
21
+ default: 'ask',
22
+ edit: 'accept-edits',
23
+ edits: 'accept-edits',
24
+ read: 'plan',
25
+ readonly: 'plan',
26
+ bypass: 'yolo',
27
+ };
28
+ return aliases[s] ?? null;
29
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * 共享 AI 任务执行层 - 支持多 ToolAdapter
3
3
  */
4
+ import { getPermissionMode } from '../permission-mode/session-mode.js';
4
5
  import { formatToolStats, formatToolCallNotification, getContextWarning, } from './utils.js';
5
6
  import { createLogger } from '../logger.js';
6
7
  const log = createLogger('AITask');
@@ -69,6 +70,19 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
69
70
  }, platformAdapter.throttleMs - elapsed);
70
71
  }
71
72
  };
73
+ const mode = getPermissionMode(ctx.userId, config.defaultPermissionMode);
74
+ // 全部交给 Claude 自己处理:yolo 用 --dangerously-skip-permissions,其他用 --permission-mode
75
+ const skipPermissions = mode === 'yolo' || config.claudeSkipPermissions;
76
+ const permissionMode = !skipPermissions
77
+ ? (mode === 'ask'
78
+ ? 'default'
79
+ : mode === 'accept-edits'
80
+ ? 'acceptEdits'
81
+ : mode === 'plan'
82
+ ? 'plan'
83
+ : undefined)
84
+ : undefined;
85
+ process.env.CC_IM_CHAT_ID = ctx.chatId;
72
86
  const handle = toolAdapter.run(prompt, ctx.sessionId, ctx.workDir, {
73
87
  onSessionId: (id) => {
74
88
  if (ctx.threadId)
@@ -149,10 +163,12 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
149
163
  resolve();
150
164
  },
151
165
  }, {
152
- skipPermissions: config.claudeSkipPermissions,
166
+ skipPermissions,
167
+ permissionMode,
153
168
  timeoutMs: config.claudeTimeoutMs,
154
169
  model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
155
170
  chatId: ctx.chatId,
171
+ hookPort: config.hookPort,
156
172
  });
157
173
  taskState = { handle, latestContent: '', settle, startedAt: Date.now() };
158
174
  platformAdapter.onTaskReady(taskState);
@@ -0,0 +1,2 @@
1
+ export declare function setChatUser(chatId: string, userId: string): void;
2
+ export declare function getUserIdByChatId(chatId: string): string | undefined;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * chatId -> userId 映射,用于权限请求时根据 chatId 查找用户
3
+ * 在用户发送消息时更新
4
+ */
5
+ const chatToUser = new Map();
6
+ export function setChatUser(chatId, userId) {
7
+ chatToUser.set(chatId, userId);
8
+ }
9
+ export function getUserIdByChatId(chatId) {
10
+ return chatToUser.get(chatId);
11
+ }
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { message } from "telegraf/filters";
4
4
  import { AccessControl } from "../access/access-control.js";
5
5
  import { RequestQueue } from "../queue/request-queue.js";
6
- import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendImageReply, } from "./message-sender.js";
6
+ import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendImageReply, sendModeKeyboard, sendDirectorySelection, } from "./message-sender.js";
7
7
  import { registerPermissionSender, resolvePermissionById, } from "../hook/permission-server.js";
8
8
  import { CommandHandler } from "../commands/handler.js";
9
9
  import { getAdapter } from "../adapters/registry.js";
@@ -11,6 +11,9 @@ import { runAITask } from "../shared/ai-task.js";
11
11
  import { startTaskCleanup } from "../shared/task-cleanup.js";
12
12
  import { TELEGRAM_THROTTLE_MS, IMAGE_DIR } from "../constants.js";
13
13
  import { setActiveChatId } from "../shared/active-chats.js";
14
+ import { setChatUser } from "../shared/chat-user-map.js";
15
+ import { setPermissionMode } from "../permission-mode/session-mode.js";
16
+ import { MODE_LABELS } from "../permission-mode/types.js";
14
17
  import { createLogger } from "../logger.js";
15
18
  const log = createLogger("TgHandler");
16
19
  // 动态节流器类 - 根据内容长度和更新频率调整间隔
@@ -74,10 +77,10 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
74
77
  config,
75
78
  sessionManager,
76
79
  requestQueue,
77
- sender: { sendTextReply },
80
+ sender: { sendTextReply, sendDirectorySelection, sendModeKeyboard },
78
81
  getRunningTasksSize: () => runningTasks.size,
79
82
  });
80
- registerPermissionSender("telegram", {});
83
+ registerPermissionSender("telegram", { sendTextReply });
81
84
  async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
82
85
  // 在用户每次发送消息时就累加计数,确保提示能轮换显示
83
86
  const currentTurns = sessionManager.addTurns(userId, 1);
@@ -310,6 +313,16 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
310
313
  resolvePermissionById(requestId, decision);
311
314
  await ctx.answerCbQuery(isAllow ? "✅ 已允许" : "❌ 已拒绝");
312
315
  }
316
+ else if (data.startsWith("mode:")) {
317
+ const parts = data.split(":");
318
+ if (parts.length >= 3 && parts[1] === userId) {
319
+ const mode = parts[2];
320
+ if (["ask", "accept-edits", "plan", "yolo"].includes(mode)) {
321
+ setPermissionMode(userId, mode);
322
+ await ctx.answerCbQuery(`✅ 已切换为 ${MODE_LABELS[mode]}`);
323
+ }
324
+ }
325
+ }
313
326
  });
314
327
  bot.on(message("text"), async (ctx) => {
315
328
  const chatId = String(ctx.chat.id);
@@ -321,6 +334,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
321
334
  return;
322
335
  }
323
336
  setActiveChatId("telegram", chatId);
337
+ setChatUser(chatId, userId);
324
338
  if (await commandHandler.dispatch(text, chatId, userId, "telegram", handleAIRequest)) {
325
339
  return;
326
340
  }
@@ -343,6 +357,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
343
357
  if (!accessControl.isAllowed(userId))
344
358
  return;
345
359
  setActiveChatId("telegram", chatId);
360
+ setChatUser(chatId, userId);
346
361
  const photos = ctx.message.photo;
347
362
  const largest = photos[photos.length - 1];
348
363
  let imagePath;
@@ -8,4 +8,5 @@ export declare function sendImageReply(chatId: string, imagePath: string): Promi
8
8
  * 发送目录选择界面
9
9
  */
10
10
  export declare function sendDirectorySelection(chatId: string, currentDir: string, userId: string): Promise<void>;
11
+ export declare function sendModeKeyboard(chatId: string, userId: string, currentMode: string): Promise<void>;
11
12
  export declare function startTypingLoop(chatId: string): () => void;
@@ -182,6 +182,20 @@ export async function sendDirectorySelection(chatId, currentDir, userId) {
182
182
  reply_markup: keyboard,
183
183
  });
184
184
  }
185
+ export async function sendModeKeyboard(chatId, userId, currentMode) {
186
+ const bot = getBot();
187
+ const { MODE_LABELS } = await import("../permission-mode/types.js");
188
+ const modes = ["ask", "accept-edits", "plan", "yolo"];
189
+ const keyboard = {
190
+ inline_keyboard: [
191
+ modes.map((m) => ({
192
+ text: currentMode === m ? `✓ ${MODE_LABELS[m]}` : MODE_LABELS[m],
193
+ callback_data: `mode:${userId}:${m}`,
194
+ })),
195
+ ],
196
+ };
197
+ await bot.telegram.sendMessage(Number(chatId), `🔐 **权限模式** (当前: ${MODE_LABELS[currentMode] ?? currentMode})\n\n点击切换:`, { parse_mode: "Markdown", reply_markup: keyboard });
198
+ }
185
199
  export function startTypingLoop(chatId) {
186
200
  const bot = getBot();
187
201
  const interval = setInterval(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.0.2-beta.2",
3
+ "version": "1.0.2",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",