@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 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`,使用 **长连接** 模式(WebSocket)
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;
@@ -17,6 +17,7 @@ export interface ClaudeRunCallbacks {
17
17
  }
18
18
  export interface ClaudeRunOptions {
19
19
  skipPermissions?: boolean;
20
+ permissionMode?: 'default' | 'acceptEdits' | 'plan';
20
21
  timeoutMs?: number;
21
22
  model?: string;
22
23
  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)
@@ -27,6 +27,7 @@ export interface ClaudeResult {
27
27
  }
28
28
  export interface ClaudeRunOptions {
29
29
  skipPermissions?: boolean;
30
+ permissionMode?: 'default' | 'acceptEdits' | 'plan';
30
31
  timeoutMs?: number;
31
32
  model?: string;
32
33
  chatId?: string;
@@ -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;
@@ -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
- '💡 提示:清除聊天历史请点击 Telegram 右上角 ⋮ → 清除历史',
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
- ? '✅ AI 会话已重置,下一条消息将使用全新上下文。\n\n' +
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
- `💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史`);
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
@@ -14,8 +14,10 @@ export interface Config {
14
14
  claudeWorkDir: string;
15
15
  allowedBaseDirs: string[];
16
16
  claudeSkipPermissions: boolean;
17
+ defaultPermissionMode: 'ask' | 'accept-edits' | 'plan' | 'yolo';
17
18
  claudeTimeoutMs: number;
18
19
  claudeModel?: string;
20
+ hookPort: number;
19
21
  logDir: string;
20
22
  logLevel: LogLevel;
21
23
  platforms: {
package/dist/config.js CHANGED
@@ -84,9 +84,13 @@ 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;
91
+ const hookPort = process.env.HOOK_PORT !== undefined
92
+ ? parseInt(process.env.HOOK_PORT, 10) || 35801
93
+ : file.hookPort ?? 35801;
90
94
  // 6. 校验 Claude CLI
91
95
  if (aiCommand === 'claude') {
92
96
  if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
@@ -164,8 +168,10 @@ export function loadConfig() {
164
168
  claudeWorkDir,
165
169
  allowedBaseDirs,
166
170
  claudeSkipPermissions,
171
+ defaultPermissionMode,
167
172
  claudeTimeoutMs,
168
173
  claudeModel: process.env.CLAUDE_MODEL ?? file.claudeModel,
174
+ hookPort,
169
175
  logDir,
170
176
  logLevel,
171
177
  platforms,
@@ -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>): 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;
@@ -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;