@wu529778790/open-im 1.0.2-beta.3 → 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 +1 -0
- package/dist/config.js +2 -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 +206 -80
- package/dist/feishu/message-sender.d.ts +13 -0
- package/dist/feishu/message-sender.js +121 -2
- package/dist/hook/permission-server.d.ts +3 -2
- package/dist/hook/permission-server.js +11 -9
- package/dist/index.js +6 -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 +16 -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 +17 -2
- package/dist/telegram/message-sender.d.ts +1 -0
- package/dist/telegram/message-sender.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,8 +87,16 @@ open-im run
|
|
|
87
87
|
|
|
88
88
|
1. 在 [飞书开放平台](https://open.feishu.cn/) 创建企业自建应用
|
|
89
89
|
2. 开启「机器人」能力
|
|
90
|
-
3. 配置事件订阅:启用 `im.message.receive_v1
|
|
91
|
-
4.
|
|
90
|
+
3. 配置事件订阅:启用 `im.message.receive_v1`(接收消息),使用 **长连接** 模式(WebSocket)
|
|
91
|
+
4. **卡片按钮(/mode、权限允许/拒绝)需额外配置回调**:
|
|
92
|
+
- 打开 [开放平台](https://open.feishu.cn/app) → 进入你的应用 → **「事件与回调」**
|
|
93
|
+
- 注意:页面有 **「事件」** 和 **「回调」** 两个 Tab,卡片在 **「回调」** 里,不在「事件」里
|
|
94
|
+
- 切换到 **「回调」** Tab → 选择 **「使用长连接接收回调」**
|
|
95
|
+
- 点击 **「添加回调」**(或类似按钮)→ 在列表中找到 **「卡片回传交互」**(`card.action.trigger`)
|
|
96
|
+
- 若列表里搜不到,可尝试:切换分类、搜「action」或「trigger」、或直接浏览「消息与群组」相关分类
|
|
97
|
+
5. 将机器人添加到目标群聊或发起私聊
|
|
98
|
+
|
|
99
|
+
**若点击 /mode 卡片按钮报错**:说明未配置卡片回调。配置较复杂时,可直接用 `/mode ask`、`/mode yolo` 等命令切换模式,无需卡片。
|
|
92
100
|
|
|
93
101
|
## 开发
|
|
94
102
|
|
|
@@ -103,6 +111,8 @@ npm run foreground # 前台运行已构建版本
|
|
|
103
111
|
| 命令 | 说明 |
|
|
104
112
|
|------|------|
|
|
105
113
|
| `/help` | 显示帮助 |
|
|
114
|
+
| `/mode` | 切换权限模式(卡片/按钮选择) |
|
|
115
|
+
| `/mode <模式>` | 直接切换:ask / accept-edits / plan / yolo |
|
|
106
116
|
| `/new` | 开始新会话 |
|
|
107
117
|
| `/status` | 显示状态(AI 工具、工作目录、费用等) |
|
|
108
118
|
| `/cd <路径>` | 切换工作目录 |
|
|
@@ -110,6 +120,17 @@ npm run foreground # 前台运行已构建版本
|
|
|
110
120
|
| `/allow` `/y` | 允许权限请求 |
|
|
111
121
|
| `/deny` `/n` | 拒绝权限请求 |
|
|
112
122
|
|
|
123
|
+
### 权限模式
|
|
124
|
+
|
|
125
|
+
与 Claude Code 官方命名一致,见 [permissions](https://code.claude.com/docs/en/permissions):
|
|
126
|
+
|
|
127
|
+
| 模式 | Claude 名 | 说明 |
|
|
128
|
+
|------|-----------|------|
|
|
129
|
+
| ask | default | 首次使用每个工具时提示确认 |
|
|
130
|
+
| accept-edits | acceptEdits | 编辑权限自动通过 |
|
|
131
|
+
| plan | plan | 仅分析,不修改文件不执行命令 |
|
|
132
|
+
| yolo | bypassPermissions | 跳过所有权限确认 |
|
|
133
|
+
|
|
113
134
|
## 📝 License
|
|
114
135
|
|
|
115
136
|
[MIT](LICENSE)
|
|
@@ -214,3 +235,14 @@ npx @wu529778790/open-im run
|
|
|
214
235
|
3. **用户 ID 白名单问题**
|
|
215
236
|
- 检查配置文件中的 `allowedUserIds` 是否包含你的用户 ID
|
|
216
237
|
- 或留空允许所有人访问(仅开发环境建议)
|
|
238
|
+
|
|
239
|
+
### Q: 飞书 /mode 卡片点击报错(如 200340)?
|
|
240
|
+
|
|
241
|
+
说明未配置**卡片回调**。注意:卡片在 **「回调」** Tab,不在「事件」Tab。
|
|
242
|
+
|
|
243
|
+
1. 打开 [飞书开放平台](https://open.feishu.cn/app) → 进入你的应用 → **「事件与回调」**
|
|
244
|
+
2. 切换到 **「回调」** Tab(不是「事件」)
|
|
245
|
+
3. 选择 **「使用长连接接收回调」**
|
|
246
|
+
4. 添加 **「卡片回传交互」**(`card.action.trigger`)— 若搜不到,可尝试搜「action」「trigger」或浏览分类
|
|
247
|
+
|
|
248
|
+
**更简单**:直接用 `/mode ask`、`/mode yolo` 等命令切换模式,无需配置卡片。
|
|
@@ -17,6 +17,7 @@ export class ClaudeAdapter {
|
|
|
17
17
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
18
18
|
const opts = {
|
|
19
19
|
skipPermissions: options?.skipPermissions,
|
|
20
|
+
permissionMode: options?.permissionMode,
|
|
20
21
|
timeoutMs: options?.timeoutMs,
|
|
21
22
|
model: options?.model,
|
|
22
23
|
chatId: options?.chatId,
|
|
@@ -21,6 +21,8 @@ export interface RunCallbacks {
|
|
|
21
21
|
}
|
|
22
22
|
export interface RunOptions {
|
|
23
23
|
skipPermissions?: boolean;
|
|
24
|
+
/** Claude --permission-mode: default | acceptEdits | plan(yolo 时用 skipPermissions) */
|
|
25
|
+
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
24
26
|
timeoutMs?: number;
|
|
25
27
|
model?: string;
|
|
26
28
|
chatId?: string;
|
|
@@ -12,8 +12,12 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
12
12
|
"--verbose",
|
|
13
13
|
"--include-partial-messages",
|
|
14
14
|
];
|
|
15
|
-
if (options?.skipPermissions)
|
|
15
|
+
if (options?.skipPermissions) {
|
|
16
16
|
args.push("--dangerously-skip-permissions");
|
|
17
|
+
}
|
|
18
|
+
else if (options?.permissionMode) {
|
|
19
|
+
args.push("--permission-mode", options.permissionMode);
|
|
20
|
+
}
|
|
17
21
|
if (options?.model)
|
|
18
22
|
args.push("--model", options.model);
|
|
19
23
|
if (sessionId)
|
|
@@ -62,8 +62,12 @@ export class ClaudeProcessPool {
|
|
|
62
62
|
"--verbose",
|
|
63
63
|
"--include-partial-messages",
|
|
64
64
|
];
|
|
65
|
-
if (options.skipPermissions)
|
|
65
|
+
if (options.skipPermissions) {
|
|
66
66
|
args.push("--dangerously-skip-permissions");
|
|
67
|
+
}
|
|
68
|
+
else if (options.permissionMode) {
|
|
69
|
+
args.push("--permission-mode", options.permissionMode);
|
|
70
|
+
}
|
|
67
71
|
if (options.model)
|
|
68
72
|
args.push("--model", options.model);
|
|
69
73
|
if (sessionId)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Config } from '../config.js';
|
|
2
2
|
import type { SessionManager } from '../session/session-manager.js';
|
|
3
3
|
import type { RequestQueue } from '../queue/request-queue.js';
|
|
4
|
+
import { type PermissionMode } from '../permission-mode/types.js';
|
|
4
5
|
import type { ThreadContext } from '../shared/types.js';
|
|
5
6
|
export type { ThreadContext };
|
|
6
7
|
export interface MessageSender {
|
|
7
8
|
sendTextReply(chatId: string, text: string, threadCtx?: ThreadContext): Promise<void>;
|
|
8
9
|
sendDirectorySelection?(chatId: string, currentDir: string, userId: string): Promise<void>;
|
|
10
|
+
sendModeCard?(chatId: string, userId: string, currentMode: PermissionMode): Promise<void>;
|
|
11
|
+
sendModeKeyboard?(chatId: string, userId: string, currentMode: PermissionMode): Promise<void>;
|
|
9
12
|
}
|
|
10
13
|
export interface CommandHandlerDeps {
|
|
11
14
|
config: Config;
|
|
@@ -19,6 +22,8 @@ export declare class CommandHandler {
|
|
|
19
22
|
private deps;
|
|
20
23
|
constructor(deps: CommandHandlerDeps);
|
|
21
24
|
dispatch(text: string, chatId: string, userId: string, platform: 'feishu' | 'telegram', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
25
|
+
private handleMode;
|
|
26
|
+
private getClearHistoryHint;
|
|
22
27
|
private handleHelp;
|
|
23
28
|
private handleNew;
|
|
24
29
|
private handlePwd;
|
package/dist/commands/handler.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolveLatestPermission, getPendingCount } from '../hook/permission-server.js';
|
|
2
|
+
import { getPermissionMode, setPermissionMode } from '../permission-mode/session-mode.js';
|
|
3
|
+
import { MODE_LABELS, MODE_DESCRIPTIONS, parsePermissionMode } from '../permission-mode/types.js';
|
|
2
4
|
import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
|
|
3
5
|
import { execFile } from 'node:child_process';
|
|
4
6
|
import { readdirSync } from 'node:fs';
|
|
@@ -16,8 +18,10 @@ export class CommandHandler {
|
|
|
16
18
|
}
|
|
17
19
|
if (t === '/help')
|
|
18
20
|
return this.handleHelp(chatId, platform);
|
|
21
|
+
if (t === '/mode' || t.startsWith('/mode '))
|
|
22
|
+
return this.handleMode(chatId, userId, platform, t.slice(6).trim());
|
|
19
23
|
if (t === '/new')
|
|
20
|
-
return this.handleNew(chatId, userId);
|
|
24
|
+
return this.handleNew(chatId, userId, platform);
|
|
21
25
|
if (t === '/pwd')
|
|
22
26
|
return this.handlePwd(chatId, userId);
|
|
23
27
|
if (t === '/status')
|
|
@@ -27,7 +31,7 @@ export class CommandHandler {
|
|
|
27
31
|
if (t === '/deny' || t === '/n')
|
|
28
32
|
return this.handleDeny(chatId);
|
|
29
33
|
if (t === '/cd' || t.startsWith('/cd ')) {
|
|
30
|
-
return this.handleCd(chatId, userId, t.slice(3).trim());
|
|
34
|
+
return this.handleCd(chatId, userId, t.slice(3).trim(), platform);
|
|
31
35
|
}
|
|
32
36
|
const cmd = t.split(/\s+/)[0];
|
|
33
37
|
if (TERMINAL_ONLY_COMMANDS.has(cmd)) {
|
|
@@ -36,11 +40,46 @@ export class CommandHandler {
|
|
|
36
40
|
}
|
|
37
41
|
return false;
|
|
38
42
|
}
|
|
43
|
+
async handleMode(chatId, userId, platform, arg) {
|
|
44
|
+
const defaultMode = this.deps.config.defaultPermissionMode;
|
|
45
|
+
const currentMode = getPermissionMode(userId, defaultMode);
|
|
46
|
+
if (arg) {
|
|
47
|
+
const parsed = parsePermissionMode(arg);
|
|
48
|
+
if (parsed) {
|
|
49
|
+
setPermissionMode(userId, parsed);
|
|
50
|
+
await this.deps.sender.sendTextReply(chatId, `✅ 权限模式已切换为 **${MODE_LABELS[parsed]}**\n${MODE_DESCRIPTIONS[parsed]}`);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
await this.deps.sender.sendTextReply(chatId, `无效模式: ${arg}\n可用: ask, accept-edits, plan, yolo`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (platform === 'feishu' && this.deps.sender.sendModeCard) {
|
|
57
|
+
await this.deps.sender.sendModeCard(chatId, userId, currentMode);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (platform === 'telegram' && this.deps.sender.sendModeKeyboard) {
|
|
61
|
+
await this.deps.sender.sendModeKeyboard(chatId, userId, currentMode);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const lines = [
|
|
65
|
+
`🔐 **权限模式** (当前: ${MODE_LABELS[currentMode]})`,
|
|
66
|
+
'',
|
|
67
|
+
...['ask', 'accept-edits', 'plan', 'yolo'].map((m) => `• \`/mode ${m}\` - ${MODE_LABELS[m]}: ${MODE_DESCRIPTIONS[m]}`),
|
|
68
|
+
];
|
|
69
|
+
await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
getClearHistoryHint(platform) {
|
|
73
|
+
return platform === 'feishu'
|
|
74
|
+
? '💡 提示:如需清除本对话的历史消息,请点击飞书聊天右上角「...」→ 清除聊天记录'
|
|
75
|
+
: '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
|
|
76
|
+
}
|
|
39
77
|
async handleHelp(chatId, platform) {
|
|
40
78
|
const help = [
|
|
41
79
|
'📋 可用命令:',
|
|
42
80
|
'',
|
|
43
81
|
'/help - 显示帮助',
|
|
82
|
+
'/mode - 切换权限模式(安全/编辑放行/只读/YOLO)',
|
|
44
83
|
'/new - 开始新会话(AI 上下文重置)',
|
|
45
84
|
'/status - 显示状态',
|
|
46
85
|
'/cd <路径> - 切换工作目录',
|
|
@@ -48,16 +87,15 @@ export class CommandHandler {
|
|
|
48
87
|
'/allow (/y) - 允许权限请求',
|
|
49
88
|
'/deny (/n) - 拒绝权限请求',
|
|
50
89
|
'',
|
|
51
|
-
|
|
90
|
+
this.getClearHistoryHint(platform),
|
|
52
91
|
].join('\n');
|
|
53
92
|
await this.deps.sender.sendTextReply(chatId, help);
|
|
54
93
|
return true;
|
|
55
94
|
}
|
|
56
|
-
async handleNew(chatId, userId) {
|
|
95
|
+
async handleNew(chatId, userId, platform) {
|
|
57
96
|
const ok = this.deps.sessionManager.newSession(userId);
|
|
58
97
|
await this.deps.sender.sendTextReply(chatId, ok
|
|
59
|
-
?
|
|
60
|
-
'💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史'
|
|
98
|
+
? `✅ AI 会话已重置,下一条消息将使用全新上下文。\n\n${this.getClearHistoryHint(platform)}`
|
|
61
99
|
: '当前没有活动会话。');
|
|
62
100
|
return true;
|
|
63
101
|
}
|
|
@@ -82,7 +120,7 @@ export class CommandHandler {
|
|
|
82
120
|
await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
|
|
83
121
|
return true;
|
|
84
122
|
}
|
|
85
|
-
async handleCd(chatId, userId, dir) {
|
|
123
|
+
async handleCd(chatId, userId, dir, platform) {
|
|
86
124
|
// 如果 dir 为空,显示目录选择界面
|
|
87
125
|
if (!dir) {
|
|
88
126
|
const currentDir = this.deps.sessionManager.getWorkDir(userId);
|
|
@@ -98,7 +136,7 @@ export class CommandHandler {
|
|
|
98
136
|
const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
|
|
99
137
|
await this.deps.sender.sendTextReply(chatId, `📁 工作目录已切换到: ${resolved}\n\n` +
|
|
100
138
|
`🔄 AI 会话已重置,下一条消息将使用全新上下文。\n` +
|
|
101
|
-
|
|
139
|
+
this.getClearHistoryHint(platform));
|
|
102
140
|
}
|
|
103
141
|
catch (err) {
|
|
104
142
|
await this.deps.sender.sendTextReply(chatId, err instanceof Error ? err.message : String(err));
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -84,6 +84,7 @@ export function loadConfig() {
|
|
|
84
84
|
const claudeSkipPermissions = process.env.CLAUDE_SKIP_PERMISSIONS !== undefined
|
|
85
85
|
? process.env.CLAUDE_SKIP_PERMISSIONS === 'true'
|
|
86
86
|
: file.claudeSkipPermissions ?? true;
|
|
87
|
+
const defaultPermissionMode = (file.defaultPermissionMode ?? 'ask');
|
|
87
88
|
const claudeTimeoutMs = process.env.CLAUDE_TIMEOUT_MS !== undefined
|
|
88
89
|
? parseInt(process.env.CLAUDE_TIMEOUT_MS, 10) || 600000
|
|
89
90
|
: file.claudeTimeoutMs ?? 600000;
|
|
@@ -167,6 +168,7 @@ export function loadConfig() {
|
|
|
167
168
|
claudeWorkDir,
|
|
168
169
|
allowedBaseDirs,
|
|
169
170
|
claudeSkipPermissions,
|
|
171
|
+
defaultPermissionMode,
|
|
170
172
|
claudeTimeoutMs,
|
|
171
173
|
claudeModel: process.env.CLAUDE_MODEL ?? file.claudeModel,
|
|
172
174
|
hookPort,
|
package/dist/feishu/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Client } from '@larksuiteoapi/node-sdk';
|
|
2
2
|
import type { Config } from '../config.js';
|
|
3
3
|
export declare function getClient(): Client;
|
|
4
|
-
export declare function initFeishu(config: Config, eventHandler: (data: unknown) => Promise<void
|
|
4
|
+
export declare function initFeishu(config: Config, eventHandler: (data: unknown) => Promise<void | Record<string, unknown>>): Promise<void>;
|
|
5
5
|
export declare function stopFeishu(): void;
|
package/dist/feishu/client.js
CHANGED
|
@@ -34,6 +34,19 @@ export async function initFeishu(config, eventHandler) {
|
|
|
34
34
|
log.error('[EVENT] Error calling event handler:', err);
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
// 卡片按钮点击回调(权限允许/拒绝等)
|
|
38
|
+
'card.action.trigger': async (data) => {
|
|
39
|
+
log.info('[EVENT] Received Feishu card action event');
|
|
40
|
+
log.info('[EVENT] Card action data:', JSON.stringify(data).slice(0, 800));
|
|
41
|
+
try {
|
|
42
|
+
const result = await eventHandler(data);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
log.error('[EVENT] Error handling card action:', err);
|
|
47
|
+
return { toast: { type: 'error', content: '处理失败' } };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
37
50
|
});
|
|
38
51
|
// Register catch-all handler using wildcard
|
|
39
52
|
eventDispatcher.register({
|
|
@@ -3,6 +3,6 @@ import type { SessionManager } from '../session/session-manager.js';
|
|
|
3
3
|
export interface FeishuEventHandlerHandle {
|
|
4
4
|
stop: () => void;
|
|
5
5
|
getRunningTaskCount: () => number;
|
|
6
|
-
handleEvent: (data: unknown) => Promise<void
|
|
6
|
+
handleEvent: (data: unknown) => Promise<void | Record<string, unknown>>;
|
|
7
7
|
}
|
|
8
8
|
export declare function setupFeishuHandlers(config: Config, sessionManager: SessionManager): FeishuEventHandlerHandle;
|
|
@@ -2,14 +2,17 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { AccessControl } from '../access/access-control.js';
|
|
4
4
|
import { RequestQueue } from '../queue/request-queue.js';
|
|
5
|
-
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendImageReply, createFeishuButtonCard, } from './message-sender.js';
|
|
5
|
+
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, sendTextReplyByOpenId, startTypingLoop, sendImageReply, createFeishuButtonCard, sendModeCard, createFeishuModeCardReadOnly, delayUpdateCard, } from './message-sender.js';
|
|
6
6
|
import { registerPermissionSender, resolvePermissionById } from '../hook/permission-server.js';
|
|
7
|
+
import { setPermissionMode } from '../permission-mode/session-mode.js';
|
|
8
|
+
import { MODE_LABELS } from '../permission-mode/types.js';
|
|
7
9
|
import { CommandHandler } from '../commands/handler.js';
|
|
8
10
|
import { getAdapter } from '../adapters/registry.js';
|
|
9
11
|
import { runAITask } from '../shared/ai-task.js';
|
|
10
12
|
import { startTaskCleanup } from '../shared/task-cleanup.js';
|
|
11
13
|
import { THROTTLE_MS, IMAGE_DIR } from '../constants.js';
|
|
12
14
|
import { setActiveChatId } from '../shared/active-chats.js';
|
|
15
|
+
import { setChatUser } from '../shared/chat-user-map.js';
|
|
13
16
|
import { createLogger } from '../logger.js';
|
|
14
17
|
const log = createLogger('FeishuHandler');
|
|
15
18
|
async function downloadFeishuImage(client, imageKey) {
|
|
@@ -107,7 +110,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
107
110
|
config,
|
|
108
111
|
sessionManager,
|
|
109
112
|
requestQueue,
|
|
110
|
-
sender: { sendTextReply },
|
|
113
|
+
sender: { sendTextReply, sendModeCard },
|
|
111
114
|
getRunningTasksSize: () => runningTasks.size,
|
|
112
115
|
});
|
|
113
116
|
registerPermissionSender('feishu', { sendTextReply, sendPermissionCard });
|
|
@@ -162,62 +165,181 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
162
165
|
sendImage: (path) => sendImageReply(chatId, path),
|
|
163
166
|
});
|
|
164
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse permission button value from card action (兼容多种格式)
|
|
170
|
+
*/
|
|
171
|
+
function parsePermissionActionValue(raw) {
|
|
172
|
+
if (!raw)
|
|
173
|
+
return null;
|
|
174
|
+
let buttonValue;
|
|
175
|
+
if (typeof raw === 'string') {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
if (parsed.action === 'permission' && parsed.value)
|
|
179
|
+
buttonValue = parsed.value;
|
|
180
|
+
else if (raw.startsWith('allow_') || raw.startsWith('deny_'))
|
|
181
|
+
buttonValue = raw;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
if (raw.startsWith('allow_') || raw.startsWith('deny_'))
|
|
185
|
+
buttonValue = raw;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (typeof raw === 'object' && raw !== null) {
|
|
189
|
+
const obj = raw;
|
|
190
|
+
if (obj.action === 'permission' && obj.value)
|
|
191
|
+
buttonValue = obj.value;
|
|
192
|
+
}
|
|
193
|
+
if (!buttonValue)
|
|
194
|
+
return null;
|
|
195
|
+
if (buttonValue.startsWith('allow_')) {
|
|
196
|
+
return { decision: 'allow', requestId: buttonValue.slice(6) };
|
|
197
|
+
}
|
|
198
|
+
if (buttonValue.startsWith('deny_')) {
|
|
199
|
+
return { decision: 'deny', requestId: buttonValue.slice(5) };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 解析 action value(兼容对象、JSON 字符串)
|
|
205
|
+
*/
|
|
206
|
+
function parseActionValue(raw) {
|
|
207
|
+
if (!raw)
|
|
208
|
+
return null;
|
|
209
|
+
let obj = null;
|
|
210
|
+
if (typeof raw === 'string') {
|
|
211
|
+
try {
|
|
212
|
+
obj = JSON.parse(raw);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (typeof raw === 'object' && raw !== null) {
|
|
219
|
+
obj = raw;
|
|
220
|
+
}
|
|
221
|
+
return obj?.action && obj?.value ? obj : null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 从卡片回调事件中提取延时更新 token(格式 c-xxxx)
|
|
225
|
+
* 飞书文档:从卡片交互返回内容获取,用于延时更新接口
|
|
226
|
+
*/
|
|
227
|
+
function extractCardToken(data) {
|
|
228
|
+
const raw = data;
|
|
229
|
+
const event = (raw?.event ?? raw);
|
|
230
|
+
const action = event?.action;
|
|
231
|
+
const context = event?.context;
|
|
232
|
+
const candidates = [
|
|
233
|
+
event?.token,
|
|
234
|
+
event?.open_api_token,
|
|
235
|
+
raw?.token,
|
|
236
|
+
action?.token,
|
|
237
|
+
context?.token,
|
|
238
|
+
].filter((t) => typeof t === 'string' && t.startsWith('c-'));
|
|
239
|
+
const token = candidates[0] ?? null;
|
|
240
|
+
if (!token) {
|
|
241
|
+
log.debug('[extractCardToken] No token found, event keys:', Object.keys(event ?? {}));
|
|
242
|
+
}
|
|
243
|
+
return token;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Handle card button click (card.action.trigger) - 需在 3 秒内返回响应
|
|
247
|
+
* 同步只返回 toast,避免 200672;用延时更新 API 异步替换为只读卡片,防止二次点击
|
|
248
|
+
*/
|
|
249
|
+
async function handleCardAction(data) {
|
|
250
|
+
// 兼容 SDK 可能嵌套的 event 结构
|
|
251
|
+
const wrapped = data;
|
|
252
|
+
const event = (wrapped?.event ?? data);
|
|
253
|
+
const actionValue = event?.action?.value;
|
|
254
|
+
const chatId = event?.context?.open_chat_id ?? event?.context?.chat_id ?? event?.context?.open_id ?? '';
|
|
255
|
+
const userId = event?.sender?.sender_id?.open_id ?? '';
|
|
256
|
+
log.info(`[handleCardAction] chatId=${chatId}, userId=${userId}, actionValue=${JSON.stringify(actionValue)}`);
|
|
257
|
+
// 处理 mode 按钮(兼容 value 为对象或 JSON 字符串)
|
|
258
|
+
const modeAv = parseActionValue(actionValue);
|
|
259
|
+
if (modeAv?.action === 'mode' && modeAv.value) {
|
|
260
|
+
const mode = modeAv.value;
|
|
261
|
+
if (['ask', 'accept-edits', 'plan', 'yolo'].includes(mode)) {
|
|
262
|
+
setPermissionMode(userId, mode);
|
|
263
|
+
const toastContent = `✅ 已切换为 ${MODE_LABELS[mode]}`;
|
|
264
|
+
const label = MODE_LABELS[mode];
|
|
265
|
+
// 异步发送文本回复,不阻塞 3 秒内返回
|
|
266
|
+
const sendReply = () => {
|
|
267
|
+
if (chatId)
|
|
268
|
+
return sendTextReply(chatId, toastContent);
|
|
269
|
+
if (userId)
|
|
270
|
+
return sendTextReplyByOpenId(userId, toastContent);
|
|
271
|
+
log.warn('[handleCardAction] No chatId/userId, cannot send text reply');
|
|
272
|
+
};
|
|
273
|
+
const p = sendReply();
|
|
274
|
+
if (p)
|
|
275
|
+
p.catch((e) => log.warn('[handleCardAction] Send reply failed:', e));
|
|
276
|
+
// 同步只返回 toast,避免 200672(同步返回 card 格式易出错)
|
|
277
|
+
const cardToken = extractCardToken(data);
|
|
278
|
+
const readOnlyCard = createFeishuModeCardReadOnly(label);
|
|
279
|
+
if (cardToken && userId) {
|
|
280
|
+
// 延时更新:异步替换为只读卡片,防止二次点击
|
|
281
|
+
delayUpdateCard(cardToken, readOnlyCard, [userId]).catch((e) => log.warn('[handleCardAction] delayUpdateCard failed:', e));
|
|
282
|
+
}
|
|
283
|
+
else if (!cardToken) {
|
|
284
|
+
log.debug('[handleCardAction] No card token in event, cannot delay-update card');
|
|
285
|
+
}
|
|
286
|
+
return { toast: { type: 'success', content: toastContent } };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const parsed = parsePermissionActionValue(actionValue);
|
|
290
|
+
if (!parsed) {
|
|
291
|
+
log.info('[handleCardAction] Unrecognized action value, returning default toast');
|
|
292
|
+
return { toast: { type: 'warning', content: '未知操作' } };
|
|
293
|
+
}
|
|
294
|
+
const { decision, requestId } = parsed;
|
|
295
|
+
log.info(`[handleCardAction] Permission button: ${decision} for ${requestId}, chatId=${chatId}`);
|
|
296
|
+
const resolved = resolvePermissionById(requestId, decision);
|
|
297
|
+
const toastContent = resolved
|
|
298
|
+
? decision === 'allow'
|
|
299
|
+
? '✅ 权限已允许'
|
|
300
|
+
: '❌ 权限已拒绝'
|
|
301
|
+
: '⚠️ 权限请求已过期或不存在';
|
|
302
|
+
const sendPermReply = () => {
|
|
303
|
+
if (chatId)
|
|
304
|
+
return sendTextReply(chatId, toastContent);
|
|
305
|
+
if (userId)
|
|
306
|
+
return sendTextReplyByOpenId(userId, toastContent);
|
|
307
|
+
};
|
|
308
|
+
const permP = sendPermReply();
|
|
309
|
+
if (permP)
|
|
310
|
+
permP.catch((err) => log.warn('Failed to send permission reply:', err));
|
|
311
|
+
return { toast: { type: resolved ? 'success' : 'warning', content: toastContent } };
|
|
312
|
+
}
|
|
165
313
|
async function handleEvent(data) {
|
|
166
|
-
log.info('[handleEvent] Called with data:', JSON.stringify(data).slice(0,
|
|
314
|
+
log.info('[handleEvent] Called with data:', JSON.stringify(data).slice(0, 800));
|
|
167
315
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// {
|
|
172
|
-
// "event_type": "im.message.receive_v1",
|
|
173
|
-
// "event_id": "...",
|
|
174
|
-
// "tenant_key": "...",
|
|
175
|
-
// "app_id": "...",
|
|
176
|
-
// "message": { "chat_id": "...", "content": "...", ... },
|
|
177
|
-
// "sender": { "sender_id": { "open_id": "..." } }
|
|
178
|
-
// "action": { // For card button clicks
|
|
179
|
-
// "action_id": "...",
|
|
180
|
-
// "value": { "action": "permission", "value": "allow_xxx" }
|
|
181
|
-
// }
|
|
182
|
-
// }
|
|
183
|
-
const event = data;
|
|
184
|
-
const eventType = event?.event_type;
|
|
316
|
+
const raw = data;
|
|
317
|
+
const event = (raw?.event ?? raw);
|
|
318
|
+
const eventType = event?.event_type ?? event?.type;
|
|
185
319
|
log.info('Feishu event type:', eventType);
|
|
186
|
-
//
|
|
320
|
+
// 1. 卡片按钮点击 (card.action.trigger) - 需快速返回响应
|
|
321
|
+
if (eventType === 'card.action.trigger') {
|
|
322
|
+
const result = await handleCardAction(data);
|
|
323
|
+
return result ?? { toast: { type: 'success', content: '已处理' } };
|
|
324
|
+
}
|
|
325
|
+
// 2. 消息接收 (im.message.receive_v1)
|
|
187
326
|
if (eventType === 'im.message.receive_v1') {
|
|
188
327
|
log.info('[handleEvent] Processing im.message.receive_v1 event');
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
}
|
|
328
|
+
// 兼容:部分场景下卡片点击可能通过 im.message 携带 action
|
|
329
|
+
if (event?.action?.value) {
|
|
330
|
+
const parsed = parsePermissionActionValue(event.action.value);
|
|
331
|
+
if (parsed) {
|
|
332
|
+
const { decision, requestId } = parsed;
|
|
333
|
+
const chatId = event.message?.chat_id ?? '';
|
|
334
|
+
log.info(`[handleEvent] Permission (via msg): ${decision} for ${requestId}`);
|
|
335
|
+
const resolved = resolvePermissionById(requestId, decision);
|
|
336
|
+
if (resolved) {
|
|
337
|
+
await sendTextReply(chatId, decision === 'allow' ? '✅ 权限已允许' : '❌ 权限已拒绝');
|
|
220
338
|
}
|
|
339
|
+
else {
|
|
340
|
+
await sendTextReply(chatId, '⚠️ 权限请求已过期或不存在');
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
221
343
|
}
|
|
222
344
|
}
|
|
223
345
|
const message = event?.message;
|
|
@@ -256,6 +378,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
256
378
|
}
|
|
257
379
|
log.info(`Access granted for sender: ${senderId}`);
|
|
258
380
|
setActiveChatId('feishu', chatId);
|
|
381
|
+
setChatUser(chatId, senderId);
|
|
259
382
|
// Handle different message types
|
|
260
383
|
if (msgType === 'text') {
|
|
261
384
|
const text = content.text?.trim() ?? '';
|
|
@@ -289,39 +412,42 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
289
412
|
}
|
|
290
413
|
else if (msgType === 'post') {
|
|
291
414
|
// Feishu rich text/post messages - extract text content
|
|
292
|
-
|
|
415
|
+
// 支持 post.content 或 zh_cn.content,content 可能是二维数组(段落→元素)
|
|
416
|
+
const post = content?.post
|
|
417
|
+
?? content?.zh_cn;
|
|
418
|
+
const rawContent = post?.content;
|
|
293
419
|
let text = '';
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
420
|
+
function extractTextFromElement(el) {
|
|
421
|
+
if (!el || typeof el !== 'object')
|
|
422
|
+
return '';
|
|
423
|
+
const obj = el;
|
|
424
|
+
const tag = obj.tag;
|
|
425
|
+
if (tag === 'text' || tag === 'plain_text') {
|
|
426
|
+
return (obj.text ?? obj.content ?? '').toString();
|
|
427
|
+
}
|
|
428
|
+
if (tag === 'a')
|
|
429
|
+
return (obj.text ?? obj.content ?? '').toString();
|
|
430
|
+
if (tag === 'heading' || tag === 'heading1' || tag === 'heading2' || tag === 'heading3') {
|
|
431
|
+
const headingText = el.text;
|
|
432
|
+
if (typeof headingText === 'string')
|
|
433
|
+
return headingText;
|
|
434
|
+
if (Array.isArray(headingText)) {
|
|
435
|
+
return headingText.map(extractTextFromElement).join('');
|
|
306
436
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
text += item.text ?? '';
|
|
318
|
-
}
|
|
319
|
-
}
|
|
437
|
+
}
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
if (rawContent && Array.isArray(rawContent)) {
|
|
441
|
+
log.info(`[MSG] Post content structure:`, JSON.stringify(rawContent).slice(0, 500));
|
|
442
|
+
for (const section of rawContent) {
|
|
443
|
+
if (Array.isArray(section)) {
|
|
444
|
+
// 二维数组:段落内多个元素
|
|
445
|
+
for (const el of section) {
|
|
446
|
+
text += extractTextFromElement(el);
|
|
320
447
|
}
|
|
321
448
|
}
|
|
322
449
|
else {
|
|
323
|
-
|
|
324
|
-
log.info(`[MSG] Unhandled post tag: ${tag}, section:`, JSON.stringify(section).slice(0, 200));
|
|
450
|
+
text += extractTextFromElement(section);
|
|
325
451
|
}
|
|
326
452
|
}
|
|
327
453
|
}
|
|
@@ -8,9 +8,22 @@ export declare function createFeishuButtonCard(title: string, content: string, b
|
|
|
8
8
|
value: string;
|
|
9
9
|
type?: 'primary' | 'default';
|
|
10
10
|
}>): string;
|
|
11
|
+
/** 只读模式卡片(无按钮,用于回调后替换原卡片防止二次点击) */
|
|
12
|
+
export declare function createFeishuModeCardReadOnly(currentMode: string): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* 延时更新消息卡片(POST /open-apis/im/v1/cards/update)
|
|
15
|
+
* 用于在卡片回调 3 秒内无法完成时,异步替换卡片为只读版本,防止二次点击
|
|
16
|
+
* @param token 从卡片交互事件中获取的 token(格式 c-xxxx)
|
|
17
|
+
* @param card 卡片内容 { config, header, elements }
|
|
18
|
+
* @param openIds 非共享卡片需指定更新的用户 open_id 列表
|
|
19
|
+
*/
|
|
20
|
+
export declare function delayUpdateCard(token: string, card: Record<string, unknown>, openIds?: string[]): Promise<void>;
|
|
21
|
+
export declare function sendModeCard(chatId: string, _userId: string, currentMode: string): Promise<void>;
|
|
11
22
|
export declare function sendThinkingMessage(chatId: string, replyToMessageId: string | undefined, toolId?: string): Promise<string>;
|
|
12
23
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
13
24
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
14
25
|
export declare function sendTextReply(chatId: string, text: string): Promise<void>;
|
|
26
|
+
/** 使用 open_id 发送(私聊时 context 可能只有 open_id) */
|
|
27
|
+
export declare function sendTextReplyByOpenId(openId: string, text: string): Promise<void>;
|
|
15
28
|
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
16
29
|
export declare function startTypingLoop(_chatId: string): () => void;
|
|
@@ -114,6 +114,108 @@ export function createFeishuButtonCard(title, content, buttons) {
|
|
|
114
114
|
};
|
|
115
115
|
return JSON.stringify(card);
|
|
116
116
|
}
|
|
117
|
+
/** 只读模式卡片(无按钮,用于回调后替换原卡片防止二次点击) */
|
|
118
|
+
export function createFeishuModeCardReadOnly(currentMode) {
|
|
119
|
+
return {
|
|
120
|
+
config: { wide_screen_mode: true },
|
|
121
|
+
header: {
|
|
122
|
+
template: 'green',
|
|
123
|
+
title: { content: '🔐 权限模式', tag: 'plain_text' },
|
|
124
|
+
},
|
|
125
|
+
elements: [
|
|
126
|
+
{
|
|
127
|
+
tag: 'div',
|
|
128
|
+
text: {
|
|
129
|
+
tag: 'lark_md',
|
|
130
|
+
content: `**当前模式:** ${currentMode}\n\n✅ 已切换成功,发送 \`/mode\` 可再次切换。`,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 延时更新消息卡片(POST /open-apis/im/v1/cards/update)
|
|
138
|
+
* 用于在卡片回调 3 秒内无法完成时,异步替换卡片为只读版本,防止二次点击
|
|
139
|
+
* @param token 从卡片交互事件中获取的 token(格式 c-xxxx)
|
|
140
|
+
* @param card 卡片内容 { config, header, elements }
|
|
141
|
+
* @param openIds 非共享卡片需指定更新的用户 open_id 列表
|
|
142
|
+
*/
|
|
143
|
+
export async function delayUpdateCard(token, card, openIds) {
|
|
144
|
+
const accessToken = await getTenantAccessToken();
|
|
145
|
+
// 非共享卡片需在 card 内指定 open_ids
|
|
146
|
+
const cardBody = { ...card };
|
|
147
|
+
if (openIds && openIds.length > 0) {
|
|
148
|
+
cardBody.open_ids = openIds;
|
|
149
|
+
}
|
|
150
|
+
const body = { token, card: cardBody };
|
|
151
|
+
const resp = await fetch('https://open.feishu.cn/open-apis/interactive/v1/card/update', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
Authorization: `Bearer ${accessToken}`,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(body),
|
|
158
|
+
});
|
|
159
|
+
const data = (await resp.json());
|
|
160
|
+
if (data.code !== 0) {
|
|
161
|
+
log.warn(`[delayUpdateCard] Failed: code=${data.code}, msg=${data.msg}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
log.info('[delayUpdateCard] Card updated successfully');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create mode switch card with action type for card callback
|
|
168
|
+
*/
|
|
169
|
+
function createFeishuModeCard(currentMode, buttons) {
|
|
170
|
+
const elements = [];
|
|
171
|
+
elements.push({
|
|
172
|
+
tag: 'div',
|
|
173
|
+
text: {
|
|
174
|
+
tag: 'lark_md',
|
|
175
|
+
content: `**当前模式:** ${currentMode}\n\n点击下方按钮切换模式:\n\n_💡 若点击报错:开放平台 → 事件与回调 → 切到「回调」Tab → 添加「卡片回传交互」。或直接用 \`/mode ask\` 等命令切换。_`,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
elements.push({ tag: 'hr' });
|
|
179
|
+
for (let i = 0; i < buttons.length; i += 4) {
|
|
180
|
+
const row = buttons.slice(i, i + 4).map((btn) => ({
|
|
181
|
+
tag: 'button',
|
|
182
|
+
text: { tag: 'plain_text', content: btn.label },
|
|
183
|
+
type: btn.type || 'default',
|
|
184
|
+
value: { action: 'mode', value: btn.value },
|
|
185
|
+
}));
|
|
186
|
+
elements.push({ tag: 'action', actions: row });
|
|
187
|
+
}
|
|
188
|
+
const card = {
|
|
189
|
+
config: { wide_screen_mode: true },
|
|
190
|
+
header: {
|
|
191
|
+
template: 'blue',
|
|
192
|
+
title: { content: '🔐 权限模式', tag: 'plain_text' },
|
|
193
|
+
},
|
|
194
|
+
elements,
|
|
195
|
+
};
|
|
196
|
+
return JSON.stringify(card);
|
|
197
|
+
}
|
|
198
|
+
export async function sendModeCard(chatId, _userId, currentMode) {
|
|
199
|
+
const { getClient } = await import('./client.js');
|
|
200
|
+
const { MODE_LABELS } = await import('../permission-mode/types.js');
|
|
201
|
+
const client = getClient();
|
|
202
|
+
const MODE_BTNS = [
|
|
203
|
+
{ label: MODE_LABELS.ask, value: 'ask', type: 'default' },
|
|
204
|
+
{ label: MODE_LABELS['accept-edits'], value: 'accept-edits', type: 'default' },
|
|
205
|
+
{ label: MODE_LABELS.plan, value: 'plan', type: 'default' },
|
|
206
|
+
{ label: MODE_LABELS.yolo, value: 'yolo', type: 'default' },
|
|
207
|
+
];
|
|
208
|
+
const currentLabel = MODE_BTNS.find((b) => b.value === currentMode)?.label ?? currentMode;
|
|
209
|
+
const cardContent = createFeishuModeCard(currentLabel, MODE_BTNS);
|
|
210
|
+
await client.im.message.create({
|
|
211
|
+
data: {
|
|
212
|
+
receive_id: chatId,
|
|
213
|
+
msg_type: 'interactive',
|
|
214
|
+
content: cardContent,
|
|
215
|
+
},
|
|
216
|
+
params: { receive_id_type: 'chat_id' },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
117
219
|
async function getTenantAccessToken() {
|
|
118
220
|
const client = getClient();
|
|
119
221
|
const resp = await client.auth.tenantAccessToken.internal({
|
|
@@ -153,8 +255,7 @@ export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'cl
|
|
|
153
255
|
}
|
|
154
256
|
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
155
257
|
const client = getClient();
|
|
156
|
-
const
|
|
157
|
-
const title = `${icon} ${getToolTitle(toolId, status)}`;
|
|
258
|
+
const title = getToolTitle(toolId, status);
|
|
158
259
|
const cardContent = createFeishuCard(title, content, status, note);
|
|
159
260
|
// Try to use patch API for in-place update (streaming)
|
|
160
261
|
try {
|
|
@@ -276,6 +377,24 @@ export async function sendTextReply(chatId, text) {
|
|
|
276
377
|
log.error('Failed to send text:', err);
|
|
277
378
|
}
|
|
278
379
|
}
|
|
380
|
+
/** 使用 open_id 发送(私聊时 context 可能只有 open_id) */
|
|
381
|
+
export async function sendTextReplyByOpenId(openId, text) {
|
|
382
|
+
const client = getClient();
|
|
383
|
+
const cardContent = createFeishuCard('📢 open-im', text, 'done');
|
|
384
|
+
try {
|
|
385
|
+
await client.im.message.create({
|
|
386
|
+
data: {
|
|
387
|
+
receive_id: openId,
|
|
388
|
+
msg_type: 'interactive',
|
|
389
|
+
content: cardContent,
|
|
390
|
+
},
|
|
391
|
+
params: { receive_id_type: 'open_id' },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
log.error('Failed to send text by open_id:', err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
279
398
|
export async function sendImageReply(chatId, imagePath) {
|
|
280
399
|
const client = getClient();
|
|
281
400
|
try {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission Server - Handles tool permission requests from Claude CLI
|
|
3
3
|
*
|
|
4
|
-
* When claudeSkipPermissions is false, Claude CLI will make
|
|
5
|
-
* to this server
|
|
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.
|
|
6
7
|
*/
|
|
7
8
|
interface MessageSender {
|
|
8
9
|
sendTextReply(chatId: string, text: string): Promise<void>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission Server - Handles tool permission requests from Claude CLI
|
|
3
3
|
*
|
|
4
|
-
* When claudeSkipPermissions is false, Claude CLI will make
|
|
5
|
-
* to this server
|
|
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.
|
|
6
7
|
*/
|
|
7
8
|
import { createServer } from 'node:http';
|
|
8
9
|
import { createLogger } from '../logger.js';
|
|
@@ -148,35 +149,36 @@ function handlePermissionRequest(req, res) {
|
|
|
148
149
|
req.on('end', async () => {
|
|
149
150
|
try {
|
|
150
151
|
const data = JSON.parse(body);
|
|
151
|
-
const
|
|
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;
|
|
152
159
|
if (!requestId || !toolName) {
|
|
153
160
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
154
161
|
res.end(JSON.stringify({ error: 'Missing required fields' }));
|
|
155
162
|
return;
|
|
156
163
|
}
|
|
157
|
-
// Get chatId from environment (set by CC_IM_CHAT_ID)
|
|
158
|
-
const chatId = process.env.CC_IM_CHAT_ID;
|
|
159
164
|
if (!chatId) {
|
|
160
|
-
log.warn('No chatId
|
|
165
|
+
log.warn('No chatId, auto-allowing permission');
|
|
161
166
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
167
|
res.end(JSON.stringify({ allowed: true }));
|
|
163
168
|
return;
|
|
164
169
|
}
|
|
165
|
-
// Check if skip permissions is enabled
|
|
166
170
|
if (process.env.CC_SKIP_PERMISSIONS === 'true') {
|
|
167
171
|
log.info(`Skip permissions enabled, auto-allowing ${toolName}`);
|
|
168
172
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
169
173
|
res.end(JSON.stringify({ allowed: true }));
|
|
170
174
|
return;
|
|
171
175
|
}
|
|
172
|
-
// Check if already resolved (race condition)
|
|
173
176
|
if (pendingRequests.has(requestId)) {
|
|
174
177
|
log.warn(`Duplicate permission request: ${requestId}`);
|
|
175
178
|
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
176
179
|
res.end(JSON.stringify({ error: 'Duplicate request' }));
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
179
|
-
// Send pending response
|
|
180
182
|
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
181
183
|
res.end(JSON.stringify({ status: 'pending' }));
|
|
182
184
|
// Create permission request
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/ac
|
|
|
17
17
|
import { initLogger, createLogger, closeLogger } from "./logger.js";
|
|
18
18
|
import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
|
|
19
19
|
import { startPermissionServer, stopPermissionServer } from "./hook/permission-server.js";
|
|
20
|
+
import { initPermissionModes } from "./permission-mode/session-mode.js";
|
|
20
21
|
import { createRequire } from "node:module";
|
|
21
22
|
const require = createRequire(import.meta.url);
|
|
22
23
|
const { version: APP_VERSION } = require("../package.json");
|
|
@@ -46,13 +47,17 @@ export async function main() {
|
|
|
46
47
|
const config = loadConfig();
|
|
47
48
|
initLogger(config.logDir, config.logLevel);
|
|
48
49
|
loadActiveChats();
|
|
50
|
+
initPermissionModes();
|
|
49
51
|
initAdapters(config);
|
|
50
52
|
// Start permission server for tool approval
|
|
51
53
|
const actualPort = startPermissionServer(config.hookPort);
|
|
52
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];
|
|
53
57
|
log.info("Starting open-im bridge...");
|
|
54
58
|
log.info(`AI 工具: ${config.aiCommand}`);
|
|
55
59
|
log.info(`工作目录: ${config.claudeWorkDir}`);
|
|
60
|
+
log.info(`默认权限模式: ${defaultModeLabel} (${config.defaultPermissionMode})`);
|
|
56
61
|
log.info(`启用平台: ${config.enabledPlatforms.join(", ")}`);
|
|
57
62
|
const sessionManager = new SessionManager(config.claudeWorkDir, config.allowedBaseDirs);
|
|
58
63
|
let telegramHandle = null;
|
|
@@ -72,6 +77,7 @@ export async function main() {
|
|
|
72
77
|
"",
|
|
73
78
|
`AI 工具: ${config.aiCommand}`,
|
|
74
79
|
`工作目录: ${config.claudeWorkDir}`,
|
|
80
|
+
`默认权限模式: ${defaultModeLabel} (${config.defaultPermissionMode})`,
|
|
75
81
|
`启用平台: ${config.enabledPlatforms.join(", ")}`,
|
|
76
82
|
].join("\n");
|
|
77
83
|
// Send notification to all enabled platforms
|
|
@@ -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,7 +163,8 @@ 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,
|
|
@@ -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,7 +77,7 @@ 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
83
|
registerPermissionSender("telegram", { sendTextReply });
|
|
@@ -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(() => {
|