@tencent-connect/openclaw-qqbot 1.0.3-alpha.0 → 1.0.5-alpha.stream

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.0.3-alpha.0",
3
+ "version": "1.0.5-alpha.stream",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,6 +13,8 @@ PKG_NAME="@tencent-connect/openclaw-qqbot"
13
13
  INSTALL_SRC=""
14
14
  APPID=""
15
15
  SECRET=""
16
+ STREAM=""
17
+ DEBUG=""
16
18
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
19
  PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18
20
 
@@ -31,6 +33,8 @@ print_usage() {
31
33
  echo " upgrade-via-npm.sh # 升级到 latest(默认)"
32
34
  echo " upgrade-via-npm.sh --version <版本号> # 升级到指定版本"
33
35
  echo " upgrade-via-npm.sh --appid <appid> --secret <secret> # 配置通道并启动"
36
+ echo " upgrade-via-npm.sh --stream <yes|no> # 是否启用流式消息(仅C2C私聊生效)"
37
+ echo " upgrade-via-npm.sh --debug <yes|no> # 是否启用 debug 模式"
34
38
  if [ -n "$LOCAL_VERSION" ]; then
35
39
  echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本($LOCAL_VERSION)"
36
40
  else
@@ -40,6 +44,8 @@ print_usage() {
40
44
  echo "也可以通过环境变量设置:"
41
45
  echo " QQBOT_APPID QQ机器人 appid"
42
46
  echo " QQBOT_SECRET QQ机器人 secret"
47
+ echo " QQBOT_STREAM 是否启用流式消息(yes/no)"
48
+ echo " QQBOT_DEBUG 是否启用 debug 模式(yes/no)"
43
49
  }
44
50
 
45
51
  while [[ $# -gt 0 ]]; do
@@ -69,6 +75,16 @@ while [[ $# -gt 0 ]]; do
69
75
  SECRET="$2"
70
76
  shift 2
71
77
  ;;
78
+ --stream)
79
+ [ -z "$2" ] && echo "❌ --stream 需要参数" && exit 1
80
+ STREAM="$2"
81
+ shift 2
82
+ ;;
83
+ --debug)
84
+ [ -z "$2" ] && echo "❌ --debug 需要参数" && exit 1
85
+ DEBUG="$2"
86
+ shift 2
87
+ ;;
72
88
  -h|--help)
73
89
  print_usage
74
90
  exit 0
@@ -81,6 +97,8 @@ INSTALL_SRC="${INSTALL_SRC:-${PKG_NAME}@latest}"
81
97
  # 使用命令行参数或环境变量
82
98
  APPID="${APPID:-$QQBOT_APPID}"
83
99
  SECRET="${SECRET:-$QQBOT_SECRET}"
100
+ STREAM="${STREAM:-$QQBOT_STREAM}"
101
+ DEBUG="${DEBUG:-$QQBOT_DEBUG}"
84
102
 
85
103
  # 检测 CLI
86
104
  CMD=""
@@ -188,6 +206,114 @@ if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
188
206
  fi
189
207
  fi
190
208
 
209
+ # 配置 stream 选项(仅在明确指定时才配置)
210
+ if [ -n "$STREAM" ]; then
211
+ echo ""
212
+ echo "配置 stream 选项..."
213
+ if [ "$STREAM" = "yes" ] || [ "$STREAM" = "y" ] || [ "$STREAM" = "true" ]; then
214
+ STREAM_VALUE="true"
215
+ echo "启用流式消息(仅C2C私聊生效)..."
216
+ else
217
+ STREAM_VALUE="false"
218
+ echo "禁用流式消息..."
219
+ fi
220
+
221
+ CURRENT_STREAM_VALUE=$(node -e "
222
+ const fs = require('fs');
223
+ const path = require('path');
224
+ const home = process.env.HOME;
225
+ for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
226
+ const f = path.join(home, '.' + app, app + '.json');
227
+ if (!fs.existsSync(f)) continue;
228
+ try {
229
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
230
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
231
+ for (const key of keys) {
232
+ const ch = cfg.channels && cfg.channels[key];
233
+ if (!ch) continue;
234
+ if (typeof ch.streamSupport === 'boolean') { process.stdout.write(String(ch.streamSupport)); process.exit(0); }
235
+ }
236
+ } catch {}
237
+ }
238
+ " 2>/dev/null || true)
239
+
240
+ if [ "$CURRENT_STREAM_VALUE" = "$STREAM_VALUE" ]; then
241
+ echo " ✅ stream 配置已是目标值,跳过写入"
242
+ elif $CMD config set channels.qqbot.streamSupport "$STREAM_VALUE" 2>&1; then
243
+ echo " ✅ stream 配置成功"
244
+ else
245
+ echo " ⚠️ $CMD config set 失败,尝试直接编辑配置文件..."
246
+ if [ -f "$APP_CONFIG" ] && node -e "
247
+ const fs = require('fs');
248
+ const cfg = JSON.parse(fs.readFileSync('$APP_CONFIG', 'utf-8'));
249
+ if (!cfg.channels) cfg.channels = {};
250
+ if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
251
+ const target = $STREAM_VALUE;
252
+ if (cfg.channels.qqbot.streamSupport === target) process.exit(0);
253
+ cfg.channels.qqbot.streamSupport = target;
254
+ fs.writeFileSync('$APP_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
255
+ " 2>&1; then
256
+ echo " ✅ stream 配置成功(直接编辑配置文件)"
257
+ else
258
+ echo " ⚠️ stream 配置设置失败,不影响后续运行"
259
+ fi
260
+ fi
261
+ fi
262
+
263
+ # 配置 debug 选项(仅在明确指定时才配置)
264
+ if [ -n "$DEBUG" ]; then
265
+ echo ""
266
+ echo "配置 debug 选项..."
267
+ if [ "$DEBUG" = "yes" ] || [ "$DEBUG" = "y" ] || [ "$DEBUG" = "true" ]; then
268
+ DEBUG_VALUE="true"
269
+ echo "启用 debug 模式..."
270
+ else
271
+ DEBUG_VALUE="false"
272
+ echo "禁用 debug 模式..."
273
+ fi
274
+
275
+ CURRENT_DEBUG_VALUE=$(node -e "
276
+ const fs = require('fs');
277
+ const path = require('path');
278
+ const home = process.env.HOME;
279
+ for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
280
+ const f = path.join(home, '.' + app, app + '.json');
281
+ if (!fs.existsSync(f)) continue;
282
+ try {
283
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
284
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
285
+ for (const key of keys) {
286
+ const ch = cfg.channels && cfg.channels[key];
287
+ if (!ch) continue;
288
+ if (typeof ch.debug === 'boolean') { process.stdout.write(String(ch.debug)); process.exit(0); }
289
+ }
290
+ } catch {}
291
+ }
292
+ " 2>/dev/null || true)
293
+
294
+ if [ "$CURRENT_DEBUG_VALUE" = "$DEBUG_VALUE" ]; then
295
+ echo " ✅ debug 配置已是目标值,跳过写入"
296
+ elif $CMD config set channels.qqbot.debug "$DEBUG_VALUE" 2>&1; then
297
+ echo " ✅ debug 配置成功"
298
+ else
299
+ echo " ⚠️ $CMD config set 失败,尝试直接编辑配置文件..."
300
+ if [ -f "$APP_CONFIG" ] && node -e "
301
+ const fs = require('fs');
302
+ const cfg = JSON.parse(fs.readFileSync('$APP_CONFIG', 'utf-8'));
303
+ if (!cfg.channels) cfg.channels = {};
304
+ if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
305
+ const target = $DEBUG_VALUE;
306
+ if (cfg.channels.qqbot.debug === target) process.exit(0);
307
+ cfg.channels.qqbot.debug = target;
308
+ fs.writeFileSync('$APP_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
309
+ " 2>&1; then
310
+ echo " ✅ debug 配置成功(直接编辑配置文件)"
311
+ else
312
+ echo " ⚠️ debug 配置设置失败,不影响后续运行"
313
+ fi
314
+ fi
315
+ fi
316
+
191
317
  # [4/4] 重启网关
192
318
  echo ""
193
319
  echo "[4/4] 重启网关..."
@@ -26,6 +26,7 @@ APPID=""
26
26
  SECRET=""
27
27
  MARKDOWN=""
28
28
  STREAM=""
29
+ DEBUG=""
29
30
 
30
31
  while [[ $# -gt 0 ]]; do
31
32
  case $1 in
@@ -45,6 +46,10 @@ while [[ $# -gt 0 ]]; do
45
46
  STREAM="$2"
46
47
  shift 2
47
48
  ;;
49
+ --debug)
50
+ DEBUG="$2"
51
+ shift 2
52
+ ;;
48
53
  -h|--help)
49
54
  echo "用法: $0 [选项]"
50
55
  echo ""
@@ -53,6 +58,7 @@ while [[ $# -gt 0 ]]; do
53
58
  echo " --secret <secret> QQ机器人 secret"
54
59
  echo " --markdown <yes|no> 是否启用 markdown 消息格式(默认: no)"
55
60
  echo " --stream <yes|no> 是否启用流式消息(仅C2C私聊生效,默认: no)"
61
+ echo " --debug <yes|no> 是否启用 debug 模式(默认: no)"
56
62
  echo " -h, --help 显示帮助信息"
57
63
  echo ""
58
64
  echo "也可以通过环境变量设置:"
@@ -61,6 +67,7 @@ while [[ $# -gt 0 ]]; do
61
67
  echo " QQBOT_TOKEN QQ机器人 token (appid:secret)"
62
68
  echo " QQBOT_MARKDOWN 是否启用 markdown(yes/no)"
63
69
  echo " QQBOT_STREAM 是否启用流式消息(yes/no)"
70
+ echo " QQBOT_DEBUG 是否启用 debug 模式(yes/no)"
64
71
  echo ""
65
72
  echo "不带参数时,将使用已有配置直接启动。"
66
73
  echo ""
@@ -80,6 +87,7 @@ APPID="${APPID:-$QQBOT_APPID}"
80
87
  SECRET="${SECRET:-$QQBOT_SECRET}"
81
88
  MARKDOWN="${MARKDOWN:-$QQBOT_MARKDOWN}"
82
89
  STREAM="${STREAM:-$QQBOT_STREAM}"
90
+ DEBUG="${DEBUG:-$QQBOT_DEBUG}"
83
91
 
84
92
  echo "========================================="
85
93
  echo " qqbot 一键更新启动脚本"
@@ -87,9 +95,10 @@ echo "========================================="
87
95
 
88
96
  # 1. 备份已有 qqbot 通道配置,防止升级过程丢失
89
97
  echo ""
90
- echo "[1/7] 备份已有配置..."
98
+ echo "[1/8] 备份已有配置..."
91
99
  SAVED_QQBOT_TOKEN=""
92
100
  SAVED_STREAM_SUPPORT=""
101
+ SAVED_DEBUG=""
93
102
  for APP_NAME in openclaw clawdbot moltbot; do
94
103
  CONFIG_FILE="$HOME/.$APP_NAME/$APP_NAME.json"
95
104
  if [ -f "$CONFIG_FILE" ]; then
@@ -116,9 +125,22 @@ for APP_NAME in openclaw clawdbot moltbot; do
116
125
  }
117
126
  " 2>/dev/null || true)
118
127
  fi
128
+ # 同时备份 debug 配置
129
+ if [ -z "$SAVED_DEBUG" ]; then
130
+ SAVED_DEBUG=$(node -e "
131
+ const cfg = JSON.parse(require('fs').readFileSync('$CONFIG_FILE', 'utf8'));
132
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
133
+ for (const key of keys) {
134
+ const ch = cfg.channels && cfg.channels[key];
135
+ if (!ch) continue;
136
+ if (typeof ch.debug === 'boolean') { process.stdout.write(String(ch.debug)); process.exit(0); }
137
+ }
138
+ " 2>/dev/null || true)
139
+ fi
119
140
  if [ -n "$SAVED_QQBOT_TOKEN" ]; then
120
141
  echo "已备份 qqbot 通道 token: ${SAVED_QQBOT_TOKEN:0:10}..."
121
142
  [ -n "$SAVED_STREAM_SUPPORT" ] && echo "已备份 streamSupport: $SAVED_STREAM_SUPPORT"
143
+ [ -n "$SAVED_DEBUG" ] && echo "已备份 debug: $SAVED_DEBUG"
122
144
  break
123
145
  fi
124
146
  fi
@@ -179,11 +201,39 @@ if [ -z "$SAVED_QQBOT_TOKEN" ] && [ -d "$HOME/.openclaw" ]; then
179
201
  echo "已从 ~/.openclaw/openclaw.json.bak* 找到 streamSupport 备份: $SAVED_STREAM_SUPPORT"
180
202
  fi
181
203
  fi
204
+
205
+ # 同时从备份文件中恢复 debug
206
+ if [ -z "$SAVED_DEBUG" ]; then
207
+ SAVED_DEBUG=$(node -e "
208
+ const fs = require('fs');
209
+ const path = require('path');
210
+ const dir = path.join(process.env.HOME, '.openclaw');
211
+ const files = fs.readdirSync(dir)
212
+ .filter((n) => /^openclaw\.json\.bak(\.\d+)?$/.test(n))
213
+ .map((n) => path.join(dir, n))
214
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
215
+ for (const f of files) {
216
+ try {
217
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
218
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
219
+ for (const key of keys) {
220
+ const ch = cfg.channels && cfg.channels[key];
221
+ if (!ch) continue;
222
+ if (typeof ch.debug === 'boolean') { process.stdout.write(String(ch.debug)); process.exit(0); }
223
+ }
224
+ } catch {}
225
+ }
226
+ " 2>/dev/null || true)
227
+
228
+ if [ -n "$SAVED_DEBUG" ]; then
229
+ echo "已从 ~/.openclaw/openclaw.json.bak* 找到 debug 备份: $SAVED_DEBUG"
230
+ fi
231
+ fi
182
232
  fi
183
233
 
184
234
  # 2. 移除老版本
185
235
  echo ""
186
- echo "[2/7] 移除老版本..."
236
+ echo "[2/8] 移除老版本..."
187
237
  if [ -f "$PROJ_DIR/scripts/cleanup-legacy-plugins.sh" ]; then
188
238
  bash "$PROJ_DIR/scripts/cleanup-legacy-plugins.sh"
189
239
  else
@@ -192,7 +242,7 @@ fi
192
242
 
193
243
  # 3. 安装当前版本
194
244
  echo ""
195
- echo "[3/7] 安装当前版本(源码安装)..."
245
+ echo "[3/8] 安装当前版本(源码安装)..."
196
246
 
197
247
  echo "检查当前目录: $(pwd)"
198
248
  echo "检查openclaw版本: $(openclaw --version 2>/dev/null || echo 'openclaw not found')"
@@ -323,7 +373,7 @@ fi
323
373
 
324
374
  # 4. 配置机器人通道(仅在需要变更时写入配置,避免无意义覆盖)
325
375
  echo ""
326
- echo "[4/7] 配置机器人通道..."
376
+ echo "[4/8] 配置机器人通道..."
327
377
 
328
378
  # 读取当前 qqbot token(兼容多 key)
329
379
  CURRENT_QQBOT_TOKEN=""
@@ -401,7 +451,7 @@ fi
401
451
 
402
452
  # 5. 配置 markdown 选项(仅在明确指定时才配置)
403
453
  echo ""
404
- echo "[5/7] 配置 markdown 选项..."
454
+ echo "[5/8] 配置 markdown 选项..."
405
455
 
406
456
  if [ -n "$MARKDOWN" ]; then
407
457
  # 设置 markdown 配置
@@ -462,7 +512,7 @@ fi
462
512
 
463
513
  # 6. 配置 stream 选项(优先命令行参数,其次自动恢复备份值)
464
514
  echo ""
465
- echo "[6/7] 配置 stream 选项..."
515
+ echo "[6/8] 配置 stream 选项..."
466
516
 
467
517
  # 如果命令行未指定 --stream,但备份中有 streamSupport 值,则自动恢复
468
518
  if [ -z "$STREAM" ] && [ "$SAVED_STREAM_SUPPORT" = "true" ]; then
@@ -557,9 +607,79 @@ else
557
607
  echo "未指定 stream 选项,使用已有配置"
558
608
  fi
559
609
 
560
- # 7. 启动 openclaw
610
+ # 7. 配置 debug 选项(优先命令行参数,其次自动恢复备份值)
611
+ echo ""
612
+ echo "[7/8] 配置 debug 选项..."
613
+
614
+ # 如果命令行未指定 --debug,但备份中有 debug 值,则自动恢复
615
+ if [ -z "$DEBUG" ] && [ "$SAVED_DEBUG" = "true" ]; then
616
+ DEBUG="yes"
617
+ echo "自动恢复备份的 debug=true 配置..."
618
+ elif [ -z "$DEBUG" ] && [ "$SAVED_DEBUG" = "false" ]; then
619
+ DEBUG="no"
620
+ echo "自动恢复备份的 debug=false 配置..."
621
+ fi
622
+
623
+ if [ -n "$DEBUG" ]; then
624
+ # 设置 debug 配置
625
+ if [ "$DEBUG" = "yes" ] || [ "$DEBUG" = "y" ] || [ "$DEBUG" = "true" ]; then
626
+ DEBUG_VALUE="true"
627
+ echo "启用 debug 模式..."
628
+ else
629
+ DEBUG_VALUE="false"
630
+ echo "禁用 debug 模式..."
631
+ fi
632
+
633
+ CURRENT_DEBUG_VALUE=$(node -e "
634
+ const fs = require('fs');
635
+ const path = require('path');
636
+ const home = process.env.HOME;
637
+ for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
638
+ const f = path.join(home, '.' + app, app + '.json');
639
+ if (!fs.existsSync(f)) continue;
640
+ try {
641
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
642
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
643
+ for (const key of keys) {
644
+ const ch = cfg.channels && cfg.channels[key];
645
+ if (!ch) continue;
646
+ if (typeof ch.debug === 'boolean') { process.stdout.write(String(ch.debug)); process.exit(0); }
647
+ }
648
+ } catch {}
649
+ }
650
+ " 2>/dev/null || true)
651
+
652
+ if [ "$CURRENT_DEBUG_VALUE" = "$DEBUG_VALUE" ]; then
653
+ echo "✅ debug 配置已是目标值,跳过写入(避免配置覆盖提示)"
654
+ elif openclaw config set channels.qqbot.debug "$DEBUG_VALUE" 2>&1; then
655
+ echo "✅ debug配置成功"
656
+ _config_changed=1
657
+ else
658
+ echo "⚠️ openclaw config set 失败,尝试直接编辑配置文件..."
659
+ OPENCLAW_CONFIG="$HOME/.openclaw/openclaw.json"
660
+ if [ -f "$OPENCLAW_CONFIG" ] && node -e "
661
+ const fs = require('fs');
662
+ const cfg = JSON.parse(fs.readFileSync('$OPENCLAW_CONFIG', 'utf-8'));
663
+ if (!cfg.channels) cfg.channels = {};
664
+ if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
665
+ const target = $DEBUG_VALUE;
666
+ if (cfg.channels.qqbot.debug === target) process.exit(0);
667
+ cfg.channels.qqbot.debug = target;
668
+ fs.writeFileSync('$OPENCLAW_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
669
+ " 2>&1; then
670
+ echo "✅ debug配置成功(直接编辑配置文件)"
671
+ _config_changed=1
672
+ else
673
+ echo "⚠️ debug配置设置失败,不影响后续运行"
674
+ fi
675
+ fi
676
+ else
677
+ echo "未指定 debug 选项,使用已有配置"
678
+ fi
679
+
680
+ # 8. 启动 openclaw
561
681
  echo ""
562
- echo "[7/7] 启动 openclaw..."
682
+ echo "[8/8] 启动 openclaw..."
563
683
  echo "========================================="
564
684
 
565
685
  # 检查openclaw是否可用
package/src/api.ts CHANGED
@@ -13,12 +13,28 @@ const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
13
13
  // 运行时配置
14
14
  let currentMarkdownSupport = false;
15
15
 
16
+ // 模块级 logger:通过 initApiConfig 注入,未注入时 fallback 到 console
17
+ let apiLog: {
18
+ info: (msg: string) => void;
19
+ error: (msg: string) => void;
20
+ } = {
21
+ info: console.log.bind(console),
22
+ error: console.error.bind(console),
23
+ };
24
+
16
25
  /**
17
26
  * 初始化 API 配置
18
27
  * @param options.markdownSupport - 是否支持 markdown 消息(默认 false,需要机器人具备该权限才能启用)
28
+ * @param options.log - 可选的 logger 接口,注入后 API 日志将通过此 logger 输出(而非 console)
19
29
  */
20
- export function initApiConfig(options: { markdownSupport?: boolean }): void {
30
+ export function initApiConfig(options: {
31
+ markdownSupport?: boolean;
32
+ log?: { info: (msg: string) => void; error: (msg: string) => void };
33
+ }): void {
21
34
  currentMarkdownSupport = options.markdownSupport === true;
35
+ if (options.log) {
36
+ apiLog = options.log;
37
+ }
22
38
  }
23
39
 
24
40
  /**
@@ -54,7 +70,7 @@ export async function getAccessToken(appId: string, clientSecret: string): Promi
54
70
  // Singleflight: 如果当前 appId 已有进行中的 Token 获取请求,复用它
55
71
  let fetchPromise = tokenFetchPromises.get(normalizedAppId);
56
72
  if (fetchPromise) {
57
- console.log(`[qqbot-api:${normalizedAppId}] Token fetch in progress, waiting for existing request...`);
73
+ apiLog.info(`[qqbot-api:${normalizedAppId}] Token fetch in progress, waiting for existing request...`);
58
74
  return fetchPromise;
59
75
  }
60
76
 
@@ -80,7 +96,9 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
80
96
  const requestHeaders = { "Content-Type": "application/json" };
81
97
 
82
98
  // 打印请求信息(隐藏敏感信息)
83
- console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL}`);
99
+ apiLog.info(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL}`);
100
+ apiLog.info(`[qqbot-api:${appId}] >>> Headers: ${JSON.stringify(requestHeaders, null, 2)}`);
101
+ apiLog.info(`[qqbot-api:${appId}] >>> Body: ${JSON.stringify({ appId, clientSecret: "***" }, null, 2)}`);
84
102
 
85
103
  let response: Response;
86
104
  try {
@@ -90,7 +108,7 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
90
108
  body: JSON.stringify(requestBody),
91
109
  });
92
110
  } catch (err) {
93
- console.error(`[qqbot-api:${appId}] <<< Network error:`, err);
111
+ apiLog.error(`[qqbot-api:${appId}] <<< Network error: ${err instanceof Error ? err.message : String(err)}`);
94
112
  throw new Error(`Network error getting access_token: ${err instanceof Error ? err.message : String(err)}`);
95
113
  }
96
114
 
@@ -99,18 +117,31 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
99
117
  response.headers.forEach((value, key) => {
100
118
  responseHeaders[key] = value;
101
119
  });
102
- console.log(`[qqbot-api:${appId}] <<< Status: ${response.status} ${response.statusText}`);
120
+ apiLog.info(`[qqbot-api:${appId}] <<< Status: ${response.status} ${response.statusText}`);
121
+ apiLog.info(`[qqbot-api:${appId}] <<< Response Headers: ${JSON.stringify(responseHeaders, null, 2)}`);
103
122
 
104
123
  let data: { access_token?: string; expires_in?: number };
105
124
  let rawBody: string;
106
125
  try {
107
126
  rawBody = await response.text();
108
- // 隐藏 token
109
- const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token": "***"');
110
- console.log(`[qqbot-api:${appId}] <<< Body:`, logBody);
127
+ // 脱敏 token 值,格式化打印
128
+ try {
129
+ const parsed = JSON.parse(rawBody);
130
+ const logParsed = { ...parsed };
131
+ if (typeof logParsed.access_token === "string" && logParsed.access_token.length > 6) {
132
+ logParsed.access_token = `${logParsed.access_token.slice(0, 6)}***`;
133
+ } else if (logParsed.access_token) {
134
+ logParsed.access_token = "***";
135
+ }
136
+ apiLog.info(`[qqbot-api:${appId}] <<< Response Body: ${JSON.stringify(logParsed, null, 2)}`);
137
+ } catch {
138
+ // raw body 也做脱敏:替换可能的 access_token 值
139
+ const sanitized = rawBody.replace(/"access_token"\s*:\s*"([^"]{6})[^"]*"/g, '"access_token":"$1***"');
140
+ apiLog.info(`[qqbot-api:${appId}] <<< Response Body (raw): ${sanitized.slice(0, 2000)}`);
141
+ }
111
142
  data = JSON.parse(rawBody) as { access_token?: string; expires_in?: number };
112
143
  } catch (err) {
113
- console.error(`[qqbot-api:${appId}] <<< Parse error:`, err);
144
+ apiLog.error(`[qqbot-api:${appId}] <<< Parse error: ${err instanceof Error ? err.message : String(err)}`);
114
145
  throw new Error(`Failed to parse access_token response: ${err instanceof Error ? err.message : String(err)}`);
115
146
  }
116
147
 
@@ -126,7 +157,7 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
126
157
  appId,
127
158
  });
128
159
 
129
- console.log(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`);
160
+ apiLog.info(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`);
130
161
  return data.access_token;
131
162
  }
132
163
 
@@ -138,10 +169,10 @@ export function clearTokenCache(appId?: string): void {
138
169
  if (appId) {
139
170
  const normalizedAppId = String(appId).trim();
140
171
  tokenCacheMap.delete(normalizedAppId);
141
- console.log(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
172
+ apiLog.info(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
142
173
  } else {
143
174
  tokenCacheMap.clear();
144
- console.log(`[qqbot-api] All token caches cleared.`);
175
+ apiLog.info(`[qqbot-api] All token caches cleared.`);
145
176
  }
146
177
  }
147
178
 
@@ -208,13 +239,26 @@ export async function apiRequest<T = unknown>(
208
239
  options.body = JSON.stringify(body);
209
240
  }
210
241
 
211
- // 打印请求信息
212
- console.log(`[qqbot-api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
242
+ // 打印请求信息:方法、URL、请求头、请求体(JSON 格式化)
243
+ apiLog.info(`[qqbot-api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
244
+ // 脱敏 Authorization 头(只显示前缀 + token 前6位 + ***)
245
+ const logHeaders = { ...headers };
246
+ if (logHeaders.Authorization) {
247
+ const parts = logHeaders.Authorization.split(" ");
248
+ if (parts.length === 2 && parts[1].length > 6) {
249
+ logHeaders.Authorization = `${parts[0]} ${parts[1].slice(0, 6)}***`;
250
+ } else {
251
+ logHeaders.Authorization = "***";
252
+ }
253
+ }
254
+ apiLog.info(`[qqbot-api] >>> Headers: ${JSON.stringify(logHeaders, null, 2)}`);
213
255
  if (body) {
214
256
  const logBody = { ...body } as Record<string, unknown>;
257
+ // 脱敏:base64 文件数据只显示长度
215
258
  if (typeof logBody.file_data === "string") {
216
259
  logBody.file_data = `<base64 ${(logBody.file_data as string).length} chars>`;
217
260
  }
261
+ apiLog.info(`[qqbot-api] >>> Body: ${JSON.stringify(logBody, null, 2)}`);
218
262
  }
219
263
 
220
264
  let res: Response;
@@ -223,25 +267,34 @@ export async function apiRequest<T = unknown>(
223
267
  } catch (err) {
224
268
  clearTimeout(timeoutId);
225
269
  if (err instanceof Error && err.name === "AbortError") {
226
- console.error(`[qqbot-api] <<< Request timeout after ${timeout}ms`);
270
+ apiLog.error(`[qqbot-api] <<< Request timeout after ${timeout}ms`);
227
271
  throw new Error(`Request timeout[${path}]: exceeded ${timeout}ms`);
228
272
  }
229
- console.error(`[qqbot-api] <<< Network error:`, err);
273
+ apiLog.error(`[qqbot-api] <<< Network error: ${err instanceof Error ? err.message : String(err)}`);
230
274
  throw new Error(`Network error [${path}]: ${err instanceof Error ? err.message : String(err)}`);
231
275
  } finally {
232
276
  clearTimeout(timeoutId);
233
277
  }
234
278
 
279
+ // 打印响应头
235
280
  const responseHeaders: Record<string, string> = {};
236
281
  res.headers.forEach((value, key) => {
237
282
  responseHeaders[key] = value;
238
283
  });
239
- console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}`);
284
+ apiLog.info(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}`);
285
+ apiLog.info(`[qqbot-api] <<< Response Headers: ${JSON.stringify(responseHeaders, null, 2)}`);
240
286
 
241
287
  let data: T;
242
288
  let rawBody: string;
243
289
  try {
244
290
  rawBody = await res.text();
291
+ // 打印响应体(尝试 JSON 格式化)
292
+ try {
293
+ const parsed = JSON.parse(rawBody);
294
+ apiLog.info(`[qqbot-api] <<< Response Body: ${JSON.stringify(parsed, null, 2)}`);
295
+ } catch {
296
+ apiLog.info(`[qqbot-api] <<< Response Body (raw): ${rawBody.slice(0, 2000)}`);
297
+ }
245
298
  data = JSON.parse(rawBody) as T;
246
299
  } catch (err) {
247
300
  throw new Error(`Failed to parse response[${path}]: ${err instanceof Error ? err.message : String(err)}`);
@@ -285,7 +338,7 @@ async function apiRequestWithRetry<T = unknown>(
285
338
 
286
339
  if (attempt < maxRetries) {
287
340
  const delay = UPLOAD_BASE_DELAY_MS * Math.pow(2, attempt);
288
- console.log(`[qqbot-api] Upload attempt ${attempt + 1} failed, retrying in ${delay}ms: ${errMsg.slice(0, 100)}`);
341
+ apiLog.info(`[qqbot-api] Upload attempt ${attempt + 1} failed, retrying in ${delay}ms: ${errMsg.slice(0, 100)}`);
289
342
  await new Promise(resolve => setTimeout(resolve, delay));
290
343
  }
291
344
  }
@@ -304,8 +357,6 @@ export async function getGatewayUrl(accessToken: string): Promise<string> {
304
357
  export interface MessageResponse {
305
358
  id: string;
306
359
  timestamp: number | string;
307
- /** 流式消息ID,用于后续分片 */
308
- stream_id?: string;
309
360
  }
310
361
 
311
362
  function buildMessageBody(
@@ -639,7 +690,7 @@ export function startBackgroundTokenRefresh(
639
690
  options?: BackgroundTokenRefreshOptions
640
691
  ): void {
641
692
  if (backgroundRefreshControllers.has(appId)) {
642
- console.log(`[qqbot-api:${appId}] Background token refresh already running`);
693
+ apiLog.info(`[qqbot-api:${appId}] Background token refresh already running`);
643
694
  return;
644
695
  }
645
696
 
package/src/config.ts CHANGED
@@ -83,6 +83,7 @@ export function resolveQQBotAccount(
83
83
  imageServerBaseUrl: qqbot?.imageServerBaseUrl,
84
84
  markdownSupport: qqbot?.markdownSupport ?? true,
85
85
  streamSupport: qqbot?.streamSupport,
86
+ debug: qqbot?.debug,
86
87
  };
87
88
  appId = normalizeAppId(qqbot?.appId);
88
89
  } else {
@@ -120,6 +121,7 @@ export function resolveQQBotAccount(
120
121
  imageServerBaseUrl: accountConfig.imageServerBaseUrl || process.env.QQBOT_IMAGE_SERVER_BASE_URL,
121
122
  markdownSupport: accountConfig.markdownSupport !== false,
122
123
  streamSupport: accountConfig.streamSupport !== false,
124
+ debug: accountConfig.debug === true,
123
125
  config: accountConfig,
124
126
  };
125
127
  }