@wu529778790/open-im 1.11.1-beta.9 → 1.11.2-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 +76 -59
- package/README.zh-CN.md +74 -57
- package/dist/adapters/claude-sdk-adapter.js +16 -1
- package/dist/clawbot/client.d.ts +1 -1
- package/dist/clawbot/client.js +71 -3
- package/dist/clawbot/event-handler.d.ts +1 -1
- package/dist/clawbot/event-handler.js +10 -3
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +33 -1
- package/dist/config-web-page-i18n.d.ts +96 -4
- package/dist/config-web-page-i18n.js +98 -4
- package/dist/config-web.js +15 -0
- package/dist/index.js +16 -2
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +21 -0
- package/dist/platform/handle-text-flow.d.ts +2 -0
- package/dist/platform/handle-text-flow.js +35 -2
- package/dist/shared/ai-task.js +6 -0
- package/dist/shared/message-wal.d.ts +31 -0
- package/dist/shared/message-wal.js +91 -0
- package/dist/shared/sentry.d.ts +22 -0
- package/dist/shared/sentry.js +101 -0
- package/package.json +3 -2
- package/web/dist/assets/index-B1c1xlbx.css +1 -0
- package/web/dist/assets/index-Dng7B2mE.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-7fl61tV1.css +0 -1
- package/web/dist/assets/index-BaLTMeeF.js +0 -57
package/README.md
CHANGED
|
@@ -2,66 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
**English** · [中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Your AI coding assistant, in every chat app.
|
|
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.
|
|
6
8
|
|
|
7
9
|
## Architecture
|
|
8
10
|
|
|
9
11
|

|
|
10
12
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
- **Seven IM platforms** — Telegram, Feishu, WeCom, DingTalk, QQ, WorkBuddy, ClawBot
|
|
14
|
-
- **Three AI backends** — Claude (Agent SDK), Codex, CodeBuddy (per-platform override supported)
|
|
15
|
-
- **Streaming, media, sessions** — live output where supported; `/new` for a fresh AI session
|
|
16
|
-
- **Web UI** — dashboard bundled in the package; default **`http://127.0.0.1:39282`**
|
|
17
|
-
|
|
18
|
-
## Requirements
|
|
19
|
-
|
|
20
|
-
- Node.js ≥ 20
|
|
21
|
-
- At least one IM platform configured + credentials for your AI tool
|
|
22
|
-
|
|
23
|
-
## Quick start
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx @wu529778790/open-im start
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Or: `npm install -g @wu529778790/open-im` then `open-im start`.
|
|
13
|
+
## Why
|
|
30
14
|
|
|
31
|
-
|
|
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
|
|
32
19
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
| Command | Description |
|
|
36
|
-
| --- | --- |
|
|
37
|
-
| `open-im init` | Interactive setup (does not start the bridge) |
|
|
38
|
-
| `open-im start` | Run the bridge in the background |
|
|
39
|
-
| `open-im stop` | Stop the background bridge |
|
|
40
|
-
| `open-im restart` | Stop then start |
|
|
41
|
-
| `open-im dashboard` | Web config server only (no bridge) |
|
|
42
|
-
|
|
43
|
-
After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:39282`**).
|
|
20
|
+
## Features
|
|
44
21
|
|
|
45
|
-
|
|
22
|
+
### Chat commands
|
|
46
23
|
|
|
47
24
|
| Command | Description |
|
|
48
25
|
| --- | --- |
|
|
49
|
-
| `/help` |
|
|
50
|
-
| `/new` |
|
|
51
|
-
| `/sessions` |
|
|
52
|
-
| `/resume [N]` | Resume session (no arg = most recent) |
|
|
53
|
-
| `/
|
|
54
|
-
| `/
|
|
55
|
-
| `/
|
|
56
|
-
|
|
57
|
-
|
|
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 |
|
|
39
|
+
|
|
40
|
+
### Session continuity
|
|
58
41
|
|
|
59
42
|
open-im and Claude Code CLI share the same session storage. In the same directory, you can seamlessly switch between phone and computer.
|
|
60
43
|
|
|
61
|
-
**Phone → Computer:** open-im auto-resumes the latest CLI session — no configuration needed.
|
|
62
|
-
|
|
63
|
-
**Computer → Phone:** use `claude --continue` (or `claude -c`) to pick up the phone conversation.
|
|
64
|
-
|
|
65
44
|
```
|
|
66
45
|
# On computer
|
|
67
46
|
cd /my-project && claude # work as usual, then Ctrl+C
|
|
@@ -75,16 +54,43 @@ claude -c # continues the phone conversation
|
|
|
75
54
|
|
|
76
55
|
> Only one side can be active at a time. Exit the CLI before sending messages from the phone, and vice versa.
|
|
77
56
|
|
|
78
|
-
|
|
57
|
+
### Platform support
|
|
79
58
|
|
|
80
|
-
|
|
59
|
+
Seven IM platforms, three AI backends, per-platform override:
|
|
81
60
|
|
|
82
|
-
|
|
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 |
|
|
70
|
+
|
|
71
|
+
Set `platforms.<name>.aiCommand` (`claude` / `codex` / `codebuddy`) per channel. Default: `claude`.
|
|
72
|
+
|
|
73
|
+
### Web dashboard
|
|
74
|
+
|
|
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`.
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx @wu529778790/open-im init # interactive setup
|
|
81
|
+
npx @wu529778790/open-im start # run the bridge
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or install globally: `npm install -g @wu529778790/open-im` then `open-im start`.
|
|
85
|
+
|
|
86
|
+
Config file: **`~/.open-im/config.json`**
|
|
87
|
+
|
|
88
|
+
### Minimal config
|
|
83
89
|
|
|
84
90
|
```json
|
|
85
91
|
{
|
|
86
92
|
"tools": {
|
|
87
|
-
"claude": { "workDir": "/path/to/project", "skipPermissions": true
|
|
93
|
+
"claude": { "workDir": "/path/to/project", "skipPermissions": true }
|
|
88
94
|
},
|
|
89
95
|
"platforms": {
|
|
90
96
|
"telegram": { "enabled": true, "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
@@ -92,11 +98,11 @@ claude -c # continues the phone conversation
|
|
|
92
98
|
}
|
|
93
99
|
```
|
|
94
100
|
|
|
95
|
-
|
|
101
|
+
Run `open-im init` for a full template with all platforms.
|
|
96
102
|
|
|
97
103
|
### Claude (Agent SDK)
|
|
98
104
|
|
|
99
|
-
No local `claude` binary required.
|
|
105
|
+
No local `claude` binary required. Supports third-party / compatible APIs:
|
|
100
106
|
|
|
101
107
|
```json
|
|
102
108
|
{
|
|
@@ -112,26 +118,37 @@ No local `claude` binary required. Third-party / compatible API:
|
|
|
112
118
|
}
|
|
113
119
|
```
|
|
114
120
|
|
|
115
|
-
###
|
|
121
|
+
### CLI reference
|
|
116
122
|
|
|
117
|
-
|
|
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) |
|
|
118
130
|
|
|
119
|
-
###
|
|
131
|
+
### Environment variables
|
|
120
132
|
|
|
121
|
-
|
|
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.
|
|
122
134
|
|
|
123
|
-
###
|
|
135
|
+
### Git co-authors
|
|
124
136
|
|
|
125
|
-
|
|
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.
|
|
126
138
|
|
|
127
139
|
### Privacy
|
|
128
140
|
|
|
129
|
-
**Anonymous** usage information may be collected to improve reliability (no chat or prompt content). To disable: **`OPEN_IM_TELEMETRY=false`** or
|
|
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`.
|
|
130
142
|
|
|
131
143
|
## Platform setup & troubleshooting
|
|
132
144
|
|
|
133
145
|
See **[docs/platforms.md](./docs/platforms.md)** for detailed per-platform configuration, credential setup, and troubleshooting.
|
|
134
146
|
|
|
147
|
+
## Requirements
|
|
148
|
+
|
|
149
|
+
- Node.js >= 20
|
|
150
|
+
- At least one IM platform configured + credentials for your AI tool
|
|
151
|
+
|
|
135
152
|
## License
|
|
136
153
|
|
|
137
154
|
[MIT](LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,65 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) · **中文**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> 你的 AI 编程助手,在每个聊天 App 里。
|
|
6
|
+
|
|
7
|
+
open-im 把 Claude Code、Codex、CodeBuddy 接入 Telegram、飞书、企业微信、钉钉、QQ、微信(WorkBuddy)、微信(ClawBot)。手机发条消息,服务器上就写好代码。
|
|
6
8
|
|
|
7
9
|
## 架构
|
|
8
10
|
|
|
9
11
|

|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## 为什么用 open-im
|
|
12
14
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
15
|
+
- **随时随地** — 通勤、排队、躺沙发上,手机发消息就能让 Claude Code 干活
|
|
16
|
+
- **无缝接力** — open-im 和 Claude Code CLI 共享 session,在手机上聊到一半,电脑上接着来
|
|
17
|
+
- **完整能力,简单界面** — 流式输出、会话管理、模型切换,全靠聊天命令搞定
|
|
18
|
+
- **一个桥接,多个平台** — 同一个 bot 支持 Telegram、飞书、钉钉、微信等
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## 功能
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
- 至少配置一个 IM 平台 + 所选 AI 的凭证
|
|
22
|
-
|
|
23
|
-
## 快速开始
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx @wu529778790/open-im start
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
或:`npm install -g @wu529778790/open-im` 后执行 `open-im start`。
|
|
30
|
-
|
|
31
|
-
配置:**`~/.open-im/config.json`**
|
|
32
|
-
|
|
33
|
-
## CLI 命令
|
|
22
|
+
### 聊天命令
|
|
34
23
|
|
|
35
24
|
| 命令 | 说明 |
|
|
36
25
|
| --- | --- |
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `open-im restart` | 重启 |
|
|
41
|
-
| `open-im dashboard` | 仅 Web 配置服务(不启动桥接) |
|
|
42
|
-
|
|
43
|
-
`start` 后会提示控制台地址(默认 **`http://127.0.0.1:39282`**)。
|
|
44
|
-
|
|
45
|
-
## IM 内命令
|
|
46
|
-
|
|
47
|
-
| 命令 | 说明 |
|
|
48
|
-
| --- | --- |
|
|
49
|
-
| `/help` | 帮助 |
|
|
50
|
-
| `/new` | 新 AI 会话 |
|
|
51
|
-
| `/sessions` | 历史会话(含摘要预览) |
|
|
26
|
+
| `/help` | 显示所有命令 |
|
|
27
|
+
| `/new` | 开启新 AI 会话 |
|
|
28
|
+
| `/sessions` | 浏览历史会话(含摘要预览) |
|
|
52
29
|
| `/resume [序号]` | 恢复会话(无参数恢复最近一条) |
|
|
53
|
-
| `/
|
|
54
|
-
| `/
|
|
30
|
+
| `/history [序号]` | 查看会话对话记录 |
|
|
31
|
+
| `/delete <序号>` | 删除历史会话 |
|
|
32
|
+
| `/rename <标题>` | 重命名当前会话 |
|
|
33
|
+
| `/fork [序号]` | 分支会话(创建副本) |
|
|
34
|
+
| `/models` | 查看可用 AI 模型 |
|
|
35
|
+
| `/context` | 查看上下文窗口占用 |
|
|
36
|
+
| `/status` | 显示 AI 工具、账号、会话信息 |
|
|
37
|
+
| `/cd <路径>` / `/pwd` | 切换工作目录(自动恢复该目录的历史会话) |
|
|
55
38
|
| `/allow` / `/y`、`/deny` / `/n` | 权限确认 |
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
open-im 和 Claude Code CLI 共享同一份 session 存储。在同一个目录下,手机和电脑可以无缝切换。
|
|
40
|
+
### 会话接力
|
|
60
41
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
**电脑接手机:** 使用 `claude --continue`(或 `claude -c`)接上 open-im 端的对话。
|
|
42
|
+
open-im 和 Claude Code CLI 共享同一份 session 存储。同一个目录下,手机和电脑可以无缝切换。
|
|
64
43
|
|
|
65
44
|
```bash
|
|
66
45
|
# 电脑端
|
|
@@ -75,16 +54,43 @@ claude -c # 接上手机端的对话
|
|
|
75
54
|
|
|
76
55
|
> 同一时刻只能有一端活跃。从手机发消息前先退出 CLI,反之亦然。
|
|
77
56
|
|
|
78
|
-
|
|
57
|
+
### 平台支持
|
|
58
|
+
|
|
59
|
+
七个 IM 平台,三种 AI 后端,可按平台覆盖:
|
|
79
60
|
|
|
80
|
-
|
|
61
|
+
| 平台 | 流式输出 | 备注 |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| Telegram | 支持 | 完整 bot 支持 |
|
|
64
|
+
| 飞书 | 支持 | 流式卡片 |
|
|
65
|
+
| 企业微信 | 支持 | 流式卡片 |
|
|
66
|
+
| 钉钉 | 部分 | 回退到纯文本 |
|
|
67
|
+
| QQ | 支持 | |
|
|
68
|
+
| WorkBuddy | 支持 | 微信生态 |
|
|
69
|
+
| ClawBot | 支持 | 微信生态 |
|
|
81
70
|
|
|
82
|
-
|
|
71
|
+
在每个平台上设置 `platforms.<name>.aiCommand`(`claude` / `codex` / `codebuddy`),默认 `claude`。
|
|
72
|
+
|
|
73
|
+
### Web 控制台
|
|
74
|
+
|
|
75
|
+
`open-im start` 在 **`http://127.0.0.1:39282`** 提供内置页面与 API(通过 `OPEN_IM_WEB_PORT` 配置端口)。局域网访问:`export OPEN_IM_WEB_HOST=0.0.0.0`。
|
|
76
|
+
|
|
77
|
+
## 快速开始
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx @wu529778790/open-im init # 交互式配置
|
|
81
|
+
npx @wu529778790/open-im start # 启动桥接
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
或全局安装:`npm install -g @wu529778790/open-im` 后执行 `open-im start`。
|
|
85
|
+
|
|
86
|
+
配置文件:**`~/.open-im/config.json`**
|
|
87
|
+
|
|
88
|
+
### 最小配置
|
|
83
89
|
|
|
84
90
|
```json
|
|
85
91
|
{
|
|
86
92
|
"tools": {
|
|
87
|
-
"claude": { "workDir": "/path/to/project", "skipPermissions": true
|
|
93
|
+
"claude": { "workDir": "/path/to/project", "skipPermissions": true }
|
|
88
94
|
},
|
|
89
95
|
"platforms": {
|
|
90
96
|
"telegram": { "enabled": true, "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
@@ -92,11 +98,11 @@ claude -c # 接上手机端的对话
|
|
|
92
98
|
}
|
|
93
99
|
```
|
|
94
100
|
|
|
95
|
-
|
|
101
|
+
完整模板请用 `open-im init`。
|
|
96
102
|
|
|
97
103
|
### Claude(Agent SDK)
|
|
98
104
|
|
|
99
|
-
无需本地 `claude`
|
|
105
|
+
无需本地 `claude` 可执行文件。支持第三方兼容接口:
|
|
100
106
|
|
|
101
107
|
```json
|
|
102
108
|
{
|
|
@@ -112,26 +118,37 @@ claude -c # 接上手机端的对话
|
|
|
112
118
|
}
|
|
113
119
|
```
|
|
114
120
|
|
|
115
|
-
###
|
|
121
|
+
### CLI 命令
|
|
116
122
|
|
|
117
|
-
|
|
123
|
+
| 命令 | 说明 |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `open-im init` | 交互配置(不启动桥接) |
|
|
126
|
+
| `open-im start` | 后台运行桥接 |
|
|
127
|
+
| `open-im stop` | 停止后台服务 |
|
|
128
|
+
| `open-im restart` | 重启 |
|
|
129
|
+
| `open-im dashboard` | 仅 Web 配置服务(不启动桥接) |
|
|
118
130
|
|
|
119
|
-
###
|
|
131
|
+
### 环境变量
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
**`ANTHROPIC_*`**(shell 或 `tools.claude.env`)、**`TELEGRAM_BOT_TOKEN`**、**`OPEN_IM_WEB_PORT`**、**`OPEN_IM_WEB_HOST`**,以及各平台的 `*_APP_ID`、`*_SECRET`、`WORKBUDDY_*` 等。
|
|
122
134
|
|
|
123
|
-
###
|
|
135
|
+
### Git 共同作者
|
|
124
136
|
|
|
125
|
-
|
|
137
|
+
默认在 AI 发起的提交里追加 `Co-authored-by`。关闭:设置环境变量 **`OPEN_IM_GIT_COAUTHOR=0`** 并重启桥接。
|
|
126
138
|
|
|
127
139
|
### 隐私
|
|
128
140
|
|
|
129
|
-
为改进稳定性,可能记录**匿名**运行信息(不含聊天或 prompt 内容)。若需关闭:环境变量 **`OPEN_IM_TELEMETRY=false`**,或
|
|
141
|
+
为改进稳定性,可能记录**匿名**运行信息(不含聊天或 prompt 内容)。若需关闭:环境变量 **`OPEN_IM_TELEMETRY=false`**,或 `config.json` 中 `"telemetry": { "enabled": false }`。
|
|
130
142
|
|
|
131
143
|
## 平台配置与故障排除
|
|
132
144
|
|
|
133
145
|
详见 **[docs/platforms.zh-CN.md](./docs/platforms.zh-CN.md)**。
|
|
134
146
|
|
|
147
|
+
## 环境要求
|
|
148
|
+
|
|
149
|
+
- Node.js >= 20
|
|
150
|
+
- 至少配置一个 IM 平台 + 所选 AI 的凭证
|
|
151
|
+
|
|
135
152
|
## License
|
|
136
153
|
|
|
137
154
|
[MIT](LICENSE)
|
|
@@ -15,6 +15,17 @@ import { createLogger } from '../logger.js';
|
|
|
15
15
|
import { refreshClaudeEnvToProcess } from '../config/file-io.js';
|
|
16
16
|
import { toReplyPlainText } from '../shared/utils.js';
|
|
17
17
|
const log = createLogger('ClaudeSDK');
|
|
18
|
+
/**
|
|
19
|
+
* 注入交互式上下文指令,让 Claude 认为这是交互式终端会话。
|
|
20
|
+
* 解决 query() 非交互环境下 Claude 跳过用户选择、直接自主决策的问题。
|
|
21
|
+
*/
|
|
22
|
+
const INTERACTIVE_CONTEXT = `[SYSTEM: You are in an interactive chat session via instant messenger. The user CAN and WILL respond to your messages — treat this like an interactive terminal session. Rules:
|
|
23
|
+
1. When you need to make a decision involving user preference (choosing an approach, selecting between options, deciding what to do next), ALWAYS present clearly numbered options and WAIT for the user's response.
|
|
24
|
+
2. Do NOT proceed autonomously when user input would be valuable.
|
|
25
|
+
3. Only proceed autonomously for obvious single-path tasks (e.g. reading a file, running a simple command).
|
|
26
|
+
4. When presenting options, format them as a numbered list and end with a question.
|
|
27
|
+
]
|
|
28
|
+
`;
|
|
18
29
|
function loadUserPluginSettings() {
|
|
19
30
|
try {
|
|
20
31
|
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
@@ -386,11 +397,15 @@ export class ClaudeSDKAdapter {
|
|
|
386
397
|
}
|
|
387
398
|
}
|
|
388
399
|
const q = query({
|
|
389
|
-
prompt,
|
|
400
|
+
prompt: INTERACTIVE_CONTEXT + prompt,
|
|
390
401
|
options: {
|
|
391
402
|
cwd: workDir,
|
|
392
403
|
model: resolvedModel,
|
|
393
404
|
permissionMode,
|
|
405
|
+
// 启用完整工具集,与 cc 终端一致
|
|
406
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
407
|
+
// 启用所有技能(superpowers, playwright 等),与 cc 终端一致
|
|
408
|
+
skills: 'all',
|
|
394
409
|
...(resumeId ? { resume: resumeId } : {}),
|
|
395
410
|
...(options?.fallbackModel ? { fallbackModel: options.fallbackModel } : {}),
|
|
396
411
|
...(options?.disallowedTools?.length ? { disallowedTools: options.disallowedTools } : {}),
|
package/dist/clawbot/client.d.ts
CHANGED
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
import type { Config } from '../config.js';
|
|
11
11
|
import type { ClawBotState } from './types.js';
|
|
12
12
|
export declare function getChannelState(): ClawBotState;
|
|
13
|
-
export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
|
|
13
|
+
export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
|
|
14
14
|
export declare function stopClawbot(): void;
|
package/dist/clawbot/client.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createLogger } from '../logger.js';
|
|
|
12
12
|
import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
|
|
13
13
|
import { cacheContextToken } from './message-sender.js';
|
|
14
14
|
import { setClawbotContextToken, clearClawbotContextToken } from '../shared/active-chats.js';
|
|
15
|
+
import { decryptAes256CbcMedia, saveBufferMedia, createMediaTargetPath } from '../shared/media-storage.js';
|
|
15
16
|
import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
|
|
16
17
|
const log = createLogger('ClawBot');
|
|
17
18
|
const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
|
|
@@ -110,6 +111,8 @@ function startPolling() {
|
|
|
110
111
|
}
|
|
111
112
|
// Process messages
|
|
112
113
|
const messages = res.messages ?? [];
|
|
114
|
+
// Step 1: Extract valid USER messages and cache context tokens
|
|
115
|
+
const userMessages = [];
|
|
113
116
|
for (const msg of messages) {
|
|
114
117
|
if (signal.aborted)
|
|
115
118
|
break;
|
|
@@ -120,7 +123,6 @@ function startPolling() {
|
|
|
120
123
|
continue;
|
|
121
124
|
const chatId = msg.from_user_id ?? '';
|
|
122
125
|
const msgId = String(msg.message_id ?? msg.seq ?? '');
|
|
123
|
-
const content = extracted;
|
|
124
126
|
if (!chatId) {
|
|
125
127
|
log.warn('ClawBot message missing from_user_id, skipping');
|
|
126
128
|
continue;
|
|
@@ -130,10 +132,34 @@ function startPolling() {
|
|
|
130
132
|
cacheContextToken(chatId, msg.context_token);
|
|
131
133
|
setClawbotContextToken(msg.context_token);
|
|
132
134
|
}
|
|
133
|
-
|
|
135
|
+
// Extract and download images from message
|
|
136
|
+
const imagePaths = await extractImages(msg);
|
|
137
|
+
userMessages.push({ chatId, msgId, content: extracted, imagePaths: imagePaths.length > 0 ? imagePaths : undefined });
|
|
138
|
+
}
|
|
139
|
+
// Step 2: Aggregate consecutive messages from the same user
|
|
140
|
+
// ClawBot splits image+text into separate messages; combine them
|
|
141
|
+
const aggregated = [];
|
|
142
|
+
for (const m of userMessages) {
|
|
143
|
+
const last = aggregated[aggregated.length - 1];
|
|
144
|
+
if (last && last.chatId === m.chatId) {
|
|
145
|
+
// Same user — merge content and image paths
|
|
146
|
+
last.content = `${last.content}\n${m.content}`;
|
|
147
|
+
if (m.imagePaths?.length) {
|
|
148
|
+
last.imagePaths = [...(last.imagePaths ?? []), ...m.imagePaths];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
aggregated.push({ chatId: m.chatId, msgId: m.msgId, content: m.content, imagePaths: m.imagePaths });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Step 3: Dispatch aggregated messages
|
|
156
|
+
for (const m of aggregated) {
|
|
157
|
+
if (signal.aborted)
|
|
158
|
+
break;
|
|
159
|
+
log.info(`ClawBot message: chatId=${m.chatId}, msgId=${m.msgId}, content="${m.content.substring(0, 100)}", images=${m.imagePaths?.length ?? 0}`);
|
|
134
160
|
if (messageHandler) {
|
|
135
161
|
try {
|
|
136
|
-
await messageHandler(chatId, msgId, content);
|
|
162
|
+
await messageHandler(m.chatId, m.msgId, m.content, m.imagePaths);
|
|
137
163
|
}
|
|
138
164
|
catch (err) {
|
|
139
165
|
log.error('Error in ClawBot message handler:', err);
|
|
@@ -212,6 +238,48 @@ function stopWatchdog() {
|
|
|
212
238
|
watchdogTimer = null;
|
|
213
239
|
}
|
|
214
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Extract and download images from a ClawBot message's item_list.
|
|
243
|
+
* Images are encrypted with AES and hosted on CDN.
|
|
244
|
+
*/
|
|
245
|
+
async function extractImages(msg) {
|
|
246
|
+
if (!msg.item_list?.length)
|
|
247
|
+
return [];
|
|
248
|
+
const paths = [];
|
|
249
|
+
for (const item of msg.item_list) {
|
|
250
|
+
if (item.type !== 2 /* MessageItemType.IMAGE */)
|
|
251
|
+
continue;
|
|
252
|
+
const media = item.image_item?.media;
|
|
253
|
+
if (!media?.cdn_url)
|
|
254
|
+
continue;
|
|
255
|
+
try {
|
|
256
|
+
// Download from CDN
|
|
257
|
+
const response = await fetch(media.cdn_url, { signal: AbortSignal.timeout(30_000) });
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
log.warn(`Image download failed: HTTP ${response.status}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
263
|
+
// Decrypt if AES key provided
|
|
264
|
+
let decrypted;
|
|
265
|
+
if (media.aes_key) {
|
|
266
|
+
decrypted = decryptAes256CbcMedia(buffer, media.aes_key);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
decrypted = buffer;
|
|
270
|
+
}
|
|
271
|
+
// Save to disk
|
|
272
|
+
const targetPath = createMediaTargetPath('.jpg', `clawbot-${Date.now()}`);
|
|
273
|
+
await saveBufferMedia(decrypted, targetPath);
|
|
274
|
+
paths.push(targetPath);
|
|
275
|
+
log.info(`ClawBot image saved: ${targetPath}`);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
log.warn('Failed to process ClawBot image:', err);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return paths;
|
|
282
|
+
}
|
|
215
283
|
/**
|
|
216
284
|
* Extract text content from an iLink message's item_list.
|
|
217
285
|
* Returns the first text item found, or a placeholder for media types.
|
|
@@ -7,6 +7,6 @@ export interface ClawBotEventHandlerHandle {
|
|
|
7
7
|
stop: () => void;
|
|
8
8
|
runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
|
|
9
9
|
getRunningTaskCount: () => number;
|
|
10
|
-
handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
|
|
10
|
+
handleEvent: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager): ClawBotEventHandlerHandle;
|
|
@@ -33,8 +33,8 @@ export function setupClawbotHandlers(config, sessionManager) {
|
|
|
33
33
|
return () => { };
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
|
-
async function handleEvent(chatId, msgId, content) {
|
|
37
|
-
log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
|
|
36
|
+
async function handleEvent(chatId, msgId, content, imagePaths) {
|
|
37
|
+
log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}", images=${imagePaths?.length ?? 0}`);
|
|
38
38
|
const userId = chatId;
|
|
39
39
|
const text = content.trim();
|
|
40
40
|
const msgIdSender = {
|
|
@@ -61,11 +61,18 @@ export function setupClawbotHandlers(config, sessionManager) {
|
|
|
61
61
|
},
|
|
62
62
|
}),
|
|
63
63
|
});
|
|
64
|
+
// Prepend image paths to prompt so Claude can read them with its Read tool
|
|
65
|
+
let enrichedText = text;
|
|
66
|
+
if (imagePaths?.length) {
|
|
67
|
+
const imgRefs = imagePaths.map(p => `[用户发送了图片: ${p}]`).join('\n');
|
|
68
|
+
enrichedText = `${imgRefs}\n${text}`;
|
|
69
|
+
}
|
|
64
70
|
await handleTextFlow({
|
|
65
71
|
platform: 'clawbot',
|
|
66
72
|
userId,
|
|
67
73
|
chatId,
|
|
68
|
-
text,
|
|
74
|
+
text: enrichedText,
|
|
75
|
+
msgId,
|
|
69
76
|
ctx,
|
|
70
77
|
handleAIRequest,
|
|
71
78
|
sendTextReply: (c, t) => sendTextReply(c, t),
|
|
@@ -24,6 +24,7 @@ export declare class CommandHandler {
|
|
|
24
24
|
/** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
|
|
25
25
|
senderOverride?: MessageSender): Promise<boolean>;
|
|
26
26
|
private handleHelp;
|
|
27
|
+
private handlePlugins;
|
|
27
28
|
private handleSessions;
|
|
28
29
|
private handleResume;
|
|
29
30
|
private handleNew;
|
package/dist/commands/handler.js
CHANGED
|
@@ -28,8 +28,9 @@ function truncateSummary(session, maxLen = 30) {
|
|
|
28
28
|
}
|
|
29
29
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
30
30
|
import { execFile } from 'node:child_process';
|
|
31
|
-
import { readdirSync } from 'node:fs';
|
|
31
|
+
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
32
32
|
import { dirname, join } from 'node:path';
|
|
33
|
+
import { homedir } from 'node:os';
|
|
33
34
|
/**
|
|
34
35
|
* Telegram 群聊等场景下命令常为 `/new@BotName`,需与 `/new` 等价。
|
|
35
36
|
* 仅去掉「第一个」命令词上的 `@suffix`,保留 `/resume 1` 等参数。
|
|
@@ -75,6 +76,8 @@ export class CommandHandler {
|
|
|
75
76
|
}
|
|
76
77
|
if (t === '/help')
|
|
77
78
|
return this.handleHelp(chatId);
|
|
79
|
+
if (t === '/plugins')
|
|
80
|
+
return this.handlePlugins(chatId);
|
|
78
81
|
if (t === '/new')
|
|
79
82
|
return this.handleNew(chatId, userId);
|
|
80
83
|
if (t === '/sessions' || t === '/resume')
|
|
@@ -125,6 +128,7 @@ export class CommandHandler {
|
|
|
125
128
|
'/rename <标题> - 重命名当前会话',
|
|
126
129
|
'/fork [序号] - 分支会话(创建副本)',
|
|
127
130
|
'/models - 查看可用模型',
|
|
131
|
+
'/plugins - 查看已安装插件',
|
|
128
132
|
'/context - 查看上下文窗口占用',
|
|
129
133
|
'/status - 显示状态',
|
|
130
134
|
'/cd <路径> - 切换工作目录',
|
|
@@ -133,6 +137,34 @@ export class CommandHandler {
|
|
|
133
137
|
await this.replySender().sendTextReply(chatId, help);
|
|
134
138
|
return true;
|
|
135
139
|
}
|
|
140
|
+
async handlePlugins(chatId) {
|
|
141
|
+
try {
|
|
142
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
143
|
+
if (!existsSync(settingsPath)) {
|
|
144
|
+
await this.replySender().sendTextReply(chatId, '📦 未找到 ~/.claude/settings.json\n💡 先在 Claude Code 终端运行一次会自动创建');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const raw = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
148
|
+
const plugins = (raw.enabledPlugins ?? {});
|
|
149
|
+
const entries = Object.entries(plugins);
|
|
150
|
+
if (entries.length === 0) {
|
|
151
|
+
await this.replySender().sendTextReply(chatId, '📦 暂无已安装插件\n💡 在 Claude Code 终端用 /install 安装插件');
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const lines = ['📦 已安装插件:', ''];
|
|
155
|
+
for (const [name, enabled] of entries) {
|
|
156
|
+
lines.push(enabled ? ` ✅ ${name}` : ` ❌ ${name}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push('💡 在 ~/.claude/settings.json 管理,或在 Claude Code 终端用 /install');
|
|
160
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
log.warn('Failed to read plugins:', e);
|
|
164
|
+
await this.replySender().sendTextReply(chatId, '❌ 读取插件列表失败');
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
136
168
|
async handleSessions(chatId, userId, _platform) {
|
|
137
169
|
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
138
170
|
const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
|