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

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.4-alpha.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,6 +13,7 @@ PKG_NAME="@tencent-connect/openclaw-qqbot"
13
13
  INSTALL_SRC=""
14
14
  APPID=""
15
15
  SECRET=""
16
+ STREAM=""
16
17
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
18
  PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18
19
 
@@ -31,6 +32,7 @@ print_usage() {
31
32
  echo " upgrade-via-npm.sh # 升级到 latest(默认)"
32
33
  echo " upgrade-via-npm.sh --version <版本号> # 升级到指定版本"
33
34
  echo " upgrade-via-npm.sh --appid <appid> --secret <secret> # 配置通道并启动"
35
+ echo " upgrade-via-npm.sh --stream <yes|no> # 是否启用流式消息(仅C2C私聊生效)"
34
36
  if [ -n "$LOCAL_VERSION" ]; then
35
37
  echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本($LOCAL_VERSION)"
36
38
  else
@@ -40,6 +42,7 @@ print_usage() {
40
42
  echo "也可以通过环境变量设置:"
41
43
  echo " QQBOT_APPID QQ机器人 appid"
42
44
  echo " QQBOT_SECRET QQ机器人 secret"
45
+ echo " QQBOT_STREAM 是否启用流式消息(yes/no)"
43
46
  }
44
47
 
45
48
  while [[ $# -gt 0 ]]; do
@@ -69,6 +72,11 @@ while [[ $# -gt 0 ]]; do
69
72
  SECRET="$2"
70
73
  shift 2
71
74
  ;;
75
+ --stream)
76
+ [ -z "$2" ] && echo "❌ --stream 需要参数" && exit 1
77
+ STREAM="$2"
78
+ shift 2
79
+ ;;
72
80
  -h|--help)
73
81
  print_usage
74
82
  exit 0
@@ -81,6 +89,7 @@ INSTALL_SRC="${INSTALL_SRC:-${PKG_NAME}@latest}"
81
89
  # 使用命令行参数或环境变量
82
90
  APPID="${APPID:-$QQBOT_APPID}"
83
91
  SECRET="${SECRET:-$QQBOT_SECRET}"
92
+ STREAM="${STREAM:-$QQBOT_STREAM}"
84
93
 
85
94
  # 检测 CLI
86
95
  CMD=""
@@ -188,6 +197,60 @@ if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
188
197
  fi
189
198
  fi
190
199
 
200
+ # 配置 stream 选项(仅在明确指定时才配置)
201
+ if [ -n "$STREAM" ]; then
202
+ echo ""
203
+ echo "配置 stream 选项..."
204
+ if [ "$STREAM" = "yes" ] || [ "$STREAM" = "y" ] || [ "$STREAM" = "true" ]; then
205
+ STREAM_VALUE="true"
206
+ echo "启用流式消息(仅C2C私聊生效)..."
207
+ else
208
+ STREAM_VALUE="false"
209
+ echo "禁用流式消息..."
210
+ fi
211
+
212
+ CURRENT_STREAM_VALUE=$(node -e "
213
+ const fs = require('fs');
214
+ const path = require('path');
215
+ const home = process.env.HOME;
216
+ for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
217
+ const f = path.join(home, '.' + app, app + '.json');
218
+ if (!fs.existsSync(f)) continue;
219
+ try {
220
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
221
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
222
+ for (const key of keys) {
223
+ const ch = cfg.channels && cfg.channels[key];
224
+ if (!ch) continue;
225
+ if (typeof ch.streamSupport === 'boolean') { process.stdout.write(String(ch.streamSupport)); process.exit(0); }
226
+ }
227
+ } catch {}
228
+ }
229
+ " 2>/dev/null || true)
230
+
231
+ if [ "$CURRENT_STREAM_VALUE" = "$STREAM_VALUE" ]; then
232
+ echo " ✅ stream 配置已是目标值,跳过写入"
233
+ elif $CMD config set channels.qqbot.streamSupport "$STREAM_VALUE" 2>&1; then
234
+ echo " ✅ stream 配置成功"
235
+ else
236
+ echo " ⚠️ $CMD config set 失败,尝试直接编辑配置文件..."
237
+ if [ -f "$APP_CONFIG" ] && node -e "
238
+ const fs = require('fs');
239
+ const cfg = JSON.parse(fs.readFileSync('$APP_CONFIG', 'utf-8'));
240
+ if (!cfg.channels) cfg.channels = {};
241
+ if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
242
+ const target = $STREAM_VALUE;
243
+ if (cfg.channels.qqbot.streamSupport === target) process.exit(0);
244
+ cfg.channels.qqbot.streamSupport = target;
245
+ fs.writeFileSync('$APP_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
246
+ " 2>&1; then
247
+ echo " ✅ stream 配置成功(直接编辑配置文件)"
248
+ else
249
+ echo " ⚠️ stream 配置设置失败,不影响后续运行"
250
+ fi
251
+ fi
252
+ fi
253
+
191
254
  # [4/4] 重启网关
192
255
  echo ""
193
256
  echo "[4/4] 重启网关..."
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,25 @@ 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 (logParsed.access_token) logParsed.access_token = "***";
132
+ apiLog.info(`[qqbot-api:${appId}] <<< Response Body: ${JSON.stringify(logParsed, null, 2)}`);
133
+ } catch {
134
+ apiLog.info(`[qqbot-api:${appId}] <<< Response Body (raw): ${rawBody.slice(0, 2000)}`);
135
+ }
111
136
  data = JSON.parse(rawBody) as { access_token?: string; expires_in?: number };
112
137
  } catch (err) {
113
- console.error(`[qqbot-api:${appId}] <<< Parse error:`, err);
138
+ apiLog.error(`[qqbot-api:${appId}] <<< Parse error: ${err instanceof Error ? err.message : String(err)}`);
114
139
  throw new Error(`Failed to parse access_token response: ${err instanceof Error ? err.message : String(err)}`);
115
140
  }
116
141
 
@@ -126,7 +151,7 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
126
151
  appId,
127
152
  });
128
153
 
129
- console.log(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`);
154
+ apiLog.info(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`);
130
155
  return data.access_token;
131
156
  }
132
157
 
@@ -138,10 +163,10 @@ export function clearTokenCache(appId?: string): void {
138
163
  if (appId) {
139
164
  const normalizedAppId = String(appId).trim();
140
165
  tokenCacheMap.delete(normalizedAppId);
141
- console.log(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
166
+ apiLog.info(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
142
167
  } else {
143
168
  tokenCacheMap.clear();
144
- console.log(`[qqbot-api] All token caches cleared.`);
169
+ apiLog.info(`[qqbot-api] All token caches cleared.`);
145
170
  }
146
171
  }
147
172
 
@@ -208,13 +233,16 @@ export async function apiRequest<T = unknown>(
208
233
  options.body = JSON.stringify(body);
209
234
  }
210
235
 
211
- // 打印请求信息
212
- console.log(`[qqbot-api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
236
+ // 打印请求信息:方法、URL、请求头、请求体(JSON 格式化)
237
+ apiLog.info(`[qqbot-api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
238
+ apiLog.info(`[qqbot-api] >>> Headers: ${JSON.stringify(headers, null, 2)}`);
213
239
  if (body) {
214
240
  const logBody = { ...body } as Record<string, unknown>;
241
+ // 脱敏:base64 文件数据只显示长度
215
242
  if (typeof logBody.file_data === "string") {
216
243
  logBody.file_data = `<base64 ${(logBody.file_data as string).length} chars>`;
217
244
  }
245
+ apiLog.info(`[qqbot-api] >>> Body: ${JSON.stringify(logBody, null, 2)}`);
218
246
  }
219
247
 
220
248
  let res: Response;
@@ -223,25 +251,34 @@ export async function apiRequest<T = unknown>(
223
251
  } catch (err) {
224
252
  clearTimeout(timeoutId);
225
253
  if (err instanceof Error && err.name === "AbortError") {
226
- console.error(`[qqbot-api] <<< Request timeout after ${timeout}ms`);
254
+ apiLog.error(`[qqbot-api] <<< Request timeout after ${timeout}ms`);
227
255
  throw new Error(`Request timeout[${path}]: exceeded ${timeout}ms`);
228
256
  }
229
- console.error(`[qqbot-api] <<< Network error:`, err);
257
+ apiLog.error(`[qqbot-api] <<< Network error: ${err instanceof Error ? err.message : String(err)}`);
230
258
  throw new Error(`Network error [${path}]: ${err instanceof Error ? err.message : String(err)}`);
231
259
  } finally {
232
260
  clearTimeout(timeoutId);
233
261
  }
234
262
 
263
+ // 打印响应头
235
264
  const responseHeaders: Record<string, string> = {};
236
265
  res.headers.forEach((value, key) => {
237
266
  responseHeaders[key] = value;
238
267
  });
239
- console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}`);
268
+ apiLog.info(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}`);
269
+ apiLog.info(`[qqbot-api] <<< Response Headers: ${JSON.stringify(responseHeaders, null, 2)}`);
240
270
 
241
271
  let data: T;
242
272
  let rawBody: string;
243
273
  try {
244
274
  rawBody = await res.text();
275
+ // 打印响应体(尝试 JSON 格式化)
276
+ try {
277
+ const parsed = JSON.parse(rawBody);
278
+ apiLog.info(`[qqbot-api] <<< Response Body: ${JSON.stringify(parsed, null, 2)}`);
279
+ } catch {
280
+ apiLog.info(`[qqbot-api] <<< Response Body (raw): ${rawBody.slice(0, 2000)}`);
281
+ }
245
282
  data = JSON.parse(rawBody) as T;
246
283
  } catch (err) {
247
284
  throw new Error(`Failed to parse response[${path}]: ${err instanceof Error ? err.message : String(err)}`);
@@ -285,7 +322,7 @@ async function apiRequestWithRetry<T = unknown>(
285
322
 
286
323
  if (attempt < maxRetries) {
287
324
  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)}`);
325
+ apiLog.info(`[qqbot-api] Upload attempt ${attempt + 1} failed, retrying in ${delay}ms: ${errMsg.slice(0, 100)}`);
289
326
  await new Promise(resolve => setTimeout(resolve, delay));
290
327
  }
291
328
  }
@@ -639,7 +676,7 @@ export function startBackgroundTokenRefresh(
639
676
  options?: BackgroundTokenRefreshOptions
640
677
  ): void {
641
678
  if (backgroundRefreshControllers.has(appId)) {
642
- console.log(`[qqbot-api:${appId}] Background token refresh already running`);
679
+ apiLog.info(`[qqbot-api:${appId}] Background token refresh already running`);
643
680
  return;
644
681
  }
645
682
 
package/src/config.ts CHANGED
@@ -120,6 +120,7 @@ export function resolveQQBotAccount(
120
120
  imageServerBaseUrl: accountConfig.imageServerBaseUrl || process.env.QQBOT_IMAGE_SERVER_BASE_URL,
121
121
  markdownSupport: accountConfig.markdownSupport !== false,
122
122
  streamSupport: accountConfig.streamSupport !== false,
123
+ debug: accountConfig.debug === true,
123
124
  config: accountConfig,
124
125
  };
125
126
  }