@wu529778790/open-im 1.7.1-beta.1 → 1.7.1-beta.10
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 +83 -104
- package/README.zh-CN.md +84 -105
- package/dist/cli.js +10 -3
- package/dist/config-web-page-i18n.d.ts +0 -3
- package/dist/config-web-page-i18n.js +0 -3
- package/dist/config-web-page-script.js +64 -11
- package/dist/config-web-page-template.js +14 -13
- package/dist/config-web.d.ts +1 -0
- package/dist/config-web.js +215 -5
- package/dist/config.d.ts +7 -1
- package/dist/config.js +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,19 +14,6 @@ Multi-platform IM bridge for AI CLI tools. Connect Telegram, Feishu, WeCom, Ding
|
|
|
14
14
|
- Isolated sessions: each user gets an independent local session, and `/new` resets it
|
|
15
15
|
- Built-in commands: `/help`, `/new`, `/cd`, `/pwd`, `/status`
|
|
16
16
|
|
|
17
|
-
## Coverage Matrix
|
|
18
|
-
|
|
19
|
-
Capability levels: `Native` = fully supported in-channel, `Fallback` = degraded behavior or text fallback, `None` = not currently supported.
|
|
20
|
-
|
|
21
|
-
| Platform | Text Inbound | Image Inbound | File Inbound | Voice Inbound | Video Inbound | Streaming Reply | Image Reply | Card Reply |
|
|
22
|
-
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
|
23
|
-
| Telegram | Native | Native | Native | Native | Native | Native | Native | Native |
|
|
24
|
-
| Feishu | Native | Native | Native | Fallback | Fallback | Native | Native | Native |
|
|
25
|
-
| QQ | Native | Fallback | Fallback | Fallback | Fallback | None | Fallback | Fallback |
|
|
26
|
-
| WeCom | Native | Fallback | Fallback | Fallback | Fallback | Native | Native | Native |
|
|
27
|
-
| DingTalk | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
28
|
-
| WeChat (experimental) | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
29
|
-
|
|
30
17
|
## Requirements
|
|
31
18
|
|
|
32
19
|
- Node.js >= 20
|
|
@@ -50,12 +37,13 @@ The config file is stored at `~/.open-im/config.json` by default.
|
|
|
50
37
|
|
|
51
38
|
## CLI Commands
|
|
52
39
|
|
|
53
|
-
| Command
|
|
54
|
-
|
|
|
55
|
-
| `open-im init`
|
|
56
|
-
| `open-im start`
|
|
57
|
-
| `open-im stop`
|
|
58
|
-
| `open-im dev`
|
|
40
|
+
| Command | Description |
|
|
41
|
+
| ----------------- | ---------------------------------------------------------------- |
|
|
42
|
+
| `open-im init` | Initialize or append configuration without starting the service |
|
|
43
|
+
| `open-im start` | Run the service in the background |
|
|
44
|
+
| `open-im stop` | Stop the background service |
|
|
45
|
+
| `open-im dev` | Run in the foreground for development/debugging |
|
|
46
|
+
| `open-im dashboard` | Run only the config web UI (no bridge) |
|
|
59
47
|
|
|
60
48
|
## Server Deployment & Config Page
|
|
61
49
|
|
|
@@ -65,59 +53,50 @@ Open the config page at [`http://127.0.0.1:39282`](http://127.0.0.1:39282) (or t
|
|
|
65
53
|
|
|
66
54
|
- **Dashboard** – Configured / Enabled platform count and service status (Idle or Running)
|
|
67
55
|
- **Platforms** – Enable and configure Telegram, Feishu, QQ, WeCom, and DingTalk (credentials, proxy, per-platform AI tool, allowed user IDs). Each platform has a “Test Configuration” button.
|
|
68
|
-
- **AI Tooling** – **General**: default AI tool (Claude / Codex / CodeBuddy), work directory, hook port, log level. **Per-tool tabs**: Claude (CLI path, timeout, proxy, config path,
|
|
56
|
+
- **AI Tooling** – **General**: default AI tool (Claude / Codex / CodeBuddy), work directory, hook port, log level. **Per-tool tabs**: Claude (CLI path, timeout, proxy, config path, ANTHROPIC\_\* fields), Codex (CLI path, timeout, proxy), CodeBuddy (CLI path, timeout).
|
|
69
57
|
- **Service control** – Validate config, Save, Start bridge, Stop bridge.
|
|
70
58
|
|
|
71
59
|
WeChat is not in the web UI; configure it in `~/.open-im/config.json` or via `open-im init` if needed.
|
|
72
60
|
|
|
73
|
-
- `open-im start` serves both the config page and the bridge.
|
|
61
|
+
- `open-im start` serves both the config page and the bridge on your local machine.
|
|
74
62
|
- `open-im dev` opens the page automatically only when setup is incomplete.
|
|
75
|
-
- To open the page when config already exists, run `open-im
|
|
76
|
-
|
|
77
|
-
### On a headless server (no GUI)
|
|
78
|
-
|
|
79
|
-
Many servers do not have a desktop environment or browser. In that case, trying to auto-launch a browser (`xdg-open`, `open`, `start`) is unnecessary and may even fail. Use this pattern instead:
|
|
80
|
-
|
|
81
|
-
- **1) Disable automatic browser launch**
|
|
82
|
-
|
|
83
|
-
On the server:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
export OPEN_IM_NO_BROWSER=1
|
|
87
|
-
open-im start
|
|
88
|
-
```
|
|
63
|
+
- To open the page when config already exists, you can also run `open-im dashboard` to launch the config UI only (without starting the bridge).
|
|
89
64
|
|
|
90
|
-
|
|
65
|
+
### Recommended server workflow
|
|
91
66
|
|
|
92
|
-
|
|
67
|
+
On a remote server, the simplest and safest pattern is:
|
|
93
68
|
|
|
94
|
-
|
|
69
|
+
1. **Use `dashboard` to configure via browser**
|
|
95
70
|
|
|
96
|
-
|
|
97
|
-
ss -lntp | grep 39282 # or: netstat -lntp | grep 39282
|
|
98
|
-
curl -v http://127.0.0.1:39282/
|
|
99
|
-
```
|
|
71
|
+
On the server:
|
|
100
72
|
|
|
101
|
-
|
|
73
|
+
```bash
|
|
74
|
+
export OPEN_IM_NO_BROWSER=1
|
|
75
|
+
# Optional: bind to all interfaces if you want to open from another device
|
|
76
|
+
# export OPEN_IM_WEB_HOST=0.0.0.0
|
|
77
|
+
open-im dashboard
|
|
78
|
+
```
|
|
102
79
|
|
|
103
|
-
-
|
|
80
|
+
- This starts only the config web UI (no bridge yet).
|
|
81
|
+
- If `OPEN_IM_WEB_HOST` is `0.0.0.0`, the server will print a one-time login URL like:
|
|
104
82
|
|
|
105
|
-
|
|
83
|
+
```text
|
|
84
|
+
http://your-server-ip:39282/?login_token=xxxx
|
|
85
|
+
```
|
|
106
86
|
|
|
107
|
-
|
|
108
|
-
# On your local machine:
|
|
109
|
-
ssh -L 39282:127.0.0.1:39282 user@your-server-ip
|
|
110
|
-
```
|
|
87
|
+
- Open this URL in your browser, complete all platform/AI settings, then click **Start bridge** in the web UI.
|
|
111
88
|
|
|
112
|
-
|
|
89
|
+
2. **Run the bridge as a background service**
|
|
113
90
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
91
|
+
After configuration is saved, you have two options:
|
|
92
|
+
- Start from the web UI: use the **Start bridge** button on the Service panel.
|
|
93
|
+
- Or start from the CLI:
|
|
117
94
|
|
|
118
|
-
|
|
95
|
+
```bash
|
|
96
|
+
open-im start
|
|
97
|
+
```
|
|
119
98
|
|
|
120
|
-
|
|
99
|
+
This runs the full bridge in the background using the saved config.
|
|
121
100
|
|
|
122
101
|
## Session Behavior
|
|
123
102
|
|
|
@@ -268,46 +247,46 @@ The following is valid JSON and can be saved directly as `~/.open-im/config.json
|
|
|
268
247
|
|
|
269
248
|
### Common Environment Variables
|
|
270
249
|
|
|
271
|
-
| Variable
|
|
272
|
-
|
|
|
273
|
-
| `AI_COMMAND`
|
|
274
|
-
| `CLAUDE_WORK_DIR`
|
|
275
|
-
| `LOG_DIR`
|
|
276
|
-
| `LOG_LEVEL`
|
|
277
|
-
| `HOOK_PORT`
|
|
278
|
-
| `CODEX_PROXY`
|
|
279
|
-
| `OPENAI_API_KEY`
|
|
280
|
-
| `CODEBUDDY_CLI_PATH`
|
|
281
|
-
| `CODEBUDDY_TIMEOUT_MS`
|
|
282
|
-
| `CODEBUDDY_SKIP_PERMISSIONS` | Override CodeBuddy skip-permissions behavior
|
|
283
|
-
| `CODEBUDDY_IDLE_TIMEOUT_MS`
|
|
284
|
-
| `CODEBUDDY_API_KEY`
|
|
285
|
-
| `CODEBUDDY_AUTH_TOKEN`
|
|
286
|
-
| `TELEGRAM_BOT_TOKEN`
|
|
287
|
-
| `TELEGRAM_PROXY`
|
|
288
|
-
| `TELEGRAM_ALLOWED_USER_IDS`
|
|
289
|
-
| `FEISHU_APP_ID`
|
|
290
|
-
| `FEISHU_APP_SECRET`
|
|
291
|
-
| `FEISHU_ALLOWED_USER_IDS`
|
|
292
|
-
| `QQ_BOT_APPID`
|
|
293
|
-
| `QQ_BOT_SECRET`
|
|
294
|
-
| `QQ_BOT_SANDBOX`
|
|
295
|
-
| `QQ_ALLOWED_USER_IDS`
|
|
296
|
-
| `DINGTALK_CLIENT_ID`
|
|
297
|
-
| `DINGTALK_CLIENT_SECRET`
|
|
298
|
-
| `DINGTALK_CARD_TEMPLATE_ID`
|
|
299
|
-
| `DINGTALK_ALLOWED_USER_IDS`
|
|
300
|
-
| `WEWORK_CORP_ID`
|
|
301
|
-
| `WEWORK_SECRET`
|
|
302
|
-
| `WEWORK_WS_URL`
|
|
303
|
-
| `WEWORK_ALLOWED_USER_IDS`
|
|
304
|
-
| `WECHAT_APP_ID`
|
|
305
|
-
| `WECHAT_APP_SECRET`
|
|
306
|
-
| `WECHAT_TOKEN`
|
|
307
|
-
| `WECHAT_GUID`
|
|
308
|
-
| `WECHAT_USER_ID`
|
|
309
|
-
| `WECHAT_WS_URL`
|
|
310
|
-
| `WECHAT_ALLOWED_USER_IDS`
|
|
250
|
+
| Variable | Description |
|
|
251
|
+
| ---------------------------- | ---------------------------------------------------------------------- |
|
|
252
|
+
| `AI_COMMAND` | Select `claude`, `codex`, or `codebuddy` |
|
|
253
|
+
| `CLAUDE_WORK_DIR` | Default session working directory |
|
|
254
|
+
| `LOG_DIR` | Log directory |
|
|
255
|
+
| `LOG_LEVEL` | Log level |
|
|
256
|
+
| `HOOK_PORT` | Permission service port |
|
|
257
|
+
| `CODEX_PROXY` | Proxy used by Codex to access `chatgpt.com` |
|
|
258
|
+
| `OPENAI_API_KEY` | Codex API key, can replace `codex login` |
|
|
259
|
+
| `CODEBUDDY_CLI_PATH` | Override CodeBuddy CLI path |
|
|
260
|
+
| `CODEBUDDY_TIMEOUT_MS` | Override CodeBuddy timeout |
|
|
261
|
+
| `CODEBUDDY_SKIP_PERMISSIONS` | Override CodeBuddy skip-permissions behavior |
|
|
262
|
+
| `CODEBUDDY_IDLE_TIMEOUT_MS` | Abort CodeBuddy when it stays silent for too long |
|
|
263
|
+
| `CODEBUDDY_API_KEY` | CodeBuddy API key, can replace `codebuddy login` |
|
|
264
|
+
| `CODEBUDDY_AUTH_TOKEN` | CodeBuddy auth token, can replace `codebuddy login` |
|
|
265
|
+
| `TELEGRAM_BOT_TOKEN` | Telegram bot token |
|
|
266
|
+
| `TELEGRAM_PROXY` | Telegram proxy URL |
|
|
267
|
+
| `TELEGRAM_ALLOWED_USER_IDS` | Telegram allowlist |
|
|
268
|
+
| `FEISHU_APP_ID` | Feishu app ID |
|
|
269
|
+
| `FEISHU_APP_SECRET` | Feishu app secret |
|
|
270
|
+
| `FEISHU_ALLOWED_USER_IDS` | Feishu allowlist |
|
|
271
|
+
| `QQ_BOT_APPID` | QQ bot app ID |
|
|
272
|
+
| `QQ_BOT_SECRET` | QQ bot app secret |
|
|
273
|
+
| `QQ_BOT_SANDBOX` | QQ bot sandbox mode (`1` / `true` to enable, disabled by default) |
|
|
274
|
+
| `QQ_ALLOWED_USER_IDS` | QQ allowlist |
|
|
275
|
+
| `DINGTALK_CLIENT_ID` | DingTalk client ID / AppKey |
|
|
276
|
+
| `DINGTALK_CLIENT_SECRET` | DingTalk client secret / AppSecret |
|
|
277
|
+
| `DINGTALK_CARD_TEMPLATE_ID` | DingTalk AI card template ID; enables single-message streaming replies |
|
|
278
|
+
| `DINGTALK_ALLOWED_USER_IDS` | DingTalk allowlist |
|
|
279
|
+
| `WEWORK_CORP_ID` | WeCom bot ID |
|
|
280
|
+
| `WEWORK_SECRET` | WeCom secret |
|
|
281
|
+
| `WEWORK_WS_URL` | WeCom WebSocket URL |
|
|
282
|
+
| `WEWORK_ALLOWED_USER_IDS` | WeCom allowlist |
|
|
283
|
+
| `WECHAT_APP_ID` | WeChat standard mode app ID |
|
|
284
|
+
| `WECHAT_APP_SECRET` | WeChat standard mode app secret |
|
|
285
|
+
| `WECHAT_TOKEN` | WeChat AGP mode token |
|
|
286
|
+
| `WECHAT_GUID` | WeChat AGP mode GUID |
|
|
287
|
+
| `WECHAT_USER_ID` | WeChat AGP mode user ID |
|
|
288
|
+
| `WECHAT_WS_URL` | WeChat WebSocket URL |
|
|
289
|
+
| `WECHAT_ALLOWED_USER_IDS` | WeChat allowlist |
|
|
311
290
|
|
|
312
291
|
### Platform Setup Sources
|
|
313
292
|
|
|
@@ -328,15 +307,15 @@ DingTalk AI card templates are already compatible with the official "Search Resu
|
|
|
328
307
|
|
|
329
308
|
## IM Commands
|
|
330
309
|
|
|
331
|
-
| Command
|
|
332
|
-
|
|
|
333
|
-
| `/help`
|
|
334
|
-
| `/new`
|
|
335
|
-
| `/status`
|
|
336
|
-
| `/cd <path>`
|
|
337
|
-
| `/pwd`
|
|
338
|
-
| `/allow` `/y` | Approve a permission request
|
|
339
|
-
| `/deny` `/n`
|
|
310
|
+
| Command | Description |
|
|
311
|
+
| ------------- | -------------------------------------------------------- |
|
|
312
|
+
| `/help` | Show help |
|
|
313
|
+
| `/new` | Start a new session |
|
|
314
|
+
| `/status` | Show AI tool, version, session directory, and session ID |
|
|
315
|
+
| `/cd <path>` | Change the session working directory |
|
|
316
|
+
| `/pwd` | Show the current session working directory |
|
|
317
|
+
| `/allow` `/y` | Approve a permission request |
|
|
318
|
+
| `/deny` `/n` | Reject a permission request |
|
|
340
319
|
|
|
341
320
|
## Troubleshooting
|
|
342
321
|
|
package/README.zh-CN.md
CHANGED
|
@@ -14,19 +14,6 @@
|
|
|
14
14
|
- 会话隔离:每个用户独立维护本地会话,`/new` 可重置
|
|
15
15
|
- 常用命令:支持 `/help`、`/new`、`/cd`、`/pwd`、`/status`
|
|
16
16
|
|
|
17
|
-
## 覆盖矩阵
|
|
18
|
-
|
|
19
|
-
能力等级说明:`Native` 表示平台内原生支持,`Fallback` 表示有降级方案或文本兜底,`None` 表示当前暂不支持。
|
|
20
|
-
|
|
21
|
-
| 平台 | 文本输入 | 图片输入 | 文件输入 | 语音输入 | 视频输入 | 流式回复 | 图片回复 | 卡片回复 |
|
|
22
|
-
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
|
23
|
-
| Telegram | Native | Native | Native | Native | Native | Native | Native | Native |
|
|
24
|
-
| 飞书 | Native | Native | Native | Fallback | Fallback | Native | Native | Native |
|
|
25
|
-
| QQ | Native | Fallback | Fallback | Fallback | Fallback | None | Fallback | Fallback |
|
|
26
|
-
| 企业微信 | Native | Fallback | Fallback | Fallback | Fallback | Native | Native | Native |
|
|
27
|
-
| 钉钉 | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
28
|
-
| 微信(测试中) | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
29
|
-
|
|
30
17
|
## 环境要求
|
|
31
18
|
|
|
32
19
|
- Node.js >= 20
|
|
@@ -50,12 +37,13 @@ open-im start
|
|
|
50
37
|
|
|
51
38
|
## CLI 命令
|
|
52
39
|
|
|
53
|
-
| 命令
|
|
54
|
-
|
|
|
55
|
-
| `open-im init`
|
|
56
|
-
| `open-im start`
|
|
57
|
-
| `open-im stop`
|
|
58
|
-
| `open-im dev`
|
|
40
|
+
| 命令 | 说明 |
|
|
41
|
+
| ----------------- | ------------------------------ |
|
|
42
|
+
| `open-im init` | 初始化或追加配置,不启动服务 |
|
|
43
|
+
| `open-im start` | 后台运行服务 |
|
|
44
|
+
| `open-im stop` | 停止后台服务 |
|
|
45
|
+
| `open-im dev` | 前台运行(调试模式) |
|
|
46
|
+
| `open-im dashboard` | 仅启动 Web 配置页(不启动桥接服务) |
|
|
59
47
|
|
|
60
48
|
## 服务器部署与图形化配置
|
|
61
49
|
|
|
@@ -71,59 +59,50 @@ open-im start
|
|
|
71
59
|
|
|
72
60
|
- **概览** – 已配置/已启用平台数量、服务状态(未启动或运行中)
|
|
73
61
|
- **平台配置** – 启用并填写 Telegram、飞书、QQ、企业微信、钉钉的凭证(Bot Token/App ID/Secret、代理、该平台使用的 AI 工具、白名单用户 ID)。每个平台提供「校验配置」按钮
|
|
74
|
-
- **AI 工具配置** – **公共**:默认 AI 工具(Claude / Codex / CodeBuddy)、工作目录、Hook 端口、日志级别。**分工具**:Claude(CLI 路径、超时、代理、配置路径、
|
|
62
|
+
- **AI 工具配置** – **公共**:默认 AI 工具(Claude / Codex / CodeBuddy)、工作目录、Hook 端口、日志级别。**分工具**:Claude(CLI 路径、超时、代理、配置路径、ANTHROPIC\_\* 等)、Codex(CLI 路径、超时、代理)、CodeBuddy(CLI 路径、超时)
|
|
75
63
|
- **服务控制** – 校验配置、保存、启动桥接、停止桥接
|
|
76
64
|
|
|
77
65
|
微信暂不在网页中配置,如需使用请在 `~/.open-im/config.json` 中手动配置或通过 `open-im init` 引导。
|
|
78
66
|
|
|
79
|
-
- `open-im start`
|
|
80
|
-
- `open-im dev`
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
### 在服务器上部署(无图形界面)
|
|
84
|
-
|
|
85
|
-
很多服务器没有桌面环境和浏览器,此时「自动打开浏览器」既没意义,还可能因为缺少 `xdg-open` 报错。推荐如下用法:
|
|
86
|
-
|
|
87
|
-
- **1)关闭自动打开浏览器**
|
|
88
|
-
|
|
89
|
-
在服务器上设置环境变量,然后启动:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
export OPEN_IM_NO_BROWSER=1
|
|
93
|
-
open-im start
|
|
94
|
-
```
|
|
67
|
+
- `open-im start` 会同时启动桥接服务并提供该配置页(本机场景)。
|
|
68
|
+
- `open-im dev` 仅在未完成配置时自动打开页面。
|
|
69
|
+
- 已有配置但想单独打开配置页时,可以使用 `open-im dashboard` 启动仅 Web 配置服务。
|
|
95
70
|
|
|
96
|
-
|
|
71
|
+
### 推荐的服务器端使用方式
|
|
97
72
|
|
|
98
|
-
|
|
73
|
+
在远程服务器上,建议的最简单、安全的方式是:
|
|
99
74
|
|
|
100
|
-
|
|
75
|
+
1. **先通过 `dashboard` 在浏览器里完成配置**
|
|
101
76
|
|
|
102
|
-
|
|
103
|
-
ss -lntp | grep 39282 # 或 netstat -lntp | grep 39282
|
|
104
|
-
curl -v http://127.0.0.1:39282/
|
|
105
|
-
```
|
|
77
|
+
在服务器上执行:
|
|
106
78
|
|
|
107
|
-
|
|
79
|
+
```bash
|
|
80
|
+
export OPEN_IM_NO_BROWSER=1
|
|
81
|
+
# 可选:如果希望从其他设备访问配置页,可以绑定到所有网卡
|
|
82
|
+
# export OPEN_IM_WEB_HOST=0.0.0.0
|
|
83
|
+
open-im dashboard
|
|
84
|
+
```
|
|
108
85
|
|
|
109
|
-
-
|
|
86
|
+
- 这只会启动 Web 配置页,不会同时启动桥接服务。
|
|
87
|
+
- 若设置了 `OPEN_IM_WEB_HOST=0.0.0.0`,服务端会输出一次性登录链接,例如:
|
|
110
88
|
|
|
111
|
-
|
|
89
|
+
```text
|
|
90
|
+
http://your-server-ip:39282/?login_token=xxxx
|
|
91
|
+
```
|
|
112
92
|
|
|
113
|
-
|
|
114
|
-
# 在本地电脑执行,将本地 39282 转发到服务器 127.0.0.1:39282
|
|
115
|
-
ssh -L 39282:127.0.0.1:39282 user@your-server-ip
|
|
116
|
-
```
|
|
93
|
+
- 在浏览器中打开该链接,按照页面提示完成各个平台 / AI 工具配置,最后在页面中点击 **「Start bridge」** 按钮启动桥接服务。
|
|
117
94
|
|
|
118
|
-
|
|
95
|
+
2. **后台运行桥接服务**
|
|
119
96
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
97
|
+
配置保存后,有两种启动方式:
|
|
98
|
+
- 在 Web 页面 Service 面板中直接点击 **「Start bridge」**;
|
|
99
|
+
- 或者在服务器上运行:
|
|
123
100
|
|
|
124
|
-
|
|
101
|
+
```bash
|
|
102
|
+
open-im start
|
|
103
|
+
```
|
|
125
104
|
|
|
126
|
-
|
|
105
|
+
这会根据已保存的配置,在后台长期运行桥接服务。
|
|
127
106
|
|
|
128
107
|
## 会话说明
|
|
129
108
|
|
|
@@ -274,46 +253,46 @@ codebuddy login
|
|
|
274
253
|
|
|
275
254
|
### 常用环境变量
|
|
276
255
|
|
|
277
|
-
| 变量
|
|
278
|
-
|
|
|
279
|
-
| `AI_COMMAND`
|
|
280
|
-
| `CLAUDE_WORK_DIR`
|
|
281
|
-
| `LOG_DIR`
|
|
282
|
-
| `LOG_LEVEL`
|
|
283
|
-
| `HOOK_PORT`
|
|
284
|
-
| `CODEX_PROXY`
|
|
285
|
-
| `OPENAI_API_KEY`
|
|
286
|
-
| `CODEBUDDY_CLI_PATH`
|
|
287
|
-
| `CODEBUDDY_TIMEOUT_MS`
|
|
288
|
-
| `CODEBUDDY_SKIP_PERMISSIONS` | 覆盖 CodeBuddy 的跳过权限确认行为
|
|
289
|
-
| `CODEBUDDY_IDLE_TIMEOUT_MS`
|
|
290
|
-
| `CODEBUDDY_API_KEY`
|
|
291
|
-
| `CODEBUDDY_AUTH_TOKEN`
|
|
292
|
-
| `TELEGRAM_BOT_TOKEN`
|
|
293
|
-
| `TELEGRAM_PROXY`
|
|
294
|
-
| `TELEGRAM_ALLOWED_USER_IDS`
|
|
295
|
-
| `FEISHU_APP_ID`
|
|
296
|
-
| `FEISHU_APP_SECRET`
|
|
297
|
-
| `FEISHU_ALLOWED_USER_IDS`
|
|
298
|
-
| `QQ_BOT_APPID`
|
|
299
|
-
| `QQ_BOT_SECRET`
|
|
300
|
-
| `QQ_BOT_SANDBOX`
|
|
301
|
-
| `QQ_ALLOWED_USER_IDS`
|
|
302
|
-
| `DINGTALK_CLIENT_ID`
|
|
303
|
-
| `DINGTALK_CLIENT_SECRET`
|
|
304
|
-
| `DINGTALK_CARD_TEMPLATE_ID`
|
|
305
|
-
| `DINGTALK_ALLOWED_USER_IDS`
|
|
306
|
-
| `WEWORK_CORP_ID`
|
|
307
|
-
| `WEWORK_SECRET`
|
|
308
|
-
| `WEWORK_WS_URL`
|
|
309
|
-
| `WEWORK_ALLOWED_USER_IDS`
|
|
310
|
-
| `WECHAT_APP_ID`
|
|
311
|
-
| `WECHAT_APP_SECRET`
|
|
312
|
-
| `WECHAT_TOKEN`
|
|
313
|
-
| `WECHAT_GUID`
|
|
314
|
-
| `WECHAT_USER_ID`
|
|
315
|
-
| `WECHAT_WS_URL`
|
|
316
|
-
| `WECHAT_ALLOWED_USER_IDS`
|
|
256
|
+
| 变量 | 说明 |
|
|
257
|
+
| ---------------------------- | ---------------------------------------------- |
|
|
258
|
+
| `AI_COMMAND` | 选择 `claude` / `codex` / `codebuddy` |
|
|
259
|
+
| `CLAUDE_WORK_DIR` | 默认会话目录 |
|
|
260
|
+
| `LOG_DIR` | 日志目录 |
|
|
261
|
+
| `LOG_LEVEL` | 日志级别 |
|
|
262
|
+
| `HOOK_PORT` | 权限服务端口 |
|
|
263
|
+
| `CODEX_PROXY` | Codex 访问 `chatgpt.com` 的代理 |
|
|
264
|
+
| `OPENAI_API_KEY` | Codex API Key,可替代 `codex login` |
|
|
265
|
+
| `CODEBUDDY_CLI_PATH` | 覆盖 CodeBuddy CLI 路径 |
|
|
266
|
+
| `CODEBUDDY_TIMEOUT_MS` | 覆盖 CodeBuddy 超时 |
|
|
267
|
+
| `CODEBUDDY_SKIP_PERMISSIONS` | 覆盖 CodeBuddy 的跳过权限确认行为 |
|
|
268
|
+
| `CODEBUDDY_IDLE_TIMEOUT_MS` | CodeBuddy 长时间无输出时自动终止 |
|
|
269
|
+
| `CODEBUDDY_API_KEY` | CodeBuddy API Key,可替代 `codebuddy login` |
|
|
270
|
+
| `CODEBUDDY_AUTH_TOKEN` | CodeBuddy Auth Token,可替代 `codebuddy login` |
|
|
271
|
+
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token |
|
|
272
|
+
| `TELEGRAM_PROXY` | Telegram 代理地址 |
|
|
273
|
+
| `TELEGRAM_ALLOWED_USER_IDS` | Telegram 白名单 |
|
|
274
|
+
| `FEISHU_APP_ID` | 飞书 App ID |
|
|
275
|
+
| `FEISHU_APP_SECRET` | 飞书 App Secret |
|
|
276
|
+
| `FEISHU_ALLOWED_USER_IDS` | 飞书白名单 |
|
|
277
|
+
| `QQ_BOT_APPID` | QQ 机器人 App ID |
|
|
278
|
+
| `QQ_BOT_SECRET` | QQ 机器人 App Secret |
|
|
279
|
+
| `QQ_BOT_SANDBOX` | QQ 机器人沙箱模式(`1`/`true` 启用,默认关闭) |
|
|
280
|
+
| `QQ_ALLOWED_USER_IDS` | QQ 白名单 |
|
|
281
|
+
| `DINGTALK_CLIENT_ID` | 钉钉应用 Client ID / AppKey |
|
|
282
|
+
| `DINGTALK_CLIENT_SECRET` | 钉钉应用 Client Secret / AppSecret |
|
|
283
|
+
| `DINGTALK_CARD_TEMPLATE_ID` | 钉钉 AI 卡片模板 ID,配置后启用单条流式回复 |
|
|
284
|
+
| `DINGTALK_ALLOWED_USER_IDS` | 钉钉白名单 |
|
|
285
|
+
| `WEWORK_CORP_ID` | 企业微信 Bot ID |
|
|
286
|
+
| `WEWORK_SECRET` | 企业微信 Secret |
|
|
287
|
+
| `WEWORK_WS_URL` | 企业微信 WebSocket 地址 |
|
|
288
|
+
| `WEWORK_ALLOWED_USER_IDS` | 企业微信白名单 |
|
|
289
|
+
| `WECHAT_APP_ID` | 微信标准模式 App ID |
|
|
290
|
+
| `WECHAT_APP_SECRET` | 微信标准模式 App Secret |
|
|
291
|
+
| `WECHAT_TOKEN` | 微信 AGP 模式 Token |
|
|
292
|
+
| `WECHAT_GUID` | 微信 AGP 模式 GUID |
|
|
293
|
+
| `WECHAT_USER_ID` | 微信 AGP 模式 User ID |
|
|
294
|
+
| `WECHAT_WS_URL` | 微信 WebSocket 地址 |
|
|
295
|
+
| `WECHAT_ALLOWED_USER_IDS` | 微信白名单 |
|
|
317
296
|
|
|
318
297
|
### 平台配置来源
|
|
319
298
|
|
|
@@ -334,15 +313,15 @@ codebuddy login
|
|
|
334
313
|
|
|
335
314
|
## IM 内命令
|
|
336
315
|
|
|
337
|
-
| 命令
|
|
338
|
-
|
|
|
339
|
-
| `/help`
|
|
340
|
-
| `/new`
|
|
341
|
-
| `/status`
|
|
342
|
-
| `/cd <路径>`
|
|
343
|
-
| `/pwd`
|
|
344
|
-
| `/allow` `/y` | 允许权限请求
|
|
345
|
-
| `/deny` `/n`
|
|
316
|
+
| 命令 | 说明 |
|
|
317
|
+
| ------------- | ------------------------------------- |
|
|
318
|
+
| `/help` | 显示帮助 |
|
|
319
|
+
| `/new` | 开始新会话 |
|
|
320
|
+
| `/status` | 显示 AI 工具、版本、会话目录、会话 ID |
|
|
321
|
+
| `/cd <路径>` | 切换会话目录 |
|
|
322
|
+
| `/pwd` | 显示当前会话目录 |
|
|
323
|
+
| `/allow` `/y` | 允许权限请求 |
|
|
324
|
+
| `/deny` `/n` | 拒绝权限请求 |
|
|
346
325
|
|
|
347
326
|
## 故障排除
|
|
348
327
|
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,13 @@ async function cmdStart() {
|
|
|
60
60
|
console.log("\nopen-im started in the background.");
|
|
61
61
|
console.log(` pid: ${child.pid}`);
|
|
62
62
|
console.log(` config page: ${getWebConfigUrl()}`);
|
|
63
|
+
if (process.env.OPEN_IM_WEB_HOST && process.env.OPEN_IM_WEB_HOST !== "127.0.0.1") {
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log("NOTE:");
|
|
66
|
+
console.log(" The config page is bound to OPEN_IM_WEB_HOST.");
|
|
67
|
+
console.log(" A one-time login URL (with login_token) has been printed by the config-web server logger.");
|
|
68
|
+
console.log(" Please use that URL (replacing 127.0.0.1 with your server IP/hostname) for the first login.");
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
async function cmdStop() {
|
|
65
72
|
const status = getManagerStatus();
|
|
@@ -113,11 +120,11 @@ async function cmdDev() {
|
|
|
113
120
|
}
|
|
114
121
|
async function cmdDashboard() {
|
|
115
122
|
// Start web config server in persistent mode (no timeout)
|
|
116
|
-
const { startWebConfigServer
|
|
123
|
+
const { startWebConfigServer } = await import("./config-web.js");
|
|
117
124
|
const server = await startWebConfigServer({ mode: "dev", cwd: process.cwd(), persistent: true });
|
|
118
|
-
|
|
125
|
+
const url = server.loginUrl ?? server.url;
|
|
126
|
+
console.log(`\nDashboard: ${url}`);
|
|
119
127
|
console.log("Press Ctrl+C to close.\n");
|
|
120
|
-
openWebConfigUrl();
|
|
121
128
|
await server.waitForResult;
|
|
122
129
|
}
|
|
123
130
|
function showHelp(exitCode = 0) {
|
|
@@ -207,9 +207,6 @@ export declare const PAGE_TEXTS: {
|
|
|
207
207
|
readonly codexProxy: "Codex 代理";
|
|
208
208
|
readonly claudeTimeout: "Claude 超时(毫秒)";
|
|
209
209
|
readonly claudeConfigPath: "配置文件位置";
|
|
210
|
-
readonly claudeAuthToken: "ANTHROPIC_AUTH_TOKEN";
|
|
211
|
-
readonly claudeBaseUrl: "ANTHROPIC_BASE_URL";
|
|
212
|
-
readonly claudeModel: "ANTHROPIC_MODEL";
|
|
213
210
|
readonly claudeProxy: "代理(可选)";
|
|
214
211
|
readonly codexTimeout: "Codex 超时(毫秒)";
|
|
215
212
|
readonly codebuddyTimeout: "CodeBuddy 超时(毫秒)";
|
|
@@ -207,9 +207,6 @@ export const PAGE_TEXTS = {
|
|
|
207
207
|
codexProxy: "Codex \u4ee3\u7406",
|
|
208
208
|
claudeTimeout: "Claude \u8d85\u65f6\uff08\u6beb\u79d2\uff09",
|
|
209
209
|
claudeConfigPath: "\u914d\u7f6e\u6587\u4ef6\u4f4d\u7f6e",
|
|
210
|
-
claudeAuthToken: "ANTHROPIC_AUTH_TOKEN",
|
|
211
|
-
claudeBaseUrl: "ANTHROPIC_BASE_URL",
|
|
212
|
-
claudeModel: "ANTHROPIC_MODEL",
|
|
213
210
|
claudeProxy: "\u4ee3\u7406\uff08\u53ef\u9009\uff09",
|
|
214
211
|
codexTimeout: "Codex \u8d85\u65f6\uff08\u6beb\u79d2\uff09",
|
|
215
212
|
codebuddyTimeout: "CodeBuddy \u8d85\u65f6\uff08\u6beb\u79d2\uff09",
|
|
@@ -201,9 +201,6 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
201
201
|
{ id: "ai-claudeWorkDir-label", key: "workDir" },
|
|
202
202
|
{ id: "ai-claudeTimeoutMs-label", key: "claudeTimeout" },
|
|
203
203
|
{ id: "ai-claudeConfigPath-label", key: "claudeConfigPath" },
|
|
204
|
-
{ id: "ai-claudeAuthToken-label", key: "claudeAuthToken" },
|
|
205
|
-
{ id: "ai-claudeBaseUrl-label", key: "claudeBaseUrl" },
|
|
206
|
-
{ id: "ai-claudeModel-label", key: "claudeModel" },
|
|
207
204
|
{ id: "ai-claudeProxy-label", key: "claudeProxy" },
|
|
208
205
|
{ id: "ai-codexCliPath-label", key: "codexCli" },
|
|
209
206
|
{ id: "ai-codexTimeoutMs-label", key: "codexTimeout" },
|
|
@@ -405,15 +402,55 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
405
402
|
return body;
|
|
406
403
|
}
|
|
407
404
|
|
|
405
|
+
async function loadClaudeSettings() {
|
|
406
|
+
const textarea = document.getElementById("claudeSettingsEditor");
|
|
407
|
+
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
|
408
|
+
try {
|
|
409
|
+
const data = await request("/api/claude/settings");
|
|
410
|
+
const raw = (data.contents || "").trim();
|
|
411
|
+
const target = raw || "{\n}\n";
|
|
412
|
+
try {
|
|
413
|
+
const parsed = JSON.parse(target);
|
|
414
|
+
textarea.value = JSON.stringify(parsed, null, 2) + "\n";
|
|
415
|
+
} catch {
|
|
416
|
+
// 如果后端返回的不是合法 JSON,就原样展示,方便用户手动修
|
|
417
|
+
textarea.value = target;
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
setMessage(error.message || String(error), "error");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function saveClaudeSettings() {
|
|
425
|
+
const textarea = document.getElementById("claudeSettingsEditor");
|
|
426
|
+
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
|
427
|
+
const edited = textarea.value;
|
|
428
|
+
// Validate JSON before sending
|
|
429
|
+
try {
|
|
430
|
+
const parsed = JSON.parse(edited);
|
|
431
|
+
textarea.value = JSON.stringify(parsed, null, 2) + "\n";
|
|
432
|
+
} catch (err) {
|
|
433
|
+
setMessage("Invalid JSON: " + (err && err.message ? err.message : String(err)), "error");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
await request("/api/claude/settings", {
|
|
439
|
+
method: "POST",
|
|
440
|
+
body: JSON.stringify({ contents: edited }),
|
|
441
|
+
});
|
|
442
|
+
setMessage("Claude settings.json saved.", "success");
|
|
443
|
+
} catch (error) {
|
|
444
|
+
setMessage(error.message || String(error), "error");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
408
448
|
// Fill form with data
|
|
409
449
|
const AI_FIELD_MAPPINGS = [
|
|
410
450
|
{ id: "ai-aiCommand", key: "aiCommand" },
|
|
411
451
|
{ id: "ai-claudeWorkDir", key: "claudeWorkDir" },
|
|
412
452
|
{ id: "ai-claudeTimeoutMs", key: "claudeTimeoutMs" },
|
|
413
453
|
{ id: "ai-claudeConfigPath", key: "claudeConfigPath" },
|
|
414
|
-
{ id: "ai-claudeAuthToken", key: "claudeAuthToken" },
|
|
415
|
-
{ id: "ai-claudeBaseUrl", key: "claudeBaseUrl" },
|
|
416
|
-
{ id: "ai-claudeModel", key: "claudeModel" },
|
|
417
454
|
{ id: "ai-claudeProxy", key: "claudeProxy" },
|
|
418
455
|
{ id: "ai-codexCliPath", key: "codexCliPath" },
|
|
419
456
|
{ id: "ai-codexTimeoutMs", key: "codexTimeoutMs" },
|
|
@@ -460,6 +497,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
460
497
|
applyLanguage();
|
|
461
498
|
const data = await request("/api/config");
|
|
462
499
|
fill(data.payload, data.meta);
|
|
500
|
+
await loadClaudeSettings();
|
|
463
501
|
// Initialize current AI tool panel from dropdown value
|
|
464
502
|
currentAiToolPanel = el("ai-aiCommand")?.value || "claude";
|
|
465
503
|
await refreshStatus();
|
|
@@ -499,6 +537,16 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
499
537
|
}
|
|
500
538
|
});
|
|
501
539
|
|
|
540
|
+
// Claude settings.json editor (advanced, inline & collapsible)
|
|
541
|
+
const claudeSettingsContainer = document.getElementById("claudeSettingsContainer");
|
|
542
|
+
if (claudeSettingsContainer && claudeSettingsContainer instanceof HTMLDetailsElement) {
|
|
543
|
+
claudeSettingsContainer.addEventListener("toggle", () => {
|
|
544
|
+
if (claudeSettingsContainer.open) {
|
|
545
|
+
void loadClaudeSettings();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
502
550
|
// AI tool switcher
|
|
503
551
|
document.querySelectorAll(".tab[data-tool]").forEach((tab) => {
|
|
504
552
|
tab.addEventListener("click", () => {
|
|
@@ -531,8 +579,16 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
531
579
|
|
|
532
580
|
// Service buttons
|
|
533
581
|
el("validateButton").onclick = validate;
|
|
534
|
-
el("saveButton").onclick =
|
|
535
|
-
|
|
582
|
+
el("saveButton").onclick = async () => {
|
|
583
|
+
// 先保存 JSON,再保存主配置
|
|
584
|
+
await saveClaudeSettings();
|
|
585
|
+
await save();
|
|
586
|
+
};
|
|
587
|
+
el("startButton").onclick = async () => {
|
|
588
|
+
// 启动前也顺带保存 JSON
|
|
589
|
+
await saveClaudeSettings();
|
|
590
|
+
await startService();
|
|
591
|
+
};
|
|
536
592
|
el("stopButton").onclick = stopService;
|
|
537
593
|
|
|
538
594
|
// Platform test buttons
|
|
@@ -652,9 +708,6 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
652
708
|
claudeWorkDir: getValue("ai-claudeWorkDir"),
|
|
653
709
|
claudeTimeoutMs: getNumber("ai-claudeTimeoutMs"),
|
|
654
710
|
claudeConfigPath: getValue("ai-claudeConfigPath"),
|
|
655
|
-
claudeAuthToken: getValue("ai-claudeAuthToken"),
|
|
656
|
-
claudeBaseUrl: getValue("ai-claudeBaseUrl"),
|
|
657
|
-
claudeModel: getValue("ai-claudeModel"),
|
|
658
711
|
claudeProxy: getValue("ai-claudeProxy"),
|
|
659
712
|
codexTimeoutMs: getNumber("ai-codexTimeoutMs"),
|
|
660
713
|
codebuddyTimeoutMs: getNumber("ai-codebuddyTimeoutMs"),
|
|
@@ -1128,19 +1128,20 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
|
|
|
1128
1128
|
<div class="form-hint" id="ai-claudeConfigPath-hint">Environment variables are saved to ~/.claude/settings.json</div>
|
|
1129
1129
|
</div>
|
|
1130
1130
|
<div class="form-group">
|
|
1131
|
-
<
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1131
|
+
<details id="claudeSettingsContainer">
|
|
1132
|
+
<summary class="form-label" style="cursor: pointer;">Edit ~/.claude/settings.json (advanced)</summary>
|
|
1133
|
+
<div class="form-hint" style="margin-top: 8px; margin-bottom: 8px;">
|
|
1134
|
+
JSON will be auto-formatted; invalid JSON 会提示错误。
|
|
1135
|
+
</div>
|
|
1136
|
+
<textarea
|
|
1137
|
+
id="claudeSettingsEditor"
|
|
1138
|
+
class="form-input mono"
|
|
1139
|
+
style="min-height: 200px; white-space: pre; font-family: var(--font-mono);"
|
|
1140
|
+
></textarea>
|
|
1141
|
+
<div class="form-hint" style="margin-top: 4px;">
|
|
1142
|
+
折叠/展开以隐藏或查看完整配置。
|
|
1143
|
+
</div>
|
|
1144
|
+
</details>
|
|
1144
1145
|
</div>
|
|
1145
1146
|
</div>
|
|
1146
1147
|
|
package/dist/config-web.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface StartedWebConfigServer {
|
|
|
5
5
|
close: () => Promise<void>;
|
|
6
6
|
url: string;
|
|
7
7
|
waitForResult: Promise<WebFlowResult>;
|
|
8
|
+
loginUrl?: string;
|
|
8
9
|
}
|
|
9
10
|
export declare function getHealthPlatformSnapshot(file: FileConfig, env?: NodeJS.ProcessEnv): Record<string, {
|
|
10
11
|
configured: boolean;
|
package/dist/config-web.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
4
7
|
import { DWClient } from "dingtalk-stream";
|
|
5
8
|
import { WEB_CONFIG_PORT } from "./constants.js";
|
|
6
9
|
import { CONFIG_PATH, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, loadConfig, loadFileConfig, saveFileConfig } from "./config.js";
|
|
@@ -10,6 +13,104 @@ import { initWeWork, stopWeWork } from "./wework/client.js";
|
|
|
10
13
|
import { createLogger } from "./logger.js";
|
|
11
14
|
const log = createLogger("ConfigWeb");
|
|
12
15
|
const TEST_TIMEOUT_MS = 10000;
|
|
16
|
+
function getClaudeSettingsPath() {
|
|
17
|
+
const home = getClaudeConfigHome();
|
|
18
|
+
const baseDir = join(home, ".claude");
|
|
19
|
+
return join(baseDir, "settings.json");
|
|
20
|
+
}
|
|
21
|
+
const pendingLogins = new Map();
|
|
22
|
+
const activeSessions = new Map();
|
|
23
|
+
function getWebConfigHost() {
|
|
24
|
+
const envHost = process.env.OPEN_IM_WEB_HOST?.trim();
|
|
25
|
+
if (envHost)
|
|
26
|
+
return envHost;
|
|
27
|
+
return "127.0.0.1";
|
|
28
|
+
}
|
|
29
|
+
function generateRandomToken(bytes = 32) {
|
|
30
|
+
return randomBytes(bytes).toString("base64url");
|
|
31
|
+
}
|
|
32
|
+
function cleanupExpiredAuth(now) {
|
|
33
|
+
for (const [token, info] of pendingLogins) {
|
|
34
|
+
if (info.expiresAt <= now)
|
|
35
|
+
pendingLogins.delete(token);
|
|
36
|
+
}
|
|
37
|
+
for (const [sessionId, info] of activeSessions) {
|
|
38
|
+
if (info.expiresAt <= now)
|
|
39
|
+
activeSessions.delete(sessionId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function createLoginToken(ttlMs) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
cleanupExpiredAuth(now);
|
|
45
|
+
const token = generateRandomToken(32);
|
|
46
|
+
pendingLogins.set(token, { expiresAt: now + ttlMs });
|
|
47
|
+
return token;
|
|
48
|
+
}
|
|
49
|
+
function createSession(request, ttlMs) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
cleanupExpiredAuth(now);
|
|
52
|
+
const sessionId = generateRandomToken(32);
|
|
53
|
+
const remoteAddr = request.socket.remoteAddress;
|
|
54
|
+
const userAgent = typeof request.headers["user-agent"] === "string" ? request.headers["user-agent"] : undefined;
|
|
55
|
+
activeSessions.set(sessionId, {
|
|
56
|
+
expiresAt: now + ttlMs,
|
|
57
|
+
remoteAddr,
|
|
58
|
+
userAgent,
|
|
59
|
+
});
|
|
60
|
+
return sessionId;
|
|
61
|
+
}
|
|
62
|
+
function parseCookies(request) {
|
|
63
|
+
const header = request.headers.cookie;
|
|
64
|
+
if (!header)
|
|
65
|
+
return {};
|
|
66
|
+
const cookies = {};
|
|
67
|
+
const parts = header.split(";");
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
const [rawKey, ...rest] = part.split("=");
|
|
70
|
+
const key = rawKey.trim();
|
|
71
|
+
if (!key)
|
|
72
|
+
continue;
|
|
73
|
+
const value = rest.join("=").trim();
|
|
74
|
+
cookies[key] = decodeURIComponent(value);
|
|
75
|
+
}
|
|
76
|
+
return cookies;
|
|
77
|
+
}
|
|
78
|
+
function getSessionIdFromRequest(request) {
|
|
79
|
+
const cookies = parseCookies(request);
|
|
80
|
+
const sessionId = cookies.openim_session;
|
|
81
|
+
return sessionId && typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
82
|
+
}
|
|
83
|
+
function isSessionValid(request) {
|
|
84
|
+
const sessionId = getSessionIdFromRequest(request);
|
|
85
|
+
if (!sessionId)
|
|
86
|
+
return false;
|
|
87
|
+
const info = activeSessions.get(sessionId);
|
|
88
|
+
if (!info)
|
|
89
|
+
return false;
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (info.expiresAt <= now) {
|
|
92
|
+
activeSessions.delete(sessionId);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// Optional: tie session to basic client fingerprint (remote address)
|
|
96
|
+
const remoteAddr = request.socket.remoteAddress;
|
|
97
|
+
if (info.remoteAddr && remoteAddr && remoteAddr !== info.remoteAddr) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function buildSessionCookie(sessionId, ttlMs) {
|
|
103
|
+
const maxAgeSec = Math.floor(ttlMs / 1000);
|
|
104
|
+
const parts = [
|
|
105
|
+
`openim_session=${encodeURIComponent(sessionId)}`,
|
|
106
|
+
"Path=/",
|
|
107
|
+
"HttpOnly",
|
|
108
|
+
"SameSite=Lax",
|
|
109
|
+
`Max-Age=${maxAgeSec}`,
|
|
110
|
+
];
|
|
111
|
+
// 不设置 Secure,方便本地 http 使用;如果放在 https 反代后,可以在代理层加 Secure
|
|
112
|
+
return parts.join("; ");
|
|
113
|
+
}
|
|
13
114
|
export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
14
115
|
const fileTelegram = file.platforms?.telegram;
|
|
15
116
|
const fileFeishu = file.platforms?.feishu;
|
|
@@ -140,7 +241,7 @@ function buildInitialPayload(file) {
|
|
|
140
241
|
: getClaudeConfigHome() + "/.claude/settings.json",
|
|
141
242
|
claudeAuthToken: claudeEnv.ANTHROPIC_AUTH_TOKEN ?? "",
|
|
142
243
|
claudeBaseUrl: claudeEnv.ANTHROPIC_BASE_URL ?? "",
|
|
143
|
-
claudeModel:
|
|
244
|
+
claudeModel: claudeEnv.ANTHROPIC_MODEL ?? "",
|
|
144
245
|
claudeProxy: file.tools?.claude?.proxy ?? "",
|
|
145
246
|
codexTimeoutMs: file.tools?.codex?.timeoutMs ?? 600000,
|
|
146
247
|
codebuddyTimeoutMs: file.tools?.codebuddy?.timeoutMs ?? 600000,
|
|
@@ -397,7 +498,7 @@ function toFileConfig(payload, existing) {
|
|
|
397
498
|
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
398
499
|
timeoutMs: payload.ai.claudeTimeoutMs,
|
|
399
500
|
proxy: clean(payload.ai.claudeProxy),
|
|
400
|
-
// model is now saved to ~/.claude/settings.json as
|
|
501
|
+
// model is now saved to ~/.claude/settings.json as env var
|
|
401
502
|
},
|
|
402
503
|
codex: {
|
|
403
504
|
...existing.tools?.codex,
|
|
@@ -514,6 +615,7 @@ export async function startWebConfigServer(options) {
|
|
|
514
615
|
resolve(value);
|
|
515
616
|
};
|
|
516
617
|
});
|
|
618
|
+
const host = getWebConfigHost();
|
|
517
619
|
const server = createServer(async (request, response) => {
|
|
518
620
|
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
519
621
|
const finishFlow = (result) => {
|
|
@@ -522,6 +624,43 @@ export async function startWebConfigServer(options) {
|
|
|
522
624
|
server.close();
|
|
523
625
|
settle(result);
|
|
524
626
|
};
|
|
627
|
+
// Auth gating:
|
|
628
|
+
// - 当仅绑定 127.0.0.1 时,保持完全本地免登录(向后兼容)
|
|
629
|
+
// - 当绑定到 0.0.0.0 或其他地址时,启用一次性登录 + Session Cookie 机制
|
|
630
|
+
const isLocalOnly = host === "127.0.0.1";
|
|
631
|
+
const hasLoginTokenFeature = !isLocalOnly;
|
|
632
|
+
if (hasLoginTokenFeature) {
|
|
633
|
+
const loginToken = requestUrl.searchParams.get("login_token");
|
|
634
|
+
if (loginToken) {
|
|
635
|
+
const info = pendingLogins.get(loginToken);
|
|
636
|
+
const now = Date.now();
|
|
637
|
+
if (info && info.expiresAt > now) {
|
|
638
|
+
// 有效的一次性登录 token:创建会话,设置 Cookie,并重定向到去掉 login_token 的 URL
|
|
639
|
+
pendingLogins.delete(loginToken);
|
|
640
|
+
const sessionTtlMs = 24 * 60 * 60 * 1000; // 24 小时
|
|
641
|
+
const sessionId = createSession(request, sessionTtlMs);
|
|
642
|
+
const cookie = buildSessionCookie(sessionId, sessionTtlMs);
|
|
643
|
+
requestUrl.searchParams.delete("login_token");
|
|
644
|
+
const redirectPath = requestUrl.pathname + (requestUrl.search ? requestUrl.search : "");
|
|
645
|
+
response.writeHead(302, {
|
|
646
|
+
Location: redirectPath || "/",
|
|
647
|
+
"Set-Cookie": cookie,
|
|
648
|
+
});
|
|
649
|
+
response.end();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// 无效或过期的一次性 token
|
|
653
|
+
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
|
654
|
+
response.end("Invalid or expired login link. Please generate a new one from the server.");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// 其他请求:必须已有有效 session
|
|
658
|
+
if (!isSessionValid(request)) {
|
|
659
|
+
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
|
660
|
+
response.end("Unauthorized. Please open the latest login URL from the server output.");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
525
664
|
if (request.method === "GET" && requestUrl.pathname === "/") {
|
|
526
665
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
527
666
|
response.end(PAGE_HTML);
|
|
@@ -573,6 +712,57 @@ export async function startWebConfigServer(options) {
|
|
|
573
712
|
json(response, 200, getServiceStatus());
|
|
574
713
|
return;
|
|
575
714
|
}
|
|
715
|
+
if (request.method === "GET" && requestUrl.pathname === "/api/claude/settings") {
|
|
716
|
+
try {
|
|
717
|
+
const settingsPath = getClaudeSettingsPath();
|
|
718
|
+
let contents = "{}";
|
|
719
|
+
if (existsSync(settingsPath)) {
|
|
720
|
+
contents = readFileSync(settingsPath, "utf-8");
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// Try to synthesize from env if file doesn't exist yet
|
|
724
|
+
const env = loadClaudeSettingsEnv();
|
|
725
|
+
if (Object.keys(env).length > 0) {
|
|
726
|
+
contents = JSON.stringify({ env }, null, 2);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
json(response, 200, { path: settingsPath, contents });
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
json(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/claude/settings") {
|
|
737
|
+
try {
|
|
738
|
+
const body = await readJson(request);
|
|
739
|
+
const raw = body.contents ?? "";
|
|
740
|
+
if (!raw.trim()) {
|
|
741
|
+
json(response, 400, { error: "contents is required" });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let parsed;
|
|
745
|
+
try {
|
|
746
|
+
parsed = JSON.parse(raw);
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
json(response, 400, { error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const pretty = JSON.stringify(parsed, null, 2);
|
|
753
|
+
const settingsPath = getClaudeSettingsPath();
|
|
754
|
+
const dir = dirname(settingsPath);
|
|
755
|
+
if (!existsSync(dir)) {
|
|
756
|
+
mkdirSync(dir, { recursive: true });
|
|
757
|
+
}
|
|
758
|
+
writeFileSync(settingsPath, pretty, "utf-8");
|
|
759
|
+
json(response, 200, { message: "Claude settings.json saved.", path: settingsPath });
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
json(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
576
766
|
if (request.method === "GET" && requestUrl.pathname === "/api/health") {
|
|
577
767
|
const file = loadFileConfig();
|
|
578
768
|
const fileTelegram = file.platforms?.telegram;
|
|
@@ -676,7 +866,7 @@ export async function startWebConfigServer(options) {
|
|
|
676
866
|
}
|
|
677
867
|
reject(error);
|
|
678
868
|
});
|
|
679
|
-
server.listen(port,
|
|
869
|
+
server.listen(port, host, () => resolve());
|
|
680
870
|
});
|
|
681
871
|
const address = server.address();
|
|
682
872
|
if (!address || typeof address === "string") {
|
|
@@ -698,6 +888,24 @@ export async function startWebConfigServer(options) {
|
|
|
698
888
|
if (timer)
|
|
699
889
|
clearTimeout(timer);
|
|
700
890
|
});
|
|
891
|
+
let loginUrlForReturn;
|
|
892
|
+
// 当绑定到非 127.0.0.1(例如 0.0.0.0)时,为远程访问生成一次性登录链接
|
|
893
|
+
if (host !== "127.0.0.1") {
|
|
894
|
+
const loginTtlMs = 15 * 60 * 1000; // 15 分钟内有效
|
|
895
|
+
const loginToken = createLoginToken(loginTtlMs);
|
|
896
|
+
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
897
|
+
const baseUrl = `http://${displayHost}:${port}`;
|
|
898
|
+
const loginUrl = `${baseUrl}/?login_token=${encodeURIComponent(loginToken)}`;
|
|
899
|
+
loginUrlForReturn = loginUrl;
|
|
900
|
+
log.info("━━━━━━━━ Web Config Login ━━━━━━━━");
|
|
901
|
+
log.info(`Host binding : ${host}`);
|
|
902
|
+
log.info(`Login URL : ${loginUrl}`);
|
|
903
|
+
if (host === "0.0.0.0") {
|
|
904
|
+
log.info("Note: replace 127.0.0.1 with your server IP or hostname when opening from another device.");
|
|
905
|
+
}
|
|
906
|
+
log.info(`This login link is valid for approximately ${Math.floor(loginTtlMs / 60000)} minutes and can be used only once.`);
|
|
907
|
+
log.info("After login, subsequent requests will use a short-lived session cookie.");
|
|
908
|
+
}
|
|
701
909
|
return {
|
|
702
910
|
close: async () => {
|
|
703
911
|
if (timer)
|
|
@@ -706,13 +914,15 @@ export async function startWebConfigServer(options) {
|
|
|
706
914
|
settle("cancel");
|
|
707
915
|
},
|
|
708
916
|
url: `http://127.0.0.1:${port}`,
|
|
917
|
+
loginUrl: loginUrlForReturn,
|
|
709
918
|
waitForResult,
|
|
710
919
|
};
|
|
711
920
|
}
|
|
712
921
|
export async function runWebConfigFlow(options) {
|
|
713
922
|
const started = await startWebConfigServer(options);
|
|
714
|
-
|
|
715
|
-
|
|
923
|
+
const targetUrl = started.loginUrl ?? started.url;
|
|
924
|
+
openBrowser(targetUrl);
|
|
925
|
+
log.info(`Opened local configuration page: ${targetUrl}`);
|
|
716
926
|
log.info(process.env.OPEN_IM_NO_BROWSER === "1" ? "Browser launch disabled. Open the URL manually." : "Save the configuration in your browser to continue.");
|
|
717
927
|
return started.waitForResult;
|
|
718
928
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface Config {
|
|
|
32
32
|
aiCommand: AiCommand;
|
|
33
33
|
codexCliPath: string;
|
|
34
34
|
codebuddyCliPath: string;
|
|
35
|
+
/** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
|
|
36
|
+
claudeProxy?: string;
|
|
35
37
|
/** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
|
|
36
38
|
codexProxy?: string;
|
|
37
39
|
claudeTimeoutMs: number;
|
|
@@ -133,10 +135,14 @@ export interface FilePlatformDingtalk {
|
|
|
133
135
|
cardTemplateId?: string;
|
|
134
136
|
}
|
|
135
137
|
export interface FileToolClaude {
|
|
138
|
+
cliPath?: string;
|
|
136
139
|
workDir?: string;
|
|
137
140
|
timeoutMs?: number;
|
|
138
|
-
|
|
141
|
+
skipPermissions?: boolean;
|
|
142
|
+
/** HTTP/HTTPS 代理,用于访问 Claude API(如 http://127.0.0.1:7890) */
|
|
139
143
|
proxy?: string;
|
|
144
|
+
/** Claude API 配置(优先级:环境变量 > tools.claude.env > ~/.claude/settings.json) */
|
|
145
|
+
env?: Record<string, string>;
|
|
140
146
|
}
|
|
141
147
|
export interface FileToolCodex {
|
|
142
148
|
cliPath?: string;
|
package/dist/config.js
CHANGED
|
@@ -55,7 +55,8 @@ function migrateToNewConfigFormat(raw) {
|
|
|
55
55
|
...tc,
|
|
56
56
|
workDir: tc.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
57
57
|
timeoutMs: tc.timeoutMs ?? raw.claudeTimeoutMs ?? 600000,
|
|
58
|
-
|
|
58
|
+
proxy: tc.proxy,
|
|
59
|
+
// model 现在通过 env 配置,不再在这里处理
|
|
59
60
|
},
|
|
60
61
|
codex: {
|
|
61
62
|
...tcod,
|
|
@@ -216,11 +217,16 @@ export function loadConfig() {
|
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
};
|
|
220
|
+
// 1. 全局 env(最低优先级之一)
|
|
219
221
|
if (file.env)
|
|
220
222
|
mergeEnv(file.env);
|
|
221
|
-
//
|
|
222
|
-
const
|
|
223
|
-
|
|
223
|
+
// 2. tools.claude.env(优先级高于 Claude settings)
|
|
224
|
+
const claudeToolEnv = file.tools?.claude?.env;
|
|
225
|
+
if (claudeToolEnv)
|
|
226
|
+
mergeEnv(claudeToolEnv);
|
|
227
|
+
// 3. 从 Claude Code 配置合并(最低优先级)
|
|
228
|
+
const claudeSettingsEnv = loadClaudeSettingsEnv();
|
|
229
|
+
mergeEnv(claudeSettingsEnv);
|
|
224
230
|
const fileTelegram = file.platforms?.telegram;
|
|
225
231
|
const fileFeishu = file.platforms?.feishu;
|
|
226
232
|
const fileQQ = file.platforms?.qq;
|
|
@@ -331,6 +337,7 @@ export function loadConfig() {
|
|
|
331
337
|
const tc = file.tools?.claude ?? {};
|
|
332
338
|
const tcod = file.tools?.codex ?? {};
|
|
333
339
|
const tcb = file.tools?.codebuddy ?? {};
|
|
340
|
+
const claudeProxy = process.env.CLAUDE_PROXY ?? tc.proxy;
|
|
334
341
|
const codexProxy = process.env.CODEX_PROXY ?? tcod.proxy;
|
|
335
342
|
let codexCliPath = process.env.CODEX_CLI_PATH ?? tcod.cliPath ?? 'codex';
|
|
336
343
|
if (process.platform === 'win32' && codexCliPath === 'codex') {
|
|
@@ -400,8 +407,9 @@ export function loadConfig() {
|
|
|
400
407
|
'方式 2:运行配置向导',
|
|
401
408
|
' open-im init',
|
|
402
409
|
'',
|
|
403
|
-
'方式 3
|
|
404
|
-
'
|
|
410
|
+
'方式 3:编辑配置文件',
|
|
411
|
+
' ~/.open-im/config.json: tools.claude.env.ANTHROPIC_MODEL = "..."',
|
|
412
|
+
' ~/.claude/settings.json: env.ANTHROPIC_MODEL = "..."(与 Claude Code 共用)',
|
|
405
413
|
'',
|
|
406
414
|
].join('\n');
|
|
407
415
|
throw new Error(errorMsg);
|
|
@@ -603,12 +611,13 @@ export function loadConfig() {
|
|
|
603
611
|
aiCommand,
|
|
604
612
|
codexCliPath,
|
|
605
613
|
codebuddyCliPath,
|
|
614
|
+
claudeProxy,
|
|
606
615
|
codexProxy,
|
|
607
616
|
claudeWorkDir,
|
|
608
617
|
claudeTimeoutMs,
|
|
609
618
|
codexTimeoutMs,
|
|
610
619
|
codebuddyTimeoutMs,
|
|
611
|
-
claudeModel: process.env.
|
|
620
|
+
claudeModel: process.env.ANTHROPIC_MODEL,
|
|
612
621
|
logDir,
|
|
613
622
|
logLevel,
|
|
614
623
|
platforms,
|