@cxyhhhhh/qqbot-cli 0.1.0-dev.202606011703
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 +239 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +25 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/chunk-WRKDI4MF.js +407 -0
- package/dist/chunk-WRKDI4MF.js.map +1 -0
- package/dist/chunk-XIJ6OSLY.js +3654 -0
- package/dist/chunk-XIJ6OSLY.js.map +1 -0
- package/dist/cli-TUC3HG75.js +14 -0
- package/dist/cli-TUC3HG75.js.map +1 -0
- package/dist/src/index.d.ts +1540 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +73 -0
- package/templates/bot.cloudagent.yaml +131 -0
- package/templates/bot.echo.yaml +55 -0
- package/templates/bot.galileo.yaml +63 -0
- package/templates/bot.openai.yaml +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# qqbot-cli
|
|
2
|
+
|
|
3
|
+
配置驱动的 AI QQ 机器人 CLI 工具。基于 [`@tencent-connect/qqbot-nodejs`](https://github.com/tencent-connect/qqbot-nodejs) SDK,支持多种 AI 后端适配。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **零代码启动** — YAML 配置即可运行
|
|
8
|
+
- **多后端适配** — Echo / OpenAI / CloudAgent ACP
|
|
9
|
+
- **双传输模式** — WebSocket(默认)/ Webhook(HTTP 回调)
|
|
10
|
+
- **流式输出** — C2C 私聊自动打字机效果
|
|
11
|
+
- **Koa Middleware** — 完整中间件链(防御/限流/脱敏/typing)
|
|
12
|
+
- **生产就绪** — 熔断/重连/敏感脱敏/优雅退出
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 克隆项目
|
|
18
|
+
|
|
19
|
+
# 安装依赖
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 依赖说明
|
|
24
|
+
|
|
25
|
+
| 包名 | 说明 |
|
|
26
|
+
|------|------|
|
|
27
|
+
| `@tencent-connect/qqbot-nodejs` | QQ 开放平台 Node.js SDK(协议/中间件/流式) |
|
|
28
|
+
| `@agentclientprotocol/sdk` | ACP 协议 SDK(CloudAgent 后端) |
|
|
29
|
+
| `commander` | CLI 命令框架 |
|
|
30
|
+
| `zod` | 配置 schema 运行时校验 |
|
|
31
|
+
| `yaml` | YAML 配置解析 |
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 1. 初始化配置文件
|
|
37
|
+
npx tsx bin/cli.ts init --template cloudagent
|
|
38
|
+
# 生成 bot.yaml
|
|
39
|
+
|
|
40
|
+
# 2. 设置环境变量
|
|
41
|
+
export QQBOT_APP_ID="你的 AppID"
|
|
42
|
+
export QQBOT_APP_SECRET="你的 AppSecret"
|
|
43
|
+
export CODEBUDDY_API_KEY="sk-xxxx"
|
|
44
|
+
|
|
45
|
+
# 3. 启动
|
|
46
|
+
npx tsx bin/cli.ts start
|
|
47
|
+
|
|
48
|
+
# 带参数启动
|
|
49
|
+
npx tsx bin/cli.ts start --config ./bot.yaml --verbose
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 配置模板
|
|
53
|
+
|
|
54
|
+
| 模板 | 命令 | 说明 |
|
|
55
|
+
|------|------|------|
|
|
56
|
+
| `cloudagent` | `init --template cloudagent` | CloudAgent ACP 协议(AI 沙箱) |
|
|
57
|
+
| `openai` | `init --template openai` | OpenAI 兼容 API(GPT-4o 等) |
|
|
58
|
+
| `echo` | `init --template echo` | 回显模式(开发测试) |
|
|
59
|
+
|
|
60
|
+
## CLI 命令
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
qqbot-cli start [--config bot.yaml] [--env .env] [--verbose]
|
|
64
|
+
qqbot-cli init [--template cloudagent|openai|echo] [--output bot.yaml]
|
|
65
|
+
qqbot-cli validate [--config bot.yaml]
|
|
66
|
+
qqbot-cli --version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 架构
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
CLI (commander)
|
|
73
|
+
→ Config Loader (yaml + zod schema)
|
|
74
|
+
→ BotRunner
|
|
75
|
+
→ QQBot SDK (WS + Koa Middleware + Streaming)
|
|
76
|
+
→ ReplyBackend (pluggable)
|
|
77
|
+
→ Echo / OpenAI / CloudAgent ACP
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 后端说明
|
|
81
|
+
|
|
82
|
+
### Echo
|
|
83
|
+
|
|
84
|
+
原样回显用户输入,用于验证 QQ 协议链路。
|
|
85
|
+
|
|
86
|
+
### OpenAI
|
|
87
|
+
|
|
88
|
+
兼容 OpenAI Chat Completions API(支持 streaming SSE),可对接:
|
|
89
|
+
- OpenAI (gpt-4o, gpt-3.5-turbo)
|
|
90
|
+
- DeepSeek
|
|
91
|
+
- 其他 OpenAI 兼容服务
|
|
92
|
+
|
|
93
|
+
### CloudAgent ACP
|
|
94
|
+
|
|
95
|
+
通过 ACP 协议与 CloudAgent(AgentOS)AI 沙箱交互:
|
|
96
|
+
- 基于 `@agentclientprotocol/sdk` 完整实现
|
|
97
|
+
- GET SSE 持久通知流 + POST SSE 响应流 + DELETE 优雅关闭
|
|
98
|
+
- 自动创建/复用沙箱 Runtime
|
|
99
|
+
- ACP 懒连接(首条消息触发 + 沙箱自动唤醒)
|
|
100
|
+
- 工具调用权限自动批准
|
|
101
|
+
- 双流时序补偿
|
|
102
|
+
- 附件透传(resource_link)
|
|
103
|
+
|
|
104
|
+
## 环境变量
|
|
105
|
+
|
|
106
|
+
| 变量 | 必填 | 说明 |
|
|
107
|
+
|------|------|------|
|
|
108
|
+
| `QQBOT_APP_ID` | ✓ | QQ 机器人 AppID |
|
|
109
|
+
| `QQBOT_APP_SECRET` | ✓ | QQ 机器人 AppSecret |
|
|
110
|
+
| `CODEBUDDY_API_KEY` | cloudagent 模式 | AgentOS API Key |
|
|
111
|
+
| `OPENAI_API_KEY` | openai 模式 | OpenAI API Key |
|
|
112
|
+
| `LOG_LEVEL` | ✗ | 日志级别 (debug/info/warn/error) |
|
|
113
|
+
|
|
114
|
+
## 项目结构
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
qqbot-cli/
|
|
118
|
+
├── bin/cli.ts # CLI 入口
|
|
119
|
+
├── src/
|
|
120
|
+
│ ├── index.ts # 程序化 API
|
|
121
|
+
│ ├── cli.ts # 命令实现
|
|
122
|
+
│ ├── runner.ts # BotRunner 编排器
|
|
123
|
+
│ ├── config/ # zod schema + 配置加载器
|
|
124
|
+
│ ├── backend/ # 可插拔后端(echo/openai/cloudagent)
|
|
125
|
+
│ ├── cloudagent/ # ACP 协议实现(基于 @agentclientprotocol/sdk)
|
|
126
|
+
│ └── utils/ # 日志/优雅退出
|
|
127
|
+
├── templates/ # 配置模板(YAML)
|
|
128
|
+
├── DESIGN.md # 设计方案
|
|
129
|
+
└── README.md
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 开发
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# 类型检查
|
|
136
|
+
npm run typecheck
|
|
137
|
+
|
|
138
|
+
# 开发模式(tsx 热重载)
|
|
139
|
+
npm run dev start -- --config bot.yaml
|
|
140
|
+
|
|
141
|
+
# 构建
|
|
142
|
+
npm run build
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Docker
|
|
146
|
+
|
|
147
|
+
### 构建镜像
|
|
148
|
+
|
|
149
|
+
采用**预构建模式**:先在本地编译 TypeScript,再将产物打入 Docker 镜像。
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cd qqbot-cli
|
|
153
|
+
|
|
154
|
+
# 1. 安装依赖 & 编译
|
|
155
|
+
pnpm install
|
|
156
|
+
pnpm build
|
|
157
|
+
|
|
158
|
+
# 2. 构建 Docker 镜像(指定 amd64 平台)
|
|
159
|
+
docker build --platform linux/amd64 -t qqbot-cli .
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 推送镜像
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# 打 tag
|
|
166
|
+
docker tag qqbot-cli qqai-images.tencentcloudcr.com/qqbot/workbuddy-qqbot-cli:latest
|
|
167
|
+
|
|
168
|
+
# 登录 & 推送
|
|
169
|
+
docker login qqai-images.tencentcloudcr.com
|
|
170
|
+
docker push qqai-images.tencentcloudcr.com/qqbot/workbuddy-qqbot-cli:latest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 启动容器
|
|
174
|
+
|
|
175
|
+
镜像内置了 `bot.yaml` 默认配置,可直接通过环境变量启动:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# 使用内置配置
|
|
179
|
+
docker run -d \
|
|
180
|
+
--name qqbot \
|
|
181
|
+
--restart unless-stopped \
|
|
182
|
+
-v /path/to/qqbot-data:/app/.qqbot-data \
|
|
183
|
+
-e QQBOT_APP_ID=你的AppID \
|
|
184
|
+
-e QQBOT_APP_SECRET=你的AppSecret \
|
|
185
|
+
-e CODEBUDDY_API_KEY=sk-xxxx \
|
|
186
|
+
qqbot-cli
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
如需自定义配置,挂载配置文件并设置 `BOT_CONFIG`:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# 使用自定义配置
|
|
193
|
+
docker run -d \
|
|
194
|
+
--name qqbot \
|
|
195
|
+
--restart unless-stopped \
|
|
196
|
+
-v /path/to/bot.yaml:/app/config/bot.yaml:ro \
|
|
197
|
+
-v /path/to/qqbot-data:/app/.qqbot-data \
|
|
198
|
+
-e BOT_CONFIG=/app/config/bot.yaml \
|
|
199
|
+
-e QQBOT_APP_ID=你的AppID \
|
|
200
|
+
-e QQBOT_APP_SECRET=你的AppSecret \
|
|
201
|
+
-e CODEBUDDY_API_KEY=sk-xxxx \
|
|
202
|
+
qqbot-cli
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| 参数 | 说明 |
|
|
206
|
+
|------|------|
|
|
207
|
+
| `-e BOT_CONFIG=...` | 指定配置文件路径(默认 `/app/bot.yaml` 内置配置) |
|
|
208
|
+
| `-v bot.yaml:/app/config/bot.yaml:ro` | 挂载自定义配置文件(只读) |
|
|
209
|
+
| `-v qqbot-data:/app/.qqbot-data` | 持久化会话数据 |
|
|
210
|
+
| `-e QQBOT_APP_ID=xxx` | 注入环境变量(`bot.yaml` 中 `${QQBOT_APP_ID}` 自动替换) |
|
|
211
|
+
| `--restart unless-stopped` | 异常退出自动重启 |
|
|
212
|
+
|
|
213
|
+
### 运维命令
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# 查看日志
|
|
217
|
+
docker logs -f qqbot
|
|
218
|
+
|
|
219
|
+
# 进入容器
|
|
220
|
+
docker exec -it qqbot sh
|
|
221
|
+
|
|
222
|
+
# 停止(触发优雅关闭)
|
|
223
|
+
docker stop qqbot
|
|
224
|
+
|
|
225
|
+
# 停止并删除
|
|
226
|
+
docker stop qqbot && docker rm qqbot
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 镜像说明
|
|
230
|
+
|
|
231
|
+
- **基础镜像**: `node:22-alpine`(~80MB)
|
|
232
|
+
- **PID 1**: [tini](https://github.com/krallin/tini),正确转发 `SIGTERM` 实现优雅停机
|
|
233
|
+
- **内置配置**: `bot.yaml` 打包在 `/app/bot.yaml`,通过 `BOT_CONFIG` 环境变量可覆盖
|
|
234
|
+
- **VOLUME**: `/app/.qqbot-data`(会话持久化)、`/app/config`(配置挂载点)
|
|
235
|
+
- 敏感信息通过 `-e` 环境变量传入,不写入镜像
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
ISC
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
APP_VERSION,
|
|
4
|
+
startCommand
|
|
5
|
+
} from "../chunk-WRKDI4MF.js";
|
|
6
|
+
import "../chunk-XIJ6OSLY.js";
|
|
7
|
+
|
|
8
|
+
// bin/cli.ts
|
|
9
|
+
import { program } from "commander";
|
|
10
|
+
program.name("qqbot-cli").description("QQ Bot CLI \u2014 \u914D\u7F6E\u9A71\u52A8\u7684 AI QQ \u673A\u5668\u4EBA").version(APP_VERSION);
|
|
11
|
+
program.command("start").description("\u542F\u52A8 QQ \u673A\u5668\u4EBA").option("-c, --config <path>", "\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84", "bot.yaml").option("-e, --env <path>", ".env \u6587\u4EF6\u8DEF\u5F84").option("-v, --verbose", "\u5F00\u542F debug \u65E5\u5FD7").action(startCommand);
|
|
12
|
+
program.command("init").description("\u4EA4\u4E92\u5F0F\u521D\u59CB\u5316\u914D\u7F6E\u6587\u4EF6").option("-t, --template <type>", "\u6A21\u677F\u7C7B\u578B: cloudagent | openai | echo", "cloudagent").option("-o, --output <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84", "bot.yaml").action(async (opts) => {
|
|
13
|
+
const { initCommand } = await import("../cli-TUC3HG75.js");
|
|
14
|
+
await initCommand(opts);
|
|
15
|
+
});
|
|
16
|
+
program.command("validate").description("\u9A8C\u8BC1\u914D\u7F6E\u6587\u4EF6").option("-c, --config <path>", "\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84", "bot.yaml").action(async (opts) => {
|
|
17
|
+
const { validateCommand } = await import("../cli-TUC3HG75.js");
|
|
18
|
+
await validateCommand(opts);
|
|
19
|
+
});
|
|
20
|
+
program.command("send").description("Send a proactive message (text and/or files) to a user or group").argument("[text...]", "Text message content").requiredOption("--to <openid>", "Target user or group openid").option("--scope <scope>", "Chat scope: c2c or group", "c2c").option("-f, --file <path...>", "Attach file(s): image, video, voice, or generic file").option("--file-prefix <prefix>", "Path prefix for files (e.g. /host for Docker bind mount)").option("-c, --config <path>", "Config file path", "bot.yaml").option("-e, --env <path>", ".env file path").option("-v, --verbose", "Enable debug logging").action(async (textArgs, opts) => {
|
|
21
|
+
const { sendCommand } = await import("../cli-TUC3HG75.js");
|
|
22
|
+
await sendCommand(textArgs, opts);
|
|
23
|
+
});
|
|
24
|
+
program.parse();
|
|
25
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * qqbot-cli — CLI 入口\n */\n\nimport { program } from \"commander\";\nimport { startCommand } from \"../src/cli.js\";\nimport { APP_VERSION } from \"../src/version.js\";\n\nprogram\n .name(\"qqbot-cli\")\n .description(\"QQ Bot CLI — 配置驱动的 AI QQ 机器人\")\n .version(APP_VERSION);\n\nprogram\n .command(\"start\")\n .description(\"启动 QQ 机器人\")\n .option(\"-c, --config <path>\", \"配置文件路径\", \"bot.yaml\")\n .option(\"-e, --env <path>\", \".env 文件路径\")\n .option(\"-v, --verbose\", \"开启 debug 日志\")\n .action(startCommand);\n\nprogram\n .command(\"init\")\n .description(\"交互式初始化配置文件\")\n .option(\"-t, --template <type>\", \"模板类型: cloudagent | openai | echo\", \"cloudagent\")\n .option(\"-o, --output <path>\", \"输出文件路径\", \"bot.yaml\")\n .action(async (opts) => {\n const { initCommand } = await import(\"../src/cli.js\");\n await initCommand(opts);\n });\n\nprogram\n .command(\"validate\")\n .description(\"验证配置文件\")\n .option(\"-c, --config <path>\", \"配置文件路径\", \"bot.yaml\")\n .action(async (opts) => {\n const { validateCommand } = await import(\"../src/cli.js\");\n await validateCommand(opts);\n });\n\nprogram\n .command(\"send\")\n .description(\"Send a proactive message (text and/or files) to a user or group\")\n .argument(\"[text...]\", \"Text message content\")\n .requiredOption(\"--to <openid>\", \"Target user or group openid\")\n .option(\"--scope <scope>\", \"Chat scope: c2c or group\", \"c2c\")\n .option(\"-f, --file <path...>\", \"Attach file(s): image, video, voice, or generic file\")\n .option(\"--file-prefix <prefix>\", \"Path prefix for files (e.g. /host for Docker bind mount)\")\n .option(\"-c, --config <path>\", \"Config file path\", \"bot.yaml\")\n .option(\"-e, --env <path>\", \".env file path\")\n .option(\"-v, --verbose\", \"Enable debug logging\")\n .action(async (textArgs, opts) => {\n const { sendCommand } = await import(\"../src/cli.js\");\n await sendCommand(textArgs, opts);\n });\n\nprogram.parse();\n"],"mappings":";;;;;;;;AAKA,SAAS,eAAe;AAIxB,QACG,KAAK,WAAW,EAChB,YAAY,2EAA8B,EAC1C,QAAQ,WAAW;AAEtB,QACG,QAAQ,OAAO,EACf,YAAY,oCAAW,EACvB,OAAO,uBAAuB,wCAAU,UAAU,EAClD,OAAO,oBAAoB,+BAAW,EACtC,OAAO,iBAAiB,iCAAa,EACrC,OAAO,YAAY;AAEtB,QACG,QAAQ,MAAM,EACd,YAAY,8DAAY,EACxB,OAAO,yBAAyB,wDAAoC,YAAY,EAChF,OAAO,uBAAuB,wCAAU,UAAU,EAClD,OAAO,OAAO,SAAS;AACtB,QAAM,EAAE,YAAY,IAAI,MAAM,OAAO,oBAAe;AACpD,QAAM,YAAY,IAAI;AACxB,CAAC;AAEH,QACG,QAAQ,UAAU,EAClB,YAAY,sCAAQ,EACpB,OAAO,uBAAuB,wCAAU,UAAU,EAClD,OAAO,OAAO,SAAS;AACtB,QAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,oBAAe;AACxD,QAAM,gBAAgB,IAAI;AAC5B,CAAC;AAEH,QACG,QAAQ,MAAM,EACd,YAAY,iEAAiE,EAC7E,SAAS,aAAa,sBAAsB,EAC5C,eAAe,iBAAiB,6BAA6B,EAC7D,OAAO,mBAAmB,4BAA4B,KAAK,EAC3D,OAAO,wBAAwB,sDAAsD,EACrF,OAAO,0BAA0B,0DAA0D,EAC3F,OAAO,uBAAuB,oBAAoB,UAAU,EAC5D,OAAO,oBAAoB,gBAAgB,EAC3C,OAAO,iBAAiB,sBAAsB,EAC9C,OAAO,OAAO,UAAU,SAAS;AAChC,QAAM,EAAE,YAAY,IAAI,MAAM,OAAO,oBAAe;AACpD,QAAM,YAAY,UAAU,IAAI;AAClC,CAAC;AAEH,QAAQ,MAAM;","names":[]}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BotRunner,
|
|
3
|
+
ConfigWatcher,
|
|
4
|
+
GALILEO_RESOURCE,
|
|
5
|
+
loadConfig,
|
|
6
|
+
setBizDefaultAttrs
|
|
7
|
+
} from "./chunk-XIJ6OSLY.js";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
|
|
13
|
+
// src/utils/logger.ts
|
|
14
|
+
import pino from "pino";
|
|
15
|
+
import { mkdirSync } from "fs";
|
|
16
|
+
import { join, resolve } from "path";
|
|
17
|
+
function createLogger(config, levelOverride) {
|
|
18
|
+
const level = levelOverride ?? config?.level ?? "info";
|
|
19
|
+
const consoleMode = config?.console ?? "pretty";
|
|
20
|
+
const file = config?.file ?? { enabled: false, dir: "./logs", maxSize: "10m", maxFiles: 7 };
|
|
21
|
+
const targets = [];
|
|
22
|
+
if (consoleMode === "pretty") {
|
|
23
|
+
targets.push({
|
|
24
|
+
target: "pino-pretty",
|
|
25
|
+
options: {
|
|
26
|
+
destination: 2,
|
|
27
|
+
colorize: process.stderr.isTTY ?? false,
|
|
28
|
+
translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
|
|
29
|
+
ignore: "pid,hostname"
|
|
30
|
+
},
|
|
31
|
+
level
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
targets.push({
|
|
35
|
+
target: "pino/file",
|
|
36
|
+
options: { destination: 2 },
|
|
37
|
+
level
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (file.enabled) {
|
|
41
|
+
const dir = resolve(file.dir);
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
targets.push({
|
|
44
|
+
target: "pino-roll",
|
|
45
|
+
options: {
|
|
46
|
+
file: join(dir, "qqbot"),
|
|
47
|
+
size: file.maxSize,
|
|
48
|
+
limit: { count: file.maxFiles },
|
|
49
|
+
mkdir: true,
|
|
50
|
+
...file.frequency ? { frequency: file.frequency } : {},
|
|
51
|
+
...file.dateFormat ? { dateFormat: file.dateFormat } : {},
|
|
52
|
+
...file.symlink ? { symlink: true } : {}
|
|
53
|
+
},
|
|
54
|
+
level
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return pino(
|
|
58
|
+
{
|
|
59
|
+
level,
|
|
60
|
+
redact: {
|
|
61
|
+
paths: ["req.headers.authorization", "access_token"],
|
|
62
|
+
censor: "***"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
pino.transport({ targets })
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/utils/graceful.ts
|
|
70
|
+
function setupGracefulShutdown(logger, cleanup) {
|
|
71
|
+
const ac = new AbortController();
|
|
72
|
+
let shuttingDown = false;
|
|
73
|
+
const shutdown = async (signal) => {
|
|
74
|
+
if (shuttingDown) return;
|
|
75
|
+
shuttingDown = true;
|
|
76
|
+
logger.info(`\u6536\u5230 ${signal}\uFF0C\u6B63\u5728\u505C\u6B62...`);
|
|
77
|
+
try {
|
|
78
|
+
ac.abort();
|
|
79
|
+
await cleanup();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warn(`\u6E05\u7406\u51FA\u9519: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
};
|
|
85
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
86
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
87
|
+
process.on("uncaughtException", (err) => {
|
|
88
|
+
logger.error(`uncaughtException: ${err.stack ?? err.message}`);
|
|
89
|
+
});
|
|
90
|
+
process.on("unhandledRejection", (err) => {
|
|
91
|
+
logger.error(`unhandledRejection: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
92
|
+
});
|
|
93
|
+
return ac;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/telemetry/index.ts
|
|
97
|
+
import { metrics, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
98
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
99
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
100
|
+
import {
|
|
101
|
+
MeterProvider,
|
|
102
|
+
PeriodicExportingMetricReader,
|
|
103
|
+
ConsoleMetricExporter,
|
|
104
|
+
AggregationTemporality
|
|
105
|
+
} from "@opentelemetry/sdk-metrics";
|
|
106
|
+
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
107
|
+
import { Resource } from "@opentelemetry/resources";
|
|
108
|
+
import { W3CTraceContextPropagator, TraceIdRatioBasedSampler } from "@opentelemetry/core";
|
|
109
|
+
|
|
110
|
+
// src/version.ts
|
|
111
|
+
import { createRequire } from "module";
|
|
112
|
+
function resolveVersion() {
|
|
113
|
+
if (true) return "0.1.0-dev.202606011703";
|
|
114
|
+
try {
|
|
115
|
+
const req = createRequire(import.meta.url);
|
|
116
|
+
try {
|
|
117
|
+
return req("../package.json").version;
|
|
118
|
+
} catch {
|
|
119
|
+
return req("../../package.json").version;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
return "0.0.0-dev";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
var APP_VERSION = resolveVersion();
|
|
126
|
+
|
|
127
|
+
// src/telemetry/index.ts
|
|
128
|
+
import { networkInterfaces } from "os";
|
|
129
|
+
function getLocalIp() {
|
|
130
|
+
const envIp = process.env.POD_IP || process.env.HOST_IP || process.env.INSTANCE_IP;
|
|
131
|
+
if (envIp) return envIp;
|
|
132
|
+
const interfaces = networkInterfaces();
|
|
133
|
+
for (const name of Object.keys(interfaces)) {
|
|
134
|
+
for (const iface of interfaces[name] ?? []) {
|
|
135
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
136
|
+
return iface.address;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
var tracerProvider = null;
|
|
143
|
+
var meterProvider = null;
|
|
144
|
+
function initTelemetry(config, logger, opts) {
|
|
145
|
+
if (!config.enabled) return;
|
|
146
|
+
if (config.debug) {
|
|
147
|
+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
|
148
|
+
}
|
|
149
|
+
const galileo = config.galileo;
|
|
150
|
+
const objectName = galileo ? `${galileo.app}.${galileo.server}` : config.serviceName;
|
|
151
|
+
const resourceAttrs = {
|
|
152
|
+
"service.name": objectName,
|
|
153
|
+
[GALILEO_RESOURCE.SERVICE_NAME]: objectName,
|
|
154
|
+
[GALILEO_RESOURCE.SDK_LANGUAGE]: "nodejs",
|
|
155
|
+
[GALILEO_RESOURCE.SDK_NAME]: "qqbot-cli",
|
|
156
|
+
[GALILEO_RESOURCE.APP_ID]: opts?.appId ?? "",
|
|
157
|
+
...config.attributes
|
|
158
|
+
};
|
|
159
|
+
if (galileo) {
|
|
160
|
+
resourceAttrs[GALILEO_RESOURCE.TARGET] = `${galileo.platform}.${objectName}`;
|
|
161
|
+
resourceAttrs[GALILEO_RESOURCE.NAMESPACE] = galileo.namespace;
|
|
162
|
+
resourceAttrs[GALILEO_RESOURCE.ENV_NAME] = galileo.envName;
|
|
163
|
+
resourceAttrs[GALILEO_RESOURCE.CONTAINER_NAME] = "";
|
|
164
|
+
resourceAttrs[GALILEO_RESOURCE.INSTANCE] = getLocalIp();
|
|
165
|
+
resourceAttrs[GALILEO_RESOURCE.VERSION] = APP_VERSION;
|
|
166
|
+
resourceAttrs[GALILEO_RESOURCE.CON_SETID] = "";
|
|
167
|
+
}
|
|
168
|
+
const resource = new Resource(resourceAttrs);
|
|
169
|
+
setBizDefaultAttrs({
|
|
170
|
+
app_id: opts?.appId ?? "",
|
|
171
|
+
instance: getLocalIp(),
|
|
172
|
+
version: APP_VERSION
|
|
173
|
+
});
|
|
174
|
+
const metricsUrl = `${config.endpoint}/v1/metrics`;
|
|
175
|
+
const tracesUrl = `${config.endpoint}/v1/traces`;
|
|
176
|
+
tracerProvider = new NodeTracerProvider({
|
|
177
|
+
resource,
|
|
178
|
+
sampler: new TraceIdRatioBasedSampler(config.sampleRate)
|
|
179
|
+
});
|
|
180
|
+
tracerProvider.addSpanProcessor(
|
|
181
|
+
new BatchSpanProcessor(new OTLPTraceExporter({ url: tracesUrl }))
|
|
182
|
+
);
|
|
183
|
+
tracerProvider.register({
|
|
184
|
+
propagator: new W3CTraceContextPropagator()
|
|
185
|
+
});
|
|
186
|
+
meterProvider = new MeterProvider({ resource });
|
|
187
|
+
meterProvider.addMetricReader(
|
|
188
|
+
new PeriodicExportingMetricReader({
|
|
189
|
+
exporter: new OTLPMetricExporter({
|
|
190
|
+
url: metricsUrl,
|
|
191
|
+
temporalityPreference: AggregationTemporality.DELTA
|
|
192
|
+
}),
|
|
193
|
+
exportIntervalMillis: config.exportIntervalMs
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
if (config.debug) {
|
|
197
|
+
meterProvider.addMetricReader(
|
|
198
|
+
new PeriodicExportingMetricReader({
|
|
199
|
+
exporter: new ConsoleMetricExporter(),
|
|
200
|
+
exportIntervalMillis: config.exportIntervalMs
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
205
|
+
logger?.info(`[telemetry] ready (metrics=${metricsUrl}, traces=${tracesUrl})`);
|
|
206
|
+
}
|
|
207
|
+
async function shutdownTelemetry() {
|
|
208
|
+
await tracerProvider?.shutdown();
|
|
209
|
+
await meterProvider?.shutdown();
|
|
210
|
+
tracerProvider = null;
|
|
211
|
+
meterProvider = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/cli.ts
|
|
215
|
+
async function startCommand(opts) {
|
|
216
|
+
if (opts.env) {
|
|
217
|
+
const { config: config2 } = await import("dotenv");
|
|
218
|
+
config2({ path: opts.env });
|
|
219
|
+
} else if (fs.existsSync(".env")) {
|
|
220
|
+
const { config: config2 } = await import("dotenv");
|
|
221
|
+
config2();
|
|
222
|
+
}
|
|
223
|
+
const logLevel = opts.verbose ? "debug" : void 0;
|
|
224
|
+
let logger = createLogger(void 0, logLevel);
|
|
225
|
+
let config;
|
|
226
|
+
try {
|
|
227
|
+
config = await loadConfig(opts.config);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
logger.error(`\u914D\u7F6E\u9519\u8BEF: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
process.exit(2);
|
|
231
|
+
}
|
|
232
|
+
const effectiveLevel = logLevel ?? config.log.level;
|
|
233
|
+
config.log.level = effectiveLevel;
|
|
234
|
+
logger = createLogger(config.log, logLevel);
|
|
235
|
+
initTelemetry(config.telemetry, logger, { appId: config.qq.appId });
|
|
236
|
+
logger.info(`qqbot-cli v${APP_VERSION} \u542F\u52A8\u4E2D...`);
|
|
237
|
+
logger.info(` \u914D\u7F6E: ${opts.config}`);
|
|
238
|
+
logger.info(` \u540E\u7AEF: ${config.backend.type}`);
|
|
239
|
+
logger.info(` \u4F20\u8F93: ${config.qq.transport}${config.qq.transport === "webhook" ? ` (port=${config.qq.webhook.port}, path=${config.qq.webhook.path})` : ""}`);
|
|
240
|
+
logger.info(` AppID: ${config.qq.appId}`);
|
|
241
|
+
if (config.telemetry.enabled) {
|
|
242
|
+
logger.info(` Telemetry: ${config.telemetry.endpoint} (sample=${config.telemetry.sampleRate})`);
|
|
243
|
+
}
|
|
244
|
+
const runner = new BotRunner(config, logger);
|
|
245
|
+
const watcher = new ConfigWatcher({
|
|
246
|
+
configPath: path.resolve(opts.config),
|
|
247
|
+
currentConfig: config,
|
|
248
|
+
logger,
|
|
249
|
+
onChange: (newConfig) => {
|
|
250
|
+
if (logLevel) {
|
|
251
|
+
newConfig.log.level = logLevel;
|
|
252
|
+
}
|
|
253
|
+
runner.applyConfigUpdate(newConfig);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
watcher.start();
|
|
257
|
+
runner.setConfigWriter((patches, currentConfig) => watcher.writeBack(patches, currentConfig));
|
|
258
|
+
const ac = setupGracefulShutdown(logger, async () => {
|
|
259
|
+
await shutdownTelemetry();
|
|
260
|
+
watcher.stop();
|
|
261
|
+
await runner.stop();
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
await runner.start(ac.signal);
|
|
265
|
+
logger.info("Bot \u5DF2\u505C\u6B62\u3002");
|
|
266
|
+
} catch (err) {
|
|
267
|
+
logger.error(`\u542F\u52A8\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function initCommand(opts) {
|
|
272
|
+
const templateFile = path.resolve(
|
|
273
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
274
|
+
`../templates/bot.${opts.template}.yaml`
|
|
275
|
+
);
|
|
276
|
+
if (!fs.existsSync(templateFile)) {
|
|
277
|
+
console.error(`\u274C \u672A\u77E5\u6A21\u677F: ${opts.template}\uFF08\u53EF\u9009: cloudagent | openai | echo\uFF09`);
|
|
278
|
+
process.exit(2);
|
|
279
|
+
}
|
|
280
|
+
if (fs.existsSync(opts.output)) {
|
|
281
|
+
console.error(`\u26A0\uFE0F ${opts.output} \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u751F\u6210\u3002`);
|
|
282
|
+
process.exit(0);
|
|
283
|
+
}
|
|
284
|
+
fs.copyFileSync(templateFile, opts.output);
|
|
285
|
+
console.log(`\u2705 \u5DF2\u751F\u6210 ${opts.output}\uFF08\u6A21\u677F: ${opts.template}\uFF09`);
|
|
286
|
+
console.log(`
|
|
287
|
+
\u4E0B\u4E00\u6B65\uFF1A`);
|
|
288
|
+
console.log(` 1. \u7F16\u8F91 ${opts.output}\uFF0C\u586B\u5165 AppID / AppSecret / API Key`);
|
|
289
|
+
console.log(` 2. \u8FD0\u884C qqbot-cli start --config ${opts.output}`);
|
|
290
|
+
}
|
|
291
|
+
async function validateCommand(opts) {
|
|
292
|
+
try {
|
|
293
|
+
const config = await loadConfig(opts.config);
|
|
294
|
+
console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6\u6709\u6548`);
|
|
295
|
+
console.log(` \u540E\u7AEF: ${config.backend.type}`);
|
|
296
|
+
console.log(` AppID: ${config.qq.appId}`);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error(`\u274C \u914D\u7F6E\u65E0\u6548: ${err instanceof Error ? err.message : String(err)}`);
|
|
299
|
+
process.exit(2);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function sendCommand(textArgs, opts) {
|
|
303
|
+
if (opts.env) {
|
|
304
|
+
const { config: config2 } = await import("dotenv");
|
|
305
|
+
config2({ path: opts.env });
|
|
306
|
+
} else if (fs.existsSync(".env")) {
|
|
307
|
+
const { config: config2 } = await import("dotenv");
|
|
308
|
+
config2();
|
|
309
|
+
}
|
|
310
|
+
const logger = createLogger(void 0, opts.verbose ? "debug" : "error");
|
|
311
|
+
let config;
|
|
312
|
+
try {
|
|
313
|
+
config = await loadConfig(opts.config);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(`\u274C Config error: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
|
+
process.exit(2);
|
|
317
|
+
}
|
|
318
|
+
if (!opts.to) {
|
|
319
|
+
console.error("\u274C --to <openid> is required");
|
|
320
|
+
process.exit(2);
|
|
321
|
+
}
|
|
322
|
+
const scope = opts.scope ?? "c2c";
|
|
323
|
+
if (scope !== "c2c" && scope !== "group") {
|
|
324
|
+
console.error(`\u274C Invalid scope: ${scope} (must be "c2c" or "group")`);
|
|
325
|
+
process.exit(2);
|
|
326
|
+
}
|
|
327
|
+
const { QQBot, MediaFileType } = await import("@tencent/qqbot-nodejs");
|
|
328
|
+
const bot = new QQBot({
|
|
329
|
+
appId: config.qq.appId,
|
|
330
|
+
appSecret: config.qq.appSecret,
|
|
331
|
+
baseUrl: config.qq.baseUrl,
|
|
332
|
+
tokenBaseUrl: config.qq.tokenBaseUrl,
|
|
333
|
+
logger
|
|
334
|
+
});
|
|
335
|
+
const target = { scope, targetId: opts.to };
|
|
336
|
+
const text = textArgs.join(" ").trim();
|
|
337
|
+
const files = opts.file ?? [];
|
|
338
|
+
const filePrefix = opts.filePrefix ?? process.env.FILE_PATH_PREFIX ?? "";
|
|
339
|
+
try {
|
|
340
|
+
if (text) {
|
|
341
|
+
const resp = await bot.sendText(target, text);
|
|
342
|
+
console.log(`\u2705 Text sent (id=${resp.id})`);
|
|
343
|
+
}
|
|
344
|
+
for (const filePath of files) {
|
|
345
|
+
const resolved = resolveFilePath(filePath, filePrefix);
|
|
346
|
+
if (!fs.existsSync(resolved)) {
|
|
347
|
+
console.error(`\u274C File not found: ${resolved}`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
351
|
+
const fileType = inferFileType(ext);
|
|
352
|
+
const fileName = path.basename(resolved);
|
|
353
|
+
const buffer = fs.readFileSync(resolved);
|
|
354
|
+
console.log(`\u{1F4E4} Uploading ${fileName} (${formatSize(buffer.length)}, type=${MediaFileType[fileType]}) ...`);
|
|
355
|
+
const { upload, message } = await bot.sendMedia({
|
|
356
|
+
target,
|
|
357
|
+
fileType,
|
|
358
|
+
buffer,
|
|
359
|
+
fileName,
|
|
360
|
+
onProgress: (uploaded, total) => {
|
|
361
|
+
const pct = total > 0 ? Math.round(uploaded / total * 100) : 0;
|
|
362
|
+
process.stdout.write(`\r Progress: ${pct}% (${formatSize(uploaded)}/${formatSize(total)})`);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
process.stdout.write("\n");
|
|
366
|
+
console.log(`\u2705 ${fileName} sent (file_uuid=${upload.file_uuid ?? "-"}, msg_id=${message?.id ?? "-"})`);
|
|
367
|
+
}
|
|
368
|
+
if (!text && files.length === 0) {
|
|
369
|
+
console.error("\u274C Nothing to send. Provide text and/or --file <path>.");
|
|
370
|
+
process.exit(2);
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.error(`\u274C Send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
|
|
378
|
+
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm"]);
|
|
379
|
+
var VOICE_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".aac", ".silk", ".amr"]);
|
|
380
|
+
function inferFileType(ext) {
|
|
381
|
+
if (IMAGE_EXTS.has(ext)) return 1;
|
|
382
|
+
if (VIDEO_EXTS.has(ext)) return 2;
|
|
383
|
+
if (VOICE_EXTS.has(ext)) return 3;
|
|
384
|
+
return 4;
|
|
385
|
+
}
|
|
386
|
+
function formatSize(bytes) {
|
|
387
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
388
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
389
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
390
|
+
}
|
|
391
|
+
function resolveFilePath(filePath, prefix) {
|
|
392
|
+
const resolved = path.resolve(filePath);
|
|
393
|
+
if (!prefix || fs.existsSync(resolved)) {
|
|
394
|
+
return resolved;
|
|
395
|
+
}
|
|
396
|
+
const prefixed = path.join(prefix, resolved);
|
|
397
|
+
return prefixed;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export {
|
|
401
|
+
APP_VERSION,
|
|
402
|
+
startCommand,
|
|
403
|
+
initCommand,
|
|
404
|
+
validateCommand,
|
|
405
|
+
sendCommand
|
|
406
|
+
};
|
|
407
|
+
//# sourceMappingURL=chunk-WRKDI4MF.js.map
|