@wu529778790/open-im 1.11.4-beta.2 → 1.11.4-beta.20

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
 
@@ -143,10 +164,11 @@ claude -c # 接上手机端的对话
143
164
  - **`TELEGRAM_BOT_TOKEN`** — Telegram Bot Token
144
165
  - **`OPEN_IM_WEB_PORT`** — Web 控制台端口(默认 39282)
145
166
  - **`OPEN_IM_WEB_HOST`** — Web 控制台监听地址
167
+ - **`OPEN_IM_SENTRY_DSN`** — Sentry 错误追踪(可选)
146
168
 
147
- ### 隐私
169
+ ### 错误追踪
148
170
 
149
- 匿名运行信息用于改进稳定性(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
171
+ 默认启用 Sentry 收集错误日志(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
150
172
 
151
173
  ## 平台配置详情
152
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
  }
@@ -5,8 +5,7 @@
5
5
  */
6
6
  import { randomBytes } from 'node:crypto';
7
7
  import { createLogger } from '../logger.js';
8
- import { splitLongContent, toReplyPlainText } from '../shared/utils.js';
9
- import { MAX_CLAWBOT_MESSAGE_LENGTH } from '../constants.js';
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
11
  const log = createLogger('ClawBotSender');
@@ -94,20 +93,9 @@ async function postMessage(chatId, text, contextToken) {
94
93
  */
95
94
  export async function sendTextReply(chatId, text, contextToken) {
96
95
  const plainText = toReplyPlainText(text);
97
- const parts = splitLongContent(plainText, MAX_CLAWBOT_MESSAGE_LENGTH);
98
- if (parts.length === 1) {
99
- log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
100
- await postMessage(chatId, plainText, contextToken);
101
- return;
102
- }
103
- log.info(`Sending ClawBot reply in ${parts.length} parts to chatId=${chatId}, totalLen=${plainText.length}`);
104
- for (let i = 0; i < parts.length; i++) {
105
- const partText = i === 0
106
- ? `${parts[i]}\n\n_(1/${parts.length})_`
107
- : `_(续 ${i + 1}/${parts.length})_\n\n${parts[i]}`;
108
- await postMessage(chatId, partText, contextToken);
109
- log.info(`ClawBot part ${i + 1}/${parts.length} sent`);
110
- }
96
+ // 发送文字消息
97
+ log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
98
+ await postMessage(chatId, plainText, contextToken);
111
99
  }
112
100
  /**
113
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.4-beta.2",
3
+ "version": "1.11.4-beta.20",
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,9 +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": "^1.0.1",
62
+ "https-proxy-agent": "^9.1.0",
63
+ "node-edge-tts": "^1.2.10",
61
64
  "prompts": "^2.4.2",
65
+ "say": "^0.16.0",
62
66
  "telegraf": "^4.16.3",
63
- "ws": "^8.20.0"
67
+ "ws": "^8.21.0"
64
68
  },
65
69
  "devDependencies": {
66
70
  "@eslint/js": "^9.15.0",