@tencent-connect/openclaw-qqbot 1.7.0 → 1.7.2
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 +216 -49
- package/README.zh.md +216 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/api.d.ts +6 -0
- package/dist/src/api.js +33 -4
- package/dist/src/approval-handler.d.ts +47 -0
- package/dist/src/approval-handler.js +372 -0
- package/dist/src/channel.js +72 -0
- package/dist/src/config.d.ts +5 -1
- package/dist/src/config.js +12 -2
- package/dist/src/gateway.js +175 -170
- package/dist/src/slash-commands.d.ts +7 -2
- package/dist/src/slash-commands.js +354 -3
- package/dist/src/tools/channel.js +1 -4
- package/dist/src/tools/remind.js +0 -1
- package/dist/src/transport/index.d.ts +10 -0
- package/dist/src/transport/index.js +9 -0
- package/dist/src/transport/webhook-transport.d.ts +67 -0
- package/dist/src/transport/webhook-transport.js +245 -0
- package/dist/src/transport/webhook-verify.d.ts +48 -0
- package/dist/src/transport/webhook-verify.js +98 -0
- package/dist/src/types.d.ts +85 -0
- package/dist/src/utils/audio-convert.js +37 -9
- package/index.ts +1 -0
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +44 -0
- package/scripts/upgrade-via-npm.sh +358 -62
- package/scripts/upgrade-via-source.sh +122 -85
- package/src/api.ts +50 -5
- package/src/approval-handler.ts +505 -0
- package/src/channel.ts +76 -0
- package/src/config.ts +15 -2
- package/src/gateway.ts +181 -169
- package/src/onboarding.ts +8 -0
- package/src/openclaw-plugin-sdk.d.ts +127 -2
- package/src/slash-commands.ts +390 -5
- package/src/tools/channel.ts +1 -7
- package/src/tools/remind.ts +0 -2
- package/src/transport/index.ts +11 -0
- package/src/transport/webhook-transport.ts +332 -0
- package/src/transport/webhook-verify.ts +119 -0
- package/src/types.ts +100 -1
- package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
- package/src/utils/audio-convert.ts +37 -9
|
@@ -252,10 +252,53 @@ for _app in openclaw clawdbot moltbot; do
|
|
|
252
252
|
fi
|
|
253
253
|
done
|
|
254
254
|
|
|
255
|
+
# ── 安全网:注册 trap 确保脚本退出时恢复暂存的 channels.qqbot 配置 ──
|
|
256
|
+
_restore_stash_on_exit() {
|
|
257
|
+
if [ -n "$_QQBOT_CHANNEL_STASH" ] && [ -n "$_STASH_CFG" ] && [ -f "$_STASH_CFG" ]; then
|
|
258
|
+
# 检查配置是否已恢复(避免重复写入)
|
|
259
|
+
local _already_restored
|
|
260
|
+
_already_restored=$(node -e "
|
|
261
|
+
const fs = require('fs');
|
|
262
|
+
const cfg = JSON.parse(fs.readFileSync('$_STASH_CFG', 'utf8'));
|
|
263
|
+
if (cfg.channels && cfg.channels.qqbot) process.stdout.write('yes');
|
|
264
|
+
else process.stdout.write('no');
|
|
265
|
+
" 2>/dev/null || echo "no")
|
|
266
|
+
if [ "$_already_restored" = "no" ]; then
|
|
267
|
+
_STASH="$_QQBOT_CHANNEL_STASH" _CFG="$_STASH_CFG" node -e '
|
|
268
|
+
const fs = require("fs");
|
|
269
|
+
const cfg = JSON.parse(fs.readFileSync(process.env._CFG, "utf8"));
|
|
270
|
+
if (!cfg.channels) cfg.channels = {};
|
|
271
|
+
cfg.channels.qqbot = JSON.parse(process.env._STASH);
|
|
272
|
+
fs.writeFileSync(process.env._CFG, JSON.stringify(cfg, null, 4) + "\n");
|
|
273
|
+
' 2>/dev/null || true
|
|
274
|
+
echo ""
|
|
275
|
+
echo " ⚠️ [trap] 已恢复暂存的 channels.qqbot 配置(脚本提前退出)"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
}
|
|
279
|
+
trap _restore_stash_on_exit EXIT
|
|
280
|
+
|
|
255
281
|
# 安装前先 stop gateway,防止 chokidar 在 plugins install 写入配置的中间状态
|
|
256
282
|
# 触发 restart,导致 "unknown channel id: qqbot" 等错误
|
|
283
|
+
|
|
284
|
+
# 动态读取 gateway 端口(从 openclaw.json 配置中获取,默认 18789)
|
|
285
|
+
_GW_PORT=$(node -e "
|
|
286
|
+
const fs = require('fs');
|
|
287
|
+
const path = require('path');
|
|
288
|
+
for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
|
|
289
|
+
const f = path.join(process.env.HOME, '.' + app, app + '.json');
|
|
290
|
+
if (!fs.existsSync(f)) continue;
|
|
291
|
+
const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
292
|
+
if (cfg.gateway && cfg.gateway.port) {
|
|
293
|
+
process.stdout.write(String(cfg.gateway.port));
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
process.stdout.write('18789');
|
|
298
|
+
" 2>/dev/null || echo "18789")
|
|
299
|
+
|
|
257
300
|
_gw_was_running=0
|
|
258
|
-
if lsof -i :
|
|
301
|
+
if lsof -i :"$_GW_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
259
302
|
_gw_was_running=1
|
|
260
303
|
echo " 暂停 gateway 服务(避免安装过程中中间状态 restart)..."
|
|
261
304
|
openclaw gateway stop 2>/dev/null || true
|
|
@@ -265,6 +308,10 @@ fi
|
|
|
265
308
|
# 清理之前可能残留的 staging 目录(extensions 和 /tmp 中都可能存在)
|
|
266
309
|
find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
|
|
267
310
|
find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
|
|
311
|
+
# 清理可能残留的 node_modules 临时备份(上次脚本中断时留下的)
|
|
312
|
+
find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-qqbot-nm-bak-*" -exec rm -rf {} + 2>/dev/null || true
|
|
313
|
+
# 清理旧版脚本遗留的项目内备份
|
|
314
|
+
[ -d "$PROJ_DIR/.node_modules_install_bak" ] && mv "$PROJ_DIR/.node_modules_install_bak" "$PROJ_DIR/node_modules" 2>/dev/null || true
|
|
268
315
|
|
|
269
316
|
# ── 清空并重新构建 dist/ ──
|
|
270
317
|
# openclaw plugins install . 只做文件复制,不执行 npm lifecycle scripts。
|
|
@@ -296,14 +343,17 @@ _INSTALL_DIR="$HOME/.openclaw/extensions/openclaw-qqbot"
|
|
|
296
343
|
# openclaw plugins install . 会把整个项目目录(含 node_modules/)复制到 extensions,
|
|
297
344
|
# 但运行时只需要 bundledDependencies 中的 3 个包 + openclaw symlink。
|
|
298
345
|
# 移走 node_modules 可避免复制数百个不必要的包,大幅加速安装。
|
|
346
|
+
# 同时临时移走 devDependencies 声明,避免 manifest dependency scan 递归解析
|
|
347
|
+
# 超出 10000 目录上限导致 "code safety scan failed"。
|
|
299
348
|
_NM_BACKUP=""
|
|
300
349
|
if [ -d "$PROJ_DIR/node_modules" ]; then
|
|
301
|
-
echo " 临时移走 node_modules
|
|
302
|
-
|
|
350
|
+
echo " 临时移走 node_modules(避免安全扫描超出目录上限)..."
|
|
351
|
+
# 备份到项目目录外部(/tmp),否则安全扫描仍会递归遍历到重命名后的目录
|
|
352
|
+
_NM_BACKUP="${TMPDIR:-/tmp}/.openclaw-qqbot-nm-bak-$$"
|
|
303
353
|
mv "$PROJ_DIR/node_modules" "$_NM_BACKUP"
|
|
304
354
|
fi
|
|
305
355
|
|
|
306
|
-
openclaw plugins install . 2>&1 | tee "$INSTALL_LOG" || true
|
|
356
|
+
openclaw plugins install . --dangerously-force-unsafe-install 2>&1 | tee "$INSTALL_LOG" || true
|
|
307
357
|
|
|
308
358
|
# ── 恢复 node_modules ──
|
|
309
359
|
if [ -n "$_NM_BACKUP" ] && [ -d "$_NM_BACKUP" ]; then
|
|
@@ -418,13 +468,13 @@ else
|
|
|
418
468
|
echo " 尝试自动修复: 清理残留并重试安装..."
|
|
419
469
|
find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
|
|
420
470
|
find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
|
|
421
|
-
# 重试时同样移走 node_modules
|
|
471
|
+
# 重试时同样移走 node_modules 避免安全扫描超出目录上限
|
|
422
472
|
_NM_BACKUP=""
|
|
423
473
|
if [ -d "$PROJ_DIR/node_modules" ]; then
|
|
424
|
-
_NM_BACKUP="$
|
|
474
|
+
_NM_BACKUP="${TMPDIR:-/tmp}/.openclaw-qqbot-nm-bak-$$"
|
|
425
475
|
mv "$PROJ_DIR/node_modules" "$_NM_BACKUP"
|
|
426
476
|
fi
|
|
427
|
-
if openclaw plugins install . 2>&1 | tee -a "$INSTALL_LOG"; then
|
|
477
|
+
if openclaw plugins install . --dangerously-force-unsafe-install 2>&1 | tee -a "$INSTALL_LOG"; then
|
|
428
478
|
for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
|
|
429
479
|
if [ -d "$HOME/.openclaw/extensions/$_candidate_name" ]; then
|
|
430
480
|
_plugin_dir_ok=1
|
|
@@ -481,16 +531,23 @@ else
|
|
|
481
531
|
done
|
|
482
532
|
if [ -n "$PLUGIN_NM" ] && [ -d "$PROJ_DIR/node_modules" ]; then
|
|
483
533
|
# 如果 extensions 中已有大量包(旧版 openclaw 安装的 peerDeps),先清理
|
|
534
|
+
# 但保留 openclaw symlink
|
|
484
535
|
if [ -d "$PLUGIN_NM" ]; then
|
|
485
|
-
_before=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
|
|
536
|
+
_before=$({ ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
486
537
|
if [ "$_before" -gt 50 ]; then
|
|
487
538
|
echo ""
|
|
488
539
|
echo "检测到 ${_before} 个包(超过阈值 50),清理多余的 peerDep 传递依赖..."
|
|
540
|
+
_has_openclaw_link=""
|
|
541
|
+
[ -L "$PLUGIN_NM/openclaw" ] && _has_openclaw_link="$(readlink "$PLUGIN_NM/openclaw")"
|
|
489
542
|
rm -rf "$PLUGIN_NM"
|
|
543
|
+
if [ -n "$_has_openclaw_link" ]; then
|
|
544
|
+
mkdir -p "$PLUGIN_NM"
|
|
545
|
+
ln -sf "$_has_openclaw_link" "$PLUGIN_NM/openclaw"
|
|
546
|
+
fi
|
|
490
547
|
fi
|
|
491
548
|
fi
|
|
492
549
|
|
|
493
|
-
# 读取 bundledDependencies
|
|
550
|
+
# 读取 bundledDependencies 及其传递依赖列表,复制到插件目录
|
|
494
551
|
_deps_to_copy=$(node -e "
|
|
495
552
|
const fs = require('fs');
|
|
496
553
|
const path = require('path');
|
|
@@ -514,23 +571,34 @@ else
|
|
|
514
571
|
if [ -n "$_deps_to_copy" ]; then
|
|
515
572
|
mkdir -p "$PLUGIN_NM"
|
|
516
573
|
_copied=0
|
|
517
|
-
|
|
574
|
+
while IFS= read -r _dep; do
|
|
575
|
+
[ -z "$_dep" ] && continue
|
|
518
576
|
_src="$PROJ_DIR/node_modules/$_dep"
|
|
519
577
|
_dst="$PLUGIN_NM/$_dep"
|
|
520
|
-
if [ -d "$_src" ]
|
|
521
|
-
# 处理 scoped 包(如 @scope/pkg)
|
|
578
|
+
if [ -d "$_src" ]; then
|
|
522
579
|
mkdir -p "$(dirname "$_dst")"
|
|
580
|
+
rm -rf "$_dst"
|
|
523
581
|
cp -r "$_src" "$_dst"
|
|
582
|
+
_copied=$((_copied + 1))
|
|
524
583
|
fi
|
|
525
|
-
done
|
|
526
|
-
|
|
527
|
-
echo " ✅ 已复制 bundled 依赖到插件目录(${_after} 个包)"
|
|
584
|
+
done <<< "$_deps_to_copy"
|
|
585
|
+
echo " ✅ 已复制 ${_copied} 个 bundled 依赖到插件目录"
|
|
528
586
|
else
|
|
529
587
|
echo " ⚠️ 未读取到 bundledDependencies,跳过依赖复制"
|
|
588
|
+
echo " 回退: 使用 npm install --omit=dev 安装运行时依赖..."
|
|
589
|
+
npm install --omit=dev --prefix "$_ext_dir" 2>&1 || true
|
|
590
|
+
fi
|
|
591
|
+
# 验证关键依赖是否就绪
|
|
592
|
+
if [ ! -d "$PLUGIN_NM/ws" ]; then
|
|
593
|
+
echo " ⚠️ ws 包仍缺失,回退: 使用 npm install --omit=dev 安装..."
|
|
594
|
+
npm install --omit=dev --prefix "$_ext_dir" 2>&1 || true
|
|
530
595
|
fi
|
|
596
|
+
elif [ -n "$PLUGIN_NM" ] && [ ! -d "$PROJ_DIR/node_modules" ]; then
|
|
597
|
+
echo " ⚠️ 源码 node_modules 不存在,使用 npm install --omit=dev 安装运行时依赖..."
|
|
598
|
+
npm install --omit=dev --prefix "$_ext_dir" 2>&1 || true
|
|
531
599
|
elif [ -n "$PLUGIN_NM" ] && [ -d "$PLUGIN_NM" ]; then
|
|
532
600
|
# node_modules 已存在(可能是旧版 openclaw 安装的),检查是否需要清理
|
|
533
|
-
_before=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
|
|
601
|
+
_before=$({ ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
534
602
|
if [ "$_before" -gt 50 ]; then
|
|
535
603
|
echo ""
|
|
536
604
|
echo "检测到 ${_before} 个包(超过阈值 50),清理多余的 peerDep 传递依赖..."
|
|
@@ -576,7 +644,7 @@ else
|
|
|
576
644
|
rm -rf "$PLUGIN_NM/$_pkg"
|
|
577
645
|
done
|
|
578
646
|
find "$PLUGIN_NM" -maxdepth 1 -type d -name '@*' -empty -delete 2>/dev/null || true
|
|
579
|
-
_after=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
|
|
647
|
+
_after=$({ ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
580
648
|
echo " 已清理: ${_before} → ${_after} 个包"
|
|
581
649
|
fi
|
|
582
650
|
else
|
|
@@ -609,54 +677,6 @@ else
|
|
|
609
677
|
find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-qqbot-backup-*" -exec rm -rf {} + 2>/dev/null || true
|
|
610
678
|
find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
|
|
611
679
|
|
|
612
|
-
# 恢复 channels.qqbot 完整配置(防止 plugins install 意外覆盖)
|
|
613
|
-
# 策略:直接用备份完整覆盖回去,确保 groups / env / prompts / allowFrom 等所有用户配置不丢失。
|
|
614
|
-
if [ -n "$SAVED_QQBOT_CHANNEL_JSON" ]; then
|
|
615
|
-
node -e "
|
|
616
|
-
const fs = require('fs');
|
|
617
|
-
const path = require('path');
|
|
618
|
-
const saved = JSON.parse(process.argv[1]);
|
|
619
|
-
for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
|
|
620
|
-
const f = path.join(process.env.HOME, '.' + app, app + '.json');
|
|
621
|
-
if (!fs.existsSync(f)) continue;
|
|
622
|
-
const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
623
|
-
if (!cfg.channels) { cfg.channels = {}; }
|
|
624
|
-
if (JSON.stringify(cfg.channels.qqbot) === JSON.stringify(saved)) {
|
|
625
|
-
process.stderr.write(' channels.qqbot 配置无变化,无需恢复\\n');
|
|
626
|
-
} else {
|
|
627
|
-
cfg.channels.qqbot = saved;
|
|
628
|
-
fs.writeFileSync(f, JSON.stringify(cfg, null, 4) + '\\n');
|
|
629
|
-
process.stderr.write(' 已完整恢复 channels.qqbot 配置(' + Object.keys(saved).join(', ') + ')\\n');
|
|
630
|
-
}
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
633
|
-
" "$SAVED_QQBOT_CHANNEL_JSON" 2>&1 || true
|
|
634
|
-
fi
|
|
635
|
-
|
|
636
|
-
# 恢复 channels.qqbot 完整配置(防止 plugins install 意外覆盖)
|
|
637
|
-
# 策略:直接用备份完整覆盖回去,确保 groups / env / prompts / allowFrom 等所有用户配置不丢失。
|
|
638
|
-
if [ -n "$SAVED_QQBOT_CHANNEL_JSON" ]; then
|
|
639
|
-
node -e "
|
|
640
|
-
const fs = require('fs');
|
|
641
|
-
const path = require('path');
|
|
642
|
-
const saved = JSON.parse(process.argv[1]);
|
|
643
|
-
for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
|
|
644
|
-
const f = path.join(process.env.HOME, '.' + app, app + '.json');
|
|
645
|
-
if (!fs.existsSync(f)) continue;
|
|
646
|
-
const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
647
|
-
if (!cfg.channels) { cfg.channels = {}; }
|
|
648
|
-
if (JSON.stringify(cfg.channels.qqbot) === JSON.stringify(saved)) {
|
|
649
|
-
process.stderr.write(' channels.qqbot 配置无变化,无需恢复\\n');
|
|
650
|
-
} else {
|
|
651
|
-
cfg.channels.qqbot = saved;
|
|
652
|
-
fs.writeFileSync(f, JSON.stringify(cfg, null, 4) + '\\n');
|
|
653
|
-
process.stderr.write(' 已完整恢复 channels.qqbot 配置(' + Object.keys(saved).join(', ') + ')\\n');
|
|
654
|
-
}
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
" "$SAVED_QQBOT_CHANNEL_JSON" 2>&1 || true
|
|
658
|
-
fi
|
|
659
|
-
|
|
660
680
|
# 记录更新后的 qqbot 插件版本
|
|
661
681
|
NEW_QQBOT_VERSION=$(node -e '
|
|
662
682
|
try {
|
|
@@ -821,12 +841,17 @@ case "$start_choice" in
|
|
|
821
841
|
_start_output=$(_openclaw_with_timeout 30 openclaw gateway start)
|
|
822
842
|
echo "$_start_output"
|
|
823
843
|
|
|
824
|
-
|
|
844
|
+
# 判断是否真正启动失败:
|
|
845
|
+
# 1. 过滤掉 Config warnings 区块中的 "plugin not installed"(其他插件的警告,非错误)
|
|
846
|
+
# 2. 过滤掉 "not loaded; re-bootstrapped"(自动恢复的正常行为)
|
|
847
|
+
# 3. 只检查 gateway 自身的启动/加载状态
|
|
848
|
+
_start_output_filtered=$(echo "$_start_output" | grep -v "^│" | grep -vi "re-bootstrapped\|re-bootstrap")
|
|
849
|
+
if echo "$_start_output_filtered" | grep -qi "not loaded\|not found\|not installed\|error\|fail"; then
|
|
825
850
|
echo ""
|
|
826
851
|
echo "⚠️ 启动异常,尝试 restart 恢复..."
|
|
827
852
|
_restart_output=$(_openclaw_with_timeout 30 openclaw gateway restart)
|
|
828
853
|
echo "$_restart_output"
|
|
829
|
-
if echo "$_restart_output" | grep -qi "not loaded\|not found\|not installed"; then
|
|
854
|
+
if echo "$_restart_output" | grep -v "^│" | grep -vi "re-bootstrapped\|re-bootstrap" | grep -qi "not loaded\|not found\|not installed"; then
|
|
830
855
|
echo ""
|
|
831
856
|
echo "⚠️ 自动恢复失败,请手动执行:"
|
|
832
857
|
echo " openclaw gateway install && openclaw gateway start"
|
|
@@ -844,11 +869,11 @@ case "$start_choice" in
|
|
|
844
869
|
echo "========================================="
|
|
845
870
|
_port_ready=0
|
|
846
871
|
for i in $(seq 1 30); do
|
|
847
|
-
if lsof -i :
|
|
872
|
+
if lsof -i :"$_GW_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
848
873
|
_port_ready=1
|
|
849
874
|
break
|
|
850
875
|
fi
|
|
851
|
-
printf "\r 等待端口
|
|
876
|
+
printf "\r 等待端口 %s 就绪... (%d/30)" "$_GW_PORT" "$i"
|
|
852
877
|
sleep 2
|
|
853
878
|
done
|
|
854
879
|
echo ""
|
|
@@ -862,7 +887,7 @@ case "$start_choice" in
|
|
|
862
887
|
echo "doctor 修复后重试 gateway restart..."
|
|
863
888
|
_openclaw_with_timeout 30 openclaw gateway restart
|
|
864
889
|
sleep 5
|
|
865
|
-
if lsof -i :
|
|
890
|
+
if lsof -i :"$_GW_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
866
891
|
echo "✅ doctor --fix 后 gateway 启动成功"
|
|
867
892
|
_port_ready=1
|
|
868
893
|
else
|
|
@@ -926,24 +951,35 @@ case "$start_choice" in
|
|
|
926
951
|
' 2>&1 || echo " ⚠️ 配置写入失败"
|
|
927
952
|
echo " ✅ 已恢复 channels.qqbot 配置(含 token/markdown)"
|
|
928
953
|
_need_reload=1
|
|
954
|
+
# 清空暂存变量,防止 EXIT trap 重复恢复
|
|
955
|
+
_QQBOT_CHANNEL_STASH=""
|
|
929
956
|
fi
|
|
930
957
|
|
|
931
|
-
#
|
|
958
|
+
# 配置写回后让 gateway 重载配置使其生效
|
|
959
|
+
# gateway 有 chokidar 文件监听,写入 openclaw.json 后通常会自动 reload。
|
|
960
|
+
# 这里发送 SIGHUP 通知进程立即重载,无需完整 restart(避免冷启动 10-20s)。
|
|
932
961
|
if [ "$_need_reload" -eq 1 ]; then
|
|
933
|
-
echo " 重载配置..."
|
|
934
|
-
sleep
|
|
935
|
-
#
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
962
|
+
echo " 通知 gateway 重载配置..."
|
|
963
|
+
sleep 2
|
|
964
|
+
# 尝试用 SIGHUP 触发热重载;若失败则回退到 restart
|
|
965
|
+
_gw_pid=$(lsof -i :"$_GW_PORT" -sTCP:LISTEN -t 2>/dev/null | head -1)
|
|
966
|
+
if [ -n "$_gw_pid" ]; then
|
|
967
|
+
kill -HUP "$_gw_pid" 2>/dev/null || true
|
|
968
|
+
echo " 已发送 SIGHUP 到 gateway (pid: $_gw_pid)"
|
|
969
|
+
sleep 3
|
|
970
|
+
else
|
|
971
|
+
# gateway 端口还在但 pid 拿不到,回退到 restart
|
|
972
|
+
_openclaw_with_timeout 30 openclaw gateway restart
|
|
973
|
+
# 等待重启后端口重新就绪
|
|
974
|
+
for _k in $(seq 1 30); do
|
|
975
|
+
if lsof -i :"$_GW_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
976
|
+
break
|
|
977
|
+
fi
|
|
978
|
+
printf "\r 等待端口 %s 重新就绪... (%d/30)" "$_GW_PORT" "$_k"
|
|
979
|
+
sleep 2
|
|
980
|
+
done
|
|
981
|
+
echo ""
|
|
982
|
+
fi
|
|
947
983
|
fi
|
|
948
984
|
# 检查 qqbot WS 是否连接成功(最多等 20 秒)
|
|
949
985
|
echo "检查 qqbot 插件连接状态..."
|
|
@@ -1001,6 +1037,7 @@ case "$start_choice" in
|
|
|
1001
1037
|
fs.writeFileSync(process.env._CFG, JSON.stringify(cfg, null, 4) + "\n");
|
|
1002
1038
|
' 2>/dev/null || true
|
|
1003
1039
|
echo " 已恢复 channels.qqbot 配置"
|
|
1040
|
+
_QQBOT_CHANNEL_STASH=""
|
|
1004
1041
|
fi
|
|
1005
1042
|
echo ""
|
|
1006
1043
|
echo "后续启动方法(兼容 openclaw 3.23+):"
|
package/src/api.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import os from "node:os";
|
|
7
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
7
8
|
import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
|
|
8
9
|
import { sanitizeFileName } from "./utils/platform.js";
|
|
10
|
+
import { resolveUserAgentSuffix } from "./config.js";
|
|
11
|
+
import { getQQBotRuntime } from "./runtime.js";
|
|
9
12
|
|
|
10
13
|
// ============ 模块级 Logger ============
|
|
11
14
|
|
|
@@ -50,8 +53,9 @@ export class ApiError extends Error {
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
const
|
|
56
|
+
// 支持环境变量覆盖,用于私有化部署/测试环境
|
|
57
|
+
export const API_BASE = (process.env.QQBOT_BASE_URL?.replace(/\/+$/, "") || "https://api.sgroup.qq.com");
|
|
58
|
+
export const TOKEN_URL = `${process.env.QQBOT_TOKEN_BASE_URL?.replace(/\/+$/, "") || "https://bots.qq.com"}/app/getAppAccessToken`;
|
|
55
59
|
|
|
56
60
|
// ============ Plugin User-Agent ============
|
|
57
61
|
// 格式: QQBotPlugin/{version} (Node/{nodeVersion}; {os}; OpenClaw/{openclawVersion})
|
|
@@ -64,8 +68,18 @@ let _openclawVersion = "unknown";
|
|
|
64
68
|
export function setOpenClawVersion(version: string) {
|
|
65
69
|
if (version) _openclawVersion = version;
|
|
66
70
|
}
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
|
|
72
|
+
export function getPluginUserAgent(): string {
|
|
73
|
+
const base = `QQBotPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()}; OpenClaw/${_openclawVersion})`;
|
|
74
|
+
let suffix = "";
|
|
75
|
+
try {
|
|
76
|
+
const rt = getQQBotRuntime();
|
|
77
|
+
// rt.config 是配置管理器,调用 .current() 获取实际配置数据
|
|
78
|
+
const cfgMgr = rt.config as { current?: () => unknown };
|
|
79
|
+
const cfg = typeof cfgMgr.current === "function" ? cfgMgr.current() : (cfgMgr as OpenClawConfig);
|
|
80
|
+
suffix = resolveUserAgentSuffix(cfg as OpenClawConfig);
|
|
81
|
+
} catch { /* runtime 未初始化时返回无后缀 UA */ }
|
|
82
|
+
return suffix ? `${base} ${suffix}` : base;
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
// 运行时配置
|
|
@@ -639,7 +653,8 @@ function buildMessageBody(
|
|
|
639
653
|
content: string,
|
|
640
654
|
msgId: string | undefined,
|
|
641
655
|
msgSeq: number,
|
|
642
|
-
messageReference?: string
|
|
656
|
+
messageReference?: string,
|
|
657
|
+
inlineKeyboard?: import("./types.js").InlineKeyboard
|
|
643
658
|
): Record<string, unknown> {
|
|
644
659
|
const body: Record<string, unknown> = currentMarkdownSupport
|
|
645
660
|
? {
|
|
@@ -659,6 +674,10 @@ function buildMessageBody(
|
|
|
659
674
|
if (messageReference && !currentMarkdownSupport) {
|
|
660
675
|
body.message_reference = { message_id: messageReference };
|
|
661
676
|
}
|
|
677
|
+
// Inline Keyboard(内嵌按钮,需审核):字段名 keyboard,结构 { content: { rows } }
|
|
678
|
+
if (inlineKeyboard) {
|
|
679
|
+
body.keyboard = inlineKeyboard;
|
|
680
|
+
}
|
|
662
681
|
return body;
|
|
663
682
|
}
|
|
664
683
|
|
|
@@ -735,6 +754,32 @@ export async function sendGroupMessage(
|
|
|
735
754
|
return sendAndNotify(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body, { text: content });
|
|
736
755
|
}
|
|
737
756
|
|
|
757
|
+
/** 发送带 Inline Keyboard 的 C2C 消息(回调型按钮,触发 INTERACTION_CREATE) */
|
|
758
|
+
export async function sendC2CMessageWithInlineKeyboard(
|
|
759
|
+
accessToken: string,
|
|
760
|
+
openid: string,
|
|
761
|
+
content: string,
|
|
762
|
+
inlineKeyboard: import("./types.js").InlineKeyboard,
|
|
763
|
+
msgId?: string,
|
|
764
|
+
): Promise<MessageResponse> {
|
|
765
|
+
const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
|
|
766
|
+
const body = buildMessageBody(content, msgId, msgSeq, undefined, inlineKeyboard);
|
|
767
|
+
return sendAndNotify(accessToken, "POST", `/v2/users/${openid}/messages`, body, { text: content });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** 发送带 Inline Keyboard 的 Group 消息(回调型按钮,触发 INTERACTION_CREATE) */
|
|
771
|
+
export async function sendGroupMessageWithInlineKeyboard(
|
|
772
|
+
accessToken: string,
|
|
773
|
+
groupOpenid: string,
|
|
774
|
+
content: string,
|
|
775
|
+
inlineKeyboard: import("./types.js").InlineKeyboard,
|
|
776
|
+
msgId?: string,
|
|
777
|
+
): Promise<MessageResponse> {
|
|
778
|
+
const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
|
|
779
|
+
const body = buildMessageBody(content, msgId, msgSeq, undefined, inlineKeyboard);
|
|
780
|
+
return sendAndNotify(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body, { text: content });
|
|
781
|
+
}
|
|
782
|
+
|
|
738
783
|
function buildProactiveMessageBody(content: string): Record<string, unknown> {
|
|
739
784
|
if (!content || content.trim().length === 0) {
|
|
740
785
|
throw new Error("主动消息内容不能为空 (markdown.content is empty)");
|