@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 +1 -1
- package/scripts/upgrade-via-npm.sh +126 -0
- package/scripts/upgrade-via-source.sh +128 -8
- package/src/api.ts +72 -21
- package/src/config.ts +2 -0
- package/src/gateway.ts +314 -395
- package/src/outbound.ts +25 -8
- package/src/stream-handlers/bracket-safe-handler.ts +73 -0
- package/src/stream-handlers/chain.ts +95 -0
- package/src/stream-handlers/index.ts +37 -0
- package/src/stream-handlers/markdown-link-handler.ts +134 -0
- package/src/stream-handlers/media-tag-handler.ts +146 -0
- package/src/stream-handlers/payload-handler.ts +46 -0
- package/src/stream-handlers/types.ts +97 -0
- package/src/types.ts +4 -0
- package/src/utils/media-tags.ts +32 -21
- package/src/utils/split-msg.ts +280 -0
package/package.json
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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 "[
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
+
apiLog.info(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
|
|
142
173
|
} else {
|
|
143
174
|
tokenCacheMap.clear();
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
apiLog.error(`[qqbot-api] <<< Request timeout after ${timeout}ms`);
|
|
227
271
|
throw new Error(`Request timeout[${path}]: exceeded ${timeout}ms`);
|
|
228
272
|
}
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|