@wu529778790/open-im 1.10.9-beta.20 → 1.10.9-beta.22
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 +21 -94
- package/README.zh-CN.md +18 -91
- package/dist/adapters/opencode-adapter.d.ts +10 -0
- package/dist/adapters/opencode-adapter.js +45 -0
- package/dist/adapters/registry.js +5 -0
- package/dist/commands/handler.js +3 -2
- package/dist/config/types.d.ts +6 -1
- package/dist/config-web.js +6 -0
- package/dist/config.js +3 -0
- package/dist/opencode/cli-runner.d.ts +32 -0
- package/dist/opencode/cli-runner.js +161 -0
- package/dist/session/session-manager.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,16 +42,6 @@ Config: **`~/.open-im/config.json`**
|
|
|
42
42
|
|
|
43
43
|
After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:39282`**).
|
|
44
44
|
|
|
45
|
-
## Git co-authors
|
|
46
|
-
|
|
47
|
-
`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.
|
|
48
|
-
|
|
49
|
-
## Web dashboard
|
|
50
|
-
|
|
51
|
-
`open-im start` and `open-im dashboard` serve the built-in SPA and **`/api/*`** on **`OPEN_IM_WEB_PORT`** (default **39282**). Open **`http://127.0.0.1:39282`** in a browser (same origin as the API). Override the displayed URL with **`OPEN_IM_PUBLIC_WEB_URL`** if behind a proxy.
|
|
52
|
-
|
|
53
|
-
**Remote / LAN:** `export OPEN_IM_WEB_HOST=0.0.0.0` — first access from another host may show a one-time login link. Optional: **`OPEN_IM_ALLOW_REMOTE_API`**, **`OPEN_IM_CORS_ORIGINS`**.
|
|
54
|
-
|
|
55
45
|
## Chat commands
|
|
56
46
|
|
|
57
47
|
| Command | Description |
|
|
@@ -64,15 +54,13 @@ After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:392
|
|
|
64
54
|
| `/cd` / `/pwd` | Working directory |
|
|
65
55
|
| `/allow` / `/y`, `/deny` / `/n` | Permission prompts |
|
|
66
56
|
|
|
67
|
-
Session state is stored in **`~/.open-im/data/sessions.json`** (per user, not IM chat logs).
|
|
68
|
-
|
|
69
57
|
## Session continuity
|
|
70
58
|
|
|
71
|
-
open-im and Claude Code CLI share the same session storage.
|
|
59
|
+
open-im and Claude Code CLI share the same session storage. In the same directory, you can seamlessly switch between phone and computer.
|
|
72
60
|
|
|
73
|
-
**Phone → Computer:** open-im
|
|
61
|
+
**Phone → Computer:** open-im auto-resumes the latest CLI session — no configuration needed.
|
|
74
62
|
|
|
75
|
-
**Computer → Phone:** use `claude --continue` (or `claude -c`) to pick up the conversation
|
|
63
|
+
**Computer → Phone:** use `claude --continue` (or `claude -c`) to pick up the phone conversation.
|
|
76
64
|
|
|
77
65
|
```
|
|
78
66
|
# On computer
|
|
@@ -85,33 +73,35 @@ cd /my-project && claude # work as usual, then Ctrl+C
|
|
|
85
73
|
claude -c # continues the phone conversation
|
|
86
74
|
```
|
|
87
75
|
|
|
88
|
-
>
|
|
76
|
+
> Only one side can be active at a time. Exit the CLI before sending messages from the phone, and vice versa.
|
|
89
77
|
|
|
90
|
-
##
|
|
78
|
+
## Git co-authors
|
|
91
79
|
|
|
92
|
-
|
|
80
|
+
`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.
|
|
93
81
|
|
|
94
|
-
|
|
82
|
+
## Minimal config
|
|
95
83
|
|
|
96
84
|
```json
|
|
97
85
|
{
|
|
86
|
+
"tools": {
|
|
87
|
+
"claude": { "workDir": "/path/to/project", "skipPermissions": true, "timeoutMs": 600000 }
|
|
88
|
+
},
|
|
98
89
|
"platforms": {
|
|
99
|
-
"telegram": { "enabled": true, "
|
|
90
|
+
"telegram": { "enabled": true, "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
100
91
|
}
|
|
101
92
|
}
|
|
102
93
|
```
|
|
103
94
|
|
|
104
|
-
|
|
95
|
+
Add other platforms under **`platforms`** as needed. Run **`open-im init`** for a full template.
|
|
105
96
|
|
|
106
|
-
|
|
97
|
+
### Claude (Agent SDK)
|
|
107
98
|
|
|
108
|
-
Third-party / compatible API
|
|
99
|
+
No local `claude` binary required. Third-party / compatible API:
|
|
109
100
|
|
|
110
101
|
```json
|
|
111
102
|
{
|
|
112
103
|
"tools": {
|
|
113
104
|
"claude": {
|
|
114
|
-
"workDir": "/path/to/project",
|
|
115
105
|
"env": {
|
|
116
106
|
"ANTHROPIC_AUTH_TOKEN": "your-token",
|
|
117
107
|
"ANTHROPIC_BASE_URL": "https://your-api-endpoint",
|
|
@@ -122,88 +112,25 @@ Third-party / compatible API example:
|
|
|
122
112
|
}
|
|
123
113
|
```
|
|
124
114
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
npm install -g @tencent-ai/codebuddy-code
|
|
129
|
-
codebuddy login
|
|
130
|
-
```
|
|
115
|
+
### Per-platform AI
|
|
131
116
|
|
|
132
|
-
|
|
117
|
+
Set **`platforms.<name>.aiCommand`** (`claude` / `codex` / `codebuddy`) per channel. Default: `claude`.
|
|
133
118
|
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
"tools": {
|
|
137
|
-
"claude": { "workDir": "/path/to/project", "skipPermissions": true, "timeoutMs": 600000 }
|
|
138
|
-
},
|
|
139
|
-
"platforms": {
|
|
140
|
-
"telegram": { "enabled": true, "aiCommand": "claude", "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
119
|
+
### Web dashboard
|
|
144
120
|
|
|
145
|
-
|
|
121
|
+
`open-im start` serves the built-in SPA and **`/api/*`** on **`OPEN_IM_WEB_PORT`** (default **39282**). For LAN access: `export OPEN_IM_WEB_HOST=0.0.0.0`.
|
|
146
122
|
|
|
147
123
|
### Environment variables
|
|
148
124
|
|
|
149
|
-
|
|
125
|
+
Typical keys: **`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.
|
|
150
126
|
|
|
151
127
|
### Privacy
|
|
152
128
|
|
|
153
129
|
**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`**.
|
|
154
130
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
| Platform | Notes |
|
|
158
|
-
| --- | --- |
|
|
159
|
-
| Telegram | [@BotFather](https://t.me/BotFather) |
|
|
160
|
-
| Feishu | [Open Platform](https://open.feishu.cn/) |
|
|
161
|
-
| QQ | [QQ Open Platform](https://bot.q.qq.com/) |
|
|
162
|
-
| DingTalk | Open Platform — bot **Stream Mode**; optional **`cardTemplateId`** for AI assistant cards |
|
|
163
|
-
| WeCom | [Admin](https://work.weixin.qq.com/) |
|
|
164
|
-
| WeChat | **`open-im init`** → WorkBuddy OAuth |
|
|
165
|
-
| ClawBot (WeChat) | QR code login via iLink API; see [ClawBot setup](#clawbot-setup) |
|
|
166
|
-
|
|
167
|
-
### ClawBot setup
|
|
168
|
-
|
|
169
|
-
ClawBot connects to WeChat via the official iLink Bot API (same protocol as `@tencent-weixin/openclaw-weixin`). It supports text, voice, image, file, and video messages.
|
|
170
|
-
|
|
171
|
-
**Setup:**
|
|
172
|
-
|
|
173
|
-
1. Enable in config:
|
|
174
|
-
```json
|
|
175
|
-
{
|
|
176
|
-
"platforms": {
|
|
177
|
-
"clawbot": { "enabled": true }
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
2. Open the Web dashboard → **ClawBot** section → **Scan QR code** with WeChat.
|
|
182
|
-
3. After scanning, `bot_token` and `apiUrl` are saved automatically.
|
|
131
|
+
## Platform setup & troubleshooting
|
|
183
132
|
|
|
184
|
-
**
|
|
185
|
-
|
|
186
|
-
| Field | Default | Description |
|
|
187
|
-
| --- | --- | --- |
|
|
188
|
-
| `apiUrl` | `https://ilinkai.weixin.qq.com` | iLink API base URL |
|
|
189
|
-
| `apiToken` | — | Bot token (auto-set after QR login) |
|
|
190
|
-
| `aiCommand` | `claude` | AI backend override |
|
|
191
|
-
|
|
192
|
-
**Protocol:** POST + JSON body + Bearer token auth. Long-polling via `ilink/bot/getupdates` with `get_updates_buf` cursor.
|
|
193
|
-
|
|
194
|
-
## Troubleshooting
|
|
195
|
-
|
|
196
|
-
| Issue | What to try |
|
|
197
|
-
| --- | --- |
|
|
198
|
-
| Telegram / network | `proxy` or **`TELEGRAM_PROXY`** |
|
|
199
|
-
| QQ | Check **`QQ_BOT_APPID`** / **`QQ_BOT_SECRET`**; update if duplicate replies |
|
|
200
|
-
| Feishu cards | **`/mode ask`** or **`/mode yolo`** without card callbacks |
|
|
201
|
-
| WeCom | Send the bot a message first |
|
|
202
|
-
| DingTalk | Stream Mode + credentials; custom bots may be text-only |
|
|
203
|
-
| Codex disconnect | **`CODEX_PROXY`** or **`tools.codex.proxy`** |
|
|
204
|
-
| CodeBuddy login | **`codebuddy login`** |
|
|
205
|
-
| WorkBuddy | Re-run **`open-im init`** (tokens expire) |
|
|
206
|
-
| ClawBot | QR re-login via Web UI; `apiUrl` defaults to `https://ilinkai.weixin.qq.com` |
|
|
133
|
+
See **[docs/platforms.md](./docs/platforms.md)** for detailed per-platform configuration, credential setup, and troubleshooting.
|
|
207
134
|
|
|
208
135
|
## License
|
|
209
136
|
|
package/README.zh-CN.md
CHANGED
|
@@ -42,16 +42,6 @@ npx @wu529778790/open-im start
|
|
|
42
42
|
|
|
43
43
|
`start` 后会提示控制台地址(默认 **`http://127.0.0.1:39282`**)。
|
|
44
44
|
|
|
45
|
-
## Git 共同作者
|
|
46
|
-
|
|
47
|
-
默认在 AI 发起的提交里追加 `Co-authored-by`。**关闭**:设置环境变量 **`OPEN_IM_GIT_COAUTHOR=0`** 并重启桥接。
|
|
48
|
-
|
|
49
|
-
## Web 控制台
|
|
50
|
-
|
|
51
|
-
`open-im start` / `open-im dashboard` 在 **`OPEN_IM_WEB_PORT`**(默认 **39282**)提供内置页面与 **`/api/*`**。浏览器打开 **`http://127.0.0.1:39282`** 即可(与 API 同源)。反向代理时可设 **`OPEN_IM_PUBLIC_WEB_URL`**。
|
|
52
|
-
|
|
53
|
-
**局域网 / 远程:** `export OPEN_IM_WEB_HOST=0.0.0.0` — 首次外网访问可能需一次性登录链接。可选 **`OPEN_IM_ALLOW_REMOTE_API`**、**`OPEN_IM_CORS_ORIGINS`**。
|
|
54
|
-
|
|
55
45
|
## IM 内命令
|
|
56
46
|
|
|
57
47
|
| 命令 | 说明 |
|
|
@@ -64,8 +54,6 @@ npx @wu529778790/open-im start
|
|
|
64
54
|
| `/cd` / `/pwd` | 工作目录 |
|
|
65
55
|
| `/allow` / `/y`、`/deny` / `/n` | 权限确认 |
|
|
66
56
|
|
|
67
|
-
会话状态保存在 **`~/.open-im/data/sessions.json`**(按用户,与 IM 聊天记录无关)。
|
|
68
|
-
|
|
69
57
|
## 会话接力
|
|
70
58
|
|
|
71
59
|
open-im 和 Claude Code CLI 共享同一份 session 存储。在同一个目录下,手机和电脑可以无缝切换。
|
|
@@ -85,33 +73,35 @@ cd /my-project && claude # 正常工作,退出时 Ctrl+C
|
|
|
85
73
|
claude -c # 接上手机端的对话
|
|
86
74
|
```
|
|
87
75
|
|
|
88
|
-
>
|
|
76
|
+
> 同一时刻只能有一端活跃。从手机发消息前先退出 CLI,反之亦然。
|
|
89
77
|
|
|
90
|
-
##
|
|
78
|
+
## Git 共同作者
|
|
91
79
|
|
|
92
|
-
|
|
80
|
+
默认在 AI 发起的提交里追加 `Co-authored-by`。**关闭**:设置环境变量 **`OPEN_IM_GIT_COAUTHOR=0`** 并重启桥接。
|
|
93
81
|
|
|
94
|
-
|
|
82
|
+
## 最小配置
|
|
95
83
|
|
|
96
84
|
```json
|
|
97
85
|
{
|
|
86
|
+
"tools": {
|
|
87
|
+
"claude": { "workDir": "/path/to/project", "skipPermissions": true, "timeoutMs": 600000 }
|
|
88
|
+
},
|
|
98
89
|
"platforms": {
|
|
99
|
-
"telegram": { "enabled": true, "
|
|
90
|
+
"telegram": { "enabled": true, "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
100
91
|
}
|
|
101
92
|
}
|
|
102
93
|
```
|
|
103
94
|
|
|
104
|
-
|
|
95
|
+
在 **`platforms`** 下按需补充其他平台。完整模板请用 **`open-im init`**。
|
|
105
96
|
|
|
106
|
-
|
|
97
|
+
### Claude(Agent SDK)
|
|
107
98
|
|
|
108
|
-
|
|
99
|
+
无需本地 `claude` 可执行文件。第三方兼容接口示例:
|
|
109
100
|
|
|
110
101
|
```json
|
|
111
102
|
{
|
|
112
103
|
"tools": {
|
|
113
104
|
"claude": {
|
|
114
|
-
"workDir": "/path/to/project",
|
|
115
105
|
"env": {
|
|
116
106
|
"ANTHROPIC_AUTH_TOKEN": "your-token",
|
|
117
107
|
"ANTHROPIC_BASE_URL": "https://your-api-endpoint",
|
|
@@ -122,88 +112,25 @@ claude -c # 接上手机端的对话
|
|
|
122
112
|
}
|
|
123
113
|
```
|
|
124
114
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
npm install -g @tencent-ai/codebuddy-code
|
|
129
|
-
codebuddy login
|
|
130
|
-
```
|
|
115
|
+
### 按平台指定 AI
|
|
131
116
|
|
|
132
|
-
|
|
117
|
+
在每个平台上设置 **`platforms.<name>.aiCommand`**(`claude` / `codex` / `codebuddy`)。默认 `claude`。
|
|
133
118
|
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
"tools": {
|
|
137
|
-
"claude": { "workDir": "/path/to/project", "skipPermissions": true, "timeoutMs": 600000 }
|
|
138
|
-
},
|
|
139
|
-
"platforms": {
|
|
140
|
-
"telegram": { "enabled": true, "aiCommand": "claude", "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
119
|
+
### Web 控制台
|
|
144
120
|
|
|
145
|
-
|
|
121
|
+
`open-im start` 在 **`OPEN_IM_WEB_PORT`**(默认 **39282**)提供内置页面与 **`/api/*`**。局域网访问:`export OPEN_IM_WEB_HOST=0.0.0.0`。
|
|
146
122
|
|
|
147
123
|
### 环境变量
|
|
148
124
|
|
|
149
|
-
|
|
125
|
+
常见:**`ANTHROPIC_*`**(shell 或 **`tools.claude.env`**)、**`TELEGRAM_BOT_TOKEN`**、**`OPEN_IM_WEB_PORT`**、**`OPEN_IM_WEB_HOST`**,以及各平台的 `*_APP_ID`、`*_SECRET`、`WORKBUDDY_*` 等。
|
|
150
126
|
|
|
151
127
|
### 隐私
|
|
152
128
|
|
|
153
129
|
为改进稳定性,可能记录**匿名**运行信息(不含聊天或 prompt 内容)。若需关闭:环境变量 **`OPEN_IM_TELEMETRY=false`**,或 **`config.json`** 中 **`"telemetry": { "enabled": false }`**。
|
|
154
130
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
| 平台 | 说明 |
|
|
158
|
-
| --- | --- |
|
|
159
|
-
| Telegram | [@BotFather](https://t.me/BotFather) |
|
|
160
|
-
| 飞书 | [开放平台](https://open.feishu.cn/) |
|
|
161
|
-
| QQ | [QQ 开放平台](https://bot.q.qq.com/) |
|
|
162
|
-
| 钉钉 | 开放平台创建应用,机器人开 **Stream Mode**;可选 **`cardTemplateId`** 走 AI 助理卡片 |
|
|
163
|
-
| 企业微信 | [管理后台](https://work.weixin.qq.com/) |
|
|
164
|
-
| 微信 | **`open-im init`** → WorkBuddy OAuth |
|
|
165
|
-
| ClawBot(微信) | 扫码登录 iLink API,参见 [ClawBot 配置](#clawbot-配置) |
|
|
166
|
-
|
|
167
|
-
### ClawBot 配置
|
|
168
|
-
|
|
169
|
-
ClawBot 通过官方 iLink Bot API 连接微信(与 `@tencent-weixin/openclaw-weixin` 协议相同)。支持文本、语音、图片、文件和视频消息。
|
|
170
|
-
|
|
171
|
-
**使用方法:**
|
|
172
|
-
|
|
173
|
-
1. 在配置中启用:
|
|
174
|
-
```json
|
|
175
|
-
{
|
|
176
|
-
"platforms": {
|
|
177
|
-
"clawbot": { "enabled": true }
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
2. 打开 Web 控制台 → **ClawBot** 区域 → **扫码登录**(用微信扫码)。
|
|
182
|
-
3. 扫码成功后 `bot_token` 和 `apiUrl` 自动保存。
|
|
131
|
+
## 平台配置与故障排除
|
|
183
132
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
| 字段 | 默认值 | 说明 |
|
|
187
|
-
| --- | --- | --- |
|
|
188
|
-
| `apiUrl` | `https://ilinkai.weixin.qq.com` | iLink API 地址 |
|
|
189
|
-
| `apiToken` | — | Bot Token(扫码后自动写入) |
|
|
190
|
-
| `aiCommand` | `claude` | AI 后端覆盖 |
|
|
191
|
-
|
|
192
|
-
**协议:** POST + JSON body + Bearer token 鉴权,通过 `ilink/bot/getupdates` 长轮询 + `get_updates_buf` 游标拉取消息。
|
|
193
|
-
|
|
194
|
-
## 故障排除
|
|
195
|
-
|
|
196
|
-
| 现象 | 处理 |
|
|
197
|
-
| --- | --- |
|
|
198
|
-
| Telegram / 网络 | 配置 `proxy` 或 **`TELEGRAM_PROXY`** |
|
|
199
|
-
| QQ | 核对 **`QQ_BOT_APPID`**、**`QQ_BOT_SECRET`**;重复回复请升级版本 |
|
|
200
|
-
| 飞书卡片 | 未配回调时用 **`/mode ask`** 或 **`/mode yolo`** |
|
|
201
|
-
| 企业微信 | 先给机器人发一条消息 |
|
|
202
|
-
| 钉钉 | 开启 Stream Mode;自定义机器人可能仅纯文本 |
|
|
203
|
-
| Codex 断流 | **`CODEX_PROXY`** 或 **`tools.codex.proxy`** |
|
|
204
|
-
| CodeBuddy 登录 | **`codebuddy login`** |
|
|
205
|
-
| WorkBuddy / 微信 | 重跑 **`open-im init`**(Token 会过期) |
|
|
206
|
-
| ClawBot | Web 控制台重新扫码;`apiUrl` 默认 `https://ilinkai.weixin.qq.com` |
|
|
133
|
+
详见 **[docs/platforms.zh-CN.md](./docs/platforms.zh-CN.md)**。
|
|
207
134
|
|
|
208
135
|
## License
|
|
209
136
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
|
|
3
|
+
*/
|
|
4
|
+
import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-adapter.interface.js';
|
|
5
|
+
export declare class OpenCodeAdapter implements ToolAdapter {
|
|
6
|
+
private cliPath;
|
|
7
|
+
readonly toolId = "opencode";
|
|
8
|
+
constructor(cliPath: string);
|
|
9
|
+
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
10
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
|
|
3
|
+
*/
|
|
4
|
+
import { runOpenCode } from '../opencode/cli-runner.js';
|
|
5
|
+
export class OpenCodeAdapter {
|
|
6
|
+
cliPath;
|
|
7
|
+
toolId = 'opencode';
|
|
8
|
+
constructor(cliPath) {
|
|
9
|
+
this.cliPath = cliPath;
|
|
10
|
+
}
|
|
11
|
+
run(prompt, sessionId, workDir, callbacks, options) {
|
|
12
|
+
return runOpenCode(this.cliPath, prompt, sessionId, workDir, {
|
|
13
|
+
onText: callbacks.onText,
|
|
14
|
+
onThinking: callbacks.onThinking,
|
|
15
|
+
onToolUse: callbacks.onToolUse,
|
|
16
|
+
onComplete: (raw) => {
|
|
17
|
+
const result = {
|
|
18
|
+
success: raw.success,
|
|
19
|
+
result: raw.result,
|
|
20
|
+
accumulated: raw.accumulated,
|
|
21
|
+
cost: raw.cost,
|
|
22
|
+
durationMs: raw.durationMs,
|
|
23
|
+
model: raw.model,
|
|
24
|
+
numTurns: raw.numTurns,
|
|
25
|
+
toolStats: raw.toolStats,
|
|
26
|
+
};
|
|
27
|
+
callbacks.onComplete(result);
|
|
28
|
+
},
|
|
29
|
+
onError: (err) => {
|
|
30
|
+
const msg = typeof err === 'string' ? err : String(err);
|
|
31
|
+
const friendly = msg.includes('session not found') ||
|
|
32
|
+
msg.includes('Session not found') ||
|
|
33
|
+
msg.includes('no sessions found')
|
|
34
|
+
? 'OpenCode 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
35
|
+
: msg;
|
|
36
|
+
callbacks.onError(friendly);
|
|
37
|
+
},
|
|
38
|
+
onSessionId: callbacks.onSessionId,
|
|
39
|
+
onSessionInvalid: callbacks.onSessionInvalid,
|
|
40
|
+
}, {
|
|
41
|
+
skipPermissions: options?.skipPermissions,
|
|
42
|
+
model: options?.model,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -2,6 +2,7 @@ import { getConfiguredAiCommands } from '../config.js';
|
|
|
2
2
|
import { ClaudeSDKAdapter, configureClaudeSdkSessionIdle } from './claude-sdk-adapter.js';
|
|
3
3
|
import { CodexAdapter } from './codex-adapter.js';
|
|
4
4
|
import { CodeBuddyAdapter } from './codebuddy-adapter.js';
|
|
5
|
+
import { OpenCodeAdapter } from './opencode-adapter.js';
|
|
5
6
|
import { createLogger } from '../logger.js';
|
|
6
7
|
import { destroyAllLiveChildren } from '../shared/process-kill.js';
|
|
7
8
|
const log = createLogger('Registry');
|
|
@@ -24,6 +25,10 @@ export function initAdapters(config) {
|
|
|
24
25
|
log.info('CodeBuddy CLI adapter enabled');
|
|
25
26
|
adapters.set('codebuddy', new CodeBuddyAdapter(config.codebuddyCliPath));
|
|
26
27
|
}
|
|
28
|
+
if (aiCommand === 'opencode') {
|
|
29
|
+
log.info('OpenCode CLI adapter enabled');
|
|
30
|
+
adapters.set('opencode', new OpenCodeAdapter(config.opencodeCliPath));
|
|
31
|
+
}
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
export function getAdapter(aiCommand) {
|
package/dist/commands/handler.js
CHANGED
|
@@ -189,12 +189,13 @@ export class CommandHandler {
|
|
|
189
189
|
}
|
|
190
190
|
getAiVersion(aiCommand) {
|
|
191
191
|
if (aiCommand === 'claude') {
|
|
192
|
-
// Claude 使用 SDK,返回 SDK 版本
|
|
193
192
|
return Promise.resolve('SDK Mode');
|
|
194
193
|
}
|
|
195
194
|
const cmd = aiCommand === 'codex'
|
|
196
195
|
? this.deps.config.codexCliPath
|
|
197
|
-
:
|
|
196
|
+
: aiCommand === 'opencode'
|
|
197
|
+
? this.deps.config.opencodeCliPath
|
|
198
|
+
: this.deps.config.codebuddyCliPath;
|
|
198
199
|
return new Promise((resolve) => {
|
|
199
200
|
execFile(cmd, ['--version'], { timeout: 5000 }, (err, stdout) => {
|
|
200
201
|
resolve(err ? '未知' : (stdout?.toString().trim() || '未知'));
|
package/dist/config/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LogLevel } from '../logger.js';
|
|
2
2
|
export type Platform = 'clawbot' | 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
|
|
3
|
-
export type AiCommand = 'claude' | 'codex' | 'codebuddy';
|
|
3
|
+
export type AiCommand = 'claude' | 'codex' | 'codebuddy' | 'opencode';
|
|
4
4
|
export interface Config {
|
|
5
5
|
enabledPlatforms: Platform[];
|
|
6
6
|
telegramBotToken?: string;
|
|
@@ -24,6 +24,7 @@ export interface Config {
|
|
|
24
24
|
clawbotAllowedUserIds: string[];
|
|
25
25
|
codexCliPath: string;
|
|
26
26
|
codebuddyCliPath: string;
|
|
27
|
+
opencodeCliPath: string;
|
|
27
28
|
/** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
|
|
28
29
|
claudeProxy?: string;
|
|
29
30
|
/** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
|
|
@@ -182,6 +183,9 @@ export interface FileToolCodex {
|
|
|
182
183
|
export interface FileToolCodeBuddy {
|
|
183
184
|
cliPath?: string;
|
|
184
185
|
}
|
|
186
|
+
export interface FileToolOpenCode {
|
|
187
|
+
cliPath?: string;
|
|
188
|
+
}
|
|
185
189
|
export interface FileConfig {
|
|
186
190
|
telegramBotToken?: string;
|
|
187
191
|
feishuAppId?: string;
|
|
@@ -203,6 +207,7 @@ export interface FileConfig {
|
|
|
203
207
|
claude?: FileToolClaude;
|
|
204
208
|
codex?: FileToolCodex;
|
|
205
209
|
codebuddy?: FileToolCodeBuddy;
|
|
210
|
+
opencode?: FileToolOpenCode;
|
|
206
211
|
};
|
|
207
212
|
logDir?: string;
|
|
208
213
|
logLevel?: LogLevel;
|
package/dist/config-web.js
CHANGED
|
@@ -351,6 +351,7 @@ function buildInitialPayload(file) {
|
|
|
351
351
|
claudeProxy: file.tools?.claude?.proxy ?? "",
|
|
352
352
|
codexCliPath: file.tools?.codex?.cliPath ?? "codex",
|
|
353
353
|
codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
|
|
354
|
+
opencodeCliPath: file.tools?.opencode?.cliPath ?? "opencode",
|
|
354
355
|
codexProxy: file.tools?.codex?.proxy ?? "",
|
|
355
356
|
codexApiKey: (() => {
|
|
356
357
|
if (process.env.OPENAI_API_KEY)
|
|
@@ -505,6 +506,7 @@ function createProbeConfig(values) {
|
|
|
505
506
|
logLevel: "INFO",
|
|
506
507
|
telemetry: { enabled: true },
|
|
507
508
|
codebuddyCliPath: "codebuddy",
|
|
509
|
+
opencodeCliPath: "opencode",
|
|
508
510
|
platforms: {},
|
|
509
511
|
...values,
|
|
510
512
|
};
|
|
@@ -704,6 +706,10 @@ function toFileConfig(payload, existing) {
|
|
|
704
706
|
...existing.tools?.codebuddy,
|
|
705
707
|
cliPath: clean(payload.ai.codebuddyCliPath) ?? "codebuddy",
|
|
706
708
|
},
|
|
709
|
+
opencode: {
|
|
710
|
+
...existing.tools?.opencode,
|
|
711
|
+
cliPath: clean(payload.ai.opencodeCliPath) ?? "opencode",
|
|
712
|
+
},
|
|
707
713
|
},
|
|
708
714
|
platforms: {
|
|
709
715
|
...existing.platforms,
|
package/dist/config.js
CHANGED
|
@@ -231,6 +231,8 @@ export function loadConfig() {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
const topencode = file.tools?.opencode ?? {};
|
|
235
|
+
const opencodeCliPath = process.env.OPENCODE_CLI_PATH ?? topencode.cliPath ?? 'opencode';
|
|
234
236
|
const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? tc.workDir ?? process.cwd();
|
|
235
237
|
const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
|
|
236
238
|
? false
|
|
@@ -563,6 +565,7 @@ export function loadConfig() {
|
|
|
563
565
|
clawbotAllowedUserIds,
|
|
564
566
|
codexCliPath,
|
|
565
567
|
codebuddyCliPath,
|
|
568
|
+
opencodeCliPath,
|
|
566
569
|
claudeProxy,
|
|
567
570
|
codexProxy,
|
|
568
571
|
claudeWorkDir,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode CLI runner — spawns `opencode run` in non-interactive mode.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode outputs plain text to stdout. Session continuation uses `-s <id>`.
|
|
5
|
+
*/
|
|
6
|
+
export interface OpenCodeRunCallbacks {
|
|
7
|
+
onText: (accumulated: string) => void;
|
|
8
|
+
onThinking?: (accumulated: string) => void;
|
|
9
|
+
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
10
|
+
onComplete: (result: {
|
|
11
|
+
success: boolean;
|
|
12
|
+
result: string;
|
|
13
|
+
accumulated: string;
|
|
14
|
+
cost: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
model?: string;
|
|
17
|
+
numTurns: number;
|
|
18
|
+
toolStats: Record<string, number>;
|
|
19
|
+
}) => void;
|
|
20
|
+
onError: (error: string) => void;
|
|
21
|
+
onSessionId?: (sessionId: string) => void;
|
|
22
|
+
onSessionInvalid?: () => void;
|
|
23
|
+
}
|
|
24
|
+
export interface OpenCodeRunOptions {
|
|
25
|
+
skipPermissions?: boolean;
|
|
26
|
+
model?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface OpenCodeRunHandle {
|
|
29
|
+
abort: () => void;
|
|
30
|
+
}
|
|
31
|
+
export declare function buildOpenCodeArgs(prompt: string, sessionId: string | undefined, workDir: string, options?: OpenCodeRunOptions): string[];
|
|
32
|
+
export declare function runOpenCode(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: OpenCodeRunCallbacks, options?: OpenCodeRunOptions): OpenCodeRunHandle;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode CLI runner — spawns `opencode run` in non-interactive mode.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode outputs plain text to stdout. Session continuation uses `-s <id>`.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { createLogger } from '../logger.js';
|
|
9
|
+
import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
|
|
10
|
+
import { killProcessTree, trackChild } from '../shared/process-kill.js';
|
|
11
|
+
const log = createLogger('OpenCodeCli');
|
|
12
|
+
export function buildOpenCodeArgs(prompt, sessionId, workDir, options) {
|
|
13
|
+
const args = ['run'];
|
|
14
|
+
// Working directory
|
|
15
|
+
args.push('--dir', workDir);
|
|
16
|
+
// Session continuation
|
|
17
|
+
if (sessionId) {
|
|
18
|
+
args.push('--session', sessionId);
|
|
19
|
+
}
|
|
20
|
+
// Model override
|
|
21
|
+
if (options?.model) {
|
|
22
|
+
args.push('--model', options.model);
|
|
23
|
+
}
|
|
24
|
+
// Skip permissions
|
|
25
|
+
if (options?.skipPermissions) {
|
|
26
|
+
args.push('--dangerously-skip-permissions');
|
|
27
|
+
}
|
|
28
|
+
// Prompt as positional argument
|
|
29
|
+
args.push(prompt);
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
export function runOpenCode(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
33
|
+
const args = buildOpenCodeArgs(prompt, sessionId, workDir, options);
|
|
34
|
+
const env = processEnvForNonClaudeCliChild();
|
|
35
|
+
log.info(`Spawning OpenCode: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
|
|
36
|
+
const child = spawn(cliPath, args, {
|
|
37
|
+
cwd: workDir,
|
|
38
|
+
detached: process.platform !== 'win32',
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
+
env,
|
|
41
|
+
windowsHide: process.platform === 'win32',
|
|
42
|
+
});
|
|
43
|
+
trackChild(child);
|
|
44
|
+
// Close stdin — prompt is passed as argument
|
|
45
|
+
child.stdin?.end();
|
|
46
|
+
let accumulated = '';
|
|
47
|
+
let completed = false;
|
|
48
|
+
const toolStats = {};
|
|
49
|
+
const startTime = Date.now();
|
|
50
|
+
const rl = createInterface({ input: child.stdout });
|
|
51
|
+
const MAX_STDERR_HEAD = 4 * 1024;
|
|
52
|
+
const MAX_STDERR_TAIL = 6 * 1024;
|
|
53
|
+
let stderrHead = '';
|
|
54
|
+
let stderrTail = '';
|
|
55
|
+
let stderrTotal = 0;
|
|
56
|
+
let stderrHeadFull = false;
|
|
57
|
+
child.stderr?.on('data', (chunk) => {
|
|
58
|
+
const text = chunk.toString();
|
|
59
|
+
stderrTotal += text.length;
|
|
60
|
+
if (!stderrHeadFull) {
|
|
61
|
+
const room = MAX_STDERR_HEAD - stderrHead.length;
|
|
62
|
+
if (room > 0) {
|
|
63
|
+
stderrHead += text.slice(0, room);
|
|
64
|
+
if (stderrHead.length >= MAX_STDERR_HEAD)
|
|
65
|
+
stderrHeadFull = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
stderrTail += text;
|
|
69
|
+
if (stderrTail.length > MAX_STDERR_TAIL) {
|
|
70
|
+
stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
|
|
71
|
+
}
|
|
72
|
+
log.debug(`[stderr] ${text.trimEnd()}`);
|
|
73
|
+
});
|
|
74
|
+
rl.on('line', (line) => {
|
|
75
|
+
if (completed)
|
|
76
|
+
return;
|
|
77
|
+
// OpenCode outputs plain text — accumulate each line
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed)
|
|
80
|
+
return;
|
|
81
|
+
// Skip spinner lines (ANSI escape sequences or common spinner chars)
|
|
82
|
+
if (/^[⠀-⣿\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏▪●◆⬤◉⬤⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]+$/.test(trimmed))
|
|
83
|
+
return;
|
|
84
|
+
accumulated += (accumulated ? '\n' : '') + line;
|
|
85
|
+
callbacks.onText(accumulated);
|
|
86
|
+
});
|
|
87
|
+
// Also capture stdout as a raw stream for binary-like streaming
|
|
88
|
+
child.stdout?.on('data', (chunk) => {
|
|
89
|
+
// The readline interface handles line-by-line, but we also
|
|
90
|
+
// want to capture any partial data for streaming updates
|
|
91
|
+
const text = chunk.toString();
|
|
92
|
+
if (!completed && text.trim()) {
|
|
93
|
+
log.debug(`[stdout chunk] ${text.substring(0, 200)}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
let exitCode = null;
|
|
97
|
+
let rlClosed = false;
|
|
98
|
+
let childClosed = false;
|
|
99
|
+
const finalize = () => {
|
|
100
|
+
if (!rlClosed || !childClosed)
|
|
101
|
+
return;
|
|
102
|
+
if (completed)
|
|
103
|
+
return;
|
|
104
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
105
|
+
let errMsg = '';
|
|
106
|
+
if (stderrTotal > 0) {
|
|
107
|
+
if (!stderrHeadFull) {
|
|
108
|
+
errMsg = stderrHead;
|
|
109
|
+
}
|
|
110
|
+
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
111
|
+
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
errMsg =
|
|
115
|
+
stderrHead +
|
|
116
|
+
`\n\n... (omitted ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} bytes) ...\n\n` +
|
|
117
|
+
stderrTail;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (sessionId &&
|
|
121
|
+
(errMsg.includes('session not found') ||
|
|
122
|
+
errMsg.includes('Session not found') ||
|
|
123
|
+
errMsg.includes('no sessions found'))) {
|
|
124
|
+
callbacks.onSessionInvalid?.();
|
|
125
|
+
}
|
|
126
|
+
callbacks.onError(errMsg || `OpenCode exited with code ${exitCode}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Exit code 0 but no onComplete fired yet — treat accumulated text as result
|
|
130
|
+
if (!completed) {
|
|
131
|
+
completed = true;
|
|
132
|
+
callbacks.onComplete({
|
|
133
|
+
success: true,
|
|
134
|
+
result: accumulated,
|
|
135
|
+
accumulated,
|
|
136
|
+
cost: 0,
|
|
137
|
+
durationMs: Date.now() - startTime,
|
|
138
|
+
numTurns: 1,
|
|
139
|
+
toolStats,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
rl.on('close', () => {
|
|
144
|
+
rlClosed = true;
|
|
145
|
+
finalize();
|
|
146
|
+
});
|
|
147
|
+
child.on('close', (code) => {
|
|
148
|
+
exitCode = code;
|
|
149
|
+
childClosed = true;
|
|
150
|
+
finalize();
|
|
151
|
+
});
|
|
152
|
+
child.on('error', (err) => {
|
|
153
|
+
log.error('OpenCode spawn error:', err);
|
|
154
|
+
callbacks.onError(`Failed to start OpenCode: ${err.message}`);
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
abort: () => {
|
|
158
|
+
killProcessTree(child);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|