@wu529778790/open-im 1.7.1-beta.0 → 1.7.1-beta.2
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 +97 -3
- package/README.zh-CN.md +102 -2
- package/dist/adapters/claude-sdk-adapter.d.ts +7 -7
- package/dist/adapters/claude-sdk-adapter.js +126 -101
- package/dist/adapters/registry.js +2 -13
- package/dist/commands/handler.d.ts +0 -6
- package/dist/commands/handler.js +5 -65
- package/dist/config-web-page-script.js +0 -5
- package/dist/config-web-page-template.js +0 -4
- package/dist/config-web.js +175 -25
- package/dist/config.d.ts +0 -15
- package/dist/config.js +2 -107
- 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 +32 -39
- package/dist/shared/ai-task.test.js +15 -4
- 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/adapters/claude-adapter.d.ts +0 -26
- package/dist/adapters/claude-adapter.js +0 -76
- package/dist/claude/cli-runner.d.ts +0 -29
- package/dist/claude/cli-runner.js +0 -231
- package/dist/claude/process-pool.d.ts +0 -84
- package/dist/claude/process-pool.js +0 -312
- 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,102 @@ 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 these patterns 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) Safest option: SSH tunnel to local browser
|
|
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
|
+
#### 4) Optional: Remote access with one-time login link
|
|
121
|
+
|
|
122
|
+
If you want to open the config UI directly from another device without SSH tunneling, you can bind the web config server to all interfaces and use a one-time login URL:
|
|
123
|
+
|
|
124
|
+
- **Bind to all interfaces and keep the browser closed on the server:**
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
export OPEN_IM_NO_BROWSER=1
|
|
128
|
+
export OPEN_IM_WEB_HOST=0.0.0.0
|
|
129
|
+
open-im start
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- By default, `OPEN_IM_WEB_HOST` is `127.0.0.1` (local only).
|
|
133
|
+
- Setting it to `0.0.0.0` makes the config page listen on all interfaces.
|
|
134
|
+
|
|
135
|
+
- **On startup, open-im will log a one-time login URL**, for example:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
━━━━━━━━ Web Config Login ━━━━━━━━
|
|
139
|
+
Host binding : 0.0.0.0
|
|
140
|
+
Login URL : http://127.0.0.1:39282/?login_token=xxxx
|
|
141
|
+
Note: replace 127.0.0.1 with your server IP or hostname when opening from another device.
|
|
142
|
+
This login link is valid for approximately 15 minutes and can be used only once.
|
|
143
|
+
After login, subsequent requests will use a short-lived session cookie.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **From your laptop/phone**, replace `127.0.0.1` with the server IP or hostname and open the URL in a browser:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
http://your-server-ip:39282/?login_token=xxxx
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The first successful visit:
|
|
153
|
+
|
|
154
|
+
- Consumes the one-time `login_token` (subsequent uses will fail with 401);
|
|
155
|
+
- Creates a short-lived session and sets a `openim_session` cookie in your browser;
|
|
156
|
+
- Redirects you to the config page without query parameters.
|
|
157
|
+
|
|
158
|
+
After that, as long as the `openim_session` cookie is valid and the process is still running, you can continue visiting:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
http://your-server-ip:39282/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> Security notes:
|
|
165
|
+
>
|
|
166
|
+
> - Binding `OPEN_IM_WEB_HOST=0.0.0.0` exposes the config port on all interfaces. Always combine this with firewall rules / security groups and consider fronting the port with HTTPS + auth via a reverse proxy.
|
|
167
|
+
> - When in doubt, prefer SSH tunneling (step 3) over direct exposure.
|
|
168
|
+
|
|
75
169
|
## Session Behavior
|
|
76
170
|
|
|
77
171
|
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,98 @@ 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
|
+
#### 4)可选:在服务器上直接访问的一次性登录链接
|
|
127
|
+
|
|
128
|
+
如果你确实希望在服务器上绑定到公网 IP,从其他设备直接访问配置页面,可以:
|
|
129
|
+
|
|
130
|
+
- **将 Web 配置服务绑定到所有网卡:**
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
export OPEN_IM_NO_BROWSER=1
|
|
134
|
+
export OPEN_IM_WEB_HOST=0.0.0.0
|
|
135
|
+
open-im start
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- 默认情况下,`OPEN_IM_WEB_HOST` 为 `127.0.0.1`(仅本机访问)。
|
|
139
|
+
- 设置为 `0.0.0.0` 后,配置页面会监听在所有网卡上。
|
|
140
|
+
|
|
141
|
+
- **启动后,open-im 会在日志中输出一次性登录链接**,类似:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
━━━━━━━━ Web Config Login ━━━━━━━━
|
|
145
|
+
Host binding : 0.0.0.0
|
|
146
|
+
Login URL : http://127.0.0.1:39282/?login_token=xxxx
|
|
147
|
+
Note: replace 127.0.0.1 with your server IP or hostname when opening from another device.
|
|
148
|
+
This login link is valid for approximately 15 minutes and can be used only once.
|
|
149
|
+
After login, subsequent requests will use a short-lived session cookie.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **在本地电脑或手机浏览器中**,将 `127.0.0.1` 换成服务器 IP 或域名,打开该链接:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
http://your-server-ip:39282/?login_token=xxxx
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
第一次成功访问会:
|
|
159
|
+
|
|
160
|
+
- 消费掉这枚一次性 `login_token`(后续再访问同一链接会 401);
|
|
161
|
+
- 在浏览器中创建一个短期会话,设置 `openim_session` Cookie;
|
|
162
|
+
- 自动重定向到不带参数的配置页。
|
|
163
|
+
|
|
164
|
+
之后,只要 `openim_session` Cookie 仍然有效、进程仍在运行,就可以直接访问:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
http://your-server-ip:39282/
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> 安全提示:
|
|
171
|
+
>
|
|
172
|
+
> - 将 `OPEN_IM_WEB_HOST=0.0.0.0` 意味着该端口会对所有网卡开放,请务必结合防火墙/安全组、尽量配合 HTTPS + 反向代理(例如 Nginx/Caddy 的 Basic Auth 或 OIDC 登录)一起使用。
|
|
173
|
+
> - 如无把握,优先使用上面的 SSH 隧道方案(第 3 步),安全性更高。
|
|
174
|
+
|
|
75
175
|
## 会话说明
|
|
76
176
|
|
|
77
177
|
会话上下文保存在本地 `~/.open-im/data/sessions.json`,与 IM 聊天记录本身无关。每个用户有独立会话目录和 session 信息,发送 `/new` 会重置当前 AI 会话。
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude SDK Adapter - 使用 Agent SDK
|
|
2
|
+
* Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
|
|
3
3
|
*
|
|
4
|
-
* 优势:
|
|
5
|
-
* 1. 进程内执行 - 无 fork/exec
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. 流式输出 -
|
|
4
|
+
* V2 API 优势:
|
|
5
|
+
* 1. 进程内执行 - 无 fork/exec 开销
|
|
6
|
+
* 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
|
|
7
|
+
* 3. 流式输出 - 支持实时增量更新
|
|
8
8
|
*
|
|
9
|
-
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
9
|
+
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
|
|
12
12
|
export declare class ClaudeSDKAdapter implements ToolAdapter {
|
|
13
13
|
readonly toolId = "claude-sdk";
|
|
14
14
|
/**
|
|
15
|
-
* 清理所有活跃的 SDK
|
|
15
|
+
* 清理所有活跃的 SDK 会话和流
|
|
16
16
|
*/
|
|
17
17
|
static destroy(): void;
|
|
18
18
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude SDK Adapter - 使用 Agent SDK
|
|
2
|
+
* Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
|
|
3
3
|
*
|
|
4
|
-
* 优势:
|
|
5
|
-
* 1. 进程内执行 - 无 fork/exec
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. 流式输出 -
|
|
4
|
+
* V2 API 优势:
|
|
5
|
+
* 1. 进程内执行 - 无 fork/exec 开销
|
|
6
|
+
* 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
|
|
7
|
+
* 3. 流式输出 - 支持实时增量更新
|
|
8
8
|
*
|
|
9
|
-
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
9
|
+
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
|
|
12
12
|
import { createLogger } from '../logger.js';
|
|
13
13
|
const log = createLogger('ClaudeSDK');
|
|
14
|
-
//
|
|
15
|
-
|
|
14
|
+
// 存储所有活跃的 SDKSession 对象,key 为 sessionId
|
|
15
|
+
// 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
|
|
16
|
+
const activeSessions = new Map();
|
|
17
|
+
// 存储正在进行的流式迭代器,用于中断
|
|
18
|
+
const activeStreams = new Set();
|
|
16
19
|
function isStreamEvent(msg) {
|
|
17
20
|
return msg.type === 'stream_event';
|
|
18
21
|
}
|
|
@@ -20,33 +23,82 @@ function isSystemInit(msg) {
|
|
|
20
23
|
const m = msg;
|
|
21
24
|
return m.type === 'system' && m.subtype === 'init';
|
|
22
25
|
}
|
|
26
|
+
function isAssistant(msg) {
|
|
27
|
+
return msg.type === 'assistant';
|
|
28
|
+
}
|
|
23
29
|
function isResult(msg) {
|
|
24
30
|
return msg.type === 'result';
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 获取或创建 SDKSession
|
|
34
|
+
* @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
|
|
35
|
+
* @param workDir 工作目录
|
|
36
|
+
* @param model 模型名称
|
|
37
|
+
* @param permissionMode 权限模式
|
|
38
|
+
* @returns SDKSession 对象和实际的 sessionId
|
|
39
|
+
*/
|
|
40
|
+
async function getOrCreateSession(sessionId, _workDir, // 保留参数以备将来使用
|
|
41
|
+
model, permissionMode) {
|
|
42
|
+
const sessionOptions = {
|
|
43
|
+
model: model || 'claude-opus-4-5',
|
|
44
|
+
permissionMode,
|
|
45
|
+
// 可以添加其他选项,如 hooks, allowedTools 等
|
|
46
|
+
};
|
|
47
|
+
let session;
|
|
48
|
+
if (sessionId) {
|
|
49
|
+
// 尝试恢复已有会话
|
|
50
|
+
try {
|
|
51
|
+
log.info(`Attempting to resume session: ${sessionId}`);
|
|
52
|
+
session = unstable_v2_resumeSession(sessionId, sessionOptions);
|
|
53
|
+
activeSessions.set(sessionId, session);
|
|
54
|
+
log.info(`Successfully resumed session: ${sessionId}`);
|
|
55
|
+
return { session, sessionId };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
|
|
59
|
+
// 恢复失败,创建新会话
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 创建新会话
|
|
63
|
+
session = unstable_v2_createSession(sessionOptions);
|
|
64
|
+
// 新会话的 sessionId 需要从第一个消息中获取
|
|
65
|
+
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
66
|
+
const tempId = `pending-${Date.now()}`;
|
|
67
|
+
activeSessions.set(tempId, session);
|
|
68
|
+
log.info(`Created new session (tempId: ${tempId})`);
|
|
69
|
+
return { session, sessionId: tempId };
|
|
28
70
|
}
|
|
29
71
|
export class ClaudeSDKAdapter {
|
|
30
72
|
toolId = 'claude-sdk';
|
|
31
73
|
/**
|
|
32
|
-
* 清理所有活跃的 SDK
|
|
74
|
+
* 清理所有活跃的 SDK 会话和流
|
|
33
75
|
*/
|
|
34
76
|
static destroy() {
|
|
35
|
-
for (const
|
|
77
|
+
for (const stream of activeStreams) {
|
|
36
78
|
try {
|
|
37
|
-
if (
|
|
38
|
-
|
|
79
|
+
if (stream && typeof stream.return === 'function') {
|
|
80
|
+
stream.return();
|
|
39
81
|
}
|
|
40
82
|
}
|
|
41
83
|
catch {
|
|
42
84
|
/* ignore */
|
|
43
85
|
}
|
|
44
86
|
}
|
|
45
|
-
|
|
87
|
+
activeStreams.clear();
|
|
88
|
+
for (const session of activeSessions.values()) {
|
|
89
|
+
try {
|
|
90
|
+
session.close();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
activeSessions.clear();
|
|
46
97
|
}
|
|
47
98
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
48
99
|
const abortController = new AbortController();
|
|
49
|
-
let
|
|
100
|
+
let streamClosed = false;
|
|
101
|
+
let actualSessionId;
|
|
50
102
|
const permissionMode = options?.skipPermissions
|
|
51
103
|
? 'bypassPermissions'
|
|
52
104
|
: options?.permissionMode === 'acceptEdits'
|
|
@@ -54,65 +106,47 @@ export class ClaudeSDKAdapter {
|
|
|
54
106
|
: options?.permissionMode === 'plan'
|
|
55
107
|
? 'plan'
|
|
56
108
|
: 'default';
|
|
57
|
-
const
|
|
109
|
+
const runSession = async () => {
|
|
58
110
|
try {
|
|
59
|
-
//
|
|
111
|
+
// 检查环境变量
|
|
60
112
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
61
113
|
const hasAuthToken = !!process.env.ANTHROPIC_AUTH_TOKEN;
|
|
62
|
-
|
|
63
|
-
if (!hasApiKey && !hasAuthToken && !hasBaseUrl) {
|
|
114
|
+
if (!hasApiKey && !hasAuthToken) {
|
|
64
115
|
log.warn('Claude SDK: No API credentials found in environment variables');
|
|
65
116
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
log.info(`[ClaudeSDK] Starting query: prompt="${prompt.slice(0, 50)}...", sessionId=${sessionId ?? 'new'}, cwd=${workDir}`);
|
|
76
|
-
let q;
|
|
77
|
-
try {
|
|
78
|
-
q = query({
|
|
79
|
-
prompt,
|
|
80
|
-
options: opts,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
catch (queryInitErr) {
|
|
84
|
-
log.error(`[ClaudeSDK] Query initialization failed:`, queryInitErr);
|
|
85
|
-
// 如果是会话文件问题,尝试作为新会话
|
|
86
|
-
if (sessionId && (queryInitErr.code === 'ENOENT' ||
|
|
87
|
-
queryInitErr.message.includes('ENOENT') ||
|
|
88
|
-
queryInitErr.message.includes('not found'))) {
|
|
89
|
-
log.warn(`[ClaudeSDK] Session file not found, starting new session`);
|
|
90
|
-
callbacks.onSessionInvalid?.();
|
|
91
|
-
// 重新创建查询,不带 resume
|
|
92
|
-
const newOpts = { ...opts, resume: undefined };
|
|
93
|
-
q = query({
|
|
94
|
-
prompt,
|
|
95
|
-
options: newOpts,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
callbacks.onError(`查询初始化失败: ${queryInitErr.message}`);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// 将查询添加到活跃列表
|
|
104
|
-
activeQueries.add(q);
|
|
117
|
+
log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
118
|
+
// 获取或创建会话
|
|
119
|
+
const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
|
|
120
|
+
// 发送用户消息
|
|
121
|
+
await session.send(prompt);
|
|
122
|
+
// 获取响应流
|
|
123
|
+
const stream = session.stream();
|
|
124
|
+
activeStreams.add(stream);
|
|
105
125
|
let accumulated = '';
|
|
106
126
|
let accumulatedThinking = '';
|
|
107
127
|
const toolStats = {};
|
|
108
128
|
try {
|
|
109
|
-
for await (const msg of
|
|
110
|
-
if (abortController.signal.aborted)
|
|
129
|
+
for await (const msg of stream) {
|
|
130
|
+
if (abortController.signal.aborted) {
|
|
131
|
+
log.info('Stream aborted by user');
|
|
111
132
|
break;
|
|
133
|
+
}
|
|
134
|
+
// 获取实际的 sessionId(从 init 消息中)
|
|
112
135
|
if (isSystemInit(msg)) {
|
|
113
|
-
|
|
136
|
+
const newSessionId = msg.session_id;
|
|
137
|
+
if (newSessionId && newSessionId !== actualSessionId) {
|
|
138
|
+
// 更新 sessionId 映射
|
|
139
|
+
if (actualSessionId && actualSessionId.startsWith('pending-')) {
|
|
140
|
+
activeSessions.delete(actualSessionId);
|
|
141
|
+
}
|
|
142
|
+
activeSessions.set(newSessionId, session);
|
|
143
|
+
actualSessionId = newSessionId;
|
|
144
|
+
log.info(`[V2] Got actual sessionId: ${newSessionId}`);
|
|
145
|
+
callbacks.onSessionId?.(newSessionId);
|
|
146
|
+
}
|
|
114
147
|
continue;
|
|
115
148
|
}
|
|
149
|
+
// 处理流式事件
|
|
116
150
|
if (isStreamEvent(msg)) {
|
|
117
151
|
const ev = msg.event;
|
|
118
152
|
if (ev?.type === 'content_block_delta' && ev.delta) {
|
|
@@ -127,6 +161,7 @@ export class ClaudeSDKAdapter {
|
|
|
127
161
|
}
|
|
128
162
|
continue;
|
|
129
163
|
}
|
|
164
|
+
// 处理助手消息(工具调用)
|
|
130
165
|
if (isAssistant(msg)) {
|
|
131
166
|
const content = msg.message?.content;
|
|
132
167
|
for (const block of content ?? []) {
|
|
@@ -137,16 +172,22 @@ export class ClaudeSDKAdapter {
|
|
|
137
172
|
}
|
|
138
173
|
continue;
|
|
139
174
|
}
|
|
175
|
+
// 处理结果消息
|
|
140
176
|
if (isResult(msg)) {
|
|
141
|
-
|
|
177
|
+
streamClosed = true;
|
|
142
178
|
const m = msg;
|
|
143
179
|
const success = m.subtype === 'success';
|
|
144
180
|
const errs = m.errors ?? [];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
|
|
182
|
+
// 检查会话错误
|
|
183
|
+
if (!success) {
|
|
184
|
+
const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
|
|
185
|
+
if (noConvErr) {
|
|
186
|
+
log.warn(`Session ${actualSessionId} not found, may need to create new one`);
|
|
187
|
+
callbacks.onSessionInvalid?.();
|
|
188
|
+
}
|
|
189
|
+
const errMsg = errs[0] || '未知错误';
|
|
190
|
+
callbacks.onError(errMsg);
|
|
150
191
|
return;
|
|
151
192
|
}
|
|
152
193
|
const resultText = m.result ?? '';
|
|
@@ -159,71 +200,55 @@ export class ClaudeSDKAdapter {
|
|
|
159
200
|
numTurns: m.num_turns ?? 0,
|
|
160
201
|
toolStats,
|
|
161
202
|
};
|
|
162
|
-
if (!result.accumulated && result.result)
|
|
203
|
+
if (!result.accumulated && result.result) {
|
|
163
204
|
result.accumulated = result.result;
|
|
205
|
+
}
|
|
164
206
|
if (!result.accumulated && !result.result && accumulated) {
|
|
165
|
-
log.debug(`Result event had no text but accumulated=${accumulated.length} chars, using accumulated`);
|
|
166
207
|
result.accumulated = accumulated;
|
|
167
208
|
result.result = accumulated;
|
|
168
209
|
}
|
|
169
|
-
if (!result.accumulated && !result.result) {
|
|
170
|
-
const errMsg = errs[0] ?? '未知错误';
|
|
171
|
-
log.warn(`SDK result empty: subtype=${m.subtype}, errors=${JSON.stringify(errs)}`);
|
|
172
|
-
callbacks.onError(errMsg);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
210
|
callbacks.onComplete(result);
|
|
176
211
|
return;
|
|
177
212
|
}
|
|
178
213
|
}
|
|
179
|
-
|
|
214
|
+
// 如果流正常结束但没有收到 result 消息
|
|
215
|
+
if (!streamClosed && accumulated) {
|
|
216
|
+
log.info('Stream ended without result message, using accumulated text');
|
|
180
217
|
callbacks.onComplete({
|
|
181
218
|
success: true,
|
|
182
219
|
result: accumulated,
|
|
183
220
|
accumulated,
|
|
184
221
|
cost: 0,
|
|
185
222
|
durationMs: 0,
|
|
186
|
-
numTurns:
|
|
223
|
+
numTurns: 1,
|
|
187
224
|
toolStats,
|
|
188
225
|
});
|
|
189
226
|
}
|
|
190
227
|
}
|
|
191
228
|
finally {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
activeQueries.delete(q);
|
|
229
|
+
// 从活跃列表中移除流
|
|
230
|
+
activeStreams.delete(stream);
|
|
195
231
|
}
|
|
196
232
|
}
|
|
197
233
|
catch (err) {
|
|
198
234
|
if (abortController.signal.aborted) {
|
|
199
|
-
log.info('
|
|
235
|
+
log.info('Session run aborted');
|
|
200
236
|
return;
|
|
201
237
|
}
|
|
202
238
|
const errorObj = err;
|
|
203
239
|
const msg = errorObj.message || String(err);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (stack) {
|
|
208
|
-
log.error(`Error stack: ${stack}`);
|
|
209
|
-
}
|
|
210
|
-
// 特别处理会话文件不存在的情况
|
|
211
|
-
if (errorObj.code === 'ENOENT' ||
|
|
212
|
-
errorObj.syscall === 'lstat' ||
|
|
213
|
-
msg.includes('ENOENT') ||
|
|
214
|
-
msg.includes('session')) {
|
|
215
|
-
log.warn(`Session file access error, likely session is corrupted or missing`);
|
|
216
|
-
callbacks.onSessionInvalid?.();
|
|
217
|
-
callbacks.onError('会话文件不存在或已损坏,请发送 /new 开始新会话');
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
callbacks.onError(msg);
|
|
240
|
+
log.error(`Claude SDK V2 error: ${msg}`);
|
|
241
|
+
if (errorObj.stack) {
|
|
242
|
+
log.error(`Error stack: ${errorObj.stack}`);
|
|
221
243
|
}
|
|
244
|
+
callbacks.onError(msg);
|
|
222
245
|
}
|
|
223
246
|
};
|
|
224
|
-
|
|
247
|
+
// 启动会话(不等待)
|
|
248
|
+
runSession();
|
|
225
249
|
return {
|
|
226
250
|
abort: () => {
|
|
251
|
+
log.info('Aborting session run');
|
|
227
252
|
abortController.abort();
|
|
228
253
|
},
|
|
229
254
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getConfiguredAiCommands } from '../config.js';
|
|
2
|
-
import { ClaudeAdapter } from './claude-adapter.js';
|
|
3
2
|
import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
4
3
|
import { CodexAdapter } from './codex-adapter.js';
|
|
5
4
|
import { CodeBuddyAdapter } from './codebuddy-adapter.js';
|
|
@@ -10,17 +9,8 @@ export function initAdapters(config) {
|
|
|
10
9
|
adapters.clear();
|
|
11
10
|
for (const aiCommand of getConfiguredAiCommands(config)) {
|
|
12
11
|
if (aiCommand === 'claude') {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
adapters.set('claude', new ClaudeSDKAdapter());
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
log.info('Claude CLI adapter enabled');
|
|
19
|
-
adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
|
|
20
|
-
useProcessPool: true,
|
|
21
|
-
idleTimeoutMs: 2 * 60 * 1000,
|
|
22
|
-
}));
|
|
23
|
-
}
|
|
12
|
+
log.info('Claude Agent SDK adapter enabled');
|
|
13
|
+
adapters.set('claude', new ClaudeSDKAdapter());
|
|
24
14
|
continue;
|
|
25
15
|
}
|
|
26
16
|
if (aiCommand === 'codex') {
|
|
@@ -38,7 +28,6 @@ export function getAdapter(aiCommand) {
|
|
|
38
28
|
return adapters.get(aiCommand);
|
|
39
29
|
}
|
|
40
30
|
export function cleanupAdapters() {
|
|
41
|
-
ClaudeAdapter.destroy();
|
|
42
31
|
ClaudeSDKAdapter.destroy();
|
|
43
32
|
adapters.clear();
|
|
44
33
|
}
|
|
@@ -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
|
/**
|