@wu529778790/open-im 1.11.4-beta.20 → 1.11.4-beta.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 +23 -45
- package/dist/clawbot/client.js +11 -44
- package/dist/clawbot/message-sender.js +69 -0
- package/dist/clawbot/types.d.ts +0 -3
- package/dist/config/types.d.ts +9 -0
- package/dist/config-web.js +6 -0
- package/dist/index.js +3 -0
- package/dist/shared/tts.d.ts +22 -0
- package/dist/shared/tts.js +72 -0
- package/package.json +3 -6
- package/web/dist/assets/index-DbHz8b6h.js +57 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-BhCQOCWI.js +0 -57
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@ 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/飞书/钉钉)
|
|
14
13
|
|
|
15
14
|
## 快速开始
|
|
16
15
|
|
|
@@ -18,11 +17,19 @@ open-im 把 Claude Code、Codex、CodeBuddy 接入 Telegram、飞书、企业微
|
|
|
18
17
|
# 安装
|
|
19
18
|
npm install -g @wu529778790/open-im
|
|
20
19
|
|
|
20
|
+
# 配置(交互式向导)
|
|
21
|
+
open-im init
|
|
22
|
+
|
|
21
23
|
# 启动
|
|
22
24
|
open-im start
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
或直接用 npx:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @wu529778790/open-im init
|
|
31
|
+
npx @wu529778790/open-im start
|
|
32
|
+
```
|
|
26
33
|
|
|
27
34
|
### 最小配置
|
|
28
35
|
|
|
@@ -39,22 +46,20 @@ open-im start
|
|
|
39
46
|
|
|
40
47
|
## 平台支持
|
|
41
48
|
|
|
42
|
-
| 平台 | 流式输出 |
|
|
43
|
-
|
|
44
|
-
| Telegram | ✅ |
|
|
45
|
-
| 飞书 | ✅ |
|
|
46
|
-
| QQ 机器人 | ✅ |
|
|
47
|
-
| 企业微信 | ✅ |
|
|
48
|
-
| 钉钉机器人 | ⚠️ 部分 |
|
|
49
|
-
| 微信助理(WorkBuddy) | ✅ |
|
|
50
|
-
| 微信客服号(ClawBot) | ✅ |
|
|
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) |
|
|
51
58
|
|
|
52
|
-
每个平台可单独配置 AI 后端(`claude` / `codex` / `codebuddy
|
|
59
|
+
每个平台可单独配置 AI 后端(`claude` / `codex` / `codebuddy`),默认 `claude`。
|
|
53
60
|
|
|
54
61
|
## 聊天命令
|
|
55
62
|
|
|
56
|
-
### 会话管理
|
|
57
|
-
|
|
58
63
|
| 命令 | 说明 |
|
|
59
64
|
|------|------|
|
|
60
65
|
| `/help` | 显示所有命令 |
|
|
@@ -65,35 +70,12 @@ open-im start
|
|
|
65
70
|
| `/delete <序号>` | 删除会话 |
|
|
66
71
|
| `/rename <标题>` | 重命名会话 |
|
|
67
72
|
| `/fork [序号]` | 分支会话 |
|
|
68
|
-
|
|
69
|
-
### 信息查看
|
|
70
|
-
|
|
71
|
-
| 命令 | 说明 |
|
|
72
|
-
|------|------|
|
|
73
73
|
| `/models` | 查看可用模型 |
|
|
74
74
|
| `/context` | 查看上下文用量 |
|
|
75
|
-
| `/plugins` | 查看已安装插件 |
|
|
76
75
|
| `/status` | 显示状态信息 |
|
|
77
76
|
| `/cd <路径>` / `/pwd` | 切换/查看工作目录 |
|
|
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` | 拒绝操作 |
|
|
77
|
+
| `/plugins` | 查看已安装插件 |
|
|
78
|
+
| `/allow` `/y` / `/deny` `/n` | 权限确认 |
|
|
97
79
|
|
|
98
80
|
## 会话接力
|
|
99
81
|
|
|
@@ -120,8 +102,6 @@ claude -c # 接上手机端的对话
|
|
|
120
102
|
- 启动/停止桥接服务
|
|
121
103
|
- 编辑配置文件
|
|
122
104
|
- 首次运行自动弹出设置向导
|
|
123
|
-
- 平台卡片支持展开/折叠
|
|
124
|
-
- 一键保存并启动
|
|
125
105
|
|
|
126
106
|
局域网访问:`export OPEN_IM_WEB_HOST=0.0.0.0`
|
|
127
107
|
|
|
@@ -134,7 +114,6 @@ claude -c # 接上手机端的对话
|
|
|
134
114
|
| `open-im stop` | 停止服务 |
|
|
135
115
|
| `open-im restart` | 重启 |
|
|
136
116
|
| `open-im dashboard` | 仅启动 Web 配置服务 |
|
|
137
|
-
| `open-im --version` | 查看版本号 |
|
|
138
117
|
|
|
139
118
|
## 配置
|
|
140
119
|
|
|
@@ -164,11 +143,10 @@ claude -c # 接上手机端的对话
|
|
|
164
143
|
- **`TELEGRAM_BOT_TOKEN`** — Telegram Bot Token
|
|
165
144
|
- **`OPEN_IM_WEB_PORT`** — Web 控制台端口(默认 39282)
|
|
166
145
|
- **`OPEN_IM_WEB_HOST`** — Web 控制台监听地址
|
|
167
|
-
- **`OPEN_IM_SENTRY_DSN`** — Sentry 错误追踪(可选)
|
|
168
146
|
|
|
169
|
-
###
|
|
147
|
+
### 隐私
|
|
170
148
|
|
|
171
|
-
|
|
149
|
+
匿名运行信息用于改进稳定性(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
|
|
172
150
|
|
|
173
151
|
## 平台配置详情
|
|
174
152
|
|
package/dist/clawbot/client.js
CHANGED
|
@@ -12,8 +12,7 @@ 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 { createMediaTargetPath } from '../shared/media-storage.js';
|
|
16
|
-
import { createDecipheriv } from 'node:crypto';
|
|
15
|
+
import { decryptAes256CbcMedia, saveBufferMedia, createMediaTargetPath } from '../shared/media-storage.js';
|
|
17
16
|
import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
|
|
18
17
|
const log = createLogger('ClawBot');
|
|
19
18
|
const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
|
|
@@ -133,10 +132,6 @@ function startPolling() {
|
|
|
133
132
|
cacheContextToken(chatId, msg.context_token);
|
|
134
133
|
setClawbotContextToken(msg.context_token);
|
|
135
134
|
}
|
|
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
|
-
}
|
|
140
135
|
// Extract and download images from message
|
|
141
136
|
const imagePaths = await extractImages(msg);
|
|
142
137
|
userMessages.push({ chatId, msgId, content: extracted, imagePaths: imagePaths.length > 0 ? imagePaths : undefined });
|
|
@@ -254,56 +249,28 @@ async function extractImages(msg) {
|
|
|
254
249
|
for (const item of msg.item_list) {
|
|
255
250
|
if (item.type !== 2 /* MessageItemType.IMAGE */)
|
|
256
251
|
continue;
|
|
257
|
-
const
|
|
258
|
-
|
|
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');
|
|
252
|
+
const media = item.image_item?.media;
|
|
253
|
+
if (!media?.cdn_url)
|
|
263
254
|
continue;
|
|
264
|
-
}
|
|
265
255
|
try {
|
|
266
|
-
//
|
|
267
|
-
const response = await fetch(
|
|
256
|
+
// Download from CDN
|
|
257
|
+
const response = await fetch(media.cdn_url, { signal: AbortSignal.timeout(30_000) });
|
|
268
258
|
if (!response.ok) {
|
|
269
259
|
log.warn(`Image download failed: HTTP ${response.status}`);
|
|
270
260
|
continue;
|
|
271
261
|
}
|
|
272
262
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
let finalBuffer;
|
|
278
|
-
if (isJpeg || isPng) {
|
|
279
|
-
// CDN 返回的是有效图片,直接使用
|
|
280
|
-
finalBuffer = buffer;
|
|
263
|
+
// Decrypt if AES key provided
|
|
264
|
+
let decrypted;
|
|
265
|
+
if (media.aes_key) {
|
|
266
|
+
decrypted = decryptAes256CbcMedia(buffer, media.aes_key);
|
|
281
267
|
}
|
|
282
268
|
else {
|
|
283
|
-
|
|
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
|
-
}
|
|
269
|
+
decrypted = buffer;
|
|
300
270
|
}
|
|
301
271
|
// Save to disk
|
|
302
272
|
const targetPath = createMediaTargetPath('.jpg', `clawbot-${Date.now()}`);
|
|
303
|
-
|
|
304
|
-
const { mkdir } = await import('node:fs/promises');
|
|
305
|
-
await mkdir('/tmp/t/open-im-images', { recursive: true });
|
|
306
|
-
await writeFile(targetPath, finalBuffer);
|
|
273
|
+
await saveBufferMedia(decrypted, targetPath);
|
|
307
274
|
paths.push(targetPath);
|
|
308
275
|
log.info(`ClawBot image saved: ${targetPath}`);
|
|
309
276
|
}
|
|
@@ -4,10 +4,12 @@
|
|
|
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';
|
|
7
8
|
import { createLogger } from '../logger.js';
|
|
8
9
|
import { toReplyPlainText } from '../shared/utils.js';
|
|
9
10
|
import { getChannelState } from './client.js';
|
|
10
11
|
import { getActiveChatId, getClawbotContextToken } from '../shared/active-chats.js';
|
|
12
|
+
import { textToSpeech, getTTSConfig } from '../shared/tts.js';
|
|
11
13
|
const log = createLogger('ClawBotSender');
|
|
12
14
|
let apiUrl = 'https://ilinkai.weixin.qq.com';
|
|
13
15
|
let apiToken = '';
|
|
@@ -88,6 +90,59 @@ async function postMessage(chatId, text, contextToken) {
|
|
|
88
90
|
return false;
|
|
89
91
|
}
|
|
90
92
|
}
|
|
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
|
+
}
|
|
91
146
|
/**
|
|
92
147
|
* Send text reply to a ClawBot chat, splitting long messages automatically.
|
|
93
148
|
*/
|
|
@@ -96,6 +151,20 @@ export async function sendTextReply(chatId, text, contextToken) {
|
|
|
96
151
|
// 发送文字消息
|
|
97
152
|
log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
|
|
98
153
|
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
|
+
}
|
|
99
168
|
}
|
|
100
169
|
/**
|
|
101
170
|
* Send error reply to a ClawBot chat.
|
package/dist/clawbot/types.d.ts
CHANGED
|
@@ -21,12 +21,9 @@ export interface TextItem {
|
|
|
21
21
|
}
|
|
22
22
|
/** Image content item */
|
|
23
23
|
export interface ImageItem {
|
|
24
|
-
aeskey?: string;
|
|
25
24
|
media?: {
|
|
26
25
|
aes_key?: string;
|
|
27
26
|
cdn_url?: string;
|
|
28
|
-
full_url?: string;
|
|
29
|
-
encrypt_query_param?: string;
|
|
30
27
|
};
|
|
31
28
|
width?: number;
|
|
32
29
|
height?: number;
|
package/dist/config/types.d.ts
CHANGED
|
@@ -95,6 +95,11 @@ 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
|
+
};
|
|
98
103
|
};
|
|
99
104
|
};
|
|
100
105
|
}
|
|
@@ -162,6 +167,10 @@ export interface FilePlatformClawbot {
|
|
|
162
167
|
allowedUserIds?: string[];
|
|
163
168
|
apiUrl?: string;
|
|
164
169
|
apiToken?: string;
|
|
170
|
+
tts?: {
|
|
171
|
+
enabled?: boolean;
|
|
172
|
+
voice?: string;
|
|
173
|
+
};
|
|
165
174
|
}
|
|
166
175
|
export interface FileToolClaude {
|
|
167
176
|
cliPath?: string;
|
package/dist/config-web.js
CHANGED
|
@@ -338,6 +338,8 @@ 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",
|
|
341
343
|
},
|
|
342
344
|
},
|
|
343
345
|
ai: {
|
|
@@ -771,6 +773,10 @@ function toFileConfig(payload, existing) {
|
|
|
771
773
|
apiUrl: clean(payload.platforms.clawbot.apiUrl) ?? "http://127.0.0.1:26322",
|
|
772
774
|
apiToken: resolveSecret(payload.platforms.clawbot.apiToken, existing.platforms?.clawbot?.apiToken),
|
|
773
775
|
allowedUserIds: splitCsv(payload.platforms.clawbot.allowedUserIds),
|
|
776
|
+
tts: {
|
|
777
|
+
enabled: payload.platforms.clawbot.ttsEnabled,
|
|
778
|
+
voice: payload.platforms.clawbot.ttsVoice || "zh-CN-XiaoxiaoNeural",
|
|
779
|
+
},
|
|
774
780
|
},
|
|
775
781
|
},
|
|
776
782
|
};
|
package/dist/index.js
CHANGED
|
@@ -104,6 +104,9 @@ 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({ enabled: true, voice: 'zh-CN-XiaoxiaoNeural' });
|
|
107
110
|
}
|
|
108
111
|
const handle = setupClawbotHandlers(config, sessionManager);
|
|
109
112
|
await initClawbot(config, handle.handleEvent);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS (Text-to-Speech) 模块
|
|
3
|
+
* 使用 edge-tts-node 调用微软 Edge TTS 服务
|
|
4
|
+
*/
|
|
5
|
+
/** TTS 配置 */
|
|
6
|
+
export interface TTSConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
voice?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 初始化 TTS
|
|
12
|
+
*/
|
|
13
|
+
export declare function initTTS(cfg?: Partial<TTSConfig>): void;
|
|
14
|
+
/**
|
|
15
|
+
* 获取 TTS 配置
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTTSConfig(): TTSConfig;
|
|
18
|
+
/**
|
|
19
|
+
* 文字转语音
|
|
20
|
+
* @returns 音频文件路径
|
|
21
|
+
*/
|
|
22
|
+
export declare function textToSpeech(text: string): Promise<string | null>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS (Text-to-Speech) 模块
|
|
3
|
+
* 使用 edge-tts-node 调用微软 Edge TTS 服务
|
|
4
|
+
*/
|
|
5
|
+
import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node';
|
|
6
|
+
import { createLogger } from '../logger.js';
|
|
7
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { APP_HOME } from '../constants.js';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
const log = createLogger('TTS');
|
|
12
|
+
/** 默认配置 */
|
|
13
|
+
const DEFAULT_TTS_CONFIG = {
|
|
14
|
+
enabled: false,
|
|
15
|
+
voice: 'zh-CN-XiaoxiaoNeural',
|
|
16
|
+
};
|
|
17
|
+
let config = DEFAULT_TTS_CONFIG;
|
|
18
|
+
let tts = null;
|
|
19
|
+
/**
|
|
20
|
+
* 初始化 TTS
|
|
21
|
+
*/
|
|
22
|
+
export function initTTS(cfg) {
|
|
23
|
+
config = { ...DEFAULT_TTS_CONFIG, ...cfg };
|
|
24
|
+
if (config.enabled) {
|
|
25
|
+
tts = new MsEdgeTTS({ enableLogger: false });
|
|
26
|
+
log.info(`TTS enabled, voice: ${config.voice}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 获取 TTS 配置
|
|
31
|
+
*/
|
|
32
|
+
export function getTTSConfig() {
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 文字转语音
|
|
37
|
+
* @returns 音频文件路径
|
|
38
|
+
*/
|
|
39
|
+
export async function textToSpeech(text) {
|
|
40
|
+
if (!config.enabled || !tts) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// 截断过长的文本(TTS 有长度限制)
|
|
45
|
+
const truncatedText = text.length > 5000 ? text.substring(0, 5000) + '...' : text;
|
|
46
|
+
// 清理 markdown 格式(TTS 不需要)
|
|
47
|
+
const cleanText = truncatedText
|
|
48
|
+
.replace(/```[\s\S]*?```/g, '代码块已省略') // 代码块
|
|
49
|
+
.replace(/`[^`]+`/g, (match) => match.slice(1, -1)) // 行内代码
|
|
50
|
+
.replace(/\*\*[^*]+\*\*/g, (match) => match.slice(2, -2)) // 粗体
|
|
51
|
+
.replace(/\*[^*]+\*/g, (match) => match.slice(1, -1)) // 斜体
|
|
52
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // 链接
|
|
53
|
+
.replace(/#{1,6}\s/g, '') // 标题
|
|
54
|
+
.replace(/\n{3,}/g, '\n\n'); // 多余换行
|
|
55
|
+
// 生成音频文件路径
|
|
56
|
+
const audioDir = join(APP_HOME, 'audio');
|
|
57
|
+
if (!existsSync(audioDir)) {
|
|
58
|
+
mkdirSync(audioDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
const audioPath = join(audioDir, `tts-${randomBytes(8).toString('hex')}.mp3`);
|
|
61
|
+
// 设置 TTS 元数据
|
|
62
|
+
await tts.setMetadata(config.voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
|
|
63
|
+
// 调用 TTS
|
|
64
|
+
await tts.toFile(audioPath, cleanText);
|
|
65
|
+
log.info(`TTS generated: ${audioPath}`);
|
|
66
|
+
return audioPath;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
log.error('TTS failed:', err);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.11.4-beta.
|
|
3
|
+
"version": "1.11.4-beta.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,13 +58,10 @@
|
|
|
58
58
|
"@sentry/node": "^10.58.0",
|
|
59
59
|
"centrifuge": "^5.5.3",
|
|
60
60
|
"dingtalk-stream": "^2.1.4",
|
|
61
|
-
"edge-tts": "^1.
|
|
62
|
-
"https-proxy-agent": "^9.1.0",
|
|
63
|
-
"node-edge-tts": "^1.2.10",
|
|
61
|
+
"edge-tts-node": "^1.5.7",
|
|
64
62
|
"prompts": "^2.4.2",
|
|
65
|
-
"say": "^0.16.0",
|
|
66
63
|
"telegraf": "^4.16.3",
|
|
67
|
-
"ws": "^8.
|
|
64
|
+
"ws": "^8.20.0"
|
|
68
65
|
},
|
|
69
66
|
"devDependencies": {
|
|
70
67
|
"@eslint/js": "^9.15.0",
|