@wu529778790/open-im 1.11.8-beta.9 → 1.11.8

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
@@ -88,12 +88,30 @@ open-im start
88
88
  | `/review` | 代码审查 |
89
89
  | `/explain` | 解释项目结构 |
90
90
 
91
- ### 权限控制
91
+ ### 权限与确认
92
92
 
93
- | 命令 | 说明 |
94
- |------|------|
95
- | `/allow` `/y` | 允许操作 |
96
- | `/deny` `/n` | 拒绝操作 |
93
+ Claude 使用 Agent SDK 集成,open-im 默认不替 Claude 做额外的允许/拒绝协议。Claude 需要用户确认时,会按它自己的原生交互语义发问;你在 IM 里回复选项、确认或补充说明即可继续同一个会话。
94
+
95
+ 如果你希望 Claude 也进入自动执行模式,可以在 Web 控制台的 **AI 工具配置 → Claude Code → 跳过 Claude 权限确认** 打开,或在配置文件里设置:
96
+
97
+ ```json
98
+ {
99
+ "tools": {
100
+ "claude": {
101
+ "skipPermissions": true
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ 也可以用环境变量临时覆盖:
108
+
109
+ ```bash
110
+ OPEN_IM_SKIP_PERMISSIONS=true open-im start # 跳过权限确认
111
+ OPEN_IM_SKIP_PERMISSIONS=false open-im start # 使用 Claude 原生确认
112
+ ```
113
+
114
+ Codex、CodeBuddy、OpenCode 仍保持原来的自动执行默认行为。
97
115
 
98
116
  ## 会话接力
99
117
 
@@ -21,6 +21,7 @@ interface ClaudeSessionMeta {
21
21
  export declare function findLatestClaudeSession(workDir: string): Promise<ClaudeSessionMeta | undefined>;
22
22
  export declare class ClaudeSDKAdapter implements ToolAdapter {
23
23
  readonly toolId = "claude-sdk";
24
+ readonly interactionMode = "native";
24
25
  /**
25
26
  * 清理所有活跃的查询
26
27
  */
@@ -205,6 +205,7 @@ function enrichClaudeErrorMessage(message, stderr) {
205
205
  }
206
206
  export class ClaudeSDKAdapter {
207
207
  toolId = 'claude-sdk';
208
+ interactionMode = 'native';
208
209
  /**
209
210
  * 清理所有活跃的查询
210
211
  */
@@ -2,6 +2,7 @@ import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-ad
2
2
  export declare class CodeBuddyAdapter implements ToolAdapter {
3
3
  private cliPath;
4
4
  readonly toolId = "codebuddy";
5
+ readonly interactionMode = "open";
5
6
  constructor(cliPath: string);
6
7
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
7
8
  }
@@ -3,6 +3,7 @@ import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
3
3
  export class CodeBuddyAdapter {
4
4
  cliPath;
5
5
  toolId = 'codebuddy';
6
+ interactionMode = 'open';
6
7
  constructor(cliPath) {
7
8
  this.cliPath = cliPath;
8
9
  }
@@ -5,6 +5,7 @@ import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from "./tool-ad
5
5
  export declare class CodexAdapter implements ToolAdapter {
6
6
  private cliPath;
7
7
  readonly toolId = "codex";
8
+ readonly interactionMode = "open";
8
9
  constructor(cliPath: string);
9
10
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
10
11
  }
@@ -6,6 +6,7 @@ import { isSessionInvalidMessage } from "../shared/session-invalid-detector.js";
6
6
  export class CodexAdapter {
7
7
  cliPath;
8
8
  toolId = "codex";
9
+ interactionMode = "open";
9
10
  constructor(cliPath) {
10
11
  this.cliPath = cliPath;
11
12
  }
@@ -1,5 +1,6 @@
1
1
  import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-adapter.interface.js';
2
2
  export declare class OpenCodeAdapter implements ToolAdapter {
3
3
  readonly toolId = "opencode";
4
+ readonly interactionMode = "open";
4
5
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
5
6
  }
@@ -2,6 +2,7 @@ import { runOpenCodeSdk } from '../opencode/sdk-runner.js';
2
2
  import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
3
3
  export class OpenCodeAdapter {
4
4
  toolId = 'opencode';
5
+ interactionMode = 'open';
5
6
  run(prompt, sessionId, workDir, callbacks, options) {
6
7
  const handle = runOpenCodeSdk(prompt, sessionId, workDir, {
7
8
  onText: callbacks.onText,
@@ -40,7 +40,9 @@ export interface RunOptions {
40
40
  export interface RunHandle {
41
41
  abort: () => void;
42
42
  }
43
+ export type InteractionMode = 'native' | 'open';
43
44
  export interface ToolAdapter {
44
45
  readonly toolId: string;
46
+ readonly interactionMode: InteractionMode;
45
47
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
46
48
  }
@@ -331,7 +331,7 @@ function extractTextContent(msg) {
331
331
  case 3 /* MessageItemType.VOICE */: {
332
332
  const transcript = item.voice_item?.text;
333
333
  if (transcript)
334
- return `[语音转文字] ${transcript}`;
334
+ return transcript;
335
335
  return '[语音消息(无文字转录)]';
336
336
  }
337
337
  case 2 /* MessageItemType.IMAGE */:
@@ -8,6 +8,7 @@ export interface QRLoginSession {
8
8
  sessionKey: string;
9
9
  qrcode: string;
10
10
  qrcodeUrl: string;
11
+ qrcodeImage?: string;
11
12
  startedAt: number;
12
13
  }
13
14
  export interface QRLoginResult {
@@ -5,6 +5,7 @@
5
5
  * Returns bot_token on success.
6
6
  */
7
7
  import { randomUUID } from 'node:crypto';
8
+ import QRCode from 'qrcode';
8
9
  import { createLogger } from '../logger.js';
9
10
  const log = createLogger('ClawBotQR');
10
11
  const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
@@ -64,10 +65,16 @@ export async function startQRLogin() {
64
65
  log.info('Starting ClawBot QR login...');
65
66
  const { qrcode, qrcodeUrl } = await fetchQRCode();
66
67
  log.info(`QR code received, url=${qrcodeUrl}`);
68
+ // iLink 返回的 qrcodeUrl 是一个 HTML 中转页(X-Frame-Options: SAMEORIGIN,
69
+ // Content-Type: text/html),前端无法用 <img> 或 iframe 直接展示。
70
+ // 把这个 URL 本身编码成二维码 PNG——用户扫码后微信打开中转页完成绑定。
71
+ const qrcodeImage = await QRCode.toDataURL(qrcodeUrl, { width: 280, margin: 1 });
72
+ log.info('QR code PNG generated (base64)');
67
73
  return {
68
74
  sessionKey: randomUUID(),
69
75
  qrcode,
70
76
  qrcodeUrl,
77
+ qrcodeImage,
71
78
  startedAt: Date.now(),
72
79
  };
73
80
  }
@@ -217,22 +217,27 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
217
217
  const event = parseCodexEvent(line);
218
218
  if (!event)
219
219
  return;
220
- const type = event.type;
221
- log.debug(`[Codex event] type=${type}`);
222
- if (type === 'thread.started') {
223
- const threadId = event.thread_id ?? '';
220
+ // Codex CLI JSONL 格式是嵌套的:顶层 type "event_msg" / "response_item",
221
+ // 真正的子类型在 payload.type 里。旧版扁平格式也兼容。
222
+ const topType = event.type;
223
+ const payload = event.payload ?? event;
224
+ const type = payload.type ?? topType;
225
+ log.debug(`[Codex event] topType=${topType} type=${type}`);
226
+ // thread.started / session_meta → 拿 session ID
227
+ if (type === 'thread.started' || type === 'session_meta') {
228
+ const threadId = payload.thread_id ?? payload.session_id ?? event.thread_id ?? '';
224
229
  if (threadId)
225
230
  callbacks.onSessionId?.(threadId);
226
231
  return;
227
232
  }
228
233
  if (type === 'turn.failed') {
229
234
  completed = true;
230
- const err = event.error;
235
+ const err = payload.error ?? event.error;
231
236
  callbacks.onError(err?.message ?? 'Codex turn failed');
232
237
  return;
233
238
  }
234
239
  if (type === 'error') {
235
- const msg = event.message;
240
+ const msg = payload.message ?? event.message;
236
241
  if (msg?.includes('Reconnecting')) {
237
242
  return;
238
243
  }
@@ -240,12 +245,12 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
240
245
  callbacks.onError(msg ?? 'Codex stream error');
241
246
  return;
242
247
  }
243
- if (type === 'item.started' || type === 'item.updated' || type === 'item.completed') {
244
- const item = event.item;
245
- if (!item)
246
- return;
247
- const itemType = item.type;
248
- if (itemType === 'reasoning' && type === 'item.completed') {
248
+ // item 事件:payload 里可能有 item 字段,或者 payload 本身就是 item
249
+ if (topType === 'response_item' || type === 'item.started' || type === 'item.updated' || type === 'item.completed') {
250
+ const item = payload.item ?? payload;
251
+ const itemType = item.type ?? type;
252
+ if (itemType === 'reasoning') {
253
+ // Codex reasoning 可能是 encrypted_content(不可读),只在有 text 时累加
249
254
  const text = item.text;
250
255
  if (text) {
251
256
  accumulatedThinking += (accumulatedThinking ? '\n\n' : '') + text;
@@ -253,23 +258,41 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
253
258
  }
254
259
  return;
255
260
  }
261
+ // function_call(Codex 新格式:response_item.type=function_call)
262
+ if (itemType === 'function_call') {
263
+ const name = item.name ?? 'unknown';
264
+ const args = item.arguments;
265
+ const toolName = name === 'exec_command' ? 'Bash' : name === 'apply_patch' ? 'Edit' : name;
266
+ toolStats[toolName] = (toolStats[toolName] || 0) + 1;
267
+ callbacks.onToolUse?.(toolName, args ? { arguments: args } : {});
268
+ return;
269
+ }
270
+ // custom_tool_call(apply_patch 等)
271
+ if (itemType === 'custom_tool_call') {
272
+ const name = item.name ?? 'unknown';
273
+ const toolName = name === 'apply_patch' ? 'Edit' : name;
274
+ toolStats[toolName] = (toolStats[toolName] || 0) + 1;
275
+ callbacks.onToolUse?.(toolName, { input: item.input });
276
+ return;
277
+ }
278
+ // command_execution(旧格式)
256
279
  if (itemType === 'command_execution') {
257
280
  const cmd = item.command;
258
- if (cmd && type === 'item.started') {
259
- const toolName = 'Bash';
260
- toolStats[toolName] = (toolStats[toolName] || 0) + 1;
261
- callbacks.onToolUse?.(toolName, { command: cmd });
281
+ if (cmd) {
282
+ toolStats['Bash'] = (toolStats['Bash'] || 0) + 1;
283
+ callbacks.onToolUse?.('Bash', { command: cmd });
262
284
  }
263
285
  return;
264
286
  }
265
- if (itemType === 'file_change' && type === 'item.completed') {
287
+ // file_change(旧格式)
288
+ if (itemType === 'file_change') {
266
289
  const changes = item.changes;
267
- const toolName = 'Edit';
268
- toolStats[toolName] = (toolStats[toolName] || 0) + 1;
269
- callbacks.onToolUse?.(toolName, { changes });
290
+ toolStats['Edit'] = (toolStats['Edit'] || 0) + 1;
291
+ callbacks.onToolUse?.('Edit', { changes });
270
292
  return;
271
293
  }
272
- if (itemType === 'mcp_tool_call' && type === 'item.started') {
294
+ // mcp_tool_call(旧格式)
295
+ if (itemType === 'mcp_tool_call') {
273
296
  const tool = item.tool;
274
297
  const server = item.server;
275
298
  if (tool) {
@@ -279,16 +302,41 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
279
302
  }
280
303
  return;
281
304
  }
282
- if (itemType === 'agent_message' && type === 'item.completed') {
283
- const text = item.text;
305
+ // agent_message / message 最终回复文本
306
+ // Codex 新格式:event_msg.payload.type=agent_message, payload.message=文本
307
+ // response_item.payload.type=message, payload.content=[{type:output_text,text:...}]
308
+ if (itemType === 'agent_message' || itemType === 'message') {
309
+ let text;
310
+ if (itemType === 'agent_message') {
311
+ text = item.message ?? item.text;
312
+ }
313
+ else {
314
+ // message 类型,content 是数组
315
+ const content = item.content;
316
+ if (Array.isArray(content)) {
317
+ text = content
318
+ .filter((c) => c.type === 'output_text' && c.text)
319
+ .map((c) => c.text)
320
+ .join('\n');
321
+ }
322
+ }
284
323
  if (text) {
324
+ // 只累加 final_answer 或 commentary,避免重复
325
+ const phase = item.phase ?? payload.phase;
285
326
  accumulated += (accumulated ? '\n\n' : '') + text;
286
327
  callbacks.onText(accumulated);
287
328
  }
288
329
  return;
289
330
  }
290
331
  }
291
- if (type === 'turn.completed') {
332
+ // task_complete / turn.completed
333
+ if (type === 'task_complete' || type === 'turn.completed') {
334
+ // task_complete 里可能有 last_agent_message
335
+ const lastMsg = payload.last_agent_message;
336
+ if (lastMsg && !accumulated) {
337
+ accumulated = lastMsg;
338
+ callbacks.onText(accumulated);
339
+ }
292
340
  completed = true;
293
341
  callbacks.onComplete({
294
342
  success: true,
@@ -38,7 +38,7 @@ export interface Config {
38
38
  */
39
39
  claudeSessionIdleTtlMinutes: number;
40
40
  claudeModel?: string;
41
- /** 是否跳过 AI 工具的权限确认(默认 true) */
41
+ /** 是否跳过 AI 工具的权限确认。未配置时由具体工具决定默认行为。 */
42
42
  skipPermissions?: boolean;
43
43
  logDir: string;
44
44
  logLevel: LogLevel;
@@ -180,6 +180,7 @@ export interface FilePlatformClawbot {
180
180
  export interface FileToolClaude {
181
181
  cliPath?: string;
182
182
  workDir?: string;
183
+ /** Claude SDK 是否跳过原生权限确认。默认 false,保留 Claude 自身确认流程。 */
183
184
  skipPermissions?: boolean;
184
185
  /** 空闲会话回收间隔(分钟),0 表示关闭 */
185
186
  sessionIdleTtlMinutes?: number;
@@ -66,6 +66,19 @@ export declare const PAGE_TEXTS: {
66
66
  readonly qrLoginFailed: "Login failed";
67
67
  readonly qrLoginExpired: "QR code expired, please try again";
68
68
  readonly qrScanHint: "Use WeChat to scan the QR code";
69
+ readonly configure: "Configure";
70
+ readonly rebind: "Rebind";
71
+ readonly platformBound: "Bound";
72
+ readonly platformUnbound: "Not bound";
73
+ readonly qrModalTitle: "Scan to Bind";
74
+ readonly qrModalHint: "Scan the QR code below with WeChat to bind.";
75
+ readonly qrModalGenerating: "Generating QR code...";
76
+ readonly qrModalScanning: "Waiting for scan...";
77
+ readonly qrModalSuccess: "Bound successfully";
78
+ readonly qrModalExpired: "QR code expired";
79
+ readonly qrModalRetry: "Scan again";
80
+ readonly qrModalClose: "Close";
81
+ readonly qrModalError: "Binding failed";
69
82
  readonly platformCredentialsTitle: "Credentials";
70
83
  readonly platformAccessTitle: "Routing and access";
71
84
  readonly platformTestNote: "Checks required credentials against the platform.";
@@ -117,6 +130,8 @@ export declare const PAGE_TEXTS: {
117
130
  readonly claudeAuthToken: "ANTHROPIC_AUTH_TOKEN";
118
131
  readonly claudeBaseUrl: "ANTHROPIC_BASE_URL";
119
132
  readonly claudeModel: "ANTHROPIC_MODEL";
133
+ readonly claudeSkipPermissions: "Skip Claude permission prompts";
134
+ readonly claudeSkipPermissionsTip: "Off by default. Claude SDK will ask for permission in its native flow; open-im just forwards the conversation. Turn this on only if you want bypassPermissions-style automation.";
120
135
  readonly claudeProxy: "Proxy (optional)";
121
136
  readonly hookPort: "Hook port";
122
137
  readonly logLevel: "Log level";
@@ -282,6 +297,19 @@ export declare const PAGE_TEXTS: {
282
297
  readonly qrLoginFailed: "登录失败";
283
298
  readonly qrLoginExpired: "二维码已过期,请重试";
284
299
  readonly qrScanHint: "请使用微信扫描二维码";
300
+ readonly configure: "配置";
301
+ readonly rebind: "重新绑定";
302
+ readonly platformBound: "已绑定";
303
+ readonly platformUnbound: "未绑定";
304
+ readonly qrModalTitle: "扫码绑定";
305
+ readonly qrModalHint: "请用微信扫描下方二维码完成绑定";
306
+ readonly qrModalGenerating: "正在生成二维码...";
307
+ readonly qrModalScanning: "等待扫码...";
308
+ readonly qrModalSuccess: "绑定成功";
309
+ readonly qrModalExpired: "二维码已过期";
310
+ readonly qrModalRetry: "重新扫码";
311
+ readonly qrModalClose: "关闭";
312
+ readonly qrModalError: "绑定失败";
285
313
  readonly platformCredentialsTitle: "凭证";
286
314
  readonly platformAccessTitle: "路由与访问";
287
315
  readonly platformTestNote: "只会检查该平台的必填凭证是否可用。";
@@ -329,6 +357,8 @@ export declare const PAGE_TEXTS: {
329
357
  readonly codexApiKey: "OPENAI_API_KEY";
330
358
  readonly codexApiKeyTip: "设置 Codex 使用的 OpenAI API Key。也可以在下方编辑 auth 文件。";
331
359
  readonly claudeConfigPath: "配置文件位置";
360
+ readonly claudeSkipPermissions: "跳过 Claude 权限确认";
361
+ readonly claudeSkipPermissionsTip: "默认关闭。Claude SDK 会按自己的原生流程请求权限,open-im 只负责转发对话。只有你想要 bypassPermissions 式自动执行时才打开。";
332
362
  readonly claudeProxy: "代理(可选)";
333
363
  readonly hookPort: "Hook 端口";
334
364
  readonly logLevel: "日志级别";
@@ -66,6 +66,19 @@ export const PAGE_TEXTS = {
66
66
  qrLoginFailed: "Login failed",
67
67
  qrLoginExpired: "QR code expired, please try again",
68
68
  qrScanHint: "Use WeChat to scan the QR code",
69
+ configure: "Configure",
70
+ rebind: "Rebind",
71
+ platformBound: "Bound",
72
+ platformUnbound: "Not bound",
73
+ qrModalTitle: "Scan to Bind",
74
+ qrModalHint: "Scan the QR code below with WeChat to bind.",
75
+ qrModalGenerating: "Generating QR code...",
76
+ qrModalScanning: "Waiting for scan...",
77
+ qrModalSuccess: "Bound successfully",
78
+ qrModalExpired: "QR code expired",
79
+ qrModalRetry: "Scan again",
80
+ qrModalClose: "Close",
81
+ qrModalError: "Binding failed",
69
82
  platformCredentialsTitle: "Credentials",
70
83
  platformAccessTitle: "Routing and access",
71
84
  platformTestNote: "Checks required credentials against the platform.",
@@ -117,6 +130,8 @@ export const PAGE_TEXTS = {
117
130
  claudeAuthToken: "ANTHROPIC_AUTH_TOKEN",
118
131
  claudeBaseUrl: "ANTHROPIC_BASE_URL",
119
132
  claudeModel: "ANTHROPIC_MODEL",
133
+ claudeSkipPermissions: "Skip Claude permission prompts",
134
+ claudeSkipPermissionsTip: "Off by default. Claude SDK will ask for permission in its native flow; open-im just forwards the conversation. Turn this on only if you want bypassPermissions-style automation.",
120
135
  claudeProxy: "Proxy (optional)",
121
136
  hookPort: "Hook port",
122
137
  logLevel: "Log level",
@@ -283,6 +298,19 @@ export const PAGE_TEXTS = {
283
298
  qrLoginFailed: "\u767b\u5f55\u5931\u8d25",
284
299
  qrLoginExpired: "\u4e8c\u7ef4\u7801\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u8bd5",
285
300
  qrScanHint: "\u8bf7\u4f7f\u7528\u5fae\u4fe1\u626b\u63cf\u4e8c\u7ef4\u7801",
301
+ configure: "\u914d\u7f6e",
302
+ rebind: "\u91cd\u65b0\u7ed1\u5b9a",
303
+ platformBound: "\u5df2\u7ed1\u5b9a",
304
+ platformUnbound: "\u672a\u7ed1\u5b9a",
305
+ qrModalTitle: "\u626b\u7801\u7ed1\u5b9a",
306
+ qrModalHint: "\u8bf7\u7528\u5fae\u4fe1\u626b\u63cf\u4e0b\u65b9\u4e8c\u7ef4\u7801\u5b8c\u6210\u7ed1\u5b9a",
307
+ qrModalGenerating: "\u6b63\u5728\u751f\u6210\u4e8c\u7ef4\u7801...",
308
+ qrModalScanning: "\u7b49\u5f85\u626b\u7801...",
309
+ qrModalSuccess: "\u7ed1\u5b9a\u6210\u529f",
310
+ qrModalExpired: "\u4e8c\u7ef4\u7801\u5df2\u8fc7\u671f",
311
+ qrModalRetry: "\u91cd\u65b0\u626b\u7801",
312
+ qrModalClose: "\u5173\u95ed",
313
+ qrModalError: "\u7ed1\u5b9a\u5931\u8d25",
286
314
  platformCredentialsTitle: "\u51ed\u8bc1",
287
315
  platformAccessTitle: "\u8def\u7531\u4e0e\u8bbf\u95ee",
288
316
  platformTestNote: "\u53ea\u4f1a\u68c0\u67e5\u8be5\u5e73\u53f0\u7684\u5fc5\u586b\u51ed\u8bc1\u662f\u5426\u53ef\u7528\u3002",
@@ -330,6 +358,8 @@ export const PAGE_TEXTS = {
330
358
  codexApiKey: "OPENAI_API_KEY",
331
359
  codexApiKeyTip: "\u8bbe\u7f6e Codex \u4f7f\u7528\u7684 OpenAI API Key\u3002\u4e5f\u53ef\u4ee5\u5728\u4e0b\u65b9\u7f16\u8f91 auth \u6587\u4ef6\u3002",
332
360
  claudeConfigPath: "\u914d\u7f6e\u6587\u4ef6\u4f4d\u7f6e",
361
+ claudeSkipPermissions: "\u8df3\u8fc7 Claude \u6743\u9650\u786e\u8ba4",
362
+ claudeSkipPermissionsTip: "\u9ed8\u8ba4\u5173\u95ed\u3002Claude SDK \u4f1a\u6309\u81ea\u5df1\u7684\u539f\u751f\u6d41\u7a0b\u8bf7\u6c42\u6743\u9650\uff0copen-im \u53ea\u8d1f\u8d23\u8f6c\u53d1\u5bf9\u8bdd\u3002\u53ea\u6709\u4f60\u60f3\u8981 bypassPermissions \u5f0f\u81ea\u52a8\u6267\u884c\u65f6\u624d\u6253\u5f00\u3002",
333
363
  claudeProxy: "\u4ee3\u7406\uff08\u53ef\u9009\uff09",
334
364
  hookPort: "Hook \u7aef\u53e3",
335
365
  logLevel: "\u65e5\u5fd7\u7ea7\u522b",
@@ -64,6 +64,7 @@ export interface WebConfigPayload {
64
64
  claudeBaseUrl: string;
65
65
  claudeModel: string;
66
66
  claudeProxy: string;
67
+ claudeSkipPermissions: boolean;
67
68
  codexCliPath: string;
68
69
  codebuddyCliPath: string;
69
70
  opencodeCliPath: string;
@@ -92,6 +92,7 @@ export function buildInitialPayload(file) {
92
92
  claudeBaseUrl: claudeEnv.ANTHROPIC_BASE_URL ?? "",
93
93
  claudeModel: claudeEnv.ANTHROPIC_MODEL ?? "",
94
94
  claudeProxy: file.tools?.claude?.proxy ?? "",
95
+ claudeSkipPermissions: file.tools?.claude?.skipPermissions ?? false,
95
96
  codexCliPath: file.tools?.codex?.cliPath ?? "codex",
96
97
  codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
97
98
  opencodeCliPath: file.tools?.opencode?.cliPath ?? "opencode",
@@ -177,6 +178,7 @@ export function toFileConfig(payload, existing) {
177
178
  ...existing.tools?.claude,
178
179
  workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
179
180
  proxy: clean(payload.ai.claudeProxy),
181
+ skipPermissions: payload.ai.claudeSkipPermissions,
180
182
  // model is now saved to ~/.claude/settings.json as env var
181
183
  },
182
184
  codex: {
@@ -355,7 +355,7 @@ export async function startWebConfigServer(options) {
355
355
  try {
356
356
  const { startQRLogin } = await import("./clawbot/qr-login.js");
357
357
  const session = await startQRLogin();
358
- json(response, 200, { success: true, qrcodeUrl: session.qrcodeUrl, sessionKey: session.sessionKey }, request);
358
+ json(response, 200, { success: true, qrcodeUrl: session.qrcodeUrl, qrcodeImage: session.qrcodeImage, qrcode: session.qrcode, sessionKey: session.sessionKey }, request);
359
359
  }
360
360
  catch (error) {
361
361
  json(response, 500, { success: false, error: toErrorMessage(error) }, request);
@@ -375,6 +375,33 @@ export async function startWebConfigServer(options) {
375
375
  }
376
376
  return;
377
377
  }
378
+ if (request.method === "POST" && requestUrl.pathname === "/api/workbuddy/qr-login/start") {
379
+ try {
380
+ const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
381
+ const QRCode = (await import("qrcode")).default;
382
+ const oauth = new WorkBuddyOAuth();
383
+ const { authUrl, state } = await oauth.fetchAuthState();
384
+ const qrcodeImage = await QRCode.toDataURL(authUrl, { width: 280, margin: 1 });
385
+ json(response, 200, { success: true, qrcodeImage, state, authUrl }, request);
386
+ }
387
+ catch (error) {
388
+ json(response, 500, { success: false, error: toErrorMessage(error) }, request);
389
+ }
390
+ return;
391
+ }
392
+ if (request.method === "POST" && requestUrl.pathname === "/api/workbuddy/qr-login/wait") {
393
+ try {
394
+ const body = await readJson(request);
395
+ const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
396
+ const oauth = new WorkBuddyOAuth();
397
+ const result = await oauth.pollToken(body.state);
398
+ json(response, 200, { success: true, accessToken: result.accessToken, refreshToken: result.refreshToken, userId: result.userId }, request);
399
+ }
400
+ catch (error) {
401
+ json(response, 500, { success: false, error: toErrorMessage(error) }, request);
402
+ }
403
+ return;
404
+ }
378
405
  if (request.method === "GET" && tryServeDashboardStatic(requestUrl, request, response, mergeCors)) {
379
406
  return;
380
407
  }
package/dist/config.js CHANGED
@@ -246,9 +246,14 @@ export function loadConfig() {
246
246
  const opencodeCliPath = process.env.OPENCODE_CLI_PATH ?? topencode.cliPath ?? resolveWindowsCliPath(AI_TOOL_BY_ID.opencode.cliDefault ?? 'opencode');
247
247
  const opencodeModel = process.env.OPENCODE_MODEL ?? topencode.model;
248
248
  const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? tc.workDir ?? process.cwd();
249
- const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
250
- ? false
251
- : (tc.skipPermissions ?? true);
249
+ const skipPermissions = (() => {
250
+ const raw = process.env.OPEN_IM_SKIP_PERMISSIONS?.trim().toLowerCase();
251
+ if (raw === 'true' || raw === '1' || raw === 'yes')
252
+ return true;
253
+ if (raw === 'false' || raw === '0' || raw === 'no')
254
+ return false;
255
+ return tc.skipPermissions;
256
+ })();
252
257
  const envIdleRaw = process.env.OPEN_IM_CLAUDE_SESSION_IDLE_TTL_MINUTES;
253
258
  const envIdleParsed = envIdleRaw !== undefined && envIdleRaw !== '' ? Number.parseInt(envIdleRaw, 10) : NaN;
254
259
  const fileIdle = tc.sessionIdleTtlMinutes;
@@ -180,6 +180,22 @@ function buildCompletionNote(result, sessionManager, ctx) {
180
180
  parts.push(ctxWarning);
181
181
  return parts.join(' | ');
182
182
  }
183
+ function buildRunOptions(config, sessionManager, ctx, aiCommand, toolAdapter) {
184
+ const defaultSkipPermissions = toolAdapter.interactionMode === 'native'
185
+ ? (config.skipPermissions ?? false)
186
+ : (config.skipPermissions ?? true);
187
+ return {
188
+ model: aiCommand === 'claude'
189
+ ? (sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel)
190
+ : aiCommand === 'opencode'
191
+ ? config.opencodeModel
192
+ : undefined,
193
+ chatId: ctx.chatId,
194
+ skipPermissions: defaultSkipPermissions,
195
+ skipAutoResume: sessionManager.isFreshSession(ctx.userId),
196
+ ...(aiCommand === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
197
+ };
198
+ }
183
199
  export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
184
200
  const { config, sessionManager } = deps;
185
201
  return new Promise((resolve) => {
@@ -295,7 +311,9 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
295
311
  toolLines.push(notification);
296
312
  if (toolLines.length > 5)
297
313
  toolLines.shift();
298
- throttledUpdate(taskState.latestContent, true);
314
+ // 不强制发送(force=false),让节流机制合并短时间内的多次工具调用,
315
+ // 避免突发大量消息触发平台频率限制(如 ClawBot ret=-2)。
316
+ throttledUpdate(taskState.latestContent, false);
299
317
  },
300
318
  onComplete: async (result) => {
301
319
  log.info(`[AITask] onComplete fired: settled=${settled}, success=${result.success}, platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
@@ -457,19 +475,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
457
475
  cleanup();
458
476
  resolve();
459
477
  },
460
- }, {
461
- model: aiCommand === 'claude'
462
- ? (sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel)
463
- : aiCommand === 'opencode'
464
- ? config.opencodeModel
465
- : undefined,
466
- chatId: ctx.chatId,
467
- // 默认跳过权限确认,保持全自动执行(可通过 config 或环境变量关闭)
468
- skipPermissions: config.skipPermissions ?? true,
469
- // /new 后跳过自动恢复 CLI session
470
- skipAutoResume: sessionManager.isFreshSession(ctx.userId),
471
- ...(aiCommand === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
472
- });
478
+ }, buildRunOptions(config, sessionManager, ctx, aiCommand, toolAdapter));
473
479
  return activeHandle;
474
480
  };
475
481
  taskState = {
@@ -61,14 +61,14 @@ async function downloadTelegramFile(bot, fileId, basenameHint, fallbackExtension
61
61
  }
62
62
  export function setupTelegramHandlers(bot, config, sessionManager) {
63
63
  // Create shared platform event context
64
- const ctx = createPlatformEventContext({
64
+ const platformCtx = createPlatformEventContext({
65
65
  platform: 'telegram',
66
66
  allowedUserIds: config.telegramAllowedUserIds,
67
67
  config,
68
68
  sessionManager,
69
69
  sender: { sendTextReply, sendDirectorySelection },
70
70
  });
71
- const { accessControl, requestQueue, runningTasks } = ctx;
71
+ const { accessControl, requestQueue, runningTasks } = platformCtx;
72
72
  // Telegram-specific sender callbacks for the factory
73
73
  const telegramSender = {
74
74
  sendThinkingMessage: async (chatId, replyToMessageId, toolId) => {
@@ -316,24 +316,14 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
316
316
  if (parts.length >= 3) {
317
317
  const choiceNum = parts[2];
318
318
  const chatId = String(ctx.chat?.id ?? "");
319
- // 将用户选择作为消息发送给 AI
320
- const { handleTextFlow } = await import("../platform/handle-text-flow.js");
321
319
  await handleTextFlow({
322
320
  platform: "telegram",
323
321
  userId,
324
322
  chatId,
325
323
  text: choiceNum,
326
- ctx: createPlatformEventContext({
327
- platform: "telegram",
328
- allowedUserIds: config.telegramAllowedUserIds,
329
- config,
330
- sessionManager,
331
- sender: { sendTextReply: async () => { } },
332
- }),
333
- handleAIRequest: async () => { },
334
- sendTextReply: async (c, t) => {
335
- await sendTextReply(c, t);
336
- },
324
+ ctx: platformCtx,
325
+ handleAIRequest,
326
+ sendTextReply,
337
327
  workDir: sessionManager.getWorkDir(userId),
338
328
  convId: sessionManager.getConvId(userId),
339
329
  });
@@ -352,7 +342,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
352
342
  userId,
353
343
  chatId,
354
344
  text,
355
- ctx,
345
+ ctx: platformCtx,
356
346
  handleAIRequest,
357
347
  sendTextReply,
358
348
  replyToMessageId: messageId,