@wu529778790/open-im 1.7.0 → 1.7.1-beta.1
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 +50 -3
- package/README.zh-CN.md +55 -2
- package/dist/commands/handler.d.ts +0 -6
- package/dist/commands/handler.js +0 -62
- package/dist/config-web-page-script.js +0 -1
- package/dist/config-web.js +26 -18
- package/dist/config.d.ts +0 -10
- package/dist/config.js +1 -54
- package/dist/dingtalk/event-handler.js +2 -4
- package/dist/dingtalk/message-sender.d.ts +0 -2
- package/dist/dingtalk/message-sender.js +1 -10
- package/dist/feishu/event-handler.js +4 -157
- package/dist/feishu/message-sender.d.ts +0 -20
- package/dist/feishu/message-sender.js +0 -155
- package/dist/index.js +7 -16
- package/dist/manager.js +5 -2
- package/dist/qq/event-handler.js +2 -4
- package/dist/qq/event-handler.test.js +0 -1
- package/dist/qq/message-sender.d.ts +0 -1
- package/dist/qq/message-sender.js +1 -6
- package/dist/session/session-manager.d.ts +5 -1
- package/dist/session/session-manager.js +11 -3
- package/dist/setup.js +3 -7
- package/dist/shared/ai-task.js +12 -26
- package/dist/shared/ai-task.test.js +0 -3
- package/dist/shared/system-messages.d.ts +0 -2
- package/dist/shared/system-messages.js +0 -32
- package/dist/shared/system-messages.test.js +1 -8
- package/dist/telegram/event-handler.js +2 -24
- package/dist/telegram/message-sender.d.ts +0 -1
- package/dist/telegram/message-sender.js +0 -14
- package/dist/wechat/event-handler.js +2 -28
- package/dist/wechat/message-sender.d.ts +0 -2
- package/dist/wechat/message-sender.js +0 -31
- package/dist/wework/event-handler.js +2 -4
- package/dist/wework/message-sender.d.ts +0 -2
- package/dist/wework/message-sender.js +1 -23
- package/package.json +1 -1
- package/dist/hook/permission-server.d.ts +0 -38
- package/dist/hook/permission-server.js +0 -301
- package/dist/hook/permission-server.test.d.ts +0 -1
- package/dist/hook/permission-server.test.js +0 -12
- package/dist/permission-mode/session-mode.d.ts +0 -7
- package/dist/permission-mode/session-mode.js +0 -59
- package/dist/permission-mode/types.d.ts +0 -11
- package/dist/permission-mode/types.js +0 -29
package/README.md
CHANGED
|
@@ -57,9 +57,11 @@ The config file is stored at `~/.open-im/config.json` by default.
|
|
|
57
57
|
| `open-im stop` | Stop the background service |
|
|
58
58
|
| `open-im dev` | Run in the foreground for development/debugging |
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## Server Deployment & Config Page
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
### Local (with browser)
|
|
63
|
+
|
|
64
|
+
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
65
|
|
|
64
66
|
- **Dashboard** – Configured / Enabled platform count and service status (Idle or Running)
|
|
65
67
|
- **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,10 +70,55 @@ Open the config page at **http://127.0.0.1:39282** (or the URL shown after `open
|
|
|
68
70
|
|
|
69
71
|
WeChat is not in the web UI; configure it in `~/.open-im/config.json` or via `open-im init` if needed.
|
|
70
72
|
|
|
71
|
-
- `open-im start` serves the config page and the bridge.
|
|
73
|
+
- `open-im start` serves both the config page and the bridge.
|
|
72
74
|
- `open-im dev` opens the page automatically only when setup is incomplete.
|
|
73
75
|
- To open the page when config already exists, run `open-im start` and visit the URL above.
|
|
74
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
|
+
```
|
|
89
|
+
|
|
90
|
+
This starts the bridge and the config web server in the background without attempting to open a browser.
|
|
91
|
+
|
|
92
|
+
- **2) Verify that the config page is listening on the server**
|
|
93
|
+
|
|
94
|
+
On the server:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
ss -lntp | grep 39282 # or: netstat -lntp | grep 39282
|
|
98
|
+
curl -v http://127.0.0.1:39282/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
If you see a `LISTEN` line for `127.0.0.1:39282` and `curl` returns HTML, the config UI is running.
|
|
102
|
+
|
|
103
|
+
- **3) Access the config UI from your local machine via SSH tunnel**
|
|
104
|
+
|
|
105
|
+
Instead of exposing port 39282 to the public internet, use SSH port forwarding:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# On your local machine:
|
|
109
|
+
ssh -L 39282:127.0.0.1:39282 user@your-server-ip
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then open in your local browser:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
http://127.0.0.1:39282/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This safely tunnels the config page from the server to your local browser.
|
|
119
|
+
|
|
120
|
+
> If you really want to expose the config UI directly, you can change the listener in `config-web.ts` from `server.listen(port, "127.0.0.1", ...)` to `0.0.0.0` and open port 39282 in your firewall / security group. For security reasons, SSH tunneling is strongly recommended instead.
|
|
121
|
+
|
|
75
122
|
## Session Behavior
|
|
76
123
|
|
|
77
124
|
Session context is stored locally in `~/.open-im/data/sessions.json` and is separate from the IM chat history itself. Each user has an independent session directory and session metadata. Sending `/new` resets the current AI session.
|
package/README.zh-CN.md
CHANGED
|
@@ -57,9 +57,17 @@ open-im start
|
|
|
57
57
|
| `open-im stop` | 停止后台服务 |
|
|
58
58
|
| `open-im dev` | 前台运行(调试模式) |
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## 服务器部署与图形化配置
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
### 本机(带浏览器)使用
|
|
63
|
+
|
|
64
|
+
在本机直接运行:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
open-im start
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
然后在浏览器中打开 [`http://127.0.0.1:39282`](http://127.0.0.1:39282)(或命令行里提示的地址),页面结构如下:
|
|
63
71
|
|
|
64
72
|
- **概览** – 已配置/已启用平台数量、服务状态(未启动或运行中)
|
|
65
73
|
- **平台配置** – 启用并填写 Telegram、飞书、QQ、企业微信、钉钉的凭证(Bot Token/App ID/Secret、代理、该平台使用的 AI 工具、白名单用户 ID)。每个平台提供「校验配置」按钮
|
|
@@ -72,6 +80,51 @@ open-im start
|
|
|
72
80
|
- `open-im dev` 仅在未完成配置时自动打开页面
|
|
73
81
|
- 已有配置但想手动打开时,执行 `open-im start` 后访问上述地址即可
|
|
74
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
|
+
```
|
|
95
|
+
|
|
96
|
+
这样只会在后台启动服务与配置页面,不会尝试执行 `xdg-open` / `open` / `start`。
|
|
97
|
+
|
|
98
|
+
- **2)检查配置页面是否已在服务器本机监听**
|
|
99
|
+
|
|
100
|
+
在服务器上执行:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
ss -lntp | grep 39282 # 或 netstat -lntp | grep 39282
|
|
104
|
+
curl -v http://127.0.0.1:39282/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
若看到 `LISTEN 0 ... 127.0.0.1:39282` 且 `curl` 返回 HTML,则说明 Web 配置页已正常启动。
|
|
108
|
+
|
|
109
|
+
- **3)通过 SSH 隧道在本地浏览器访问**
|
|
110
|
+
|
|
111
|
+
不建议直接对外开放 39282 端口,而是使用 SSH 端口转发:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 在本地电脑执行,将本地 39282 转发到服务器 127.0.0.1:39282
|
|
115
|
+
ssh -L 39282:127.0.0.1:39282 user@your-server-ip
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
然后在本地浏览器访问:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
http://127.0.0.1:39282/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
即可打开服务器上的配置页面。
|
|
125
|
+
|
|
126
|
+
> 如确有需要,也可以自行修改 `config-web.ts` 中的监听地址,将 `server.listen(port, "127.0.0.1", ...)` 调整为 `0.0.0.0`,并在防火墙/安全组放行 39282 端口。但出于安全考虑,官方推荐使用 SSH 隧道方式。
|
|
127
|
+
|
|
75
128
|
## 会话说明
|
|
76
129
|
|
|
77
130
|
会话上下文保存在本地 `~/.open-im/data/sessions.json`,与 IM 聊天记录本身无关。每个用户有独立会话目录和 session 信息,发送 `/new` 会重置当前 AI 会话。
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { type Config } from '../config.js';
|
|
2
2
|
import type { SessionManager } from '../session/session-manager.js';
|
|
3
3
|
import type { RequestQueue } from '../queue/request-queue.js';
|
|
4
|
-
import { type PermissionMode } from '../permission-mode/types.js';
|
|
5
4
|
import type { ThreadContext } from '../shared/types.js';
|
|
6
5
|
export type { ThreadContext };
|
|
7
6
|
export interface MessageSender {
|
|
8
7
|
sendTextReply(chatId: string, text: string, threadCtx?: ThreadContext): Promise<void>;
|
|
9
8
|
sendDirectorySelection?(chatId: string, currentDir: string, userId: string): Promise<void>;
|
|
10
|
-
sendModeCard?(chatId: string, userId: string, currentMode: PermissionMode): Promise<void>;
|
|
11
|
-
sendModeKeyboard?(chatId: string, userId: string, currentMode: PermissionMode): Promise<void>;
|
|
12
9
|
}
|
|
13
10
|
export interface CommandHandlerDeps {
|
|
14
11
|
config: Config;
|
|
@@ -22,15 +19,12 @@ export declare class CommandHandler {
|
|
|
22
19
|
private deps;
|
|
23
20
|
constructor(deps: CommandHandlerDeps);
|
|
24
21
|
dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
25
|
-
private handleMode;
|
|
26
22
|
private getClearHistoryHint;
|
|
27
23
|
private handleHelp;
|
|
28
24
|
private handleNew;
|
|
29
25
|
private handlePwd;
|
|
30
26
|
private handleStatus;
|
|
31
27
|
private handleCd;
|
|
32
|
-
private handleAllow;
|
|
33
|
-
private handleDeny;
|
|
34
28
|
private getAiVersion;
|
|
35
29
|
}
|
|
36
30
|
/**
|
package/dist/commands/handler.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { resolvePlatformAiCommand } from '../config.js';
|
|
2
|
-
import { resolveLatestPermission, getPendingCount } from '../hook/permission-server.js';
|
|
3
2
|
import { escapePathForMarkdown } from '../shared/utils.js';
|
|
4
|
-
import { getPermissionMode, setPermissionMode } from '../permission-mode/session-mode.js';
|
|
5
|
-
import { MODE_LABELS, MODE_DESCRIPTIONS, parsePermissionMode } from '../permission-mode/types.js';
|
|
6
3
|
import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
|
|
7
4
|
import { execFile } from 'node:child_process';
|
|
8
5
|
import { readdirSync } from 'node:fs';
|
|
@@ -20,18 +17,12 @@ export class CommandHandler {
|
|
|
20
17
|
}
|
|
21
18
|
if (t === '/help')
|
|
22
19
|
return this.handleHelp(chatId, platform);
|
|
23
|
-
if (t === '/mode' || t.startsWith('/mode '))
|
|
24
|
-
return this.handleMode(chatId, userId, platform, t.slice(6).trim());
|
|
25
20
|
if (t === '/new')
|
|
26
21
|
return this.handleNew(chatId, userId, platform);
|
|
27
22
|
if (t === '/pwd')
|
|
28
23
|
return this.handlePwd(chatId, userId);
|
|
29
24
|
if (t === '/status')
|
|
30
25
|
return this.handleStatus(chatId, userId, platform);
|
|
31
|
-
if (t === '/allow' || t === '/y')
|
|
32
|
-
return this.handleAllow(chatId);
|
|
33
|
-
if (t === '/deny' || t === '/n')
|
|
34
|
-
return this.handleDeny(chatId);
|
|
35
26
|
if (t === '/cd' || t.startsWith('/cd ')) {
|
|
36
27
|
return this.handleCd(chatId, userId, t.slice(3).trim(), platform);
|
|
37
28
|
}
|
|
@@ -42,35 +33,6 @@ export class CommandHandler {
|
|
|
42
33
|
}
|
|
43
34
|
return false;
|
|
44
35
|
}
|
|
45
|
-
async handleMode(chatId, userId, platform, arg) {
|
|
46
|
-
const defaultMode = this.deps.config.defaultPermissionMode;
|
|
47
|
-
const currentMode = getPermissionMode(userId, defaultMode);
|
|
48
|
-
if (arg) {
|
|
49
|
-
const parsed = parsePermissionMode(arg);
|
|
50
|
-
if (parsed) {
|
|
51
|
-
setPermissionMode(userId, parsed);
|
|
52
|
-
await this.deps.sender.sendTextReply(chatId, `✅ 权限模式已切换为 **${MODE_LABELS[parsed]}**\n${MODE_DESCRIPTIONS[parsed]}`);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
await this.deps.sender.sendTextReply(chatId, `无效模式: ${arg}\n可用: ask, accept-edits, plan, yolo`);
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
if (platform === 'feishu' && this.deps.sender.sendModeCard) {
|
|
59
|
-
await this.deps.sender.sendModeCard(chatId, userId, currentMode);
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
if (platform === 'telegram' && this.deps.sender.sendModeKeyboard) {
|
|
63
|
-
await this.deps.sender.sendModeKeyboard(chatId, userId, currentMode);
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
const lines = [
|
|
67
|
-
`🔐 **权限模式** (当前: ${MODE_LABELS[currentMode]})`,
|
|
68
|
-
'',
|
|
69
|
-
...['ask', 'accept-edits', 'plan', 'yolo'].map((m) => `• \`/mode ${m}\` - ${MODE_LABELS[m]}: ${MODE_DESCRIPTIONS[m]}`),
|
|
70
|
-
];
|
|
71
|
-
await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
36
|
getClearHistoryHint(platform) {
|
|
75
37
|
return platform === 'feishu'
|
|
76
38
|
? '💡 提示:如需清除本对话的历史消息,请点击飞书聊天右上角「...」→ 清除聊天记录'
|
|
@@ -85,13 +47,10 @@ export class CommandHandler {
|
|
|
85
47
|
'📋 可用命令:',
|
|
86
48
|
'',
|
|
87
49
|
'/help - 显示帮助',
|
|
88
|
-
'/mode - 切换权限模式(安全/编辑放行/只读/YOLO)',
|
|
89
50
|
'/new - 开始新会话(AI 上下文重置)',
|
|
90
51
|
'/status - 显示状态',
|
|
91
52
|
'/cd <路径> - 切换工作目录',
|
|
92
53
|
'/pwd - 当前工作目录',
|
|
93
|
-
'/allow (/y) - 允许权限请求',
|
|
94
|
-
'/deny (/n) - 拒绝权限请求',
|
|
95
54
|
'',
|
|
96
55
|
this.getClearHistoryHint(platform),
|
|
97
56
|
].join('\n');
|
|
@@ -150,27 +109,6 @@ export class CommandHandler {
|
|
|
150
109
|
}
|
|
151
110
|
return true;
|
|
152
111
|
}
|
|
153
|
-
async handleAllow(chatId) {
|
|
154
|
-
const reqId = resolveLatestPermission(chatId, 'allow');
|
|
155
|
-
if (reqId) {
|
|
156
|
-
const remaining = getPendingCount(chatId);
|
|
157
|
-
await this.deps.sender.sendTextReply(chatId, `✅ 权限已允许${remaining > 0 ? `(还有 ${remaining} 个待确认)` : ''}`);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
await this.deps.sender.sendTextReply(chatId, 'ℹ️ 没有待确认的权限请求');
|
|
161
|
-
}
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
async handleDeny(chatId) {
|
|
165
|
-
const reqId = resolveLatestPermission(chatId, 'deny');
|
|
166
|
-
if (reqId) {
|
|
167
|
-
await this.deps.sender.sendTextReply(chatId, '❌ 权限已拒绝');
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
await this.deps.sender.sendTextReply(chatId, 'ℹ️ 没有待确认的权限请求');
|
|
171
|
-
}
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
112
|
getAiVersion(aiCommand) {
|
|
175
113
|
if (aiCommand === 'claude') {
|
|
176
114
|
// Claude 使用 SDK,返回 SDK 版本
|
|
@@ -650,7 +650,6 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
650
650
|
ai: {
|
|
651
651
|
aiCommand: getValue("ai-aiCommand"),
|
|
652
652
|
claudeWorkDir: getValue("ai-claudeWorkDir"),
|
|
653
|
-
claudeSkipPermissions: true,
|
|
654
653
|
claudeTimeoutMs: getNumber("ai-claudeTimeoutMs"),
|
|
655
654
|
claudeConfigPath: getValue("ai-claudeConfigPath"),
|
|
656
655
|
claudeAuthToken: getValue("ai-claudeAuthToken"),
|
package/dist/config-web.js
CHANGED
|
@@ -134,7 +134,6 @@ function buildInitialPayload(file) {
|
|
|
134
134
|
ai: {
|
|
135
135
|
aiCommand: file.aiCommand ?? "claude",
|
|
136
136
|
claudeWorkDir: file.tools?.claude?.workDir ?? process.cwd(),
|
|
137
|
-
claudeSkipPermissions: file.tools?.claude?.skipPermissions ?? true,
|
|
138
137
|
claudeTimeoutMs: file.tools?.claude?.timeoutMs ?? 600000,
|
|
139
138
|
claudeConfigPath: process.platform === 'win32'
|
|
140
139
|
? getClaudeConfigHome() + "\\.claude\\settings.json"
|
|
@@ -148,8 +147,6 @@ function buildInitialPayload(file) {
|
|
|
148
147
|
codexCliPath: file.tools?.codex?.cliPath ?? "codex",
|
|
149
148
|
codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
|
|
150
149
|
codexProxy: file.tools?.codex?.proxy ?? "",
|
|
151
|
-
defaultPermissionMode: file.defaultPermissionMode ?? "ask",
|
|
152
|
-
hookPort: file.hookPort ?? 35801,
|
|
153
150
|
logDir: file.logDir ?? "",
|
|
154
151
|
logLevel: file.logLevel ?? "default",
|
|
155
152
|
},
|
|
@@ -186,8 +183,6 @@ function validatePayload(payload) {
|
|
|
186
183
|
errors.push("Codex timeout must be positive.");
|
|
187
184
|
if (!Number.isFinite(payload.ai.codebuddyTimeoutMs) || payload.ai.codebuddyTimeoutMs <= 0)
|
|
188
185
|
errors.push("CodeBuddy timeout must be positive.");
|
|
189
|
-
if (!Number.isFinite(payload.ai.hookPort) || payload.ai.hookPort <= 0)
|
|
190
|
-
errors.push("Hook port must be positive.");
|
|
191
186
|
return errors;
|
|
192
187
|
}
|
|
193
188
|
function validateConfigForPlatform(platform, config) {
|
|
@@ -266,12 +261,9 @@ function createProbeConfig(values) {
|
|
|
266
261
|
aiCommand: "claude",
|
|
267
262
|
codexCliPath: "codex",
|
|
268
263
|
claudeWorkDir: process.cwd(),
|
|
269
|
-
claudeSkipPermissions: true,
|
|
270
|
-
defaultPermissionMode: "ask",
|
|
271
264
|
claudeTimeoutMs: 600000,
|
|
272
265
|
codexTimeoutMs: 600000,
|
|
273
266
|
codebuddyTimeoutMs: 600000,
|
|
274
|
-
hookPort: 35801,
|
|
275
267
|
logDir: "",
|
|
276
268
|
logLevel: "INFO",
|
|
277
269
|
codebuddyCliPath: "codebuddy",
|
|
@@ -397,15 +389,12 @@ function toFileConfig(payload, existing) {
|
|
|
397
389
|
return {
|
|
398
390
|
...existing,
|
|
399
391
|
aiCommand: payload.ai.aiCommand,
|
|
400
|
-
defaultPermissionMode: payload.ai.defaultPermissionMode ?? existing.defaultPermissionMode ?? "ask",
|
|
401
|
-
hookPort: payload.ai.hookPort,
|
|
402
392
|
logDir: payload.ai.logDir === undefined ? existing.logDir : clean(payload.ai.logDir),
|
|
403
393
|
logLevel: payload.ai.logLevel === "default" ? undefined : payload.ai.logLevel,
|
|
404
394
|
tools: {
|
|
405
395
|
claude: {
|
|
406
396
|
...existing.tools?.claude,
|
|
407
397
|
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
408
|
-
skipPermissions: payload.ai.claudeSkipPermissions,
|
|
409
398
|
timeoutMs: payload.ai.claudeTimeoutMs,
|
|
410
399
|
proxy: clean(payload.ai.claudeProxy),
|
|
411
400
|
// model is now saved to ~/.claude/settings.json as ANTHROPIC_MODEL
|
|
@@ -414,14 +403,12 @@ function toFileConfig(payload, existing) {
|
|
|
414
403
|
...existing.tools?.codex,
|
|
415
404
|
cliPath: clean(payload.ai.codexCliPath) ?? "codex",
|
|
416
405
|
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
417
|
-
skipPermissions: existing.tools?.codex?.skipPermissions ?? payload.ai.claudeSkipPermissions,
|
|
418
406
|
timeoutMs: payload.ai.codexTimeoutMs,
|
|
419
407
|
proxy: clean(payload.ai.codexProxy),
|
|
420
408
|
},
|
|
421
409
|
codebuddy: {
|
|
422
410
|
...existing.tools?.codebuddy,
|
|
423
411
|
cliPath: clean(payload.ai.codebuddyCliPath) ?? "codebuddy",
|
|
424
|
-
skipPermissions: existing.tools?.codebuddy?.skipPermissions ?? payload.ai.claudeSkipPermissions,
|
|
425
412
|
timeoutMs: payload.ai.codebuddyTimeoutMs,
|
|
426
413
|
},
|
|
427
414
|
},
|
|
@@ -472,18 +459,38 @@ function toFileConfig(payload, existing) {
|
|
|
472
459
|
};
|
|
473
460
|
}
|
|
474
461
|
function openBrowser(url) {
|
|
462
|
+
// 显式关闭自动打开浏览器(服务器环境推荐设置)
|
|
475
463
|
if (process.env.OPEN_IM_NO_BROWSER === "1") {
|
|
476
464
|
return;
|
|
477
465
|
}
|
|
466
|
+
// 在无 TTY 且无图形环境(常见于服务器)时直接跳过,避免无意义的 xdg-open 调用
|
|
467
|
+
if (!process.stdout.isTTY && !process.env.DISPLAY) {
|
|
468
|
+
log.info(`Skipping browser launch for URL ${url} (no TTY/DISPLAY detected).`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const safeSpawn = (command, args) => {
|
|
472
|
+
try {
|
|
473
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: process.platform === "win32" });
|
|
474
|
+
// 防止 ENOENT 之类的错误变成未捕获异常
|
|
475
|
+
child.on("error", (error) => {
|
|
476
|
+
log.warn(`Failed to launch browser command "${command}": ${error.code ?? error.message}`);
|
|
477
|
+
});
|
|
478
|
+
child.unref();
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
log.warn(`Failed to spawn browser command "${command}": ${error instanceof Error ? error.message : String(error)}`);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
478
484
|
if (process.platform === "win32") {
|
|
479
|
-
|
|
485
|
+
safeSpawn("cmd", ["/c", "start", "", url]);
|
|
480
486
|
return;
|
|
481
487
|
}
|
|
482
488
|
if (process.platform === "darwin") {
|
|
483
|
-
|
|
489
|
+
safeSpawn("open", [url]);
|
|
484
490
|
return;
|
|
485
491
|
}
|
|
486
|
-
|
|
492
|
+
// linux / 其他 UNIX 平台:优先尝试 xdg-open,失败时仅记录日志,不抛出
|
|
493
|
+
safeSpawn("xdg-open", [url]);
|
|
487
494
|
}
|
|
488
495
|
export function getWebConfigPort() {
|
|
489
496
|
const fromEnv = process.env.OPEN_IM_WEB_PORT ? parseInt(process.env.OPEN_IM_WEB_PORT, 10) : NaN;
|
|
@@ -623,8 +630,9 @@ export async function startWebConfigServer(options) {
|
|
|
623
630
|
}
|
|
624
631
|
if (request.method === "POST" && requestUrl.pathname === "/api/service/start") {
|
|
625
632
|
try {
|
|
626
|
-
loadConfig();
|
|
627
|
-
const
|
|
633
|
+
const config = loadConfig();
|
|
634
|
+
const workDir = config.claudeWorkDir ?? options.cwd;
|
|
635
|
+
const started = startBackgroundService(workDir);
|
|
628
636
|
json(response, 200, { message: `Bridge started with pid ${started.pid}.`, pid: started.pid });
|
|
629
637
|
if (!options.persistent) {
|
|
630
638
|
setTimeout(() => finishFlow("saved"), 120);
|
package/dist/config.d.ts
CHANGED
|
@@ -38,10 +38,7 @@ export interface Config {
|
|
|
38
38
|
codexTimeoutMs: number;
|
|
39
39
|
codebuddyTimeoutMs: number;
|
|
40
40
|
claudeWorkDir: string;
|
|
41
|
-
claudeSkipPermissions: boolean;
|
|
42
|
-
defaultPermissionMode: 'ask' | 'accept-edits' | 'plan' | 'yolo';
|
|
43
41
|
claudeModel?: string;
|
|
44
|
-
hookPort: number;
|
|
45
42
|
logDir: string;
|
|
46
43
|
logLevel: LogLevel;
|
|
47
44
|
platforms: {
|
|
@@ -137,7 +134,6 @@ export interface FilePlatformDingtalk {
|
|
|
137
134
|
}
|
|
138
135
|
export interface FileToolClaude {
|
|
139
136
|
workDir?: string;
|
|
140
|
-
skipPermissions?: boolean;
|
|
141
137
|
timeoutMs?: number;
|
|
142
138
|
model?: string;
|
|
143
139
|
proxy?: string;
|
|
@@ -146,16 +142,12 @@ export interface FileToolCodex {
|
|
|
146
142
|
cliPath?: string;
|
|
147
143
|
workDir?: string;
|
|
148
144
|
timeoutMs?: number;
|
|
149
|
-
/** 是否跳过权限确认(默认 true) */
|
|
150
|
-
skipPermissions?: boolean;
|
|
151
145
|
/** HTTP/HTTPS 代理,用于访问 chatgpt.com(如 http://127.0.0.1:7890) */
|
|
152
146
|
proxy?: string;
|
|
153
147
|
}
|
|
154
148
|
export interface FileToolCodeBuddy {
|
|
155
149
|
cliPath?: string;
|
|
156
150
|
timeoutMs?: number;
|
|
157
|
-
/** 是否跳过权限确认(默认 true) */
|
|
158
|
-
skipPermissions?: boolean;
|
|
159
151
|
}
|
|
160
152
|
export interface FileConfig {
|
|
161
153
|
telegramBotToken?: string;
|
|
@@ -177,8 +169,6 @@ export interface FileConfig {
|
|
|
177
169
|
codex?: FileToolCodex;
|
|
178
170
|
codebuddy?: FileToolCodeBuddy;
|
|
179
171
|
};
|
|
180
|
-
defaultPermissionMode?: 'ask' | 'accept-edits' | 'plan' | 'yolo';
|
|
181
|
-
hookPort?: number;
|
|
182
172
|
logDir?: string;
|
|
183
173
|
logLevel?: LogLevel;
|
|
184
174
|
}
|
package/dist/config.js
CHANGED
|
@@ -19,7 +19,7 @@ const CODEX_AUTH_PATHS = [
|
|
|
19
19
|
join(homedir(), 'AppData', 'Roaming', 'codex', 'auth.json'),
|
|
20
20
|
];
|
|
21
21
|
const OLD_ROOT_KEYS = [
|
|
22
|
-
'claudeWorkDir',
|
|
22
|
+
'claudeWorkDir',
|
|
23
23
|
'claudeTimeoutMs', 'claudeModel',
|
|
24
24
|
];
|
|
25
25
|
function hasOldConfigFormat(raw) {
|
|
@@ -54,7 +54,6 @@ function migrateToNewConfigFormat(raw) {
|
|
|
54
54
|
claude: {
|
|
55
55
|
...tc,
|
|
56
56
|
workDir: tc.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
57
|
-
skipPermissions: tc.skipPermissions ?? raw.claudeSkipPermissions ?? true,
|
|
58
57
|
timeoutMs: tc.timeoutMs ?? raw.claudeTimeoutMs ?? 600000,
|
|
59
58
|
model: tc.model ?? raw.claudeModel,
|
|
60
59
|
},
|
|
@@ -62,14 +61,12 @@ function migrateToNewConfigFormat(raw) {
|
|
|
62
61
|
...tcod,
|
|
63
62
|
cliPath: tcod.cliPath ?? 'codex',
|
|
64
63
|
workDir: tcod.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
65
|
-
skipPermissions: tcod.skipPermissions ?? raw.claudeSkipPermissions ?? true,
|
|
66
64
|
timeoutMs: tcod.timeoutMs ?? raw.claudeTimeoutMs ?? 600000,
|
|
67
65
|
proxy: tcod.proxy,
|
|
68
66
|
},
|
|
69
67
|
codebuddy: {
|
|
70
68
|
...tcb,
|
|
71
69
|
cliPath: tcb.cliPath ?? 'codebuddy',
|
|
72
|
-
skipPermissions: tcb.skipPermissions ?? raw.claudeSkipPermissions ?? true,
|
|
73
70
|
timeoutMs: tcb.timeoutMs ?? raw.claudeTimeoutMs ?? 600000,
|
|
74
71
|
},
|
|
75
72
|
};
|
|
@@ -78,36 +75,6 @@ function migrateToNewConfigFormat(raw) {
|
|
|
78
75
|
}
|
|
79
76
|
return migrated;
|
|
80
77
|
}
|
|
81
|
-
/** 确保 codex/codebuddy 有 skipPermissions(缺失时从 claude 继承并写回) */
|
|
82
|
-
function ensureToolsSkipPermissions(raw) {
|
|
83
|
-
const tools = raw.tools;
|
|
84
|
-
if (!tools || typeof tools !== 'object')
|
|
85
|
-
return false;
|
|
86
|
-
const tc = tools.claude || {};
|
|
87
|
-
const fallback = tc.skipPermissions ?? true;
|
|
88
|
-
let changed = false;
|
|
89
|
-
if (tools.codex && typeof tools.codex === 'object') {
|
|
90
|
-
const cod = tools.codex;
|
|
91
|
-
if (cod.skipPermissions === undefined) {
|
|
92
|
-
cod.skipPermissions = fallback;
|
|
93
|
-
changed = true;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (tools.codebuddy && typeof tools.codebuddy === 'object') {
|
|
97
|
-
const codebuddy = tools.codebuddy;
|
|
98
|
-
if (codebuddy.skipPermissions === undefined) {
|
|
99
|
-
codebuddy.skipPermissions = fallback;
|
|
100
|
-
changed = true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (changed) {
|
|
104
|
-
const dir = dirname(CONFIG_PATH);
|
|
105
|
-
if (!existsSync(dir))
|
|
106
|
-
mkdirSync(dir, { recursive: true });
|
|
107
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2), 'utf-8');
|
|
108
|
-
}
|
|
109
|
-
return changed;
|
|
110
|
-
}
|
|
111
78
|
export function loadFileConfig() {
|
|
112
79
|
try {
|
|
113
80
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
@@ -121,7 +88,6 @@ export function loadFileConfig() {
|
|
|
121
88
|
writeFileSync(CONFIG_PATH, JSON.stringify(migrated, null, 2), 'utf-8');
|
|
122
89
|
return migrated;
|
|
123
90
|
}
|
|
124
|
-
ensureToolsSkipPermissions(raw);
|
|
125
91
|
return raw;
|
|
126
92
|
}
|
|
127
93
|
catch {
|
|
@@ -401,19 +367,6 @@ export function loadConfig() {
|
|
|
401
367
|
}
|
|
402
368
|
}
|
|
403
369
|
const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? tc.workDir ?? process.cwd();
|
|
404
|
-
// 按当前 AI 工具选择 skipPermissions:claude 用 tools.claude,其他 CLI 工具优先读各自配置,再回退到 claude
|
|
405
|
-
const claudeSkipPermissions = (() => {
|
|
406
|
-
if (process.env.CLAUDE_SKIP_PERMISSIONS !== undefined)
|
|
407
|
-
return process.env.CLAUDE_SKIP_PERMISSIONS === 'true';
|
|
408
|
-
if (process.env.CODEBUDDY_SKIP_PERMISSIONS !== undefined && aiCommand === 'codebuddy')
|
|
409
|
-
return process.env.CODEBUDDY_SKIP_PERMISSIONS === 'true';
|
|
410
|
-
if (aiCommand === 'codex')
|
|
411
|
-
return tcod.skipPermissions ?? tc.skipPermissions ?? true;
|
|
412
|
-
if (aiCommand === 'codebuddy')
|
|
413
|
-
return tcb.skipPermissions ?? tc.skipPermissions ?? true;
|
|
414
|
-
return tc.skipPermissions ?? true;
|
|
415
|
-
})();
|
|
416
|
-
const defaultPermissionMode = (file.defaultPermissionMode ?? 'ask');
|
|
417
370
|
const claudeTimeoutMs = process.env.CLAUDE_TIMEOUT_MS !== undefined
|
|
418
371
|
? parseInt(process.env.CLAUDE_TIMEOUT_MS, 10) || 600000
|
|
419
372
|
: tc.timeoutMs ?? 600000;
|
|
@@ -423,9 +376,6 @@ export function loadConfig() {
|
|
|
423
376
|
const codebuddyTimeoutMs = process.env.CODEBUDDY_TIMEOUT_MS !== undefined
|
|
424
377
|
? parseInt(process.env.CODEBUDDY_TIMEOUT_MS, 10) || 600000
|
|
425
378
|
: tcb.timeoutMs ?? 600000;
|
|
426
|
-
const hookPort = process.env.HOOK_PORT !== undefined
|
|
427
|
-
? parseInt(process.env.HOOK_PORT, 10) || 35801
|
|
428
|
-
: file.hookPort ?? 35801;
|
|
429
379
|
// 6. 校验 Claude API 凭证(SDK 模式需要)
|
|
430
380
|
// 支持:官方 API Key、Auth Token、或自定义 API(第三方模型等,BASE_URL + token)
|
|
431
381
|
if (aiCommand === 'claude') {
|
|
@@ -655,13 +605,10 @@ export function loadConfig() {
|
|
|
655
605
|
codebuddyCliPath,
|
|
656
606
|
codexProxy,
|
|
657
607
|
claudeWorkDir,
|
|
658
|
-
claudeSkipPermissions,
|
|
659
|
-
defaultPermissionMode,
|
|
660
608
|
claudeTimeoutMs,
|
|
661
609
|
codexTimeoutMs,
|
|
662
610
|
codebuddyTimeoutMs,
|
|
663
611
|
claudeModel: process.env.CLAUDE_MODEL ?? tc.model,
|
|
664
|
-
hookPort,
|
|
665
612
|
logDir,
|
|
666
613
|
logLevel,
|
|
667
614
|
platforms,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { resolvePlatformAiCommand } from '../config.js';
|
|
2
2
|
import { AccessControl } from '../access/access-control.js';
|
|
3
3
|
import { RequestQueue } from '../queue/request-queue.js';
|
|
4
|
-
import { configureDingTalkMessageSender, sendThinkingMessage, updateMessage, sendFinalMessages, sendErrorMessage, sendTextReply, sendImageReply, startTypingLoop,
|
|
4
|
+
import { configureDingTalkMessageSender, sendThinkingMessage, updateMessage, sendFinalMessages, sendErrorMessage, sendTextReply, sendImageReply, startTypingLoop, sendDirectorySelection, } from './message-sender.js';
|
|
5
5
|
import { ackMessage, downloadRobotMessageFile, registerSessionWebhook } from './client.js';
|
|
6
|
-
import { registerPermissionSender } from '../hook/permission-server.js';
|
|
7
6
|
import { CommandHandler } from '../commands/handler.js';
|
|
8
7
|
import { getAdapter } from '../adapters/registry.js';
|
|
9
8
|
import { runAITask } from '../shared/ai-task.js';
|
|
@@ -163,10 +162,9 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
163
162
|
config,
|
|
164
163
|
sessionManager,
|
|
165
164
|
requestQueue,
|
|
166
|
-
sender: { sendTextReply,
|
|
165
|
+
sender: { sendTextReply, sendDirectorySelection },
|
|
167
166
|
getRunningTasksSize: () => runningTasks.size,
|
|
168
167
|
});
|
|
169
|
-
registerPermissionSender('dingtalk', { sendTextReply, sendPermissionCard });
|
|
170
168
|
async function enqueuePrompt(userId, chatId, prompt, dingtalkTarget) {
|
|
171
169
|
const workDir = sessionManager.getWorkDir(userId);
|
|
172
170
|
const convId = sessionManager.getConvId(userId);
|
|
@@ -14,8 +14,6 @@ export declare function sendErrorMessage(chatId: string, messageId: string, erro
|
|
|
14
14
|
export declare function sendTextReply(chatId: string, text: string, _threadCtx?: ThreadContext | string): Promise<void>;
|
|
15
15
|
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
16
16
|
export declare function sendProactiveTextReply(target: string | DingTalkActiveTarget, text: string): Promise<void>;
|
|
17
|
-
export declare function sendPermissionCard(chatId: string, requestId: string, toolName: string, toolInput: string): Promise<void>;
|
|
18
|
-
export declare function sendModeCard(chatId: string, _userId: string, currentMode: string): Promise<void>;
|
|
19
17
|
export declare function sendDirectorySelection(chatId: string, currentDir: string, userId: string): Promise<void>;
|
|
20
18
|
export declare function startTypingLoop(_chatId: string): () => void;
|
|
21
19
|
export {};
|
|
@@ -8,7 +8,7 @@ import { MAX_DINGTALK_MESSAGE_LENGTH } from '../constants.js';
|
|
|
8
8
|
import { buildImageFallbackMessage } from '../channels/capabilities.js';
|
|
9
9
|
import { buildMessageTitle, OPEN_IM_SYSTEM_TITLE } from '../shared/message-title.js';
|
|
10
10
|
import { buildTextNote } from '../shared/message-note.js';
|
|
11
|
-
import { buildDirectoryMessage,
|
|
11
|
+
import { buildDirectoryMessage, } from '../shared/system-messages.js';
|
|
12
12
|
const log = createLogger('DingTalkSender');
|
|
13
13
|
const STATUS_ICONS = {
|
|
14
14
|
thinking: '🔵',
|
|
@@ -344,15 +344,6 @@ export async function sendProactiveTextReply(target, text) {
|
|
|
344
344
|
const targetId = typeof target === 'string' ? target : target.chatId;
|
|
345
345
|
log.info(`Proactive text sent to DingTalk chat ${targetId}`);
|
|
346
346
|
}
|
|
347
|
-
export async function sendPermissionCard(chatId, requestId, toolName, toolInput) {
|
|
348
|
-
const message = buildPermissionRequestMessage(toolName, toolInput, requestId);
|
|
349
|
-
await sendTextWithRetry(chatId, message);
|
|
350
|
-
}
|
|
351
|
-
export async function sendModeCard(chatId, _userId, currentMode) {
|
|
352
|
-
const { MODE_LABELS } = await import('../permission-mode/types.js');
|
|
353
|
-
const message = buildModeMessage(MODE_LABELS[currentMode] || currentMode);
|
|
354
|
-
await sendTextWithRetry(chatId, message);
|
|
355
|
-
}
|
|
356
347
|
export async function sendDirectorySelection(chatId, currentDir, userId) {
|
|
357
348
|
const directories = listDirectories(currentDir);
|
|
358
349
|
const dirName = basename(currentDir) || currentDir;
|