@wu529778790/open-im 1.0.3-beta.1 → 1.0.3-beta.3
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 +29 -28
- package/dist/claude/cli-runner.js +8 -37
- package/dist/claude/process-pool.js +8 -31
- package/dist/cli.js +77 -26
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +3 -1
- package/dist/config.d.ts +10 -1
- package/dist/config.js +35 -2
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +3 -0
- package/dist/feishu/event-handler.js +13 -2
- package/dist/index.js +16 -0
- package/dist/setup.js +93 -7
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/shared/ai-task.js +5 -2
- package/dist/wechat/client.d.ts +25 -0
- package/dist/wechat/client.js +279 -0
- package/dist/wechat/event-handler.d.ts +11 -0
- package/dist/wechat/event-handler.js +218 -0
- package/dist/wechat/message-sender.d.ts +36 -0
- package/dist/wechat/message-sender.js +216 -0
- package/dist/wechat/types.d.ts +87 -0
- package/dist/wechat/types.js +5 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# open-im
|
|
2
2
|
|
|
3
|
-
多平台 IM 桥接,将 Telegram
|
|
3
|
+
多平台 IM 桥接,将 Telegram、飞书 (Feishu/Lark) 和微信连接到 AI CLI 工具(Claude Code、Codex、Cursor),实现移动端/远程访问 AI 编程助手。
|
|
4
4
|
|
|
5
5
|
## 功能特性
|
|
6
6
|
|
|
7
|
-
- **多平台**:支持 Telegram
|
|
7
|
+
- **多平台**:支持 Telegram、飞书和微信,可同时启用
|
|
8
8
|
- **多 AI 工具**:通过配置切换 Claude Code / Codex / Cursor
|
|
9
9
|
- **流式输出**:节流更新,实时展示 AI 回复
|
|
10
10
|
- **会话管理**:每用户独立 session,`/new` 重置会话
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
npm install @wu529778790/open-im -g
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## 快速开始
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# 使用 npx 快速体验(无需全局安装)
|
|
28
|
-
npx @wu529778790/open-im
|
|
29
|
-
npx @wu529778790/open-im
|
|
28
|
+
npx @wu529778790/open-im init # 初始化配置
|
|
29
|
+
npx @wu529778790/open-im start # 后台运行
|
|
30
|
+
npx @wu529778790/open-im stop # 停止后台服务
|
|
30
31
|
npx @wu529778790/open-im dev # 前台运行(调试),Ctrl+C 停止
|
|
31
32
|
```
|
|
32
33
|
|
|
@@ -34,28 +35,32 @@ npx @wu529778790/open-im dev # 前台运行(调试),Ctrl+C 停止
|
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
37
|
npm install @wu529778790/open-im -g
|
|
37
|
-
open-im
|
|
38
|
+
open-im init # 初始化配置
|
|
39
|
+
open-im start # 后台运行
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
配置保存到 `~/.open-im/config.json`。
|
|
41
43
|
|
|
42
|
-
##
|
|
44
|
+
## 命令说明
|
|
43
45
|
|
|
44
46
|
| 命令 | 说明 |
|
|
45
47
|
|------|------|
|
|
48
|
+
| `open-im init` | 初始化配置(不启动服务) |
|
|
46
49
|
| `open-im start` | 后台运行,适合长期使用 |
|
|
47
|
-
| `open-im stop` |
|
|
48
|
-
| `open-im dev`
|
|
50
|
+
| `open-im stop` | 停止后台服务 |
|
|
51
|
+
| `open-im dev` | 前台运行(调试模式),Ctrl+C 停止 |
|
|
49
52
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
**会话上下文存储在本地**(`~/.open-im/data/sessions.json`),与 IM 聊天记录无关。每用户在本地维护独立的 session 和 Claude 会话 ID,`/new` 可重置当前会话。
|
|
53
|
+
## 开发
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
|
-
npm
|
|
56
|
-
|
|
56
|
+
npm run build # 构建编译
|
|
57
|
+
npm run dev # 直接运行源码(tsx,无需 build)
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
## 会话说明
|
|
61
|
+
|
|
62
|
+
**会话上下文存储在本地**(`~/.open-im/data/sessions.json`),与 IM 聊天记录无关。每用户在本地维护独立的 session 和 Claude 会话 ID,`/new` 可重置当前会话。
|
|
63
|
+
|
|
59
64
|
### 环境变量
|
|
60
65
|
|
|
61
66
|
| 变量 | 说明 |
|
|
@@ -63,6 +68,9 @@ open-im run
|
|
|
63
68
|
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token(从 @BotFather 获取) |
|
|
64
69
|
| `FEISHU_APP_ID` | 飞书应用 App ID |
|
|
65
70
|
| `FEISHU_APP_SECRET` | 飞书应用 App Secret |
|
|
71
|
+
| `WECHAT_APP_ID` | 微信应用 App ID(AGP 协议) |
|
|
72
|
+
| `WECHAT_APP_SECRET` | 微信应用 App Secret |
|
|
73
|
+
| `WECHAT_WS_URL` | AGP WebSocket URL(可选,默认使用官方服务) |
|
|
66
74
|
| `ALLOWED_USER_IDS` | 白名单用户 ID(逗号分隔,空=所有人) |
|
|
67
75
|
| `AI_COMMAND` | `claude` \| `codex` \| `cursor`,默认 `claude` |
|
|
68
76
|
| `CLAUDE_CLI_PATH` | Claude CLI 路径,默认 `claude` |
|
|
@@ -78,10 +86,11 @@ open-im run
|
|
|
78
86
|
|
|
79
87
|
配置优先级:环境变量 > `~/.open-im/config.json` > 默认值。
|
|
80
88
|
|
|
81
|
-
至少需配置 **Telegram
|
|
89
|
+
至少需配置 **Telegram**、**飞书** 或 **微信** 其中一个:
|
|
82
90
|
|
|
83
91
|
- **Telegram**:`TELEGRAM_BOT_TOKEN` 或 `telegramBotToken`
|
|
84
92
|
- **飞书**:`FEISHU_APP_ID` + `FEISHU_APP_SECRET` 或 `feishuAppId` + `feishuAppSecret`
|
|
93
|
+
- **微信**:`WECHAT_APP_ID` + `WECHAT_APP_SECRET` 或 `wechatAppId` + `wechatAppSecret`
|
|
85
94
|
|
|
86
95
|
### 飞书配置说明
|
|
87
96
|
|
|
@@ -98,14 +107,6 @@ open-im run
|
|
|
98
107
|
|
|
99
108
|
**若点击 /mode 卡片按钮报错**:说明未配置卡片回调。配置较复杂时,可直接用 `/mode ask`、`/mode yolo` 等命令切换模式,无需卡片。
|
|
100
109
|
|
|
101
|
-
## 开发
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
npm run build # 构建
|
|
105
|
-
npm run dev # 直接运行源码(tsx,无需 build)
|
|
106
|
-
npm run foreground # 前台运行已构建版本
|
|
107
|
-
```
|
|
108
|
-
|
|
109
110
|
## IM 内命令
|
|
110
111
|
|
|
111
112
|
| 命令 | 说明 |
|
|
@@ -159,11 +160,11 @@ npm run foreground # 前台运行已构建版本
|
|
|
159
160
|
mkdir -p ~/.open-im
|
|
160
161
|
cat > ~/.open-im/config.json << 'EOF'
|
|
161
162
|
{
|
|
162
|
-
"telegramBotToken": "你的Bot Token",
|
|
163
|
-
"allowedUserIds": ["你的Telegram用户ID"],
|
|
164
163
|
"platforms": {
|
|
165
164
|
"telegram": {
|
|
166
|
-
"
|
|
165
|
+
"enabled": true,
|
|
166
|
+
"botToken": "你的Bot Token",
|
|
167
|
+
"allowedUserIds": ["你的Telegram用户ID"]
|
|
167
168
|
}
|
|
168
169
|
},
|
|
169
170
|
"claudeWorkDir": "$(pwd)",
|
|
@@ -183,7 +184,7 @@ tail -f ~/.open-im/logs/*.log
|
|
|
183
184
|
|
|
184
185
|
# 重新配置
|
|
185
186
|
rm ~/.open-im/config.json
|
|
186
|
-
|
|
187
|
+
open-im init
|
|
187
188
|
```
|
|
188
189
|
|
|
189
190
|
### Q: 如何获取 Telegram Bot Token?
|
|
@@ -35,44 +35,15 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
35
35
|
env.CC_IM_CHAT_ID = options.chatId;
|
|
36
36
|
if (options?.hookPort)
|
|
37
37
|
env.CC_IM_HOOK_PORT = String(options.hookPort);
|
|
38
|
-
//
|
|
38
|
+
// 使用 shell: false 直接 spawn,避免 shell 对参数按空格拆分
|
|
39
|
+
// (用户 prompt 如 "npm 你好" 在 shell: true 下会被拆成 "npm" 和 "你好",CLI 只收到第一个)
|
|
39
40
|
log.info(`Spawning CLI: path=${cliPath}, platform=${process.platform}`);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
log.info(`Detected environment: Git Bash=${isGitBash ? "yes" : "no"}`);
|
|
47
|
-
if (isGitBash) {
|
|
48
|
-
// In Git Bash, use shell for proper path resolution
|
|
49
|
-
log.info(`Using shell spawn for Git Bash environment`);
|
|
50
|
-
child = spawn(cliPath, args, {
|
|
51
|
-
cwd: workDir,
|
|
52
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
-
env,
|
|
54
|
-
shell: true,
|
|
55
|
-
windowsHide: true,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// In pure cmd/PowerShell, direct spawn works best
|
|
60
|
-
log.info(`Using direct spawn for Windows cmd/PowerShell`);
|
|
61
|
-
child = spawn(cliPath, args, {
|
|
62
|
-
cwd: workDir,
|
|
63
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
-
env,
|
|
65
|
-
windowsHide: true,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
child = spawn(cliPath, args, {
|
|
71
|
-
cwd: workDir,
|
|
72
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
73
|
-
env,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
41
|
+
const child = spawn(cliPath, args, {
|
|
42
|
+
cwd: workDir,
|
|
43
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
+
env,
|
|
45
|
+
windowsHide: process.platform === "win32",
|
|
46
|
+
});
|
|
76
47
|
log.info(`Claude CLI: pid=${child.pid}, cwd=${workDir}, session=${sessionId ?? "new"}`);
|
|
77
48
|
let accumulated = "";
|
|
78
49
|
let accumulatedThinking = "";
|
|
@@ -85,37 +85,14 @@ export class ClaudeProcessPool {
|
|
|
85
85
|
env.CC_IM_CHAT_ID = options.chatId;
|
|
86
86
|
if (options.hookPort)
|
|
87
87
|
env.CC_IM_HOOK_PORT = String(options.hookPort);
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
cwd: workDir,
|
|
97
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
-
env,
|
|
99
|
-
shell: true,
|
|
100
|
-
windowsHide: true,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
child = spawn(cliPath, args, {
|
|
105
|
-
cwd: workDir,
|
|
106
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
107
|
-
env,
|
|
108
|
-
windowsHide: true,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
child = spawn(cliPath, args, {
|
|
114
|
-
cwd: workDir,
|
|
115
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
-
env,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
88
|
+
// 使用 shell: false 直接 spawn,避免 shell 对参数按空格拆分
|
|
89
|
+
// (用户 prompt 如 "npm 你好" 在 shell: true 下会被拆成 "npm" 和 "你好",CLI 只收到第一个)
|
|
90
|
+
const child = spawn(cliPath, args, {
|
|
91
|
+
cwd: workDir,
|
|
92
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
+
env,
|
|
94
|
+
windowsHide: process.platform === "win32",
|
|
95
|
+
});
|
|
119
96
|
log.info(`Started process: pid=${child.pid}, key=${key}`);
|
|
120
97
|
// Track active process
|
|
121
98
|
this.activeProcesses.set(key, child);
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const PID_FILE = join(APP_HOME, "open-im.pid");
|
|
11
11
|
const PORT_FILE = join(APP_HOME, "open-im.port");
|
|
12
12
|
const INDEX_JS = join(__dirname, "index.js");
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// PID 文件管理
|
|
15
|
+
// ============================================================================
|
|
13
16
|
function getPid() {
|
|
14
17
|
if (!existsSync(PID_FILE))
|
|
15
18
|
return null;
|
|
@@ -47,32 +50,42 @@ function isRunning(pid) {
|
|
|
47
50
|
return false;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
removePid();
|
|
57
|
-
// 在前台先完成配置校验与配置向导(与 dev 行为保持一致)
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// 配置校验
|
|
55
|
+
// ============================================================================
|
|
56
|
+
async function validateOrSetup() {
|
|
58
57
|
if (needsSetup()) {
|
|
59
58
|
console.log("\n━━━ open-im 首次配置 ━━━\n");
|
|
60
59
|
console.log("检测到尚未配置,将先进入配置向导...\n");
|
|
61
60
|
const saved = await runInteractiveSetup();
|
|
62
61
|
if (!saved) {
|
|
63
62
|
console.log("配置未完成,已取消启动。");
|
|
64
|
-
|
|
63
|
+
return false;
|
|
65
64
|
}
|
|
66
65
|
console.log("");
|
|
67
66
|
}
|
|
68
|
-
// 校验配置是否有效(避免后台静默失败)
|
|
69
67
|
try {
|
|
70
68
|
loadConfig();
|
|
69
|
+
return true;
|
|
71
70
|
}
|
|
72
71
|
catch (err) {
|
|
73
72
|
const msg = err instanceof Error ? err.message : String(err);
|
|
74
73
|
console.error("配置无效或缺少必要字段:", msg);
|
|
75
|
-
console.log("\n请运行以下命令重新配置:\n npx @wu529778790/open-im
|
|
74
|
+
console.log("\n请运行以下命令重新配置:\n npx @wu529778790/open-im init");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// 命令处理
|
|
80
|
+
// ============================================================================
|
|
81
|
+
async function cmdStart() {
|
|
82
|
+
const pid = getPid();
|
|
83
|
+
if (pid && isRunning(pid)) {
|
|
84
|
+
console.log(`open-im 已在后台运行 (pid=${pid})`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
removePid();
|
|
88
|
+
if (!(await validateOrSetup())) {
|
|
76
89
|
process.exit(1);
|
|
77
90
|
}
|
|
78
91
|
const child = spawn(process.execPath, [INDEX_JS], {
|
|
@@ -113,7 +126,7 @@ async function cmdStop() {
|
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
catch {
|
|
116
|
-
|
|
129
|
+
// HTTP 失败则用 SIGTERM 兜底
|
|
117
130
|
process.kill(pid, "SIGTERM");
|
|
118
131
|
await new Promise((r) => setTimeout(r, 500));
|
|
119
132
|
}
|
|
@@ -130,29 +143,67 @@ async function cmdStop() {
|
|
|
130
143
|
}
|
|
131
144
|
console.log(`open-im 已停止 (pid=${pid})`);
|
|
132
145
|
}
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
console.
|
|
146
|
+
async function cmdInit() {
|
|
147
|
+
if (!needsSetup()) {
|
|
148
|
+
console.log("检测到已存在配置文件。如需重新配置,请先删除配置文件:");
|
|
149
|
+
console.log(` ${join(APP_HOME, "config.json")}`);
|
|
150
|
+
console.log("\n或直接编辑配置文件后重新运行。");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
console.log("\n━━━ open-im 配置向导 ━━━\n");
|
|
154
|
+
const saved = await runInteractiveSetup();
|
|
155
|
+
if (saved) {
|
|
156
|
+
console.log("\n✅ 配置完成!");
|
|
157
|
+
console.log("\n现在可以运行以下命令启动服务:");
|
|
158
|
+
console.log(" open-im start # 后台运行");
|
|
159
|
+
console.log(" open-im dev # 前台运行(调试)");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log("\n❌ 配置未完成,已取消。");
|
|
137
163
|
process.exit(1);
|
|
138
|
-
}
|
|
164
|
+
}
|
|
139
165
|
}
|
|
140
|
-
|
|
141
|
-
|
|
166
|
+
function showHelp(exitCode = 0) {
|
|
167
|
+
console.log(`
|
|
168
|
+
用法: open-im <command>
|
|
169
|
+
|
|
170
|
+
命令:
|
|
171
|
+
start 后台运行服务
|
|
172
|
+
stop 停止后台服务
|
|
173
|
+
init 初始化配置(不启动服务)
|
|
174
|
+
dev 前台运行(调试模式),Ctrl+C 停止
|
|
175
|
+
|
|
176
|
+
选项:
|
|
177
|
+
-h, --help 显示此帮助信息
|
|
178
|
+
`);
|
|
179
|
+
process.exit(exitCode);
|
|
180
|
+
}
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// 命令路由
|
|
183
|
+
// ============================================================================
|
|
184
|
+
const cmd = process.argv[2];
|
|
185
|
+
const commands = {
|
|
186
|
+
start: cmdStart,
|
|
187
|
+
stop: cmdStop,
|
|
188
|
+
init: cmdInit,
|
|
189
|
+
dev: main,
|
|
190
|
+
};
|
|
191
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
192
|
+
showHelp(0);
|
|
193
|
+
}
|
|
194
|
+
else if (cmd === undefined) {
|
|
195
|
+
main().catch((err) => {
|
|
142
196
|
console.error(err);
|
|
143
197
|
process.exit(1);
|
|
144
198
|
});
|
|
145
199
|
}
|
|
146
|
-
else if (cmd
|
|
147
|
-
|
|
200
|
+
else if (commands[cmd]) {
|
|
201
|
+
commands[cmd]().catch((err) => {
|
|
148
202
|
console.error(err);
|
|
149
203
|
process.exit(1);
|
|
150
204
|
});
|
|
151
205
|
}
|
|
152
206
|
else {
|
|
153
|
-
console.
|
|
154
|
-
|
|
155
|
-
stop - 停止后台进程
|
|
156
|
-
dev - 前台运行(调试),Ctrl+C 停止`);
|
|
157
|
-
process.exit(cmd === "--help" || cmd === "-h" ? 0 : 1);
|
|
207
|
+
console.error(`未知命令: ${cmd}`);
|
|
208
|
+
showHelp(1);
|
|
158
209
|
}
|
|
@@ -21,7 +21,7 @@ export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: stri
|
|
|
21
21
|
export declare class CommandHandler {
|
|
22
22
|
private deps;
|
|
23
23
|
constructor(deps: CommandHandlerDeps);
|
|
24
|
-
dispatch(text: string, chatId: string, userId: string, platform: 'feishu' | 'telegram', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
24
|
+
dispatch(text: string, chatId: string, userId: string, platform: 'feishu' | 'telegram' | 'wechat', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
25
25
|
private handleMode;
|
|
26
26
|
private getClearHistoryHint;
|
|
27
27
|
private handleHelp;
|
package/dist/commands/handler.js
CHANGED
|
@@ -72,7 +72,9 @@ export class CommandHandler {
|
|
|
72
72
|
getClearHistoryHint(platform) {
|
|
73
73
|
return platform === 'feishu'
|
|
74
74
|
? '💡 提示:如需清除本对话的历史消息,请点击飞书聊天右上角「...」→ 清除聊天记录'
|
|
75
|
-
:
|
|
75
|
+
: platform === 'wechat'
|
|
76
|
+
? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
|
|
77
|
+
: '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
|
|
76
78
|
}
|
|
77
79
|
async handleHelp(chatId, platform) {
|
|
78
80
|
const help = [
|
package/dist/config.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { LogLevel } from './logger.js';
|
|
2
|
-
export type Platform = 'feishu' | 'telegram';
|
|
2
|
+
export type Platform = 'feishu' | 'telegram' | 'wechat';
|
|
3
3
|
export type AiCommand = 'claude' | 'codex' | 'cursor';
|
|
4
4
|
export interface Config {
|
|
5
5
|
enabledPlatforms: Platform[];
|
|
6
6
|
telegramBotToken?: string;
|
|
7
7
|
feishuAppId?: string;
|
|
8
8
|
feishuAppSecret?: string;
|
|
9
|
+
wechatAppId?: string;
|
|
10
|
+
wechatAppSecret?: string;
|
|
11
|
+
wechatWsUrl?: string;
|
|
9
12
|
allowedUserIds: string[];
|
|
10
13
|
telegramAllowedUserIds: string[];
|
|
11
14
|
feishuAllowedUserIds: string[];
|
|
15
|
+
wechatAllowedUserIds: string[];
|
|
12
16
|
aiCommand: AiCommand;
|
|
13
17
|
claudeCliPath: string;
|
|
14
18
|
claudeWorkDir: string;
|
|
@@ -30,6 +34,11 @@ export interface Config {
|
|
|
30
34
|
enabled: boolean;
|
|
31
35
|
allowedUserIds: string[];
|
|
32
36
|
};
|
|
37
|
+
wechat?: {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
wsUrl?: string;
|
|
40
|
+
allowedUserIds: string[];
|
|
41
|
+
};
|
|
33
42
|
};
|
|
34
43
|
}
|
|
35
44
|
/** 检测是否需要交互式配置(无 token 且无环境变量) */
|
package/dist/config.js
CHANGED
|
@@ -24,12 +24,16 @@ export function needsSetup() {
|
|
|
24
24
|
return false;
|
|
25
25
|
if (process.env.FEISHU_APP_ID && process.env.FEISHU_APP_SECRET)
|
|
26
26
|
return false;
|
|
27
|
+
if (process.env.WECHAT_APP_ID && process.env.WECHAT_APP_SECRET)
|
|
28
|
+
return false;
|
|
27
29
|
const file = loadFileConfig();
|
|
28
30
|
const tg = file.platforms?.telegram;
|
|
29
31
|
const fs = file.platforms?.feishu;
|
|
32
|
+
const wc = file.platforms?.wechat;
|
|
30
33
|
const hasTelegram = !!tg?.botToken;
|
|
31
34
|
const hasFeishu = !!(fs?.appId && fs?.appSecret);
|
|
32
|
-
|
|
35
|
+
const hasWechat = !!(wc?.appId && wc?.appSecret);
|
|
36
|
+
return !hasTelegram && !hasFeishu && !hasWechat;
|
|
33
37
|
}
|
|
34
38
|
function parseCommaSeparated(value) {
|
|
35
39
|
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
@@ -38,6 +42,7 @@ export function loadConfig() {
|
|
|
38
42
|
const file = loadFileConfig();
|
|
39
43
|
const fileTelegram = file.platforms?.telegram;
|
|
40
44
|
const fileFeishu = file.platforms?.feishu;
|
|
45
|
+
const fileWechat = file.platforms?.wechat;
|
|
41
46
|
// 1. 加载各平台凭证(env 优先,其次新结构,最后旧字段)
|
|
42
47
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ??
|
|
43
48
|
fileTelegram?.botToken ??
|
|
@@ -48,18 +53,28 @@ export function loadConfig() {
|
|
|
48
53
|
const feishuAppSecret = process.env.FEISHU_APP_SECRET ??
|
|
49
54
|
fileFeishu?.appSecret ??
|
|
50
55
|
file.feishuAppSecret;
|
|
56
|
+
const wechatAppId = process.env.WECHAT_APP_ID ??
|
|
57
|
+
fileWechat?.appId;
|
|
58
|
+
const wechatAppSecret = process.env.WECHAT_APP_SECRET ??
|
|
59
|
+
fileWechat?.appSecret;
|
|
60
|
+
const wechatWsUrl = process.env.WECHAT_WS_URL ??
|
|
61
|
+
fileWechat?.wsUrl;
|
|
51
62
|
// 2. 计算启用平台
|
|
52
63
|
const enabledPlatforms = [];
|
|
53
64
|
const telegramEnabledFlag = fileTelegram?.enabled;
|
|
54
65
|
const feishuEnabledFlag = fileFeishu?.enabled;
|
|
66
|
+
const wechatEnabledFlag = fileWechat?.enabled;
|
|
55
67
|
const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
|
|
56
68
|
const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
|
|
69
|
+
const wechatEnabled = !!(wechatAppId && wechatAppSecret) && (wechatEnabledFlag !== false);
|
|
57
70
|
if (telegramEnabled)
|
|
58
71
|
enabledPlatforms.push('telegram');
|
|
59
72
|
if (feishuEnabled)
|
|
60
73
|
enabledPlatforms.push('feishu');
|
|
74
|
+
if (wechatEnabled)
|
|
75
|
+
enabledPlatforms.push('wechat');
|
|
61
76
|
if (enabledPlatforms.length === 0) {
|
|
62
|
-
throw new Error('至少需要配置 Telegram 或
|
|
77
|
+
throw new Error('至少需要配置 Telegram、Feishu 或 WeChat 其中一个平台(可以通过环境变量或 config.json)');
|
|
63
78
|
}
|
|
64
79
|
// 3. 全局白名单(旧字段,向后兼容,主要用于作为 per-platform 的兜底)
|
|
65
80
|
const allowedUserIds = process.env.ALLOWED_USER_IDS !== undefined
|
|
@@ -72,6 +87,9 @@ export function loadConfig() {
|
|
|
72
87
|
const feishuAllowedUserIds = process.env.FEISHU_ALLOWED_USER_IDS !== undefined
|
|
73
88
|
? parseCommaSeparated(process.env.FEISHU_ALLOWED_USER_IDS)
|
|
74
89
|
: fileFeishu?.allowedUserIds ?? allowedUserIds;
|
|
90
|
+
const wechatAllowedUserIds = process.env.WECHAT_ALLOWED_USER_IDS !== undefined
|
|
91
|
+
? parseCommaSeparated(process.env.WECHAT_ALLOWED_USER_IDS)
|
|
92
|
+
: fileWechat?.allowedUserIds ?? allowedUserIds;
|
|
75
93
|
// 5. AI / 工作目录 / 安全配置
|
|
76
94
|
const aiCommand = (process.env.AI_COMMAND ?? file.aiCommand ?? 'claude');
|
|
77
95
|
const claudeCliPath = process.env.CLAUDE_CLI_PATH ?? file.claudeCliPath ?? 'claude';
|
|
@@ -154,15 +172,30 @@ export function loadConfig() {
|
|
|
154
172
|
enabled: false,
|
|
155
173
|
allowedUserIds: feishuAllowedUserIds,
|
|
156
174
|
},
|
|
175
|
+
wechat: wechatEnabled
|
|
176
|
+
? {
|
|
177
|
+
enabled: true,
|
|
178
|
+
wsUrl: wechatWsUrl,
|
|
179
|
+
allowedUserIds: wechatAllowedUserIds,
|
|
180
|
+
}
|
|
181
|
+
: {
|
|
182
|
+
enabled: false,
|
|
183
|
+
wsUrl: wechatWsUrl,
|
|
184
|
+
allowedUserIds: wechatAllowedUserIds,
|
|
185
|
+
},
|
|
157
186
|
};
|
|
158
187
|
return {
|
|
159
188
|
enabledPlatforms,
|
|
160
189
|
telegramBotToken: telegramBotToken ?? '',
|
|
161
190
|
feishuAppId: feishuAppId ?? '',
|
|
162
191
|
feishuAppSecret: feishuAppSecret ?? '',
|
|
192
|
+
wechatAppId: wechatAppId ?? '',
|
|
193
|
+
wechatAppSecret: wechatAppSecret ?? '',
|
|
194
|
+
wechatWsUrl: wechatWsUrl,
|
|
163
195
|
allowedUserIds,
|
|
164
196
|
telegramAllowedUserIds,
|
|
165
197
|
feishuAllowedUserIds,
|
|
198
|
+
wechatAllowedUserIds,
|
|
166
199
|
aiCommand,
|
|
167
200
|
claudeCliPath,
|
|
168
201
|
claudeWorkDir,
|
package/dist/constants.d.ts
CHANGED
|
@@ -9,5 +9,8 @@ export declare const DEDUP_TTL_MS: number;
|
|
|
9
9
|
export declare const FEISHU_THROTTLE_MS = 200;
|
|
10
10
|
/** Telegram 编辑消息节流:200ms(open-im 默认值) */
|
|
11
11
|
export declare const TELEGRAM_THROTTLE_MS = 200;
|
|
12
|
+
/** WeChat 流式更新节流:1000ms(AGP 协议建议值) */
|
|
13
|
+
export declare const WECHAT_THROTTLE_MS = 1000;
|
|
12
14
|
export declare const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
|
|
13
15
|
export declare const MAX_FEISHU_MESSAGE_LENGTH = 4000;
|
|
16
|
+
export declare const MAX_WECHAT_MESSAGE_LENGTH = 2048;
|
package/dist/constants.js
CHANGED
|
@@ -38,5 +38,8 @@ export const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
|
38
38
|
export const FEISHU_THROTTLE_MS = 200;
|
|
39
39
|
/** Telegram 编辑消息节流:200ms(open-im 默认值) */
|
|
40
40
|
export const TELEGRAM_THROTTLE_MS = 200;
|
|
41
|
+
/** WeChat 流式更新节流:1000ms(AGP 协议建议值) */
|
|
42
|
+
export const WECHAT_THROTTLE_MS = 1000;
|
|
41
43
|
export const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
|
|
42
44
|
export const MAX_FEISHU_MESSAGE_LENGTH = 4000;
|
|
45
|
+
export const MAX_WECHAT_MESSAGE_LENGTH = 2048;
|
|
@@ -252,7 +252,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
252
252
|
const event = (wrapped?.event ?? data);
|
|
253
253
|
const actionValue = event?.action?.value;
|
|
254
254
|
const chatId = event?.context?.open_chat_id ?? event?.context?.chat_id ?? event?.context?.open_id ?? '';
|
|
255
|
-
const userId = event?.sender?.sender_id?.open_id ?? '';
|
|
255
|
+
const userId = event?.sender?.sender_id?.open_id ?? event?.operator?.open_id ?? '';
|
|
256
256
|
log.info(`[handleCardAction] chatId=${chatId}, userId=${userId}, actionValue=${JSON.stringify(actionValue)}`);
|
|
257
257
|
// 处理 mode 按钮(兼容 value 为对象或 JSON 字符串)
|
|
258
258
|
const modeAv = parseActionValue(actionValue);
|
|
@@ -381,7 +381,18 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
381
381
|
setChatUser(chatId, senderId);
|
|
382
382
|
// Handle different message types
|
|
383
383
|
if (msgType === 'text') {
|
|
384
|
-
|
|
384
|
+
// 飞书 text 消息的 content.text 可能是 HTML(如 <p>...</p>),并且包含空格 /
|
|
385
|
+
// 这里做一次轻量级清洗,保证空格和文本都被完整保留,而不是被简单截断。
|
|
386
|
+
const rawText = content.text ?? '';
|
|
387
|
+
let text = rawText;
|
|
388
|
+
// 去掉最常见的段落标签,保留内容
|
|
389
|
+
text = text.replace(/<\/?p[^>]*>/gi, '');
|
|
390
|
+
// 将 <br> 转成换行
|
|
391
|
+
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
392
|
+
// 将 等价替换为空格
|
|
393
|
+
text = text.replace(/ /gi, ' ');
|
|
394
|
+
// 最后做一次首尾 trim,但不动中间的空格
|
|
395
|
+
text = text.trim();
|
|
385
396
|
log.info(`[MSG] Type=text, User=${senderId}, Length=${text.length}, Content="${text}"`);
|
|
386
397
|
log.info(`[MSG] Full content keys:`, Object.keys(content).join(', '));
|
|
387
398
|
// Handle commands
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import { sendTextReply as sendTelegramTextReply } from "./telegram/message-sende
|
|
|
11
11
|
import { initFeishu, stopFeishu } from "./feishu/client.js";
|
|
12
12
|
import { setupFeishuHandlers } from "./feishu/event-handler.js";
|
|
13
13
|
import { sendTextReply as sendFeishuTextReply } from "./feishu/message-sender.js";
|
|
14
|
+
import { initWeChat, stopWeChat } from "./wechat/client.js";
|
|
15
|
+
import { setupWeChatHandlers } from "./wechat/event-handler.js";
|
|
16
|
+
import { sendTextReply as sendWeChatTextReply } from "./wechat/message-sender.js";
|
|
14
17
|
import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
|
|
15
18
|
import { SessionManager } from "./session/session-manager.js";
|
|
16
19
|
import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
|
|
@@ -25,6 +28,7 @@ const log = createLogger("Main");
|
|
|
25
28
|
async function sendLifecycleNotification(platform, message) {
|
|
26
29
|
const telegramChatId = getActiveChatId("telegram");
|
|
27
30
|
const feishuChatId = getActiveChatId("feishu");
|
|
31
|
+
const wechatChatId = getActiveChatId("wechat");
|
|
28
32
|
const sendPromises = [];
|
|
29
33
|
if (platform === "telegram" && telegramChatId) {
|
|
30
34
|
sendPromises.push(sendTelegramTextReply(telegramChatId, message).catch((err) => {
|
|
@@ -36,6 +40,11 @@ async function sendLifecycleNotification(platform, message) {
|
|
|
36
40
|
log.debug("Failed to send Feishu notification:", err);
|
|
37
41
|
}));
|
|
38
42
|
}
|
|
43
|
+
if (platform === "wechat" && wechatChatId) {
|
|
44
|
+
sendPromises.push(sendWeChatTextReply(wechatChatId, message).catch((err) => {
|
|
45
|
+
log.debug("Failed to send WeChat notification:", err);
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
39
48
|
await Promise.all(sendPromises);
|
|
40
49
|
}
|
|
41
50
|
export async function main() {
|
|
@@ -62,6 +71,7 @@ export async function main() {
|
|
|
62
71
|
const sessionManager = new SessionManager(config.claudeWorkDir, config.allowedBaseDirs);
|
|
63
72
|
let telegramHandle = null;
|
|
64
73
|
let feishuHandle = null;
|
|
74
|
+
let wechatHandle = null;
|
|
65
75
|
if (config.enabledPlatforms.includes("telegram")) {
|
|
66
76
|
await initTelegram(config, (bot) => {
|
|
67
77
|
telegramHandle = setupTelegramHandlers(bot, config, sessionManager);
|
|
@@ -71,6 +81,10 @@ export async function main() {
|
|
|
71
81
|
feishuHandle = setupFeishuHandlers(config, sessionManager);
|
|
72
82
|
await initFeishu(config, feishuHandle.handleEvent);
|
|
73
83
|
}
|
|
84
|
+
if (config.enabledPlatforms.includes("wechat")) {
|
|
85
|
+
wechatHandle = setupWeChatHandlers(config, sessionManager);
|
|
86
|
+
await initWeChat(config, wechatHandle.handleEvent);
|
|
87
|
+
}
|
|
74
88
|
log.info("Service is running. Press Ctrl+C to stop.");
|
|
75
89
|
const startupMsg = [
|
|
76
90
|
`🟢 open-im v${APP_VERSION} 服务已启动`,
|
|
@@ -109,6 +123,8 @@ export async function main() {
|
|
|
109
123
|
stopTelegram();
|
|
110
124
|
feishuHandle?.stop();
|
|
111
125
|
stopFeishu();
|
|
126
|
+
wechatHandle?.stop();
|
|
127
|
+
stopWeChat();
|
|
112
128
|
stopPermissionServer();
|
|
113
129
|
sessionManager.destroy();
|
|
114
130
|
cleanupAdapters();
|