@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
|
@@ -1,12 +1,292 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
+
}
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -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
|
|
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,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(() => {
|