@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 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
@@ -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