@wu529778790/open-im 1.11.2-beta.2 → 1.11.2-beta.20
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 +106 -98
- package/dist/clawbot/event-handler.js +4 -2
- package/dist/config-web-page-i18n.d.ts +7 -7
- package/dist/config-web-page-i18n.js +7 -7
- package/dist/index.js +65 -23
- package/dist/logger.d.ts +0 -3
- package/dist/logger.js +5 -36
- package/dist/shared/ai-task.js +3 -49
- package/dist/shared/task-cleanup.js +1 -11
- package/dist/workbuddy/event-handler.js +5 -5
- package/package.json +1 -1
- package/web/dist/assets/index-CWE8jASz.js +57 -0
- package/web/dist/assets/index-UZ04hBdn.css +1 -0
- package/web/dist/index.html +2 -2
- package/README.zh-CN.md +0 -154
- package/dist/telemetry/hash-user.d.ts +0 -2
- package/dist/telemetry/hash-user.js +0 -5
- package/dist/telemetry/telemetry-sanitize.d.ts +0 -2
- package/dist/telemetry/telemetry-sanitize.js +0 -30
- package/web/dist/assets/index-B1c1xlbx.css +0 -1
- package/web/dist/assets/index-Dng7B2mE.js +0 -57
package/README.md
CHANGED
|
@@ -1,108 +1,127 @@
|
|
|
1
1
|
# open-im
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 你的 AI 编程助手,在每个聊天 App 里。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
open-im 把 Claude Code、Codex、CodeBuddy 接入 Telegram、飞书、企业微信、钉钉、QQ 机器人、微信助理、微信客服号。手机发条消息,电脑上就写好代码。
|
|
6
6
|
|
|
7
|
-
open-im
|
|
7
|
+
## 为什么用 open-im
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **随时随地** — 通勤、排队、躺沙发上,手机发消息就能让 AI 干活
|
|
10
|
+
- **无缝接力** — 和 Claude Code CLI 共享 session,手机聊一半,电脑接着来
|
|
11
|
+
- **完整能力** — 流式输出、会话管理、模型切换,全靠聊天命令
|
|
12
|
+
- **一个桥接,多个平台** — 同一个 bot 支持 7 个 IM 平台
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
## 快速开始
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **Seamless handoff** — open-im shares sessions with the Claude Code CLI; pick up on your computer exactly where you left off on your phone
|
|
17
|
-
- **Full power, simple interface** — stream responses, manage sessions, switch models — all through chat commands
|
|
18
|
-
- **One bridge, many platforms** — same bot works on Telegram, Feishu, DingTalk, WeChat, and more
|
|
19
|
-
|
|
20
|
-
## Features
|
|
21
|
-
|
|
22
|
-
### Chat commands
|
|
16
|
+
```bash
|
|
17
|
+
# 安装
|
|
18
|
+
npm install -g @wu529778790/open-im
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| `/help` | Show all commands |
|
|
27
|
-
| `/new` | Start a fresh AI session |
|
|
28
|
-
| `/sessions` | Browse session history with previews |
|
|
29
|
-
| `/resume [N]` | Resume a session (no arg = most recent) |
|
|
30
|
-
| `/history [N]` | View conversation messages in a session |
|
|
31
|
-
| `/delete <N>` | Delete a session |
|
|
32
|
-
| `/rename <title>` | Rename the current session |
|
|
33
|
-
| `/fork [N]` | Fork a session (create a branch) |
|
|
34
|
-
| `/models` | List available AI models |
|
|
35
|
-
| `/context` | Show context window usage |
|
|
36
|
-
| `/status` | Show AI tool, account, and session info |
|
|
37
|
-
| `/cd <path>` / `/pwd` | Switch work directory (auto-resumes that dir's session) |
|
|
38
|
-
| `/allow` / `/y`, `/deny` / `/n` | Respond to permission prompts |
|
|
20
|
+
# 配置(交互式向导)
|
|
21
|
+
open-im init
|
|
39
22
|
|
|
40
|
-
|
|
23
|
+
# 启动
|
|
24
|
+
open-im start
|
|
25
|
+
```
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
或直接用 npx:
|
|
43
28
|
|
|
29
|
+
```bash
|
|
30
|
+
npx @wu529778790/open-im init
|
|
31
|
+
npx @wu529778790/open-im start
|
|
44
32
|
```
|
|
45
|
-
# On computer
|
|
46
|
-
cd /my-project && claude # work as usual, then Ctrl+C
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
"help me fix the login bug" # open-im auto-resumes the same session
|
|
34
|
+
### 最小配置
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"tools": {
|
|
39
|
+
"claude": { "workDir": "/path/to/project" }
|
|
40
|
+
},
|
|
41
|
+
"platforms": {
|
|
42
|
+
"telegram": { "enabled": true, "botToken": "YOUR_TOKEN" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
53
45
|
```
|
|
54
46
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
## 平台支持
|
|
48
|
+
|
|
49
|
+
| 平台 | 流式输出 | 接入指南 |
|
|
50
|
+
|------|---------|---------|
|
|
51
|
+
| Telegram | ✅ | [Bot 文档](https://core.telegram.org/bots#creating-a-new-bot) |
|
|
52
|
+
| 飞书 | ✅ | [开放平台](https://open.feishu.cn/) |
|
|
53
|
+
| QQ 机器人 | ✅ | [开放平台](https://bot.q.qq.com/) |
|
|
54
|
+
| 企业微信 | ✅ | [管理后台](https://work.weixin.qq.com/) |
|
|
55
|
+
| 钉钉机器人 | ⚠️ 部分 | [开放平台](https://open-dev.dingtalk.com/) |
|
|
56
|
+
| 微信助理(WorkBuddy) | ✅ | [接入指南](https://www.codebuddy.cn/docs/workbuddy/Claw) |
|
|
57
|
+
| 微信客服号(ClawBot) | ✅ | [接入指南](https://www.codebuddy.cn/docs/workbuddy/Claw) |
|
|
58
|
+
|
|
59
|
+
每个平台可单独配置 AI 后端(`claude` / `codex` / `codebuddy`),默认 `claude`。
|
|
60
|
+
|
|
61
|
+
## 聊天命令
|
|
62
|
+
|
|
63
|
+
| 命令 | 说明 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| `/help` | 显示所有命令 |
|
|
66
|
+
| `/new` | 开启新 AI 会话 |
|
|
67
|
+
| `/sessions` | 浏览历史会话 |
|
|
68
|
+
| `/resume [序号]` | 恢复会话 |
|
|
69
|
+
| `/history [序号]` | 查看对话记录 |
|
|
70
|
+
| `/delete <序号>` | 删除会话 |
|
|
71
|
+
| `/rename <标题>` | 重命名会话 |
|
|
72
|
+
| `/fork [序号]` | 分支会话 |
|
|
73
|
+
| `/models` | 查看可用模型 |
|
|
74
|
+
| `/context` | 查看上下文用量 |
|
|
75
|
+
| `/status` | 显示状态信息 |
|
|
76
|
+
| `/cd <路径>` / `/pwd` | 切换/查看工作目录 |
|
|
77
|
+
| `/plugins` | 查看已安装插件 |
|
|
78
|
+
| `/allow` `/y` / `/deny` `/n` | 权限确认 |
|
|
79
|
+
|
|
80
|
+
## 会话接力
|
|
81
|
+
|
|
82
|
+
open-im 和 Claude Code CLI 共享 session 存储。同一目录下,手机和电脑无缝切换:
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
```bash
|
|
85
|
+
# 电脑端
|
|
86
|
+
cd /my-project && claude
|
|
60
87
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
| Telegram | Yes | Images | Full bot support |
|
|
64
|
-
| Feishu | Yes | Images | Streaming card |
|
|
65
|
-
| WeCom | Yes | Images | Streaming card |
|
|
66
|
-
| DingTalk | Partial | Images | Fallback to text |
|
|
67
|
-
| QQ | Yes | Images | |
|
|
68
|
-
| WorkBuddy | Yes | Images | WeChat-based |
|
|
69
|
-
| ClawBot | Yes | Images | WeChat-based |
|
|
88
|
+
# 手机端
|
|
89
|
+
"帮我修复登录 bug" # 自动接续同一个 session
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
# 回到电脑端
|
|
92
|
+
claude -c # 接上手机端的对话
|
|
93
|
+
```
|
|
72
94
|
|
|
73
|
-
|
|
95
|
+
> 同一时刻只能有一端活跃。发消息前先退出 CLI,反之亦然。
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
## Web 控制台
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
`open-im start` 在 `http://127.0.0.1:39282` 提供管理界面:
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
- 配置所有平台凭证
|
|
102
|
+
- 启动/停止桥接服务
|
|
103
|
+
- 编辑配置文件
|
|
104
|
+
- 首次运行自动弹出设置向导
|
|
83
105
|
|
|
84
|
-
|
|
106
|
+
局域网访问:`export OPEN_IM_WEB_HOST=0.0.0.0`
|
|
85
107
|
|
|
86
|
-
|
|
108
|
+
## CLI 命令
|
|
87
109
|
|
|
88
|
-
|
|
110
|
+
| 命令 | 说明 |
|
|
111
|
+
|------|------|
|
|
112
|
+
| `open-im init` | 交互式配置 |
|
|
113
|
+
| `open-im start` | 后台运行 |
|
|
114
|
+
| `open-im stop` | 停止服务 |
|
|
115
|
+
| `open-im restart` | 重启 |
|
|
116
|
+
| `open-im dashboard` | 仅启动 Web 配置服务 |
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
"tools": {
|
|
93
|
-
"claude": { "workDir": "/path/to/project", "skipPermissions": true }
|
|
94
|
-
},
|
|
95
|
-
"platforms": {
|
|
96
|
-
"telegram": { "enabled": true, "botToken": "YOUR_TELEGRAM_BOT_TOKEN" }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
```
|
|
118
|
+
## 配置
|
|
100
119
|
|
|
101
|
-
|
|
120
|
+
配置文件:`~/.open-im/config.json`
|
|
102
121
|
|
|
103
|
-
### Claude
|
|
122
|
+
### Claude(Agent SDK)
|
|
104
123
|
|
|
105
|
-
|
|
124
|
+
无需本地 `claude` 可执行文件。支持第三方兼容接口:
|
|
106
125
|
|
|
107
126
|
```json
|
|
108
127
|
{
|
|
@@ -111,43 +130,32 @@ No local `claude` binary required. Supports third-party / compatible APIs:
|
|
|
111
130
|
"env": {
|
|
112
131
|
"ANTHROPIC_AUTH_TOKEN": "your-token",
|
|
113
132
|
"ANTHROPIC_BASE_URL": "https://your-api-endpoint",
|
|
114
|
-
"ANTHROPIC_MODEL": "
|
|
133
|
+
"ANTHROPIC_MODEL": "model-name"
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
}
|
|
119
138
|
```
|
|
120
139
|
|
|
121
|
-
###
|
|
122
|
-
|
|
123
|
-
| Command | Description |
|
|
124
|
-
| --- | --- |
|
|
125
|
-
| `open-im init` | Interactive setup (does not start the bridge) |
|
|
126
|
-
| `open-im start` | Run the bridge in the background |
|
|
127
|
-
| `open-im stop` | Stop the background bridge |
|
|
128
|
-
| `open-im restart` | Stop then start |
|
|
129
|
-
| `open-im dashboard` | Web config server only (no bridge) |
|
|
130
|
-
|
|
131
|
-
### Environment variables
|
|
132
|
-
|
|
133
|
-
**`ANTHROPIC_*`** (shell or `tools.claude.env`), **`TELEGRAM_BOT_TOKEN`**, **`OPEN_IM_WEB_PORT`**, **`OPEN_IM_WEB_HOST`**, plus platform-specific `*_APP_ID`, `*_SECRET`, `WORKBUDDY_*`, etc.
|
|
134
|
-
|
|
135
|
-
### Git co-authors
|
|
140
|
+
### 环境变量
|
|
136
141
|
|
|
137
|
-
|
|
142
|
+
- **`ANTHROPIC_*`** — Claude API 配置
|
|
143
|
+
- **`TELEGRAM_BOT_TOKEN`** — Telegram Bot Token
|
|
144
|
+
- **`OPEN_IM_WEB_PORT`** — Web 控制台端口(默认 39282)
|
|
145
|
+
- **`OPEN_IM_WEB_HOST`** — Web 控制台监听地址
|
|
138
146
|
|
|
139
|
-
###
|
|
147
|
+
### 隐私
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
匿名运行信息用于改进稳定性(不含聊天内容)。关闭:`OPEN_IM_TELEMETRY=false`
|
|
142
150
|
|
|
143
|
-
##
|
|
151
|
+
## 平台配置详情
|
|
144
152
|
|
|
145
|
-
|
|
153
|
+
详见 [docs/platforms.md](./docs/platforms.md)
|
|
146
154
|
|
|
147
|
-
##
|
|
155
|
+
## 环境要求
|
|
148
156
|
|
|
149
157
|
- Node.js >= 20
|
|
150
|
-
-
|
|
158
|
+
- 至少配置一个 IM 平台 + AI 凭证
|
|
151
159
|
|
|
152
160
|
## License
|
|
153
161
|
|
|
@@ -23,8 +23,10 @@ export function setupClawbotHandlers(config, sessionManager) {
|
|
|
23
23
|
});
|
|
24
24
|
const stopTaskCleanup = startTaskCleanup(ctx.runningTasks);
|
|
25
25
|
const platformSender = {
|
|
26
|
-
sendThinkingMessage: async (
|
|
27
|
-
|
|
26
|
+
sendThinkingMessage: async (chatId, _replyToMessageId, _toolId) => {
|
|
27
|
+
// ClawBot 不支持 typing indicator,先发一条"思考中"消息给用户反馈
|
|
28
|
+
await sendTextReply(chatId, '🤔 正在处理...');
|
|
29
|
+
return 'clawbot_thinking';
|
|
28
30
|
},
|
|
29
31
|
sendTextReply: async (chatId, text) => {
|
|
30
32
|
await sendTextReply(chatId, text);
|
|
@@ -52,11 +52,11 @@ export declare const PAGE_TEXTS: {
|
|
|
52
52
|
readonly credentialProgress: "{done}/{total} credentials";
|
|
53
53
|
readonly telegramSummary: "Bot token and optional proxy.";
|
|
54
54
|
readonly feishuSummary: "App ID, App Secret, and allowed user scope.";
|
|
55
|
-
readonly qqSummary: "App ID and secret for
|
|
56
|
-
readonly weworkSummary: "Corp ID and secret for enterprise
|
|
55
|
+
readonly qqSummary: "App ID and secret for QQ robot.";
|
|
56
|
+
readonly weworkSummary: "Corp ID and secret for enterprise WeChat.";
|
|
57
57
|
readonly dingtalkSummary: "Client credentials plus optional card template.";
|
|
58
|
-
readonly workbuddySummary: "
|
|
59
|
-
readonly clawbotSummary: "WeChat
|
|
58
|
+
readonly workbuddySummary: "WeChat assistant via CodeBuddy OAuth.";
|
|
59
|
+
readonly clawbotSummary: "WeChat customer service via ClawBot.";
|
|
60
60
|
readonly clawbotApiUrl: "API URL";
|
|
61
61
|
readonly clawbotApiToken: "API Token (Bearer)";
|
|
62
62
|
readonly clawbotHelp: "Click \"Scan QR Login\" to authenticate via WeChat, or paste token manually.";
|
|
@@ -86,7 +86,7 @@ export declare const PAGE_TEXTS: {
|
|
|
86
86
|
readonly clientId: "Client ID / AppKey";
|
|
87
87
|
readonly clientSecret: "Client Secret / AppSecret";
|
|
88
88
|
readonly dingtalkHelp: "Get credentials: Create an enterprise internal app on <a href=\"https://open-dev.dingtalk.com/\" target=\"_blank\">DingTalk Open Platform</a>, enable Stream Mode, and get Client ID / Client Secret";
|
|
89
|
-
readonly workbuddyHelp: "Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens.
|
|
89
|
+
readonly workbuddyHelp: "Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. Connects WeChat assistant through Centrifuge WebSocket.";
|
|
90
90
|
readonly secret: "Secret";
|
|
91
91
|
readonly cardTemplateId: "Card template ID";
|
|
92
92
|
readonly workbuddyAccessToken: "Access Token";
|
|
@@ -266,8 +266,8 @@ export declare const PAGE_TEXTS: {
|
|
|
266
266
|
readonly qqSummary: "QQ 机器人 App ID 与 Secret。";
|
|
267
267
|
readonly weworkSummary: "企业微信 Corp ID 与 Secret。";
|
|
268
268
|
readonly dingtalkSummary: "钉钉 Client 凭证,可选配置卡片模板 ID。";
|
|
269
|
-
readonly workbuddySummary: "CodeBuddy OAuth
|
|
270
|
-
readonly clawbotSummary: "通过 ClawBot
|
|
269
|
+
readonly workbuddySummary: "通过 CodeBuddy OAuth 连接微信助理。";
|
|
270
|
+
readonly clawbotSummary: "通过 ClawBot 连接微信客服号。";
|
|
271
271
|
readonly clawbotApiUrl: "API 地址";
|
|
272
272
|
readonly clawbotApiToken: "API Token (Bearer)";
|
|
273
273
|
readonly clawbotHelp: "点击“扫码登录”通过微信认证,或手动粘贴 Token。";
|
|
@@ -52,11 +52,11 @@ export const PAGE_TEXTS = {
|
|
|
52
52
|
credentialProgress: "{done}/{total} credentials",
|
|
53
53
|
telegramSummary: "Bot token and optional proxy.",
|
|
54
54
|
feishuSummary: "App ID, App Secret, and allowed user scope.",
|
|
55
|
-
qqSummary: "App ID and secret for
|
|
56
|
-
weworkSummary: "Corp ID and secret for enterprise
|
|
55
|
+
qqSummary: "App ID and secret for QQ robot.",
|
|
56
|
+
weworkSummary: "Corp ID and secret for enterprise WeChat.",
|
|
57
57
|
dingtalkSummary: "Client credentials plus optional card template.",
|
|
58
|
-
workbuddySummary: "
|
|
59
|
-
clawbotSummary: "WeChat
|
|
58
|
+
workbuddySummary: "WeChat assistant via CodeBuddy OAuth.",
|
|
59
|
+
clawbotSummary: "WeChat customer service via ClawBot.",
|
|
60
60
|
clawbotApiUrl: "API URL",
|
|
61
61
|
clawbotApiToken: "API Token (Bearer)",
|
|
62
62
|
clawbotHelp: 'Click "Scan QR Login" to authenticate via WeChat, or paste token manually.',
|
|
@@ -86,7 +86,7 @@ export const PAGE_TEXTS = {
|
|
|
86
86
|
clientId: "Client ID / AppKey",
|
|
87
87
|
clientSecret: "Client Secret / AppSecret",
|
|
88
88
|
dingtalkHelp: 'Get credentials: Create an enterprise internal app on <a href="https://open-dev.dingtalk.com/" target="_blank">DingTalk Open Platform</a>, enable Stream Mode, and get Client ID / Client Secret',
|
|
89
|
-
workbuddyHelp: 'Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens.
|
|
89
|
+
workbuddyHelp: 'Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. Connects WeChat assistant through Centrifuge WebSocket.',
|
|
90
90
|
secret: "Secret",
|
|
91
91
|
cardTemplateId: "Card template ID",
|
|
92
92
|
workbuddyAccessToken: "Access Token",
|
|
@@ -267,8 +267,8 @@ export const PAGE_TEXTS = {
|
|
|
267
267
|
qqSummary: "QQ \u673a\u5668\u4eba App ID \u4e0e Secret\u3002",
|
|
268
268
|
weworkSummary: "\u4f01\u4e1a\u5fae\u4fe1 Corp ID \u4e0e Secret\u3002",
|
|
269
269
|
dingtalkSummary: "\u9489\u9489 Client \u51ed\u8bc1\uff0c\u53ef\u9009\u914d\u7f6e\u5361\u7247\u6a21\u677f ID\u3002",
|
|
270
|
-
workbuddySummary: "CodeBuddy OAuth \u8fde\u63a5\u5fae\u4fe1\
|
|
271
|
-
clawbotSummary: "\u901a\u8fc7 ClawBot \u8fde\u63a5\u5fae\u4fe1
|
|
270
|
+
workbuddySummary: "\u901a\u8fc7 CodeBuddy OAuth \u8fde\u63a5\u5fae\u4fe1\u52a9\u7406\u3002",
|
|
271
|
+
clawbotSummary: "\u901a\u8fc7 ClawBot \u8fde\u63a5\u5fae\u4fe1\u5ba2\u670d\u53f7\u3002",
|
|
272
272
|
clawbotApiUrl: "API \u5730\u5740",
|
|
273
273
|
clawbotApiToken: "API Token (Bearer)",
|
|
274
274
|
clawbotHelp: '\u70b9\u51fb\u201c\u626b\u7801\u767b\u5f55\u201d\u901a\u8fc7\u5fae\u4fe1\u8ba4\u8bc1\uff0c\u6216\u624b\u52a8\u7c98\u8d34 Token\u3002',
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
|
|
|
32
32
|
import { SessionManager } from "./session/session-manager.js";
|
|
33
33
|
import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
|
|
34
34
|
import { destroyAllLiveChildren } from "./shared/process-kill.js";
|
|
35
|
-
import { initLogger, createLogger, closeLogger,
|
|
35
|
+
import { initLogger, createLogger, closeLogger, } from "./logger.js";
|
|
36
36
|
import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
|
|
37
37
|
import { createRequire } from "node:module";
|
|
38
38
|
import { escapePathForMarkdown, getAIToolDisplayName } from "./shared/utils.js";
|
|
@@ -137,6 +137,55 @@ const PLATFORM_DISPLAY_NAMES = {
|
|
|
137
137
|
workbuddy: '微信',
|
|
138
138
|
clawbot: '微信 (ClawBot)',
|
|
139
139
|
};
|
|
140
|
+
/** 读取已启用的插件列表 */
|
|
141
|
+
function getEnabledPlugins() {
|
|
142
|
+
try {
|
|
143
|
+
const { readFileSync, existsSync } = require("fs");
|
|
144
|
+
const { join } = require("path");
|
|
145
|
+
const { homedir } = require("os");
|
|
146
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
147
|
+
if (!existsSync(settingsPath))
|
|
148
|
+
return [];
|
|
149
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
150
|
+
const plugins = settings.enabledPlugins ?? {};
|
|
151
|
+
return Object.entries(plugins)
|
|
152
|
+
.filter(([, v]) => v === true)
|
|
153
|
+
.map(([k]) => k.split("@")[0]);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 统一通知格式模板
|
|
161
|
+
* 所有发送给 IM 的通知都使用这个格式,保持一致性
|
|
162
|
+
*/
|
|
163
|
+
function buildNotification(opts) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
// 标题行
|
|
166
|
+
lines.push(`${opts.emoji} ${opts.title}`);
|
|
167
|
+
// 详情行
|
|
168
|
+
const details = [];
|
|
169
|
+
if (opts.platform)
|
|
170
|
+
details.push(`📱 平台: ${opts.platform}`);
|
|
171
|
+
if (opts.aiCommand)
|
|
172
|
+
details.push(`🤖 AI: ${opts.aiCommand}`);
|
|
173
|
+
if (opts.dir)
|
|
174
|
+
details.push(`📁 目录: ${opts.dir}`);
|
|
175
|
+
const plugins = getEnabledPlugins();
|
|
176
|
+
if (plugins.length > 0)
|
|
177
|
+
details.push(`🧩 插件: ${plugins.join(", ")}`);
|
|
178
|
+
if (opts.uptime)
|
|
179
|
+
details.push(`⏱️ 运行: ${opts.uptime}`);
|
|
180
|
+
if (details.length > 0) {
|
|
181
|
+
lines.push("", ...details);
|
|
182
|
+
}
|
|
183
|
+
// 额外信息
|
|
184
|
+
if (opts.extra?.length) {
|
|
185
|
+
lines.push("", ...opts.extra);
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
140
189
|
function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, sessionManager) {
|
|
141
190
|
let sessionDir;
|
|
142
191
|
// Telegram 私聊、企业微信当前实现里,活跃 chatId 可直接对应到 session userId。
|
|
@@ -149,21 +198,21 @@ function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, se
|
|
|
149
198
|
const platformName = PLATFORM_DISPLAY_NAMES[platform] ?? platform;
|
|
150
199
|
const toolName = getAIToolDisplayName(aiCommand);
|
|
151
200
|
const dir = sessionDir ? escapePathForMarkdown(sessionDir) : '发送 `/pwd` 查看';
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
201
|
+
return buildNotification({
|
|
202
|
+
emoji: "✅",
|
|
203
|
+
title: `open-im v${appVersion} 已就绪`,
|
|
204
|
+
platform: platformName,
|
|
205
|
+
aiCommand: toolName,
|
|
206
|
+
dir,
|
|
207
|
+
});
|
|
159
208
|
}
|
|
160
209
|
function buildShutdownMessage(uptimeMinutes) {
|
|
161
|
-
const
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
"",
|
|
165
|
-
|
|
166
|
-
|
|
210
|
+
const uptimeStr = uptimeMinutes < 1 ? '< 1 分钟' : `${uptimeMinutes} 分钟`;
|
|
211
|
+
return buildNotification({
|
|
212
|
+
emoji: "🛑",
|
|
213
|
+
title: "open-im 正在关闭",
|
|
214
|
+
uptime: uptimeStr,
|
|
215
|
+
});
|
|
167
216
|
}
|
|
168
217
|
export async function main() {
|
|
169
218
|
const startupCwd = process.cwd();
|
|
@@ -267,10 +316,6 @@ export async function main() {
|
|
|
267
316
|
}
|
|
268
317
|
log.info("Service is running. Press Ctrl+C to stop.");
|
|
269
318
|
log.info(`Successfully initialized platforms: ${successfulPlatforms.join(", ")}`);
|
|
270
|
-
emitStructuredEvent("Main", "service.platform.init", {
|
|
271
|
-
platforms: successfulPlatforms,
|
|
272
|
-
version: APP_VERSION,
|
|
273
|
-
});
|
|
274
319
|
// Send notification only to successfully initialized platforms
|
|
275
320
|
for (const platform of successfulPlatforms) {
|
|
276
321
|
const startupMsg = buildStartupMessage(platform, APP_VERSION, resolvePlatformAiCommand(config, platform), startupCwd, sessionManager);
|
|
@@ -328,7 +373,6 @@ export async function main() {
|
|
|
328
373
|
sessionManager.destroy();
|
|
329
374
|
cleanupAdapters();
|
|
330
375
|
flushActiveChats();
|
|
331
|
-
await shutdownLoggerTelemetry();
|
|
332
376
|
await flushSentry();
|
|
333
377
|
await closeLogger();
|
|
334
378
|
process.exit(0);
|
|
@@ -381,8 +425,7 @@ export async function main() {
|
|
|
381
425
|
}
|
|
382
426
|
}
|
|
383
427
|
}
|
|
384
|
-
void
|
|
385
|
-
.then(() => closeLogger())
|
|
428
|
+
void closeLogger()
|
|
386
429
|
.finally(() => process.exit(1));
|
|
387
430
|
});
|
|
388
431
|
}
|
|
@@ -391,8 +434,7 @@ const isEntry = process.argv[1]?.replace(/\\/g, "/").endsWith("/index.js") ||
|
|
|
391
434
|
if (isEntry) {
|
|
392
435
|
main().catch((err) => {
|
|
393
436
|
log.error("Fatal error:", err);
|
|
394
|
-
void
|
|
395
|
-
.then(() => closeLogger())
|
|
437
|
+
void closeLogger()
|
|
396
438
|
.finally(() => process.exit(1));
|
|
397
439
|
});
|
|
398
440
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -21,14 +21,11 @@ export declare function createLogger(tag: string): {
|
|
|
21
21
|
warn: (msg: string, ...args: unknown[]) => void;
|
|
22
22
|
error: (msg: string, ...args: unknown[]) => void;
|
|
23
23
|
debug: (msg: string, ...args: unknown[]) => void;
|
|
24
|
-
infoEvent: (event: string, data?: Record<string, unknown>, msg?: string) => void;
|
|
25
24
|
};
|
|
26
25
|
/**
|
|
27
26
|
* Audit log — records user interactions for debugging and compliance.
|
|
28
27
|
* Always enabled, writes to audit.log.
|
|
29
28
|
*/
|
|
30
29
|
export declare function auditLog(platform: string, userId: string, action: string, detail?: Record<string, unknown>): void;
|
|
31
|
-
export declare function emitStructuredEvent(tag: string, event: string, data?: Record<string, unknown>, level?: LogLevel, msg?: string): void;
|
|
32
|
-
export declare function shutdownLoggerTelemetry(): Promise<void>;
|
|
33
30
|
export declare function closeLogger(): Promise<void>;
|
|
34
31
|
export {};
|
package/dist/logger.js
CHANGED
|
@@ -3,16 +3,13 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { finished } from 'node:stream/promises';
|
|
4
4
|
import { sanitize } from './sanitize.js';
|
|
5
5
|
import { APP_HOME } from './constants.js';
|
|
6
|
-
import { sanitizeTelemetryData } from './telemetry/telemetry-sanitize.js';
|
|
7
6
|
const DEFAULT_LOG_DIR = join(APP_HOME, 'logs');
|
|
8
7
|
const MAX_LOG_FILES = 10;
|
|
9
8
|
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
10
9
|
let logDir = DEFAULT_LOG_DIR;
|
|
11
10
|
let minLevel = LOG_LEVELS.DEBUG;
|
|
12
11
|
let logStream;
|
|
13
|
-
let eventsStream;
|
|
14
12
|
let auditStream;
|
|
15
|
-
let telemetryEnabled = false;
|
|
16
13
|
function pad(n) {
|
|
17
14
|
return String(n).padStart(2, '0');
|
|
18
15
|
}
|
|
@@ -80,15 +77,6 @@ export function initLogger(dirOrOpts, level, telemetry) {
|
|
|
80
77
|
auditStream = undefined;
|
|
81
78
|
}
|
|
82
79
|
auditStream = createWriteStream(join(logDir, 'audit.log'), { flags: 'a' });
|
|
83
|
-
telemetryEnabled = !!tel?.enabled;
|
|
84
|
-
if (eventsStream) {
|
|
85
|
-
eventsStream.end();
|
|
86
|
-
eventsStream = undefined;
|
|
87
|
-
}
|
|
88
|
-
if (telemetryEnabled) {
|
|
89
|
-
rotateOldJsonl();
|
|
90
|
-
eventsStream = createWriteStream(join(logDir, getEventsFileName()), { flags: 'a' });
|
|
91
|
-
}
|
|
92
80
|
}
|
|
93
81
|
function write(level, tag, msg, ...args) {
|
|
94
82
|
if (LOG_LEVELS[level] < minLevel)
|
|
@@ -109,7 +97,6 @@ export function createLogger(tag) {
|
|
|
109
97
|
warn: (msg, ...args) => write('WARN', tag, msg, ...args),
|
|
110
98
|
error: (msg, ...args) => write('ERROR', tag, msg, ...args),
|
|
111
99
|
debug: (msg, ...args) => write('DEBUG', tag, msg, ...args),
|
|
112
|
-
infoEvent: (event, data, msg) => emitStructuredEvent(tag, event, data, 'INFO', msg),
|
|
113
100
|
};
|
|
114
101
|
}
|
|
115
102
|
/**
|
|
@@ -126,31 +113,13 @@ export function auditLog(platform, userId, action, detail) {
|
|
|
126
113
|
};
|
|
127
114
|
auditStream?.write(JSON.stringify(entry) + '\n');
|
|
128
115
|
}
|
|
129
|
-
export function emitStructuredEvent(tag, event, data, level = 'INFO', msg = '') {
|
|
130
|
-
if (!telemetryEnabled)
|
|
131
|
-
return;
|
|
132
|
-
const payload = {
|
|
133
|
-
v: 1,
|
|
134
|
-
ts: new Date().toISOString(),
|
|
135
|
-
level,
|
|
136
|
-
tag,
|
|
137
|
-
event,
|
|
138
|
-
msg,
|
|
139
|
-
data: sanitizeTelemetryData(data),
|
|
140
|
-
};
|
|
141
|
-
const line = `${JSON.stringify(payload)}\n`;
|
|
142
|
-
eventsStream?.write(line);
|
|
143
|
-
}
|
|
144
|
-
export async function shutdownLoggerTelemetry() {
|
|
145
|
-
// Local event logging only — no upload to clean up
|
|
146
|
-
}
|
|
147
116
|
export async function closeLogger() {
|
|
148
|
-
if (
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
117
|
+
if (auditStream) {
|
|
118
|
+
const as = auditStream;
|
|
119
|
+
auditStream = undefined;
|
|
120
|
+
as.end();
|
|
152
121
|
try {
|
|
153
|
-
await finished(
|
|
122
|
+
await finished(as);
|
|
154
123
|
}
|
|
155
124
|
catch {
|
|
156
125
|
/* ignore */
|