@wu529778790/open-im 1.7.0-beta.3 → 1.7.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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,27 +37,66 @@ The config file is stored at `~/.open-im/config.json` by default.
50
37
 
51
38
  ## CLI Commands
52
39
 
53
- | Command | Description |
54
- | ---- | ---- |
55
- | `open-im init` | Initialize or append configuration without starting the service |
56
- | `open-im start` | Run the service in the background |
57
- | `open-im stop` | Stop the background service |
58
- | `open-im dev` | Run in the foreground for development/debugging |
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) |
47
+
48
+ ## Server Deployment & Config Page
59
49
 
60
- ## Graphical Config Page
50
+ ### Local (with browser)
61
51
 
62
- Open the config page at **http://127.0.0.1:39282** (or the URL shown after `open-im start`). The page includes:
52
+ Open the config page at [`http://127.0.0.1:39282`](http://127.0.0.1:39282) (or the URL shown after `open-im start`). The page includes:
63
53
 
64
54
  - **Dashboard** – Configured / Enabled platform count and service status (Idle or Running)
65
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.
66
- - **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).
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).
67
57
  - **Service control** – Validate config, Save, Start bridge, Stop bridge.
68
58
 
69
59
  WeChat is not in the web UI; configure it in `~/.open-im/config.json` or via `open-im init` if needed.
70
60
 
71
- - `open-im start` serves the config page and the bridge.
61
+ - `open-im start` serves both the config page and the bridge on your local machine.
72
62
  - `open-im dev` opens the page automatically only when setup is incomplete.
73
- - To open the page when config already exists, run `open-im start` and visit the URL above.
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).
64
+
65
+ ### Recommended server workflow
66
+
67
+ On a remote server, the simplest and safest pattern is:
68
+
69
+ 1. **Use `dashboard` to configure via browser**
70
+
71
+ On the server:
72
+
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
+ ```
79
+
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:
82
+
83
+ ```text
84
+ http://your-server-ip:39282/?login_token=xxxx
85
+ ```
86
+
87
+ - Open this URL in your browser, complete all platform/AI settings, then click **Start bridge** in the web UI.
88
+
89
+ 2. **Run the bridge as a background service**
90
+
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:
94
+
95
+ ```bash
96
+ open-im start
97
+ ```
98
+
99
+ This runs the full bridge in the background using the saved config.
74
100
 
75
101
  ## Session Behavior
76
102
 
@@ -221,46 +247,46 @@ The following is valid JSON and can be saved directly as `~/.open-im/config.json
221
247
 
222
248
  ### Common Environment Variables
223
249
 
224
- | Variable | Description |
225
- | ---- | ---- |
226
- | `AI_COMMAND` | Select `claude`, `codex`, or `codebuddy` |
227
- | `CLAUDE_WORK_DIR` | Default session working directory |
228
- | `LOG_DIR` | Log directory |
229
- | `LOG_LEVEL` | Log level |
230
- | `HOOK_PORT` | Permission service port |
231
- | `CODEX_PROXY` | Proxy used by Codex to access `chatgpt.com` |
232
- | `OPENAI_API_KEY` | Codex API key, can replace `codex login` |
233
- | `CODEBUDDY_CLI_PATH` | Override CodeBuddy CLI path |
234
- | `CODEBUDDY_TIMEOUT_MS` | Override CodeBuddy timeout |
235
- | `CODEBUDDY_SKIP_PERMISSIONS` | Override CodeBuddy skip-permissions behavior |
236
- | `CODEBUDDY_IDLE_TIMEOUT_MS` | Abort CodeBuddy when it stays silent for too long |
237
- | `CODEBUDDY_API_KEY` | CodeBuddy API key, can replace `codebuddy login` |
238
- | `CODEBUDDY_AUTH_TOKEN` | CodeBuddy auth token, can replace `codebuddy login` |
239
- | `TELEGRAM_BOT_TOKEN` | Telegram bot token |
240
- | `TELEGRAM_PROXY` | Telegram proxy URL |
241
- | `TELEGRAM_ALLOWED_USER_IDS` | Telegram allowlist |
242
- | `FEISHU_APP_ID` | Feishu app ID |
243
- | `FEISHU_APP_SECRET` | Feishu app secret |
244
- | `FEISHU_ALLOWED_USER_IDS` | Feishu allowlist |
245
- | `QQ_BOT_APPID` | QQ bot app ID |
246
- | `QQ_BOT_SECRET` | QQ bot app secret |
247
- | `QQ_BOT_SANDBOX` | QQ bot sandbox mode (`1` / `true` to enable, disabled by default) |
248
- | `QQ_ALLOWED_USER_IDS` | QQ allowlist |
249
- | `DINGTALK_CLIENT_ID` | DingTalk client ID / AppKey |
250
- | `DINGTALK_CLIENT_SECRET` | DingTalk client secret / AppSecret |
251
- | `DINGTALK_CARD_TEMPLATE_ID` | DingTalk AI card template ID; enables single-message streaming replies |
252
- | `DINGTALK_ALLOWED_USER_IDS` | DingTalk allowlist |
253
- | `WEWORK_CORP_ID` | WeCom bot ID |
254
- | `WEWORK_SECRET` | WeCom secret |
255
- | `WEWORK_WS_URL` | WeCom WebSocket URL |
256
- | `WEWORK_ALLOWED_USER_IDS` | WeCom allowlist |
257
- | `WECHAT_APP_ID` | WeChat standard mode app ID |
258
- | `WECHAT_APP_SECRET` | WeChat standard mode app secret |
259
- | `WECHAT_TOKEN` | WeChat AGP mode token |
260
- | `WECHAT_GUID` | WeChat AGP mode GUID |
261
- | `WECHAT_USER_ID` | WeChat AGP mode user ID |
262
- | `WECHAT_WS_URL` | WeChat WebSocket URL |
263
- | `WECHAT_ALLOWED_USER_IDS` | WeChat allowlist |
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 |
264
290
 
265
291
  ### Platform Setup Sources
266
292
 
@@ -281,15 +307,15 @@ DingTalk AI card templates are already compatible with the official "Search Resu
281
307
 
282
308
  ## IM Commands
283
309
 
284
- | Command | Description |
285
- | ---- | ---- |
286
- | `/help` | Show help |
287
- | `/new` | Start a new session |
288
- | `/status` | Show AI tool, version, session directory, and session ID |
289
- | `/cd <path>` | Change the session working directory |
290
- | `/pwd` | Show the current session working directory |
291
- | `/allow` `/y` | Approve a permission request |
292
- | `/deny` `/n` | Reject a permission request |
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 |
293
319
 
294
320
  ## Troubleshooting
295
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,27 +37,72 @@ 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
 
62
- 在浏览器中打开 **http://127.0.0.1:39282**(或执行 `open-im start` 后提示的地址),页面结构如下:
50
+ ### 本机(带浏览器)使用
51
+
52
+ 在本机直接运行:
53
+
54
+ ```bash
55
+ open-im start
56
+ ```
57
+
58
+ 然后在浏览器中打开 [`http://127.0.0.1:39282`](http://127.0.0.1:39282)(或命令行里提示的地址),页面结构如下:
63
59
 
64
60
  - **概览** – 已配置/已启用平台数量、服务状态(未启动或运行中)
65
61
  - **平台配置** – 启用并填写 Telegram、飞书、QQ、企业微信、钉钉的凭证(Bot Token/App ID/Secret、代理、该平台使用的 AI 工具、白名单用户 ID)。每个平台提供「校验配置」按钮
66
- - **AI 工具配置** – **公共**:默认 AI 工具(Claude / Codex / CodeBuddy)、工作目录、Hook 端口、日志级别。**分工具**:Claude(CLI 路径、超时、代理、配置路径、ANTHROPIC_* 等)、Codex(CLI 路径、超时、代理)、CodeBuddy(CLI 路径、超时)
62
+ - **AI 工具配置** – **公共**:默认 AI 工具(Claude / Codex / CodeBuddy)、工作目录、Hook 端口、日志级别。**分工具**:Claude(CLI 路径、超时、代理、配置路径、ANTHROPIC\_\* 等)、Codex(CLI 路径、超时、代理)、CodeBuddy(CLI 路径、超时)
67
63
  - **服务控制** – 校验配置、保存、启动桥接、停止桥接
68
64
 
69
65
  微信暂不在网页中配置,如需使用请在 `~/.open-im/config.json` 中手动配置或通过 `open-im init` 引导。
70
66
 
71
- - `open-im start` 会同时启动桥接服务并提供该配置页
72
- - `open-im dev` 仅在未完成配置时自动打开页面
73
- - 已有配置但想手动打开时,执行 `open-im start` 后访问上述地址即可
67
+ - `open-im start` 会同时启动桥接服务并提供该配置页(本机场景)。
68
+ - `open-im dev` 仅在未完成配置时自动打开页面。
69
+ - 已有配置但想单独打开配置页时,可以使用 `open-im dashboard` 启动仅 Web 配置服务。
70
+
71
+ ### 推荐的服务器端使用方式
72
+
73
+ 在远程服务器上,建议的最简单、安全的方式是:
74
+
75
+ 1. **先通过 `dashboard` 在浏览器里完成配置**
76
+
77
+ 在服务器上执行:
78
+
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
+ ```
85
+
86
+ - 这只会启动 Web 配置页,不会同时启动桥接服务。
87
+ - 若设置了 `OPEN_IM_WEB_HOST=0.0.0.0`,服务端会输出一次性登录链接,例如:
88
+
89
+ ```text
90
+ http://your-server-ip:39282/?login_token=xxxx
91
+ ```
92
+
93
+ - 在浏览器中打开该链接,按照页面提示完成各个平台 / AI 工具配置,最后在页面中点击 **「Start bridge」** 按钮启动桥接服务。
94
+
95
+ 2. **后台运行桥接服务**
96
+
97
+ 配置保存后,有两种启动方式:
98
+ - 在 Web 页面 Service 面板中直接点击 **「Start bridge」**;
99
+ - 或者在服务器上运行:
100
+
101
+ ```bash
102
+ open-im start
103
+ ```
104
+
105
+ 这会根据已保存的配置,在后台长期运行桥接服务。
74
106
 
75
107
  ## 会话说明
76
108
 
@@ -221,46 +253,46 @@ codebuddy login
221
253
 
222
254
  ### 常用环境变量
223
255
 
224
- | 变量 | 说明 |
225
- | ---- | ---- |
226
- | `AI_COMMAND` | 选择 `claude` / `codex` / `codebuddy` |
227
- | `CLAUDE_WORK_DIR` | 默认会话目录 |
228
- | `LOG_DIR` | 日志目录 |
229
- | `LOG_LEVEL` | 日志级别 |
230
- | `HOOK_PORT` | 权限服务端口 |
231
- | `CODEX_PROXY` | Codex 访问 `chatgpt.com` 的代理 |
232
- | `OPENAI_API_KEY` | Codex API Key,可替代 `codex login` |
233
- | `CODEBUDDY_CLI_PATH` | 覆盖 CodeBuddy CLI 路径 |
234
- | `CODEBUDDY_TIMEOUT_MS` | 覆盖 CodeBuddy 超时 |
235
- | `CODEBUDDY_SKIP_PERMISSIONS` | 覆盖 CodeBuddy 的跳过权限确认行为 |
236
- | `CODEBUDDY_IDLE_TIMEOUT_MS` | CodeBuddy 长时间无输出时自动终止 |
237
- | `CODEBUDDY_API_KEY` | CodeBuddy API Key,可替代 `codebuddy login` |
238
- | `CODEBUDDY_AUTH_TOKEN` | CodeBuddy Auth Token,可替代 `codebuddy login` |
239
- | `TELEGRAM_BOT_TOKEN` | Telegram Bot Token |
240
- | `TELEGRAM_PROXY` | Telegram 代理地址 |
241
- | `TELEGRAM_ALLOWED_USER_IDS` | Telegram 白名单 |
242
- | `FEISHU_APP_ID` | 飞书 App ID |
243
- | `FEISHU_APP_SECRET` | 飞书 App Secret |
244
- | `FEISHU_ALLOWED_USER_IDS` | 飞书白名单 |
245
- | `QQ_BOT_APPID` | QQ 机器人 App ID |
246
- | `QQ_BOT_SECRET` | QQ 机器人 App Secret |
247
- | `QQ_BOT_SANDBOX` | QQ 机器人沙箱模式(`1`/`true` 启用,默认关闭) |
248
- | `QQ_ALLOWED_USER_IDS` | QQ 白名单 |
249
- | `DINGTALK_CLIENT_ID` | 钉钉应用 Client ID / AppKey |
250
- | `DINGTALK_CLIENT_SECRET` | 钉钉应用 Client Secret / AppSecret |
251
- | `DINGTALK_CARD_TEMPLATE_ID` | 钉钉 AI 卡片模板 ID,配置后启用单条流式回复 |
252
- | `DINGTALK_ALLOWED_USER_IDS` | 钉钉白名单 |
253
- | `WEWORK_CORP_ID` | 企业微信 Bot ID |
254
- | `WEWORK_SECRET` | 企业微信 Secret |
255
- | `WEWORK_WS_URL` | 企业微信 WebSocket 地址 |
256
- | `WEWORK_ALLOWED_USER_IDS` | 企业微信白名单 |
257
- | `WECHAT_APP_ID` | 微信标准模式 App ID |
258
- | `WECHAT_APP_SECRET` | 微信标准模式 App Secret |
259
- | `WECHAT_TOKEN` | 微信 AGP 模式 Token |
260
- | `WECHAT_GUID` | 微信 AGP 模式 GUID |
261
- | `WECHAT_USER_ID` | 微信 AGP 模式 User ID |
262
- | `WECHAT_WS_URL` | 微信 WebSocket 地址 |
263
- | `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` | 微信白名单 |
264
296
 
265
297
  ### 平台配置来源
266
298
 
@@ -281,15 +313,15 @@ codebuddy login
281
313
 
282
314
  ## IM 内命令
283
315
 
284
- | 命令 | 说明 |
285
- | ---- | ---- |
286
- | `/help` | 显示帮助 |
287
- | `/new` | 开始新会话 |
288
- | `/status` | 显示 AI 工具、版本、会话目录、会话 ID |
289
- | `/cd <路径>` | 切换会话目录 |
290
- | `/pwd` | 显示当前会话目录 |
291
- | `/allow` `/y` | 允许权限请求 |
292
- | `/deny` `/n` | 拒绝权限请求 |
316
+ | 命令 | 说明 |
317
+ | ------------- | ------------------------------------- |
318
+ | `/help` | 显示帮助 |
319
+ | `/new` | 开始新会话 |
320
+ | `/status` | 显示 AI 工具、版本、会话目录、会话 ID |
321
+ | `/cd <路径>` | 切换会话目录 |
322
+ | `/pwd` | 显示当前会话目录 |
323
+ | `/allow` `/y` | 允许权限请求 |
324
+ | `/deny` `/n` | 拒绝权限请求 |
293
325
 
294
326
  ## 故障排除
295
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, openWebConfigUrl } = await import("./config-web.js");
123
+ const { startWebConfigServer } = await import("./config-web.js");
117
124
  const server = await startWebConfigServer({ mode: "dev", cwd: process.cwd(), persistent: true });
118
- console.log(`\nDashboard: ${server.url}`);
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 = save;
535
- el("startButton").onclick = startService;
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
@@ -650,12 +706,8 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
650
706
  ai: {
651
707
  aiCommand: getValue("ai-aiCommand"),
652
708
  claudeWorkDir: getValue("ai-claudeWorkDir"),
653
- claudeSkipPermissions: true,
654
709
  claudeTimeoutMs: getNumber("ai-claudeTimeoutMs"),
655
710
  claudeConfigPath: getValue("ai-claudeConfigPath"),
656
- claudeAuthToken: getValue("ai-claudeAuthToken"),
657
- claudeBaseUrl: getValue("ai-claudeBaseUrl"),
658
- claudeModel: getValue("ai-claudeModel"),
659
711
  claudeProxy: getValue("ai-claudeProxy"),
660
712
  codexTimeoutMs: getNumber("ai-codexTimeoutMs"),
661
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
- <label class="form-label" id="ai-claudeAuthToken-label">ANTHROPIC_AUTH_TOKEN</label>
1132
- <input id="ai-claudeAuthToken" class="form-input mono" type="password" />
1133
- <div class="form-hint" id="ai-claudeAuthToken-hint">Auth token for API access (optional, overrides env)</div>
1134
- </div>
1135
- <div class="form-group">
1136
- <label class="form-label" id="ai-claudeBaseUrl-label">ANTHROPIC_BASE_URL</label>
1137
- <input id="ai-claudeBaseUrl" class="form-input mono" type="text" />
1138
- <div class="form-hint" id="ai-claudeBaseUrl-hint">Custom API base URL (optional, overrides env)</div>
1139
- </div>
1140
- <div class="form-group">
1141
- <label class="form-label" id="ai-claudeModel-label">ANTHROPIC_MODEL</label>
1142
- <input id="ai-claudeModel" class="form-input mono" type="text" />
1143
- <div class="form-hint" id="ai-claudeModel-hint">Model name (optional, overrides env)</div>
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
 
@@ -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;
@@ -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;
@@ -459,18 +560,38 @@ function toFileConfig(payload, existing) {
459
560
  };
460
561
  }
461
562
  function openBrowser(url) {
563
+ // 显式关闭自动打开浏览器(服务器环境推荐设置)
462
564
  if (process.env.OPEN_IM_NO_BROWSER === "1") {
463
565
  return;
464
566
  }
567
+ // 在无 TTY 且无图形环境(常见于服务器)时直接跳过,避免无意义的 xdg-open 调用
568
+ if (!process.stdout.isTTY && !process.env.DISPLAY) {
569
+ log.info(`Skipping browser launch for URL ${url} (no TTY/DISPLAY detected).`);
570
+ return;
571
+ }
572
+ const safeSpawn = (command, args) => {
573
+ try {
574
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: process.platform === "win32" });
575
+ // 防止 ENOENT 之类的错误变成未捕获异常
576
+ child.on("error", (error) => {
577
+ log.warn(`Failed to launch browser command "${command}": ${error.code ?? error.message}`);
578
+ });
579
+ child.unref();
580
+ }
581
+ catch (error) {
582
+ log.warn(`Failed to spawn browser command "${command}": ${error instanceof Error ? error.message : String(error)}`);
583
+ }
584
+ };
465
585
  if (process.platform === "win32") {
466
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true }).unref();
586
+ safeSpawn("cmd", ["/c", "start", "", url]);
467
587
  return;
468
588
  }
469
589
  if (process.platform === "darwin") {
470
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
590
+ safeSpawn("open", [url]);
471
591
  return;
472
592
  }
473
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
593
+ // linux / 其他 UNIX 平台:优先尝试 xdg-open,失败时仅记录日志,不抛出
594
+ safeSpawn("xdg-open", [url]);
474
595
  }
475
596
  export function getWebConfigPort() {
476
597
  const fromEnv = process.env.OPEN_IM_WEB_PORT ? parseInt(process.env.OPEN_IM_WEB_PORT, 10) : NaN;
@@ -494,6 +615,7 @@ export async function startWebConfigServer(options) {
494
615
  resolve(value);
495
616
  };
496
617
  });
618
+ const host = getWebConfigHost();
497
619
  const server = createServer(async (request, response) => {
498
620
  const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
499
621
  const finishFlow = (result) => {
@@ -502,6 +624,43 @@ export async function startWebConfigServer(options) {
502
624
  server.close();
503
625
  settle(result);
504
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
+ }
505
664
  if (request.method === "GET" && requestUrl.pathname === "/") {
506
665
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
507
666
  response.end(PAGE_HTML);
@@ -553,6 +712,57 @@ export async function startWebConfigServer(options) {
553
712
  json(response, 200, getServiceStatus());
554
713
  return;
555
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
+ }
556
766
  if (request.method === "GET" && requestUrl.pathname === "/api/health") {
557
767
  const file = loadFileConfig();
558
768
  const fileTelegram = file.platforms?.telegram;
@@ -656,7 +866,7 @@ export async function startWebConfigServer(options) {
656
866
  }
657
867
  reject(error);
658
868
  });
659
- server.listen(port, "127.0.0.1", () => resolve());
869
+ server.listen(port, host, () => resolve());
660
870
  });
661
871
  const address = server.address();
662
872
  if (!address || typeof address === "string") {
@@ -678,6 +888,24 @@ export async function startWebConfigServer(options) {
678
888
  if (timer)
679
889
  clearTimeout(timer);
680
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
+ }
681
909
  return {
682
910
  close: async () => {
683
911
  if (timer)
@@ -686,13 +914,15 @@ export async function startWebConfigServer(options) {
686
914
  settle("cancel");
687
915
  },
688
916
  url: `http://127.0.0.1:${port}`,
917
+ loginUrl: loginUrlForReturn,
689
918
  waitForResult,
690
919
  };
691
920
  }
692
921
  export async function runWebConfigFlow(options) {
693
922
  const started = await startWebConfigServer(options);
694
- openBrowser(started.url);
695
- log.info(`Opened local configuration page: ${started.url}`);
923
+ const targetUrl = started.loginUrl ?? started.url;
924
+ openBrowser(targetUrl);
925
+ log.info(`Opened local configuration page: ${targetUrl}`);
696
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.");
697
927
  return started.waitForResult;
698
928
  }
package/dist/index.js CHANGED
@@ -103,6 +103,7 @@ function buildShutdownMessage(uptimeMinutes) {
103
103
  ].join("\n");
104
104
  }
105
105
  export async function main() {
106
+ const startupCwd = process.cwd();
106
107
  if (needsSetup()) {
107
108
  const saved = process.stdin.isTTY
108
109
  ? (await runWebConfigFlow({ mode: "dev", cwd: process.cwd() })) === "saved"
@@ -158,9 +159,12 @@ export async function main() {
158
159
  });
159
160
  log.info("Starting open-im bridge...");
160
161
  log.info(`AI 工具: ${getConfiguredAiCommands(config).join(", ")}`);
161
- log.info(`默认会话目录: ${config.claudeWorkDir}`);
162
+ log.info(`默认会话目录(本次启动 cwd): ${startupCwd}`);
163
+ if (startupCwd !== config.claudeWorkDir) {
164
+ log.info(`历史默认会话目录(配置中的 claudeWorkDir): ${config.claudeWorkDir}`);
165
+ }
162
166
  log.info(`启用平台: ${config.enabledPlatforms.join(", ")}`);
163
- const sessionManager = new SessionManager(config.claudeWorkDir);
167
+ const sessionManager = new SessionManager(startupCwd, config.claudeWorkDir);
164
168
  // CLI 工具(Codex/CodeBuddy)的 session 是进程级别的,服务重启后一定无效。
165
169
  // 启动时仅清除 CLI 工具自己的 sessionId,保留 Claude 的持久上下文。
166
170
  sessionManager.clearAllCliSessionIds();
@@ -241,7 +245,7 @@ export async function main() {
241
245
  log.info(`Successfully initialized platforms: ${successfulPlatforms.join(", ")}`);
242
246
  // Send notification only to successfully initialized platforms
243
247
  for (const platform of successfulPlatforms) {
244
- const startupMsg = buildStartupMessage(platform, APP_VERSION, resolvePlatformAiCommand(config, platform), config.claudeWorkDir, sessionManager);
248
+ const startupMsg = buildStartupMessage(platform, APP_VERSION, resolvePlatformAiCommand(config, platform), startupCwd, sessionManager);
245
249
  await sendLifecycleNotification(platform, startupMsg).catch((err) => {
246
250
  log.warn(`Failed to send startup notification to ${platform}:`, err);
247
251
  });
@@ -5,7 +5,11 @@ export declare class SessionManager {
5
5
  private convSessionMap;
6
6
  private defaultWorkDir;
7
7
  private saveTimer;
8
- constructor(defaultWorkDir: string);
8
+ /**
9
+ * @param defaultWorkDir 本次进程的默认工作目录(通常为进程启动时的 cwd)
10
+ * @param previousDefaultWorkDir 旧版本/旧配置使用的默认目录(用于迁移仍跟随默认值的会话)
11
+ */
12
+ constructor(defaultWorkDir: string, previousDefaultWorkDir?: string);
9
13
  getSessionIdForConv(userId: string, convId: string, toolId: ToolId): string | undefined;
10
14
  setSessionIdForConv(userId: string, convId: string, toolId: ToolId, sessionId: string): void;
11
15
  /** 清除指定会话的 sessionId(用于 SDK 报 "No conversation found" 时) */
@@ -30,9 +30,13 @@ export class SessionManager {
30
30
  convSessionMap = new Map();
31
31
  defaultWorkDir;
32
32
  saveTimer = null;
33
- constructor(defaultWorkDir) {
33
+ /**
34
+ * @param defaultWorkDir 本次进程的默认工作目录(通常为进程启动时的 cwd)
35
+ * @param previousDefaultWorkDir 旧版本/旧配置使用的默认目录(用于迁移仍跟随默认值的会话)
36
+ */
37
+ constructor(defaultWorkDir, previousDefaultWorkDir) {
34
38
  this.defaultWorkDir = defaultWorkDir;
35
- this.load();
39
+ this.load(previousDefaultWorkDir);
36
40
  }
37
41
  getSessionIdForConv(userId, convId, toolId) {
38
42
  const s = this.sessions.get(userId);
@@ -234,12 +238,16 @@ export class SessionManager {
234
238
  throw new Error(`目录不存在: \`${resolved}\``);
235
239
  return realpath(resolved);
236
240
  }
237
- load() {
241
+ load(previousDefaultWorkDir) {
238
242
  try {
239
243
  if (existsSync(SESSIONS_FILE)) {
240
244
  const data = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
241
245
  for (const [k, v] of Object.entries(data)) {
242
246
  if (v && typeof v.workDir === 'string') {
247
+ // 如果该会话目录等于旧默认目录,则迁移到新的默认目录(认为用户没有手动 /cd 过)
248
+ if (previousDefaultWorkDir && v.workDir === previousDefaultWorkDir) {
249
+ v.workDir = this.defaultWorkDir;
250
+ }
243
251
  if (!v.activeConvId)
244
252
  v.activeConvId = randomBytes(4).toString('hex');
245
253
  if (!v.sessionIds)
@@ -35,6 +35,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
35
35
  let wasThinking = false;
36
36
  let thinkingText = '';
37
37
  let currentSessionId = ctx.sessionId;
38
+ let hadSessionInvalid = false;
38
39
  let activeHandle = null;
39
40
  const toolLines = [];
40
41
  const minDelta = platformAdapter.minContentDeltaChars ?? 0;
@@ -105,8 +106,11 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
105
106
  log.info(`[AITask] No threadId or convId, sessionId not persisted to storage`);
106
107
  },
107
108
  onSessionInvalid: () => {
109
+ hadSessionInvalid = true;
108
110
  if (ctx.convId)
109
111
  sessionManager.clearSessionForConv(ctx.userId, ctx.convId, aiCommand);
112
+ const ok = sessionManager.newSession(ctx.userId);
113
+ log.info(`[AITask] Session invalid for user ${ctx.userId}, aiCommand=${aiCommand}; auto /new applied, ok=${ok}`);
110
114
  },
111
115
  onThinking: (t) => {
112
116
  if (!firstContentLogged) {
@@ -208,8 +212,11 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
208
212
  else if (aiCommand === 'codex' && isUsageLimitError(error)) {
209
213
  log.info(`Keeping codex session for user ${ctx.userId} after usage limit error`);
210
214
  }
215
+ const friendlyError = hadSessionInvalid
216
+ ? '当前 Claude 会话已失效,已自动执行 /new 重置会话,请重新发送刚才的问题。'
217
+ : error;
211
218
  try {
212
- await platformAdapter.sendError(error);
219
+ await platformAdapter.sendError(friendlyError);
213
220
  }
214
221
  catch (err) {
215
222
  log.error('Failed to send error:', err);
@@ -221,6 +228,8 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
221
228
  timeoutMs,
222
229
  model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
223
230
  chatId: ctx.chatId,
231
+ // Claude 默认跳过权限确认,保持与之前 CLI 行为一致(全自动执行)
232
+ ...(aiCommand === 'claude' ? { skipPermissions: true } : {}),
224
233
  ...(aiCommand === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
225
234
  });
226
235
  return activeHandle;
@@ -35,13 +35,10 @@ describe("runAITask", () => {
35
35
  aiCommand: "codex",
36
36
  platforms: {},
37
37
  enabledPlatforms: [],
38
- defaultPermissionMode: "ask",
39
38
  codexTimeoutMs: 600000,
40
39
  claudeTimeoutMs: 600000,
41
40
  codebuddyTimeoutMs: 600000,
42
- claudeSkipPermissions: false,
43
41
  claudeModel: "",
44
- hookPort: 35801,
45
42
  codexProxy: "",
46
43
  wechatToken: "",
47
44
  wechatGuid: "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.7.0-beta.3",
3
+ "version": "1.7.0-beta.5",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",