@wu529778790/open-im 0.3.12 → 1.0.0

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,121 +1,115 @@
1
1
  # open-im
2
2
 
3
- > 🚀 把你的 AI 助手装进口袋里 - Telegram 随时随地使用 Claude Code
3
+ 多平台 IM 桥接,将 Telegram 和飞书 (Feishu/Lark) 连接到 AI CLI 工具(Claude Code、Codex、Cursor),实现移动端/远程访问 AI 编程助手。
4
4
 
5
- 还在受限于终端吗?用手机也能 Coding 了!
5
+ ## 功能特性
6
6
 
7
- open-im 是一个轻量级的 IM 桥接工具,让你通过 Telegram 就能使用 Claude Code、Codex、Cursor 等 AI CLI 工具。无论是在咖啡厅、地铁上,还是躺在床上,你的 AI 助手随时在线。
7
+ - **多平台**:支持 Telegram 和飞书,可同时启用
8
+ - **多 AI 工具**:通过配置切换 Claude Code / Codex / Cursor
9
+ - **流式输出**:节流更新,实时展示 AI 回复
10
+ - **会话管理**:每用户独立 session,`/new` 重置会话
11
+ - **命令支持**:`/help` `/new` `/cd` `/pwd` `/status` `/allow` `/deny`
8
12
 
9
- ## ✨ 为什么选择 open-im
10
-
11
- - **📱 移动友好** - 告别终端,用手机照样写代码
12
- - **⚡ 实时流式输出** - AI 思考过程实时可见,像在终端一样流畅
13
- - **🔒 安全可控** - 支持白名单,只有你能用
14
- - **🔄 独立会话** - 每个人独立 session,互不干扰
15
- - **🛠️ 多 AI 支持** - Claude / Codex / Cursor 随意切换
13
+ ## 环境要求
16
14
 
17
- ## 🚀 快速开始
15
+ - **Node.js** >= 20
16
+ - **AI CLI**:已安装 Claude Code CLI(或 Codex/Cursor)并加入 PATH
18
17
 
19
- ### 方式一:npx(无需安装)
18
+ ## 安装
20
19
 
21
20
  ```bash
22
- npx @wu529778790/open-im run
21
+ npm install @wu529778790/open-im -g
23
22
  ```
24
23
 
25
- ### 方式二:全局安装(推荐常用用户)
24
+ ## ✨ 为什么选择 open-im
26
25
 
27
26
  ```bash
28
- npm i @wu529778790/open-im -g
29
- open-im run
27
+ # 使用 npx 快速体验(无需全局安装)
28
+ npx @wu529778790/open-im start # 后台运行
29
+ npx @wu529778790/open-im stop # 停止后台进程
30
+ npx @wu529778790/open-im dev # 前台运行(调试),Ctrl+C 停止
30
31
  ```
31
32
 
32
- 首次运行会引导你完成配置,30 秒即可搞定。
33
-
34
- 如果配置引导未出现,可以手动运行:
33
+ 或全局安装后直接使用:
35
34
 
36
35
  ```bash
37
- npx @wu529778790/open-im init
36
+ npm install @wu529778790/open-im -g
37
+ open-im start
38
38
  ```
39
39
 
40
- ## ⚙️ 配置说明
40
+ 首次运行会进入交互式配置向导,按提示输入 Token 后自动启动。配置保存到 `~/.open-im/config.json`。
41
41
 
42
- 配置文件位置:`~/.open-im/config.json`
43
-
44
- 配置文件示例:
45
-
46
- ```json
47
- {
48
- "telegramBotToken": "你的Bot Token(从 @BotFather 获取)",
49
- "allowedUserIds": ["你的Telegram用户ID"],
50
- "claudeWorkDir": "/path/to/your/work/dir",
51
- "claudeSkipPermissions": true,
52
- "aiCommand": "claude"
53
- }
54
- ```
42
+ ## 运行方式
55
43
 
56
- 获取 Telegram Bot Token:
57
- 1. 在 Telegram 中搜索 @BotFather
58
- 2. 发送 `/newbot` 创建新机器人
59
- 3. 按提示设置机器人名称
60
- 4. BotFather 会返回 Token,格式如:`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
44
+ | 命令 | 说明 |
45
+ |------|------|
46
+ | `open-im start` | 后台运行,适合长期使用 |
47
+ | `open-im stop` | 停止后台进程 |
48
+ | `open-im dev` `open-im` | 前台运行(调试),Ctrl+C 停止 |
61
49
 
62
- 获取 Telegram 用户 ID(可选):
63
- 1. 在 Telegram 中搜索 @userinfobot
64
- 2. 发送任意消息
65
- 3. 机器人会返回你的用户 ID
66
- 4. 如不设置,则所有人都可以使用你的机器人
50
+ ## 会话说明
67
51
 
68
- **或者通过环境变量配置:**
52
+ **会话上下文存储在本地**(`~/.open-im/data/sessions.json`),与 IM 聊天记录无关。每用户在本地维护独立的 session 和 Claude 会话 ID,`/new` 可重置当前会话。
69
53
 
70
54
  ```bash
71
- export TELEGRAM_BOT_TOKEN="你的Bot Token"
72
- export ALLOWED_USER_IDS="用户ID1,用户ID2"
55
+ npm i @wu529778790/open-im -g
73
56
  open-im run
74
57
  ```
75
58
 
76
- ## 📖 常用命令
59
+ ### 环境变量
77
60
 
78
- | 命令 | 说明 |
61
+ | 变量 | 说明 |
79
62
  |------|------|
80
- | `open-im` / `open-im run` | 前台运行(首次使用会引导配置) |
81
- | `open-im init` | 初始化配置(首次使用或重新配置) |
82
- | `open-im start` | 后台启动服务 |
83
- | `open-im stop` | 停止服务 |
84
- | `open-im restart` | 重启服务 |
85
-
86
- ### Telegram 机器人命令
63
+ | `TELEGRAM_BOT_TOKEN` | Telegram Bot Token(从 @BotFather 获取) |
64
+ | `FEISHU_APP_ID` | 飞书应用 App ID |
65
+ | `FEISHU_APP_SECRET` | 飞书应用 App Secret |
66
+ | `ALLOWED_USER_IDS` | 白名单用户 ID(逗号分隔,空=所有人) |
67
+ | `AI_COMMAND` | `claude` \| `codex` \| `cursor`,默认 `claude` |
68
+ | `CLAUDE_CLI_PATH` | Claude CLI 路径,默认 `claude` |
69
+ | `CLAUDE_WORK_DIR` | 工作目录 |
70
+ | `CLAUDE_SKIP_PERMISSIONS` | 跳过权限确认,默认 `true` |
71
+ | `CLAUDE_TIMEOUT_MS` | Claude 超时(毫秒),默认 600000 |
72
+ | `CLAUDE_MODEL` | Claude 模型(可选) |
73
+ | `ALLOWED_BASE_DIRS` | 允许访问的目录(逗号分隔) |
74
+ | `LOG_DIR` | 日志目录,默认 `~/.open-im/logs` |
75
+ | `LOG_LEVEL` | 日志级别:INFO/DEBUG/WARN/ERROR |
87
76
 
88
- | 命令 | 功能 |
89
- |------|------|
90
- | `/help` | 查看帮助 |
91
- | `/new` | 开启新会话 |
92
- | `/cd <路径>` | 切换工作目录 |
93
- | `/pwd` | 查看当前目录 |
94
- | `/status` | 查看运行状态 |
77
+ ### 配置文件
95
78
 
96
- ## 💡 使用场景
79
+ 配置优先级:环境变量 > `~/.open-im/config.json` > 默认值。
97
80
 
98
- - 🚇 **通勤路上** - 用手机处理简单的代码问题
99
- - ☕ **咖啡厅** - 没带电脑也能快速调试
100
- - 🛋️ **沙发模式** - 躺着看 AI 帮你写代码
101
- - 🌙 **紧急修复** - 半夜收到报警,手机直接处理
81
+ 至少需配置 **Telegram** **飞书** 其一:
102
82
 
103
- ## 📦 安装方式
83
+ - **Telegram**:`TELEGRAM_BOT_TOKEN` 或 `telegramBotToken`
84
+ - **飞书**:`FEISHU_APP_ID` + `FEISHU_APP_SECRET` 或 `feishuAppId` + `feishuAppSecret`
104
85
 
105
- ```bash
106
- # npx(无需安装)
107
- npx @wu529778790/open-im run
86
+ ### 飞书配置说明
108
87
 
109
- # npm 全局安装
110
- npm i @wu529778790/open-im -g
88
+ 1. [飞书开放平台](https://open.feishu.cn/) 创建企业自建应用
89
+ 2. 开启「机器人」能力
90
+ 3. 配置事件订阅:启用 `im.message.receive_v1`,使用 **长连接** 模式(WebSocket)
91
+ 4. 将机器人添加到目标群聊或发起私聊
111
92
 
112
- # yarn 全局安装
113
- yarn global add @wu529778790/open-im
93
+ ## 开发
114
94
 
115
- # pnpm 全局安装
116
- pnpm i @wu529778790/open-im -g
95
+ ```bash
96
+ npm run build # 构建
97
+ npm run dev # 直接运行源码(tsx,无需 build)
98
+ npm run foreground # 前台运行已构建版本
117
99
  ```
118
100
 
101
+ ## IM 内命令
102
+
103
+ | 命令 | 说明 |
104
+ |------|------|
105
+ | `/help` | 显示帮助 |
106
+ | `/new` | 开始新会话 |
107
+ | `/status` | 显示状态(AI 工具、工作目录、费用等) |
108
+ | `/cd <路径>` | 切换工作目录 |
109
+ | `/pwd` | 显示当前工作目录 |
110
+ | `/allow` `/y` | 允许权限请求 |
111
+ | `/deny` `/n` | 拒绝权限请求 |
112
+
119
113
  ## 📝 License
120
114
 
121
115
  [MIT](LICENSE)
@@ -127,22 +121,30 @@ pnpm i @wu529778790/open-im -g
127
121
  如果配置引导没有出现,尝试以下方法:
128
122
 
129
123
  1. **手动运行配置命令:**
124
+
130
125
  ```bash
131
126
  npx @wu529778790/open-im init
132
127
  ```
133
128
 
134
129
  2. **检查是否已有配置文件:**
130
+
135
131
  ```bash
136
132
  cat ~/.open-im/config.json
137
133
  ```
138
134
 
139
135
  3. **手动创建配置文件:**
136
+
140
137
  ```bash
141
138
  mkdir -p ~/.open-im
142
139
  cat > ~/.open-im/config.json << 'EOF'
143
140
  {
144
141
  "telegramBotToken": "你的Bot Token",
145
142
  "allowedUserIds": ["你的Telegram用户ID"],
143
+ "platforms": {
144
+ "telegram": {
145
+ "proxy": "http://127.0.0.1:7890"
146
+ }
147
+ },
146
148
  "claudeWorkDir": "$(pwd)",
147
149
  "claudeSkipPermissions": true,
148
150
  "aiCommand": "claude"
@@ -175,3 +177,40 @@ npx @wu529778790/open-im run
175
177
  1. 在 Telegram 中搜索 @userinfobot
176
178
  2. 点击"START"或发送任意消息
177
179
  3. 机器人会返回你的用户 ID(数字)
180
+
181
+ ### Q: 如何配置代理?
182
+
183
+ 如果你的网络环境无法直接访问 Telegram,需要在配置文件中添加代理设置:
184
+
185
+ ```json
186
+ {
187
+ "platforms": {
188
+ "telegram": {
189
+ "proxy": "http://127.0.0.1:7890"
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ 支持的代理格式:
196
+
197
+ - HTTP:`http://127.0.0.1:7890`
198
+ - HTTPS:`https://127.0.0.1:7890`
199
+ - SOCKS5:`socks5://127.0.0.1:1080`
200
+
201
+ 注意:代理仅用于访问 Telegram API,不影响 AI 工具的网络请求。
202
+
203
+ ### Q: Telegram 机器人无响应?
204
+
205
+ 可能原因及解决方法:
206
+
207
+ 1. **网络问题 - Telegram 被阻断**
208
+ - 配置代理(见上方"如何配置代理")
209
+ - 测试代理是否可用:`curl -x http://127.0.0.1:7890 https://api.telegram.org`
210
+
211
+ 2. **Token 错误**
212
+ - 重新获取 Token:在 @BotFather 中使用 `/revoke` 命令
213
+
214
+ 3. **用户 ID 白名单问题**
215
+ - 检查配置文件中的 `allowedUserIds` 是否包含你的用户 ID
216
+ - 或留空允许所有人访问(仅开发环境建议)
@@ -1,11 +1,18 @@
1
+ import { createLogger } from '../logger.js';
2
+ const log = createLogger('AccessControl');
1
3
  export class AccessControl {
2
4
  allowedUserIds;
3
5
  constructor(allowedUserIds) {
4
6
  this.allowedUserIds = new Set(allowedUserIds);
7
+ log.info(`AccessControl initialized with ${allowedUserIds.length} allowed users:`, allowedUserIds);
5
8
  }
6
9
  isAllowed(userId) {
7
- if (this.allowedUserIds.size === 0)
10
+ if (this.allowedUserIds.size === 0) {
11
+ log.debug(`Allowing user ${userId} (no whitelist configured)`);
8
12
  return true;
9
- return this.allowedUserIds.has(userId);
13
+ }
14
+ const allowed = this.allowedUserIds.has(userId);
15
+ log.info(`Checking user ${userId}: ${allowed ? 'ALLOWED' : 'DENIED'}`);
16
+ return allowed;
10
17
  }
11
18
  }
@@ -1,20 +1,29 @@
1
- import { spawn } from 'node:child_process';
2
- import { createInterface } from 'node:readline';
3
- import { parseStreamLine, extractTextDelta, extractThinkingDelta, extractResult } from './stream-parser.js';
4
- import { isStreamInit, isContentBlockStart, isContentBlockDelta, isContentBlockStop } from './types.js';
5
- import { createLogger } from '../logger.js';
6
- const log = createLogger('CliRunner');
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import { parseStreamLine, extractTextDelta, extractThinkingDelta, extractResult, } from "./stream-parser.js";
4
+ import { isStreamInit, isContentBlockStart, isContentBlockDelta, isContentBlockStop, } from "./types.js";
5
+ import { createLogger } from "../logger.js";
6
+ const log = createLogger("CliRunner");
7
7
  export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, options) {
8
- const args = ['-p', '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];
8
+ const args = [
9
+ "-p",
10
+ "--output-format",
11
+ "stream-json",
12
+ "--verbose",
13
+ "--include-partial-messages",
14
+ ];
9
15
  if (options?.skipPermissions)
10
- args.push('--dangerously-skip-permissions');
16
+ args.push("--dangerously-skip-permissions");
11
17
  if (options?.model)
12
- args.push('--model', options.model);
18
+ args.push("--model", options.model);
13
19
  if (sessionId)
14
- args.push('--resume', sessionId);
15
- args.push('--', prompt);
20
+ args.push("--resume", sessionId);
21
+ args.push("--", prompt);
16
22
  const env = {};
17
23
  for (const [k, v] of Object.entries(process.env)) {
24
+ // Skip CLAUDECODE to prevent nested session detection
25
+ if (k === "CLAUDECODE")
26
+ continue;
18
27
  if (v !== undefined)
19
28
  env[k] = v;
20
29
  }
@@ -22,29 +31,67 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
22
31
  env.CC_IM_CHAT_ID = options.chatId;
23
32
  if (options?.hookPort)
24
33
  env.CC_IM_HOOK_PORT = String(options.hookPort);
25
- const child = spawn(cliPath, args, { cwd: workDir, stdio: ['ignore', 'pipe', 'pipe'], env, windowsHide: true });
26
- log.debug(`Claude CLI spawned: pid=${child.pid}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
27
- let accumulated = '';
28
- let accumulatedThinking = '';
34
+ // Try different spawn strategies based on platform
35
+ log.info(`Spawning CLI: path=${cliPath}, platform=${process.platform}`);
36
+ let child;
37
+ if (process.platform === "win32") {
38
+ // Check if running in Git Bash (MINGW) or MSYS
39
+ const isGitBash = process.env.MSYSTEM ||
40
+ process.env.MINGW_PREFIX ||
41
+ process.env.SHELL?.includes("bash");
42
+ log.info(`Detected environment: Git Bash=${isGitBash ? "yes" : "no"}`);
43
+ if (isGitBash) {
44
+ // In Git Bash, use shell for proper path resolution
45
+ log.info(`Using shell spawn for Git Bash environment`);
46
+ child = spawn(cliPath, args, {
47
+ cwd: workDir,
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ env,
50
+ shell: true,
51
+ });
52
+ }
53
+ else {
54
+ // In pure cmd/PowerShell, direct spawn works best
55
+ log.info(`Using direct spawn for Windows cmd/PowerShell`);
56
+ child = spawn(cliPath, args, {
57
+ cwd: workDir,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ env,
60
+ windowsHide: true,
61
+ });
62
+ }
63
+ }
64
+ else {
65
+ child = spawn(cliPath, args, {
66
+ cwd: workDir,
67
+ stdio: ["ignore", "pipe", "pipe"],
68
+ env,
69
+ });
70
+ }
71
+ log.info(`Claude CLI: pid=${child.pid}, cwd=${workDir}, session=${sessionId ?? "new"}`);
72
+ let accumulated = "";
73
+ let accumulatedThinking = "";
29
74
  let completed = false;
30
- let model = '';
75
+ let model = "";
31
76
  const toolStats = {};
32
77
  const pendingToolInputs = new Map();
33
78
  const MAX_TIMEOUT = 2_147_483_647;
34
- const timeoutMs = options?.timeoutMs && options.timeoutMs > 0 ? Math.min(options.timeoutMs, MAX_TIMEOUT) : 0;
79
+ const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
80
+ ? Math.min(options.timeoutMs, MAX_TIMEOUT)
81
+ : 0;
35
82
  let timeoutHandle = null;
36
83
  if (timeoutMs > 0) {
37
84
  timeoutHandle = setTimeout(() => {
38
85
  if (!completed && !child.killed) {
39
86
  completed = true;
40
87
  log.warn(`Claude CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
41
- child.kill('SIGTERM');
88
+ child.kill("SIGTERM");
42
89
  callbacks.onError(`执行超时(${timeoutMs}ms),已终止进程`);
43
90
  }
44
91
  }, timeoutMs);
45
92
  }
46
93
  const rl = createInterface({ input: child.stdout });
47
- rl.on('line', (line) => {
94
+ rl.on("line", (line) => {
48
95
  const event = parseStreamLine(line);
49
96
  if (!event)
50
97
  return;
@@ -64,16 +111,18 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
64
111
  callbacks.onThinking?.(accumulatedThinking);
65
112
  return;
66
113
  }
67
- if (isContentBlockStart(event) && event.event.content_block?.type === 'tool_use') {
114
+ if (isContentBlockStart(event) &&
115
+ event.event.content_block?.type === "tool_use") {
68
116
  const name = event.event.content_block.name;
69
117
  if (name)
70
- pendingToolInputs.set(event.event.index, { name, json: '' });
118
+ pendingToolInputs.set(event.event.index, { name, json: "" });
71
119
  return;
72
120
  }
73
- if (isContentBlockDelta(event) && event.event.delta?.type === 'input_json_delta') {
121
+ if (isContentBlockDelta(event) &&
122
+ event.event.delta?.type === "input_json_delta") {
74
123
  const pending = pendingToolInputs.get(event.event.index);
75
124
  if (pending)
76
- pending.json += event.event.delta.partial_json ?? '';
125
+ pending.json += event.event.delta.partial_json ?? "";
77
126
  return;
78
127
  }
79
128
  if (isContentBlockStop(event)) {
@@ -134,17 +183,19 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
134
183
  }
135
184
  }
136
185
  };
137
- child.on('close', (code) => {
186
+ child.on("close", (code) => {
187
+ log.info(`Claude CLI closed: exitCode=${code}, pid=${child.pid}`);
138
188
  exitCode = code;
139
189
  childClosed = true;
140
190
  finalize();
141
191
  });
142
- rl.on('close', () => {
192
+ rl.on("close", () => {
143
193
  rlClosed = true;
144
194
  finalize();
145
195
  });
146
- child.on('error', (err) => {
147
- log.error(`Claude CLI error: ${err.message}`);
196
+ child.on("error", (err) => {
197
+ const errorCode = err.code;
198
+ log.error(`Claude CLI spawn error: ${err.message}, code=${errorCode}, path=${cliPath}`);
148
199
  if (timeoutHandle)
149
200
  clearTimeout(timeoutHandle);
150
201
  if (!completed) {
@@ -160,7 +211,7 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
160
211
  clearTimeout(timeoutHandle);
161
212
  rl.close();
162
213
  if (!child.killed)
163
- child.kill('SIGTERM');
214
+ child.kill("SIGTERM");
164
215
  },
165
216
  };
166
217
  }