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

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
@@ -10,6 +10,7 @@ open-im 把 Claude Code、Codex、CodeBuddy 接入 Telegram、飞书、企业微
10
10
  - **无缝接力** — 和 Claude Code CLI 共享 session,手机聊一半,电脑接着来
11
11
  - **完整能力** — 流式输出、会话管理、模型切换,全靠聊天命令
12
12
  - **一个桥接,多个平台** — 同一个 bot 支持 7 个 IM 平台
13
+ - **交互式选择** — AI 问"选 1/2/3"时,IM 显示按钮(Telegram/飞书/钉钉)
13
14
 
14
15
  ## 快速开始
15
16
 
@@ -17,19 +18,11 @@ open-im 把 Claude Code、Codex、CodeBuddy 接入 Telegram、飞书、企业微
17
18
  # 安装
18
19
  npm install -g @wu529778790/open-im
19
20
 
20
- # 配置(交互式向导)
21
- open-im init
22
-
23
21
  # 启动
24
22
  open-im start
25
23
  ```
26
24
 
27
- 或直接用 npx:
28
-
29
- ```bash
30
- npx @wu529778790/open-im init
31
- npx @wu529778790/open-im start
32
- ```
25
+ 首次启动会自动打开 Web 控制台,引导你完成配置。
33
26
 
34
27
  ### 最小配置
35
28
 
@@ -46,20 +39,22 @@ npx @wu529778790/open-im start
46
39
 
47
40
  ## 平台支持
48
41
 
49
- | 平台 | 流式输出 | 接入指南 |
50
- |------|---------|---------|
51
- | Telegram | ✅ | [Bot 文档](https://core.telegram.org/bots#creating-a-new-bot) |
52
- | 飞书 | ✅ | [开放平台](https://open.feishu.cn/) |
53
- | QQ 机器人 | ✅ | [开放平台](https://bot.q.qq.com/) |
54
- | 企业微信 | ✅ | [管理后台](https://work.weixin.qq.com/) |
55
- | 钉钉机器人 | ⚠️ 部分 | [开放平台](https://open-dev.dingtalk.com/) |
56
- | 微信助理(WorkBuddy) | ✅ | [接入指南](https://www.codebuddy.cn/docs/workbuddy/Claw) |
57
- | 微信客服号(ClawBot) | ✅ | [接入指南](https://www.codebuddy.cn/docs/workbuddy/Claw) |
42
+ | 平台 | 流式输出 | 图片 | 交互按钮 | 接入指南 |
43
+ |------|---------|------|---------|---------|
44
+ | Telegram | ✅ | ✅ | ✅ | [Bot 文档](https://core.telegram.org/bots#creating-a-new-bot) |
45
+ | 飞书 | ✅ | ✅ | ✅ | [开放平台](https://open.feishu.cn/) |
46
+ | QQ 机器人 | ✅ | ✅ | ❌ | [开放平台](https://bot.q.qq.com/) |
47
+ | 企业微信 | ✅ | ✅ | ❌ | [管理后台](https://work.weixin.qq.com/) |
48
+ | 钉钉机器人 | ⚠️ 部分 | ✅ | ✅ | [开放平台](https://open-dev.dingtalk.com/) |
49
+ | 微信助理(WorkBuddy) | ✅ | ❌ | ❌ | [接入指南](https://www.codebuddy.cn/docs/workbuddy/Claw) |
50
+ | 微信客服号(ClawBot) | ✅ | ⚠️ 下载中 | ❌ | [接入指南](https://www.codebuddy.cn/docs/workbuddy/Claw) |
58
51
 
59
- 每个平台可单独配置 AI 后端(`claude` / `codex` / `codebuddy`),默认 `claude`。
52
+ 每个平台可单独配置 AI 后端(`claude` / `codex` / `codebuddy` / `opencode`),默认 `claude`。
60
53
 
61
54
  ## 聊天命令
62
55
 
56
+ ### 会话管理
57
+
63
58
  | 命令 | 说明 |
64
59
  |------|------|
65
60
  | `/help` | 显示所有命令 |
@@ -70,12 +65,35 @@ npx @wu529778790/open-im start
70
65
  | `/delete <序号>` | 删除会话 |
71
66
  | `/rename <标题>` | 重命名会话 |
72
67
  | `/fork [序号]` | 分支会话 |
68
+
69
+ ### 信息查看
70
+
71
+ | 命令 | 说明 |
72
+ |------|------|
73
73
  | `/models` | 查看可用模型 |
74
74
  | `/context` | 查看上下文用量 |
75
+ | `/plugins` | 查看已安装插件 |
75
76
  | `/status` | 显示状态信息 |
76
77
  | `/cd <路径>` / `/pwd` | 切换/查看工作目录 |
77
- | `/plugins` | 查看已安装插件 |
78
- | `/allow` `/y` / `/deny` `/n` | 权限确认 |
78
+
79
+ ### 快捷命令
80
+
81
+ | 命令 | 说明 |
82
+ |------|------|
83
+ | `/git commit` | 提交代码 |
84
+ | `/git push` | 推送到远程 |
85
+ | `/git pull` | 拉取远程更新 |
86
+ | `/test` | 运行测试 |
87
+ | `/build` | 构建项目 |
88
+ | `/review` | 代码审查 |
89
+ | `/explain` | 解释项目结构 |
90
+
91
+ ### 权限控制
92
+
93
+ | 命令 | 说明 |
94
+ |------|------|
95
+ | `/allow` `/y` | 允许操作 |
96
+ | `/deny` `/n` | 拒绝操作 |
79
97
 
80
98
  ## 会话接力
81
99
 
@@ -102,6 +120,8 @@ claude -c # 接上手机端的对话
102
120
  - 启动/停止桥接服务
103
121
  - 编辑配置文件
104
122
  - 首次运行自动弹出设置向导
123
+ - 平台卡片支持展开/折叠
124
+ - 一键保存并启动
105
125
 
106
126
  局域网访问:`export OPEN_IM_WEB_HOST=0.0.0.0`
107
127
 
@@ -114,6 +134,7 @@ claude -c # 接上手机端的对话
114
134
  | `open-im stop` | 停止服务 |
115
135
  | `open-im restart` | 重启 |
116
136
  | `open-im dashboard` | 仅启动 Web 配置服务 |
137
+ | `open-im --version` | 查看版本号 |
117
138
 
118
139
  ## 配置
119
140
 
@@ -137,37 +158,17 @@ claude -c # 接上手机端的对话
137
158
  }
138
159
  ```
139
160
 
140
- ### 语音回复(可选)
141
-
142
- ClawBot 支持语音回复,需要 Python 3 + edge-tts:
143
-
144
- ```bash
145
- # 安装依赖
146
- pip3 install edge-tts
147
-
148
- # 启用语音
149
- # 在管理页面 http://127.0.0.1:39282 打开 ClawBot 的「语音回复」开关
150
- ```
151
-
152
- 支持的中文声音:
153
- - 晓晓(女声,温柔)
154
- - 晓伊(女声,活泼)
155
- - 云希(男声,年轻)
156
- - 云健(男声,沉稳)
157
- - 云扬(男声,专业)
158
-
159
- > 不安装 Python 也能正常使用 open-im,语音回复是可选功能。
160
-
161
161
  ### 环境变量
162
162
 
163
163
  - **`ANTHROPIC_*`** — Claude API 配置
164
164
  - **`TELEGRAM_BOT_TOKEN`** — Telegram Bot Token
165
165
  - **`OPEN_IM_WEB_PORT`** — Web 控制台端口(默认 39282)
166
166
  - **`OPEN_IM_WEB_HOST`** — Web 控制台监听地址
167
+ - **`OPEN_IM_SENTRY_DSN`** — Sentry 错误追踪(可选)
167
168
 
168
- ### 隐私
169
+ ### 错误追踪
169
170
 
170
- 匿名运行信息用于改进稳定性(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
171
+ 默认启用 Sentry 收集错误日志(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
171
172
 
172
173
  ## 平台配置详情
173
174
 
@@ -12,7 +12,8 @@ import { createLogger } from '../logger.js';
12
12
  import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
13
13
  import { cacheContextToken } from './message-sender.js';
14
14
  import { setClawbotContextToken, clearClawbotContextToken } from '../shared/active-chats.js';
15
- import { decryptAes256CbcMedia, saveBufferMedia, createMediaTargetPath } from '../shared/media-storage.js';
15
+ import { createMediaTargetPath } from '../shared/media-storage.js';
16
+ import { createDecipheriv } from 'node:crypto';
16
17
  import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
17
18
  const log = createLogger('ClawBot');
18
19
  const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
@@ -132,6 +133,10 @@ function startPolling() {
132
133
  cacheContextToken(chatId, msg.context_token);
133
134
  setClawbotContextToken(msg.context_token);
134
135
  }
136
+ // Debug: log raw item_list for image messages
137
+ if (extracted === '[图片]') {
138
+ log.info(`Image message raw item_list: ${JSON.stringify(msg.item_list).substring(0, 2000)}`);
139
+ }
135
140
  // Extract and download images from message
136
141
  const imagePaths = await extractImages(msg);
137
142
  userMessages.push({ chatId, msgId, content: extracted, imagePaths: imagePaths.length > 0 ? imagePaths : undefined });
@@ -249,28 +254,56 @@ async function extractImages(msg) {
249
254
  for (const item of msg.item_list) {
250
255
  if (item.type !== 2 /* MessageItemType.IMAGE */)
251
256
  continue;
252
- const media = item.image_item?.media;
253
- if (!media?.cdn_url)
257
+ const imageItem = item.image_item;
258
+ const media = imageItem?.media;
259
+ // iLink API 使用 full_url 而非 cdn_url
260
+ const imageUrl = media?.full_url || media?.cdn_url;
261
+ if (!imageUrl) {
262
+ log.warn('Image item missing full_url/cdn_url');
254
263
  continue;
264
+ }
255
265
  try {
256
- // Download from CDN
257
- const response = await fetch(media.cdn_url, { signal: AbortSignal.timeout(30_000) });
266
+ // 直接从 full_url 下载图片(CDN 返回的是已解密的图片数据)
267
+ const response = await fetch(imageUrl, { signal: AbortSignal.timeout(30_000) });
258
268
  if (!response.ok) {
259
269
  log.warn(`Image download failed: HTTP ${response.status}`);
260
270
  continue;
261
271
  }
262
272
  const buffer = Buffer.from(await response.arrayBuffer());
263
- // Decrypt if AES key provided
264
- let decrypted;
265
- if (media.aes_key) {
266
- decrypted = decryptAes256CbcMedia(buffer, media.aes_key);
273
+ // 检查是否是有效图片
274
+ const first4 = buffer.subarray(0, 4).toString('hex');
275
+ const isJpeg = first4 === 'ffd8ffe0' || first4 === 'ffd8ffe1';
276
+ const isPng = first4 === '89504e47';
277
+ let finalBuffer;
278
+ if (isJpeg || isPng) {
279
+ // CDN 返回的是有效图片,直接使用
280
+ finalBuffer = buffer;
267
281
  }
268
282
  else {
269
- decrypted = buffer;
283
+ // CDN 返回的是加密数据,尝试解密
284
+ const aesKeyHex = imageItem?.aeskey;
285
+ if (aesKeyHex && aesKeyHex.length === 32) {
286
+ try {
287
+ const keyBuf = Buffer.from(aesKeyHex, 'hex');
288
+ const iv = keyBuf.subarray(0, 16);
289
+ const decipher = createDecipheriv('aes-128-cbc', keyBuf, iv);
290
+ finalBuffer = Buffer.concat([decipher.update(buffer), decipher.final()]);
291
+ }
292
+ catch {
293
+ log.info('AES decryption failed, using raw image data');
294
+ finalBuffer = buffer;
295
+ }
296
+ }
297
+ else {
298
+ finalBuffer = buffer;
299
+ }
270
300
  }
271
301
  // Save to disk
272
302
  const targetPath = createMediaTargetPath('.jpg', `clawbot-${Date.now()}`);
273
- await saveBufferMedia(decrypted, targetPath);
303
+ const { writeFile } = await import('node:fs/promises');
304
+ const { mkdir } = await import('node:fs/promises');
305
+ await mkdir('/tmp/t/open-im-images', { recursive: true });
306
+ await writeFile(targetPath, finalBuffer);
274
307
  paths.push(targetPath);
275
308
  log.info(`ClawBot image saved: ${targetPath}`);
276
309
  }
@@ -4,12 +4,10 @@
4
4
  * Uses POST + JSON body + Bearer token auth (iLink protocol).
5
5
  */
6
6
  import { randomBytes } from 'node:crypto';
7
- import { readFileSync } from 'node:fs';
8
7
  import { createLogger } from '../logger.js';
9
8
  import { toReplyPlainText } from '../shared/utils.js';
10
9
  import { getChannelState } from './client.js';
11
10
  import { getActiveChatId, getClawbotContextToken } from '../shared/active-chats.js';
12
- import { textToSpeech, getTTSConfig } from '../shared/tts.js';
13
11
  const log = createLogger('ClawBotSender');
14
12
  let apiUrl = 'https://ilinkai.weixin.qq.com';
15
13
  let apiToken = '';
@@ -90,59 +88,6 @@ async function postMessage(chatId, text, contextToken) {
90
88
  return false;
91
89
  }
92
90
  }
93
- /**
94
- * 发送语音消息
95
- */
96
- async function postVoiceMessage(chatId, audioPath, contextToken) {
97
- if (getChannelState() !== 'connected') {
98
- log.warn('ClawBot not connected, cannot send voice message');
99
- return false;
100
- }
101
- const token = contextToken ?? getCachedContextToken(chatId);
102
- if (!token) {
103
- log.warn(`ClawBot no context_token for chatId=${chatId}, cannot send voice`);
104
- return false;
105
- }
106
- try {
107
- // 读取音频文件并转为 base64
108
- const audioBuffer = readFileSync(audioPath);
109
- const audioBase64 = audioBuffer.toString('base64');
110
- const url = `${apiUrl}/ilink/bot/sendmessage`;
111
- const body = JSON.stringify({
112
- msg: {
113
- from_user_id: '',
114
- to_user_id: chatId,
115
- client_id: generateClientId(),
116
- message_type: 2, // BOT
117
- message_state: 2, // FINISH
118
- item_list: [{
119
- type: 3, // VOICE
120
- voice_item: {
121
- media: { cdn_url: `data:audio/mp3;base64,${audioBase64}` },
122
- },
123
- }],
124
- context_token: token,
125
- },
126
- base_info: { channel_version: '0.1.0' },
127
- });
128
- const res = await fetch(url, {
129
- method: 'POST',
130
- headers: buildHeaders(),
131
- body,
132
- });
133
- const data = await res.json();
134
- const ok = data.ret === 0 || data.ret === undefined;
135
- if (!ok) {
136
- log.error(`ClawBot voice message failed: ret=${data.ret} errcode=${data.errcode} errmsg=${data.errmsg}`);
137
- return false;
138
- }
139
- return true;
140
- }
141
- catch (err) {
142
- log.error('ClawBot voice message error:', err);
143
- return false;
144
- }
145
- }
146
91
  /**
147
92
  * Send text reply to a ClawBot chat, splitting long messages automatically.
148
93
  */
@@ -151,20 +96,6 @@ export async function sendTextReply(chatId, text, contextToken) {
151
96
  // 发送文字消息
152
97
  log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
153
98
  await postMessage(chatId, plainText, contextToken);
154
- // 如果 TTS 启用,同时发送语音消息
155
- const ttsConfig = getTTSConfig();
156
- if (ttsConfig.enabled && plainText.length > 10) {
157
- try {
158
- const audioPath = await textToSpeech(plainText);
159
- if (audioPath) {
160
- await postVoiceMessage(chatId, audioPath, contextToken);
161
- log.info(`Voice message sent to chatId=${chatId}`);
162
- }
163
- }
164
- catch (err) {
165
- log.warn('Failed to send voice message:', err);
166
- }
167
- }
168
99
  }
169
100
  /**
170
101
  * Send error reply to a ClawBot chat.
@@ -21,9 +21,12 @@ export interface TextItem {
21
21
  }
22
22
  /** Image content item */
23
23
  export interface ImageItem {
24
+ aeskey?: string;
24
25
  media?: {
25
26
  aes_key?: string;
26
27
  cdn_url?: string;
28
+ full_url?: string;
29
+ encrypt_query_param?: string;
27
30
  };
28
31
  width?: number;
29
32
  height?: number;
@@ -95,11 +95,6 @@ export interface Config {
95
95
  allowedUserIds: string[];
96
96
  apiUrl?: string;
97
97
  apiToken?: string;
98
- /** TTS 配置 */
99
- tts?: {
100
- enabled?: boolean;
101
- voice?: string;
102
- };
103
98
  };
104
99
  };
105
100
  }
@@ -167,10 +162,6 @@ export interface FilePlatformClawbot {
167
162
  allowedUserIds?: string[];
168
163
  apiUrl?: string;
169
164
  apiToken?: string;
170
- tts?: {
171
- enabled?: boolean;
172
- voice?: string;
173
- };
174
165
  }
175
166
  export interface FileToolClaude {
176
167
  cliPath?: string;
@@ -338,8 +338,6 @@ function buildInitialPayload(file) {
338
338
  apiUrl: file.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322",
339
339
  apiToken: maskSecret(file.platforms?.clawbot?.apiToken),
340
340
  allowedUserIds: (file.platforms?.clawbot?.allowedUserIds ?? []).join(", "),
341
- ttsEnabled: file.platforms?.clawbot?.tts?.enabled ?? false,
342
- ttsVoice: file.platforms?.clawbot?.tts?.voice ?? "zh-CN-XiaoxiaoNeural",
343
341
  },
344
342
  },
345
343
  ai: {
@@ -773,10 +771,6 @@ function toFileConfig(payload, existing) {
773
771
  apiUrl: clean(payload.platforms.clawbot.apiUrl) ?? "http://127.0.0.1:26322",
774
772
  apiToken: resolveSecret(payload.platforms.clawbot.apiToken, existing.platforms?.clawbot?.apiToken),
775
773
  allowedUserIds: splitCsv(payload.platforms.clawbot.allowedUserIds),
776
- tts: {
777
- enabled: payload.platforms.clawbot.ttsEnabled,
778
- voice: payload.platforms.clawbot.ttsVoice || "zh-CN-XiaoxiaoNeural",
779
- },
780
774
  },
781
775
  },
782
776
  };
package/dist/config.js CHANGED
@@ -493,7 +493,6 @@ export function loadConfig() {
493
493
  allowedUserIds: clawbotAllowedUserIds,
494
494
  apiUrl: clawbotApiUrl,
495
495
  apiToken: clawbotApiToken,
496
- tts: file.platforms?.clawbot?.tts,
497
496
  }
498
497
  : {
499
498
  enabled: false,
package/dist/index.js CHANGED
@@ -104,12 +104,6 @@ const PLATFORM_MODULES = {
104
104
  const pc = config.platforms.clawbot;
105
105
  if (pc?.apiUrl && pc?.apiToken) {
106
106
  initClawBotSender(pc.apiUrl, pc.apiToken);
107
- // 初始化 TTS — 从配置读取
108
- const { initTTS } = await import('./shared/tts.js');
109
- initTTS({
110
- enabled: pc.tts?.enabled ?? false,
111
- voice: pc.tts?.voice ?? 'zh',
112
- });
113
107
  }
114
108
  const handle = setupClawbotHandlers(config, sessionManager);
115
109
  await initClawbot(config, handle.handleEvent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.4-beta.9",
3
+ "version": "1.11.4",
4
4
  "description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -58,10 +58,13 @@
58
58
  "@sentry/node": "^10.58.0",
59
59
  "centrifuge": "^5.5.3",
60
60
  "dingtalk-stream": "^2.1.4",
61
- "edge-tts-node": "^1.5.7",
61
+ "edge-tts": "^1.0.1",
62
+ "https-proxy-agent": "^9.1.0",
63
+ "node-edge-tts": "^1.2.10",
62
64
  "prompts": "^2.4.2",
65
+ "say": "^0.16.0",
63
66
  "telegraf": "^4.16.3",
64
- "ws": "^8.20.0"
67
+ "ws": "^8.21.0"
65
68
  },
66
69
  "devDependencies": {
67
70
  "@eslint/js": "^9.15.0",