@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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # open-im
2
2
 
3
- 多平台 IM 桥接,将 Telegram 和飞书 (Feishu/Lark) 连接到 AI CLI 工具(Claude Code、Codex、Cursor),实现移动端/远程访问 AI 编程助手。
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
- ## ✨ 为什么选择 open-im
24
+ ## 快速开始
25
25
 
26
26
  ```bash
27
27
  # 使用 npx 快速体验(无需全局安装)
28
- npx @wu529778790/open-im start # 后台运行
29
- npx @wu529778790/open-im stop # 停止后台进程
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 start
38
+ open-im init # 初始化配置
39
+ open-im start # 后台运行
38
40
  ```
39
41
 
40
- 首次运行会进入交互式配置向导,按提示输入 Token 后自动启动。配置保存到 `~/.open-im/config.json`。
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` 或 `open-im` | 前台运行(调试),Ctrl+C 停止 |
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 i @wu529778790/open-im -g
56
- open-im run
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
- "proxy": "http://127.0.0.1:7890"
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
- npx @wu529778790/open-im run
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
- // Try different spawn strategies based on platform
38
+ // 使用 shell: false 直接 spawn,避免 shell 对参数按空格拆分
39
+ // (用户 prompt 如 "npm 你好" 在 shell: true 下会被拆成 "npm" 和 "你好",CLI 只收到第一个)
39
40
  log.info(`Spawning CLI: path=${cliPath}, platform=${process.platform}`);
40
- let child;
41
- if (process.platform === "win32") {
42
- // Check if running in Git Bash (MINGW) or MSYS
43
- const isGitBash = process.env.MSYSTEM ||
44
- process.env.MINGW_PREFIX ||
45
- process.env.SHELL?.includes("bash");
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
- // Platform-specific spawn
89
- let child;
90
- if (process.platform === "win32") {
91
- const isGitBash = process.env.MSYSTEM ||
92
- process.env.MINGW_PREFIX ||
93
- process.env.SHELL?.includes("bash");
94
- if (isGitBash) {
95
- child = spawn(cliPath, args, {
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
- async function cmdStart() {
51
- const pid = getPid();
52
- if (pid && isRunning(pid)) {
53
- console.log(`open-im 已在后台运行 (pid=${pid})`);
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
- process.exit(1);
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 dev\n或:\n npx @wu529778790/open-im init\n");
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
- /* HTTP 失败则用 SIGTERM 兜底 */
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
- const cmd = process.argv[2];
134
- if (cmd === "start") {
135
- cmdStart().catch((err) => {
136
- console.error(err);
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
- else if (cmd === "stop") {
141
- cmdStop().catch((err) => {
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 === "dev" || cmd === "run" || cmd === undefined) {
147
- main().catch((err) => {
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.log(`用法: open-im [start|stop|dev]
154
- start - 后台运行
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;
@@ -72,7 +72,9 @@ export class CommandHandler {
72
72
  getClearHistoryHint(platform) {
73
73
  return platform === 'feishu'
74
74
  ? '💡 提示:如需清除本对话的历史消息,请点击飞书聊天右上角「...」→ 清除聊天记录'
75
- : '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
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
- return !hasTelegram && !hasFeishu;
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 或 Feishu 其中一个平台(可以通过环境变量或 config.json)');
77
+ throw new Error('至少需要配置 Telegram、FeishuWeChat 其中一个平台(可以通过环境变量或 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,
@@ -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
- const text = content.text?.trim() ?? '';
384
+ // 飞书 text 消息的 content.text 可能是 HTML(如 <p>...</p>),并且包含空格 / &nbsp;
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
+ // 将 &nbsp; 等价替换为空格
393
+ text = text.replace(/&nbsp;/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();