@wu529778790/open-im 1.11.2-beta.9 → 1.11.3-beta.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,108 +1,127 @@
1
1
  # open-im
2
2
 
3
- **English** · [中文](./README.zh-CN.md)
3
+ > 你的 AI 编程助手,在每个聊天 App 里。
4
4
 
5
- > Your AI coding assistant, in every chat app.
5
+ open-im Claude Code、Codex、CodeBuddy 接入 Telegram、飞书、企业微信、钉钉、QQ 机器人、微信助理、微信客服号。手机发条消息,电脑上就写好代码。
6
6
 
7
- open-im bridges Claude Code, Codex, and CodeBuddy to Telegram, Feishu, WeCom, DingTalk, QQ, WeChat (WorkBuddy), and WeChat (ClawBot). Send a message from your phone, get code written on your server.
7
+ ## 为什么用 open-im
8
8
 
9
- ## Architecture
9
+ - **随时随地** — 通勤、排队、躺沙发上,手机发消息就能让 AI 干活
10
+ - **无缝接力** — 和 Claude Code CLI 共享 session,手机聊一半,电脑接着来
11
+ - **完整能力** — 流式输出、会话管理、模型切换,全靠聊天命令
12
+ - **一个桥接,多个平台** — 同一个 bot 支持 7 个 IM 平台
10
13
 
11
- ![Open-IM Architecture](./diagram/architecture/open-im-architecture.svg)
14
+ ## 快速开始
12
15
 
13
- ## Why
14
-
15
- - **Work from anywhere** — trigger Claude Code from your phone while commuting, waiting in line, or on the couch
16
- - **Seamless handoff** — open-im shares sessions with the Claude Code CLI; pick up on your computer exactly where you left off on your phone
17
- - **Full power, simple interface** — stream responses, manage sessions, switch models — all through chat commands
18
- - **One bridge, many platforms** — same bot works on Telegram, Feishu, DingTalk, WeChat, and more
19
-
20
- ## Features
21
-
22
- ### Chat commands
16
+ ```bash
17
+ # 安装
18
+ npm install -g @wu529778790/open-im
23
19
 
24
- | Command | Description |
25
- | --- | --- |
26
- | `/help` | Show all commands |
27
- | `/new` | Start a fresh AI session |
28
- | `/sessions` | Browse session history with previews |
29
- | `/resume [N]` | Resume a session (no arg = most recent) |
30
- | `/history [N]` | View conversation messages in a session |
31
- | `/delete <N>` | Delete a session |
32
- | `/rename <title>` | Rename the current session |
33
- | `/fork [N]` | Fork a session (create a branch) |
34
- | `/models` | List available AI models |
35
- | `/context` | Show context window usage |
36
- | `/status` | Show AI tool, account, and session info |
37
- | `/cd <path>` / `/pwd` | Switch work directory (auto-resumes that dir's session) |
38
- | `/allow` / `/y`, `/deny` / `/n` | Respond to permission prompts |
20
+ # 配置(交互式向导)
21
+ open-im init
39
22
 
40
- ### Session continuity
23
+ # 启动
24
+ open-im start
25
+ ```
41
26
 
42
- open-im and Claude Code CLI share the same session storage. In the same directory, you can seamlessly switch between phone and computer.
27
+ 或直接用 npx:
43
28
 
29
+ ```bash
30
+ npx @wu529778790/open-im init
31
+ npx @wu529778790/open-im start
44
32
  ```
45
- # On computer
46
- cd /my-project && claude # work as usual, then Ctrl+C
47
33
 
48
- # On phone (via IM)
49
- "help me fix the login bug" # open-im auto-resumes the same session
34
+ ### 最小配置
50
35
 
51
- # Back on computer
52
- claude -c # continues the phone conversation
36
+ ```json
37
+ {
38
+ "tools": {
39
+ "claude": { "workDir": "/path/to/project" }
40
+ },
41
+ "platforms": {
42
+ "telegram": { "enabled": true, "botToken": "YOUR_TOKEN" }
43
+ }
44
+ }
53
45
  ```
54
46
 
55
- > Only one side can be active at a time. Exit the CLI before sending messages from the phone, and vice versa.
56
-
57
- ### Platform support
47
+ ## 平台支持
48
+
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) |
58
+
59
+ 每个平台可单独配置 AI 后端(`claude` / `codex` / `codebuddy`),默认 `claude`。
60
+
61
+ ## 聊天命令
62
+
63
+ | 命令 | 说明 |
64
+ |------|------|
65
+ | `/help` | 显示所有命令 |
66
+ | `/new` | 开启新 AI 会话 |
67
+ | `/sessions` | 浏览历史会话 |
68
+ | `/resume [序号]` | 恢复会话 |
69
+ | `/history [序号]` | 查看对话记录 |
70
+ | `/delete <序号>` | 删除会话 |
71
+ | `/rename <标题>` | 重命名会话 |
72
+ | `/fork [序号]` | 分支会话 |
73
+ | `/models` | 查看可用模型 |
74
+ | `/context` | 查看上下文用量 |
75
+ | `/status` | 显示状态信息 |
76
+ | `/cd <路径>` / `/pwd` | 切换/查看工作目录 |
77
+ | `/plugins` | 查看已安装插件 |
78
+ | `/allow` `/y` / `/deny` `/n` | 权限确认 |
79
+
80
+ ## 会话接力
81
+
82
+ open-im 和 Claude Code CLI 共享 session 存储。同一目录下,手机和电脑无缝切换:
58
83
 
59
- Seven IM platforms, three AI backends, per-platform override:
84
+ ```bash
85
+ # 电脑端
86
+ cd /my-project && claude
60
87
 
61
- | Platform | Streaming | Media | Notes |
62
- | --- | --- | --- | --- |
63
- | Telegram | Yes | Images | Full bot support |
64
- | Feishu | Yes | Images | Streaming card |
65
- | WeCom | Yes | Images | Streaming card |
66
- | DingTalk | Partial | Images | Fallback to text |
67
- | QQ | Yes | Images | |
68
- | WorkBuddy | Yes | Images | WeChat-based |
69
- | ClawBot | Yes | Images | WeChat-based |
88
+ # 手机端
89
+ "帮我修复登录 bug" # 自动接续同一个 session
70
90
 
71
- Set `platforms.<name>.aiCommand` (`claude` / `codex` / `codebuddy`) per channel. Default: `claude`.
91
+ # 回到电脑端
92
+ claude -c # 接上手机端的对话
93
+ ```
72
94
 
73
- ### Web dashboard
95
+ > 不能同时使用两端,但无需退出 CLI。只需等待当前操作完成即可切换。
74
96
 
75
- `open-im start` serves a built-in SPA and API at **`http://127.0.0.1:39282`** (configurable via `OPEN_IM_WEB_PORT`). For LAN access: `export OPEN_IM_WEB_HOST=0.0.0.0`.
97
+ ## Web 控制台
76
98
 
77
- ## Quick start
99
+ `open-im start` 在 `http://127.0.0.1:39282` 提供管理界面:
78
100
 
79
- ```bash
80
- npx @wu529778790/open-im init # interactive setup
81
- npx @wu529778790/open-im start # run the bridge
82
- ```
101
+ - 配置所有平台凭证
102
+ - 启动/停止桥接服务
103
+ - 编辑配置文件
104
+ - 首次运行自动弹出设置向导
83
105
 
84
- Or install globally: `npm install -g @wu529778790/open-im` then `open-im start`.
106
+ 局域网访问:`export OPEN_IM_WEB_HOST=0.0.0.0`
85
107
 
86
- Config file: **`~/.open-im/config.json`**
108
+ ## CLI 命令
87
109
 
88
- ### Minimal config
110
+ | 命令 | 说明 |
111
+ |------|------|
112
+ | `open-im init` | 交互式配置 |
113
+ | `open-im start` | 后台运行 |
114
+ | `open-im stop` | 停止服务 |
115
+ | `open-im restart` | 重启 |
116
+ | `open-im dashboard` | 仅启动 Web 配置服务 |
89
117
 
90
- ```json
91
- {
92
- "tools": {
93
- "claude": { "workDir": "/path/to/project", "skipPermissions": true }
94
- },
95
- "platforms": {
96
- "telegram": { "enabled": true, "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
97
- }
98
- }
99
- ```
118
+ ## 配置
100
119
 
101
- Run `open-im init` for a full template with all platforms.
120
+ 配置文件:`~/.open-im/config.json`
102
121
 
103
- ### Claude (Agent SDK)
122
+ ### ClaudeAgent SDK
104
123
 
105
- No local `claude` binary required. Supports third-party / compatible APIs:
124
+ 无需本地 `claude` 可执行文件。支持第三方兼容接口:
106
125
 
107
126
  ```json
108
127
  {
@@ -111,43 +130,32 @@ No local `claude` binary required. Supports third-party / compatible APIs:
111
130
  "env": {
112
131
  "ANTHROPIC_AUTH_TOKEN": "your-token",
113
132
  "ANTHROPIC_BASE_URL": "https://your-api-endpoint",
114
- "ANTHROPIC_MODEL": "glm-4.7"
133
+ "ANTHROPIC_MODEL": "model-name"
115
134
  }
116
135
  }
117
136
  }
118
137
  }
119
138
  ```
120
139
 
121
- ### CLI reference
122
-
123
- | Command | Description |
124
- | --- | --- |
125
- | `open-im init` | Interactive setup (does not start the bridge) |
126
- | `open-im start` | Run the bridge in the background |
127
- | `open-im stop` | Stop the background bridge |
128
- | `open-im restart` | Stop then start |
129
- | `open-im dashboard` | Web config server only (no bridge) |
130
-
131
- ### Environment variables
132
-
133
- **`ANTHROPIC_*`** (shell or `tools.claude.env`), **`TELEGRAM_BOT_TOKEN`**, **`OPEN_IM_WEB_PORT`**, **`OPEN_IM_WEB_HOST`**, plus platform-specific `*_APP_ID`, `*_SECRET`, `WORKBUDDY_*`, etc.
134
-
135
- ### Git co-authors
140
+ ### 环境变量
136
141
 
137
- `Co-authored-by` is appended by default on AI-driven commits. Disable: set **`OPEN_IM_GIT_COAUTHOR=0`** in the environment and restart the bridge.
142
+ - **`ANTHROPIC_*`** Claude API 配置
143
+ - **`TELEGRAM_BOT_TOKEN`** — Telegram Bot Token
144
+ - **`OPEN_IM_WEB_PORT`** — Web 控制台端口(默认 39282)
145
+ - **`OPEN_IM_WEB_HOST`** — Web 控制台监听地址
138
146
 
139
- ### Privacy
147
+ ### 隐私
140
148
 
141
- **Anonymous** usage information may be collected to improve reliability (no chat or prompt content). To disable: **`OPEN_IM_TELEMETRY=false`** or `"telemetry": { "enabled": false }` in `config.json`.
149
+ 匿名运行信息用于改进稳定性(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
142
150
 
143
- ## Platform setup & troubleshooting
151
+ ## 平台配置详情
144
152
 
145
- See **[docs/platforms.md](./docs/platforms.md)** for detailed per-platform configuration, credential setup, and troubleshooting.
153
+ 详见 [docs/platforms.md](./docs/platforms.md)
146
154
 
147
- ## Requirements
155
+ ## 环境要求
148
156
 
149
157
  - Node.js >= 20
150
- - At least one IM platform configured + credentials for your AI tool
158
+ - 至少配置一个 IM 平台 + AI 凭证
151
159
 
152
160
  ## License
153
161
 
@@ -23,8 +23,10 @@ export function setupClawbotHandlers(config, sessionManager) {
23
23
  });
24
24
  const stopTaskCleanup = startTaskCleanup(ctx.runningTasks);
25
25
  const platformSender = {
26
- sendThinkingMessage: async (_chatId, _replyToMessageId, _toolId) => {
27
- return 'clawbot_no_thinking';
26
+ sendThinkingMessage: async (chatId, _replyToMessageId, _toolId) => {
27
+ // ClawBot 不支持 typing indicator,先发一条"思考中"消息给用户反馈
28
+ await sendTextReply(chatId, '🤔 正在处理...');
29
+ return 'clawbot_thinking';
28
30
  },
29
31
  sendTextReply: async (chatId, text) => {
30
32
  await sendTextReply(chatId, text);
package/dist/cli.js CHANGED
@@ -189,7 +189,15 @@ const commands = {
189
189
  dev: cmdDev,
190
190
  dashboard: cmdDashboard,
191
191
  };
192
- if (cmd === "--help" || cmd === "-h") {
192
+ if (cmd === "--version" || cmd === "-v") {
193
+ const { readFileSync } = await import("node:fs");
194
+ const { join } = await import("node:path");
195
+ const { fileURLToPath } = await import("node:url");
196
+ const __dirname = join(fileURLToPath(import.meta.url), "..");
197
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
198
+ console.log(pkg.version);
199
+ }
200
+ else if (cmd === "--help" || cmd === "-h") {
193
201
  showHelp(0);
194
202
  }
195
203
  else if (cmd === undefined) {
@@ -20,7 +20,7 @@ export declare class CommandHandler {
20
20
  private deps;
21
21
  constructor(deps: CommandHandlerDeps);
22
22
  private replySender;
23
- dispatch(text: string, chatId: string, userId: string, platform: Platform, _handleClaudeRequest: ClaudeRequestHandler,
23
+ dispatch(text: string, chatId: string, userId: string, platform: Platform, handleClaudeRequest: ClaudeRequestHandler,
24
24
  /** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
25
25
  senderOverride?: MessageSender): Promise<boolean>;
26
26
  private handleHelp;
@@ -38,6 +38,12 @@ export declare class CommandHandler {
38
38
  private handleModels;
39
39
  private handleContext;
40
40
  private getAiVersion;
41
+ /**
42
+ * 快捷命令 — 将预设 prompt 发送给 AI
43
+ */
44
+ private handleQuickCommand;
45
+ /** 临时存储 dispatch 传入的 handler */
46
+ private quickCommandHandler;
41
47
  }
42
48
  /**
43
49
  * 列出目录并返回目录信息
@@ -65,9 +65,11 @@ export class CommandHandler {
65
65
  replySender() {
66
66
  return commandReplySender.getStore() ?? this.deps.sender;
67
67
  }
68
- async dispatch(text, chatId, userId, platform, _handleClaudeRequest,
68
+ async dispatch(text, chatId, userId, platform, handleClaudeRequest,
69
69
  /** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
70
70
  senderOverride) {
71
+ // 存储 handler 供快捷命令使用
72
+ this.quickCommandHandler = handleClaudeRequest;
71
73
  const runBody = async () => {
72
74
  const t = normalizeSlashCommandForDispatch(text);
73
75
  if (platform === 'telegram' && t === '/start') {
@@ -100,6 +102,21 @@ export class CommandHandler {
100
102
  return this.handlePwd(chatId, userId);
101
103
  if (t === '/status')
102
104
  return this.handleStatus(chatId, userId, platform);
105
+ // 快捷命令 — 直接发送预设 prompt 给 AI
106
+ if (t === '/git commit')
107
+ return this.handleQuickCommand(chatId, userId, 'git commit -m "AI generated commit"', platform);
108
+ if (t === '/git push')
109
+ return this.handleQuickCommand(chatId, userId, 'git push origin main', platform);
110
+ if (t === '/git pull')
111
+ return this.handleQuickCommand(chatId, userId, 'git pull origin main', platform);
112
+ if (t === '/test')
113
+ return this.handleQuickCommand(chatId, userId, 'npm test', platform);
114
+ if (t === '/build')
115
+ return this.handleQuickCommand(chatId, userId, 'npm run build', platform);
116
+ if (t === '/review')
117
+ return this.handleQuickCommand(chatId, userId, '请审查当前代码,找出潜在问题和改进建议', platform);
118
+ if (t === '/explain')
119
+ return this.handleQuickCommand(chatId, userId, '请解释当前目录的项目结构和核心逻辑', platform);
103
120
  if (t === '/cd' || t.startsWith('/cd ')) {
104
121
  return this.handleCd(chatId, userId, t.slice(3).trim(), platform);
105
122
  }
@@ -133,6 +150,15 @@ export class CommandHandler {
133
150
  '/status - 显示状态',
134
151
  '/cd <路径> - 切换工作目录',
135
152
  '/pwd - 当前工作目录',
153
+ '',
154
+ '⚡ 快捷命令:',
155
+ '/git commit - 提交代码',
156
+ '/git push - 推送到远程',
157
+ '/git pull - 拉取远程更新',
158
+ '/test - 运行测试',
159
+ '/build - 构建项目',
160
+ '/review - 代码审查',
161
+ '/explain - 解释项目结构',
136
162
  ].join('\n');
137
163
  await this.replySender().sendTextReply(chatId, help);
138
164
  return true;
@@ -445,6 +471,18 @@ export class CommandHandler {
445
471
  });
446
472
  });
447
473
  }
474
+ /**
475
+ * 快捷命令 — 将预设 prompt 发送给 AI
476
+ */
477
+ async handleQuickCommand(chatId, userId, prompt, platform) {
478
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
479
+ const convId = this.deps.sessionManager.getConvId(userId);
480
+ // 使用 dispatch 传入的 handleClaudeRequest
481
+ await this.quickCommandHandler(userId, chatId, prompt, workDir, convId);
482
+ return true;
483
+ }
484
+ /** 临时存储 dispatch 传入的 handler */
485
+ quickCommandHandler = async () => { };
448
486
  }
449
487
  /**
450
488
  * 列出目录并返回目录信息
@@ -52,11 +52,11 @@ export declare const PAGE_TEXTS: {
52
52
  readonly credentialProgress: "{done}/{total} credentials";
53
53
  readonly telegramSummary: "Bot token and optional proxy.";
54
54
  readonly feishuSummary: "App ID, App Secret, and allowed user scope.";
55
- readonly qqSummary: "App ID and secret for bot access.";
56
- readonly weworkSummary: "Corp ID and secret for enterprise delivery.";
55
+ readonly qqSummary: "App ID and secret for QQ robot.";
56
+ readonly weworkSummary: "Corp ID and secret for enterprise WeChat.";
57
57
  readonly dingtalkSummary: "Client credentials plus optional card template.";
58
- readonly workbuddySummary: "CodeBuddy OAuth for WeChat customer service.";
59
- readonly clawbotSummary: "WeChat iLink API via ClawBot.";
58
+ readonly workbuddySummary: "WeChat assistant via CodeBuddy OAuth.";
59
+ readonly clawbotSummary: "WeChat customer service via ClawBot.";
60
60
  readonly clawbotApiUrl: "API URL";
61
61
  readonly clawbotApiToken: "API Token (Bearer)";
62
62
  readonly clawbotHelp: "Click \"Scan QR Login\" to authenticate via WeChat, or paste token manually.";
@@ -86,7 +86,7 @@ export declare const PAGE_TEXTS: {
86
86
  readonly clientId: "Client ID / AppKey";
87
87
  readonly clientSecret: "Client Secret / AppSecret";
88
88
  readonly dingtalkHelp: "Get credentials: Create an enterprise internal app on <a href=\"https://open-dev.dingtalk.com/\" target=\"_blank\">DingTalk Open Platform</a>, enable Stream Mode, and get Client ID / Client Secret";
89
- readonly workbuddyHelp: "Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. WorkBuddy connects WeChat customer service through Centrifuge WebSocket.";
89
+ readonly workbuddyHelp: "Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. Connects WeChat assistant through Centrifuge WebSocket.";
90
90
  readonly secret: "Secret";
91
91
  readonly cardTemplateId: "Card template ID";
92
92
  readonly workbuddyAccessToken: "Access Token";
@@ -266,8 +266,8 @@ export declare const PAGE_TEXTS: {
266
266
  readonly qqSummary: "QQ 机器人 App ID 与 Secret。";
267
267
  readonly weworkSummary: "企业微信 Corp ID 与 Secret。";
268
268
  readonly dingtalkSummary: "钉钉 Client 凭证,可选配置卡片模板 ID。";
269
- readonly workbuddySummary: "CodeBuddy OAuth 连接微信客服。";
270
- readonly clawbotSummary: "通过 ClawBot 连接微信 iLink API。";
269
+ readonly workbuddySummary: "通过 CodeBuddy OAuth 连接微信助理。";
270
+ readonly clawbotSummary: "通过 ClawBot 连接微信客服号。";
271
271
  readonly clawbotApiUrl: "API 地址";
272
272
  readonly clawbotApiToken: "API Token (Bearer)";
273
273
  readonly clawbotHelp: "点击“扫码登录”通过微信认证,或手动粘贴 Token。";
@@ -52,11 +52,11 @@ export const PAGE_TEXTS = {
52
52
  credentialProgress: "{done}/{total} credentials",
53
53
  telegramSummary: "Bot token and optional proxy.",
54
54
  feishuSummary: "App ID, App Secret, and allowed user scope.",
55
- qqSummary: "App ID and secret for bot access.",
56
- weworkSummary: "Corp ID and secret for enterprise delivery.",
55
+ qqSummary: "App ID and secret for QQ robot.",
56
+ weworkSummary: "Corp ID and secret for enterprise WeChat.",
57
57
  dingtalkSummary: "Client credentials plus optional card template.",
58
- workbuddySummary: "CodeBuddy OAuth for WeChat customer service.",
59
- clawbotSummary: "WeChat iLink API via ClawBot.",
58
+ workbuddySummary: "WeChat assistant via CodeBuddy OAuth.",
59
+ clawbotSummary: "WeChat customer service via ClawBot.",
60
60
  clawbotApiUrl: "API URL",
61
61
  clawbotApiToken: "API Token (Bearer)",
62
62
  clawbotHelp: 'Click "Scan QR Login" to authenticate via WeChat, or paste token manually.',
@@ -86,7 +86,7 @@ export const PAGE_TEXTS = {
86
86
  clientId: "Client ID / AppKey",
87
87
  clientSecret: "Client Secret / AppSecret",
88
88
  dingtalkHelp: 'Get credentials: Create an enterprise internal app on <a href="https://open-dev.dingtalk.com/" target="_blank">DingTalk Open Platform</a>, enable Stream Mode, and get Client ID / Client Secret',
89
- workbuddyHelp: 'Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. WorkBuddy connects WeChat customer service through Centrifuge WebSocket.',
89
+ workbuddyHelp: 'Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. Connects WeChat assistant through Centrifuge WebSocket.',
90
90
  secret: "Secret",
91
91
  cardTemplateId: "Card template ID",
92
92
  workbuddyAccessToken: "Access Token",
@@ -267,8 +267,8 @@ export const PAGE_TEXTS = {
267
267
  qqSummary: "QQ \u673a\u5668\u4eba App ID \u4e0e Secret\u3002",
268
268
  weworkSummary: "\u4f01\u4e1a\u5fae\u4fe1 Corp ID \u4e0e Secret\u3002",
269
269
  dingtalkSummary: "\u9489\u9489 Client \u51ed\u8bc1\uff0c\u53ef\u9009\u914d\u7f6e\u5361\u7247\u6a21\u677f ID\u3002",
270
- workbuddySummary: "CodeBuddy OAuth \u8fde\u63a5\u5fae\u4fe1\u5ba2\u670d\u3002",
271
- clawbotSummary: "\u901a\u8fc7 ClawBot \u8fde\u63a5\u5fae\u4fe1 iLink API\u3002",
270
+ workbuddySummary: "\u901a\u8fc7 CodeBuddy OAuth \u8fde\u63a5\u5fae\u4fe1\u52a9\u7406\u3002",
271
+ clawbotSummary: "\u901a\u8fc7 ClawBot \u8fde\u63a5\u5fae\u4fe1\u5ba2\u670d\u53f7\u3002",
272
272
  clawbotApiUrl: "API \u5730\u5740",
273
273
  clawbotApiToken: "API Token (Bearer)",
274
274
  clawbotHelp: '\u70b9\u51fb\u201c\u626b\u7801\u767b\u5f55\u201d\u901a\u8fc7\u5fae\u4fe1\u8ba4\u8bc1\uff0c\u6216\u624b\u52a8\u7c98\u8d34 Token\u3002',
package/dist/index.js CHANGED
@@ -137,6 +137,77 @@ const PLATFORM_DISPLAY_NAMES = {
137
137
  workbuddy: '微信',
138
138
  clawbot: '微信 (ClawBot)',
139
139
  };
140
+ /** 随机提示列表 */
141
+ const TIPS = [
142
+ '/help — 查看所有命令',
143
+ '/new — 开始新会话',
144
+ '/sessions — 浏览历史会话',
145
+ '/git commit — 快速提交代码',
146
+ '/review — 代码审查',
147
+ '/test — 运行测试',
148
+ '/explain — 解释项目结构',
149
+ '/plugins — 查看已安装插件',
150
+ '/context — 查看上下文用量',
151
+ '/models — 切换 AI 模型',
152
+ '/cd <路径> — 切换工作目录',
153
+ '/fork — 分支会话,保留当前上下文',
154
+ ];
155
+ /** 读取已启用的插件列表 */
156
+ function getEnabledPlugins() {
157
+ try {
158
+ const { readFileSync, existsSync } = require("fs");
159
+ const { join } = require("path");
160
+ const { homedir } = require("os");
161
+ const settingsPath = join(homedir(), ".claude", "settings.json");
162
+ if (!existsSync(settingsPath))
163
+ return [];
164
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
165
+ const plugins = settings.enabledPlugins ?? {};
166
+ return Object.entries(plugins)
167
+ .filter(([, v]) => v === true)
168
+ .map(([k]) => k.split("@")[0]);
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ }
174
+ /**
175
+ * 统一通知格式模板
176
+ * 所有发送给 IM 的通知都使用这个格式,保持一致性
177
+ * 使用 \n\n 分隔各部分,ClawBot/微信支持双换行
178
+ */
179
+ function buildNotification(opts) {
180
+ const lines = [];
181
+ // 标题
182
+ lines.push(`${opts.emoji} ${opts.title}`);
183
+ // 详情
184
+ const details = [];
185
+ if (opts.platform)
186
+ details.push(`📱 平台: ${opts.platform}`);
187
+ if (opts.aiCommand)
188
+ details.push(`🤖 AI: ${opts.aiCommand}`);
189
+ if (opts.dir)
190
+ details.push(`📁 目录: ${opts.dir}`);
191
+ // 插件:最多显示 3 个,超过则截断
192
+ const plugins = getEnabledPlugins();
193
+ if (plugins.length > 0) {
194
+ const display = plugins.length <= 3
195
+ ? plugins.join(", ")
196
+ : `${plugins.slice(0, 3).join(", ")} 等${plugins.length}个`;
197
+ details.push(`🧩 插件: ${display}`);
198
+ }
199
+ if (opts.uptime)
200
+ details.push(`⏱️ 运行: ${opts.uptime}`);
201
+ if (details.length > 0) {
202
+ lines.push("", ...details);
203
+ }
204
+ // 随机提示(仅启动时显示)
205
+ if (opts.emoji === "✅") {
206
+ const tip = TIPS[Math.floor(Math.random() * TIPS.length)];
207
+ lines.push("", `💡 ${tip}`);
208
+ }
209
+ return lines.join("\n\n");
210
+ }
140
211
  function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, sessionManager) {
141
212
  let sessionDir;
142
213
  // Telegram 私聊、企业微信当前实现里,活跃 chatId 可直接对应到 session userId。
@@ -149,21 +220,21 @@ function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, se
149
220
  const platformName = PLATFORM_DISPLAY_NAMES[platform] ?? platform;
150
221
  const toolName = getAIToolDisplayName(aiCommand);
151
222
  const dir = sessionDir ? escapePathForMarkdown(sessionDir) : '发送 `/pwd` 查看';
152
- return [
153
- `✅ open-im v${appVersion} 已就绪`,
154
- "",
155
- `📱 平台: ${platformName}`,
156
- `🤖 AI: ${toolName}`,
157
- `📁 目录: ${dir}`,
158
- ].join("\n\n");
223
+ return buildNotification({
224
+ emoji: "✅",
225
+ title: `open-im v${appVersion} 已就绪`,
226
+ platform: platformName,
227
+ aiCommand: toolName,
228
+ dir,
229
+ });
159
230
  }
160
231
  function buildShutdownMessage(uptimeMinutes) {
161
- const uptime = uptimeMinutes < 1 ? '< 1' : String(uptimeMinutes);
162
- return [
163
- `🛑 open-im 正在关闭`,
164
- "",
165
- `⏱️ 运行时长: ${uptime} 分钟`,
166
- ].join("\n\n");
232
+ const uptimeStr = uptimeMinutes < 1 ? '< 1 分钟' : `${uptimeMinutes} 分钟`;
233
+ return buildNotification({
234
+ emoji: "🛑",
235
+ title: "open-im 正在关闭",
236
+ uptime: uptimeStr,
237
+ });
167
238
  }
168
239
  export async function main() {
169
240
  const startupCwd = process.cwd();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 选择检测器 — 从 AI 输出中检测并提取编号选项
3
+ *
4
+ * 当 AI 问"请选择 1/2/3"时,提取选项并返回结构化数据。
5
+ */
6
+ export interface DetectedChoice {
7
+ number: number;
8
+ text: string;
9
+ }
10
+ export interface DetectionResult {
11
+ /** 是否检测到选择 */
12
+ hasChoices: boolean;
13
+ /** 提取的选项列表 */
14
+ choices: DetectedChoice[];
15
+ /** 去除选项后的纯文本(用于显示) */
16
+ cleanText: string;
17
+ }
18
+ /**
19
+ * 检测 AI 输出中的编号选择
20
+ *
21
+ * 支持格式:
22
+ * - "1. Option A\n2. Option B\n3. Option C"
23
+ * - "1)Option A\n2)Option B"
24
+ * - "**选择 1:** Option A"
25
+ */
26
+ export declare function detectChoices(text: string): DetectionResult;
27
+ /**
28
+ * 构建 Telegram inline keyboard
29
+ */
30
+ export declare function buildChoiceKeyboard(choices: DetectedChoice[], userId: string): {
31
+ inline_keyboard: Array<Array<{
32
+ text: string;
33
+ callback_data: string;
34
+ }>>;
35
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 选择检测器 — 从 AI 输出中检测并提取编号选项
3
+ *
4
+ * 当 AI 问"请选择 1/2/3"时,提取选项并返回结构化数据。
5
+ */
6
+ /**
7
+ * 检测 AI 输出中的编号选择
8
+ *
9
+ * 支持格式:
10
+ * - "1. Option A\n2. Option B\n3. Option C"
11
+ * - "1)Option A\n2)Option B"
12
+ * - "**选择 1:** Option A"
13
+ */
14
+ export function detectChoices(text) {
15
+ // 匹配 "1." 或 "1)" 或 "1:" 开头的行
16
+ const choicePattern = /^\s*(\d+)[.):]\s*(.+)$/gm;
17
+ const matches = [];
18
+ let match;
19
+ while ((match = choicePattern.exec(text)) !== null) {
20
+ const num = parseInt(match[1], 10);
21
+ const content = match[2].trim();
22
+ if (num >= 1 && num <= 9 && content.length > 0) {
23
+ matches.push({ number: num, text: content });
24
+ }
25
+ }
26
+ // 检查是否有选择提示(如"请选择"、"选择哪个"等)
27
+ const hasChoicePrompt = /请选择|选择哪个|选一个|pick|choose|select/i.test(text);
28
+ // 至少需要 2 个选项且有选择提示
29
+ const hasChoices = matches.length >= 2 && hasChoicePrompt;
30
+ if (!hasChoices) {
31
+ return { hasChoices: false, choices: [], cleanText: text };
32
+ }
33
+ // 去除选项行,保留其他内容
34
+ const lines = text.split('\n');
35
+ const cleanLines = lines.filter(line => {
36
+ const trimmed = line.trim();
37
+ return !/^\s*\d+[.):]\s*.+/.test(trimmed);
38
+ });
39
+ return {
40
+ hasChoices: true,
41
+ choices: matches,
42
+ cleanText: cleanLines.join('\n').trim(),
43
+ };
44
+ }
45
+ /**
46
+ * 构建 Telegram inline keyboard
47
+ */
48
+ export function buildChoiceKeyboard(choices, userId) {
49
+ const buttons = [];
50
+ // 每行 1 个按钮(选项通常较长)
51
+ for (const choice of choices) {
52
+ buttons.push([{
53
+ text: `${choice.number}. ${choice.text}`,
54
+ callback_data: `choice:${userId}:${choice.number}`,
55
+ }]);
56
+ }
57
+ return { inline_keyboard: buttons };
58
+ }