@tencent-connect/openclaw-qqbot 1.6.0-alpha.1 → 1.6.0-alpha.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/package.json +1 -1
- package/scripts/upgrade-via-source.sh +119 -72
- package/skills/qqbot-media/SKILL.md +1 -1
- package/src/gateway.ts +132 -43
- package/src/slash-commands.ts +162 -24
- package/src/types.ts +1 -1
- package/src/update-checker.ts +102 -0
- package/src/utils/media-tags.ts +34 -2
- package/dist/index.d.ts +0 -17
- package/dist/index.js +0 -22
- package/dist/scripts/upgrade-via-npm.sh +0 -168
- package/dist/src/api.d.ts +0 -147
- package/dist/src/api.js +0 -583
- package/dist/src/channel.d.ts +0 -3
- package/dist/src/channel.js +0 -337
- package/dist/src/config.d.ts +0 -25
- package/dist/src/config.js +0 -161
- package/dist/src/gateway.d.ts +0 -18
- package/dist/src/gateway.js +0 -2483
- package/dist/src/image-server.d.ts +0 -68
- package/dist/src/image-server.js +0 -462
- package/dist/src/known-users.d.ts +0 -100
- package/dist/src/known-users.js +0 -263
- package/dist/src/message-queue.d.ts +0 -267
- package/dist/src/message-queue.js +0 -558
- package/dist/src/onboarding.d.ts +0 -10
- package/dist/src/onboarding.js +0 -203
- package/dist/src/outbound.d.ts +0 -203
- package/dist/src/outbound.js +0 -1103
- package/dist/src/proactive.d.ts +0 -170
- package/dist/src/proactive.js +0 -399
- package/dist/src/ref-index-store.d.ts +0 -70
- package/dist/src/ref-index-store.js +0 -274
- package/dist/src/runtime.d.ts +0 -3
- package/dist/src/runtime.js +0 -10
- package/dist/src/session-store.d.ts +0 -52
- package/dist/src/session-store.js +0 -254
- package/dist/src/slash-commands.d.ts +0 -61
- package/dist/src/slash-commands.js +0 -211
- package/dist/src/types.d.ts +0 -169
- package/dist/src/types.js +0 -1
- package/dist/src/user-messages.d.ts +0 -43
- package/dist/src/user-messages.js +0 -65
- package/dist/src/utils/audio-convert.d.ts +0 -89
- package/dist/src/utils/audio-convert.js +0 -704
- package/dist/src/utils/file-utils.d.ts +0 -46
- package/dist/src/utils/file-utils.js +0 -107
- package/dist/src/utils/image-size.d.ts +0 -51
- package/dist/src/utils/image-size.js +0 -234
- package/dist/src/utils/media-tags.d.ts +0 -14
- package/dist/src/utils/media-tags.js +0 -130
- package/dist/src/utils/payload.d.ts +0 -112
- package/dist/src/utils/payload.js +0 -186
- package/dist/src/utils/platform.d.ts +0 -127
- package/dist/src/utils/platform.js +0 -374
- package/dist/src/utils/upload-cache.d.ts +0 -34
- package/dist/src/utils/upload-cache.js +0 -93
package/package.json
CHANGED
|
@@ -186,6 +186,19 @@ INSTALL_LOG="/tmp/openclaw-install-$(date +%s).log"
|
|
|
186
186
|
echo "安装日志文件: $INSTALL_LOG"
|
|
187
187
|
echo "详细信息将记录到日志文件中..."
|
|
188
188
|
|
|
189
|
+
# 安装前先 stop gateway,防止 chokidar 在 plugins install 写入配置的中间状态
|
|
190
|
+
# 触发 restart,导致 "unknown channel id: qqbot" 等错误
|
|
191
|
+
_gw_was_running=0
|
|
192
|
+
if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
193
|
+
_gw_was_running=1
|
|
194
|
+
echo " 暂停 gateway 服务(避免安装过程中中间状态 restart)..."
|
|
195
|
+
openclaw gateway stop 2>/dev/null || true
|
|
196
|
+
sleep 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# 清理之前可能残留的 staging 目录
|
|
200
|
+
find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
|
|
201
|
+
|
|
189
202
|
# 尝试安装并捕获详细输出
|
|
190
203
|
if ! openclaw plugins install . 2>&1 | tee "$INSTALL_LOG"; then
|
|
191
204
|
echo ""
|
|
@@ -243,9 +256,70 @@ if ! openclaw plugins install . 2>&1 | tee "$INSTALL_LOG"; then
|
|
|
243
256
|
esac
|
|
244
257
|
else
|
|
245
258
|
echo ""
|
|
246
|
-
echo "✅
|
|
259
|
+
echo "✅ 插件安装命令执行完成"
|
|
247
260
|
echo "安装日志已保存到: $INSTALL_LOG"
|
|
248
261
|
|
|
262
|
+
# 验证插件目录是否真正创建(防止 "安装成功" 但目录缺失的情况)
|
|
263
|
+
_plugin_dir_ok=0
|
|
264
|
+
for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
|
|
265
|
+
if [ -d "$HOME/.openclaw/extensions/$_candidate_name" ] && \
|
|
266
|
+
[ -f "$HOME/.openclaw/extensions/$_candidate_name/package.json" ]; then
|
|
267
|
+
_plugin_dir_ok=1
|
|
268
|
+
echo " ✅ 插件目录验证通过: ~/.openclaw/extensions/$_candidate_name/"
|
|
269
|
+
break
|
|
270
|
+
fi
|
|
271
|
+
done
|
|
272
|
+
if [ "$_plugin_dir_ok" -eq 0 ]; then
|
|
273
|
+
echo ""
|
|
274
|
+
echo "⚠️ 警告: 插件目录不存在!安装命令返回成功但目录未创建"
|
|
275
|
+
echo " 可能原因: staging 目录未正确 rename(参考 openclaw/issues)"
|
|
276
|
+
echo " 检查 staging 残留:"
|
|
277
|
+
ls -la "$HOME/.openclaw/extensions/" 2>/dev/null
|
|
278
|
+
echo ""
|
|
279
|
+
echo " 尝试自动修复: 清理残留并重试安装..."
|
|
280
|
+
find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
|
|
281
|
+
if openclaw plugins install . 2>&1 | tee -a "$INSTALL_LOG"; then
|
|
282
|
+
for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
|
|
283
|
+
if [ -d "$HOME/.openclaw/extensions/$_candidate_name" ]; then
|
|
284
|
+
_plugin_dir_ok=1
|
|
285
|
+
echo " ✅ 重试安装成功: ~/.openclaw/extensions/$_candidate_name/"
|
|
286
|
+
break
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
fi
|
|
290
|
+
if [ "$_plugin_dir_ok" -eq 0 ]; then
|
|
291
|
+
echo " ❌ 重试安装仍失败,插件目录不存在"
|
|
292
|
+
echo " 请手动排查: ls -la ~/.openclaw/extensions/"
|
|
293
|
+
# 清理无效的配置条目,防止 gateway 启动时报错
|
|
294
|
+
echo " 清理无效的配置条目..."
|
|
295
|
+
node -e "
|
|
296
|
+
const fs = require('fs');
|
|
297
|
+
const path = require('path');
|
|
298
|
+
for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
|
|
299
|
+
const f = path.join(process.env.HOME, '.' + app, app + '.json');
|
|
300
|
+
if (!fs.existsSync(f)) continue;
|
|
301
|
+
const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
302
|
+
let changed = false;
|
|
303
|
+
// 移除 channels.qqbot(插件未加载时此配置会导致 unknown channel id 错误)
|
|
304
|
+
if (cfg.channels && cfg.channels.qqbot) { delete cfg.channels.qqbot; changed = true; }
|
|
305
|
+
// 清理 plugins.allow 中的 openclaw-qqbot
|
|
306
|
+
if (cfg.plugins && Array.isArray(cfg.plugins.allow)) {
|
|
307
|
+
cfg.plugins.allow = cfg.plugins.allow.filter(x => x !== 'openclaw-qqbot');
|
|
308
|
+
changed = true;
|
|
309
|
+
}
|
|
310
|
+
if (changed) fs.writeFileSync(f, JSON.stringify(cfg, null, 4) + '\n');
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
" 2>/dev/null || true
|
|
314
|
+
echo " 已清理无效配置,gateway 可正常启动(无 qqbot 插件状态)"
|
|
315
|
+
read -t 10 -p "是否继续? (y/N): " _cont || _cont="N"
|
|
316
|
+
case "$_cont" in
|
|
317
|
+
[Yy]* ) echo "继续..." ;;
|
|
318
|
+
* ) exit 1 ;;
|
|
319
|
+
esac
|
|
320
|
+
fi
|
|
321
|
+
fi
|
|
322
|
+
|
|
249
323
|
# 清理多余的 peerDependencies 传递依赖(兼容旧版 openclaw):
|
|
250
324
|
# openclaw v2026.3.4 之前的 plugins install 缺少 --omit=peer,会把 peerDeps
|
|
251
325
|
# (openclaw 平台及其 400+ 传递依赖)也安装到插件 node_modules 中。
|
|
@@ -310,31 +384,8 @@ else
|
|
|
310
384
|
fi
|
|
311
385
|
fi
|
|
312
386
|
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
# 如果 gateway 未运行,则无需等待(最终 Step 6 会启动)。
|
|
316
|
-
echo ""
|
|
317
|
-
if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
318
|
-
echo "等待 gateway 自动重启完成..."
|
|
319
|
-
_gw_restarted=0
|
|
320
|
-
for _w in $(seq 1 20); do
|
|
321
|
-
sleep 1
|
|
322
|
-
if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
323
|
-
sleep 2
|
|
324
|
-
if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
325
|
-
_gw_restarted=1
|
|
326
|
-
break
|
|
327
|
-
fi
|
|
328
|
-
fi
|
|
329
|
-
done
|
|
330
|
-
if [ "$_gw_restarted" -eq 1 ]; then
|
|
331
|
-
echo " gateway 已自动重启完成"
|
|
332
|
-
else
|
|
333
|
-
echo " 等待超时,将在最后一步重新启动"
|
|
334
|
-
fi
|
|
335
|
-
else
|
|
336
|
-
echo " gateway 当前未运行,跳过自动重启等待(将在最后一步启动)"
|
|
337
|
-
fi
|
|
387
|
+
# gateway 已在安装前 stop,此时不会有自动 restart 的问题
|
|
388
|
+
# 所有配置写入完成后,在 Step 6 统一启动
|
|
338
389
|
|
|
339
390
|
# 记录更新后的 qqbot 插件版本
|
|
340
391
|
NEW_QQBOT_VERSION=$(node -e '
|
|
@@ -528,52 +579,33 @@ start_choice=$(printf '%s' "$start_choice" | tr '[:upper:]' '[:lower:]')
|
|
|
528
579
|
case "$start_choice" in
|
|
529
580
|
y|yes)
|
|
530
581
|
echo ""
|
|
531
|
-
#
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
_need_restart=1
|
|
536
|
-
if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
537
|
-
# gateway 已在运行,如果 Step 3 的自动 restart 已成功加载新插件,
|
|
538
|
-
# 且 Step 4/5 只做了 hot reload,理论上不需要再 restart。
|
|
539
|
-
# 但为安全起见:如果版本有变化或配置有变化,仍 restart 一次。
|
|
540
|
-
if [ "$OLD_QQBOT_VERSION" = "$NEW_QQBOT_VERSION" ] && [ "${_config_changed:-0}" -eq 0 ]; then
|
|
541
|
-
echo "插件版本未变化且配置无变更,跳过冗余重启"
|
|
542
|
-
_need_restart=0
|
|
543
|
-
fi
|
|
544
|
-
fi
|
|
545
|
-
|
|
546
|
-
if [ "$_need_restart" -eq 1 ]; then
|
|
547
|
-
echo "正在后台重启 openclaw 网关服务..."
|
|
548
|
-
_restart_output=$(openclaw gateway restart 2>&1) || true
|
|
549
|
-
echo "$_restart_output"
|
|
582
|
+
# gateway 在 Step 3 安装前已 stop,此处统一 restart
|
|
583
|
+
echo "正在后台重启 openclaw 网关服务..."
|
|
584
|
+
_restart_output=$(openclaw gateway restart 2>&1) || true
|
|
585
|
+
echo "$_restart_output"
|
|
550
586
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
587
|
+
if echo "$_restart_output" | grep -qi "not loaded\|not found\|not running\|not installed"; then
|
|
588
|
+
echo ""
|
|
589
|
+
echo "⚠️ gateway 服务未加载,尝试自动恢复..."
|
|
590
|
+
echo ""
|
|
591
|
+
echo " [1/2] 注册 gateway 服务..."
|
|
592
|
+
_install_out=$(openclaw gateway install 2>&1) || true
|
|
593
|
+
echo " $_install_out"
|
|
594
|
+
echo ""
|
|
595
|
+
echo " [2/2] 启动 gateway 服务..."
|
|
596
|
+
_start_out=$(openclaw gateway start 2>&1) || true
|
|
597
|
+
echo " $_start_out"
|
|
598
|
+
if echo "$_start_out" | grep -qi "restart\|started\|bootstrap"; then
|
|
560
599
|
echo ""
|
|
561
|
-
echo "
|
|
562
|
-
_start_out=$(openclaw gateway start 2>&1) || true
|
|
563
|
-
echo " $_start_out"
|
|
564
|
-
# 检查恢复是否成功
|
|
565
|
-
if echo "$_start_out" | grep -qi "restart\|started\|bootstrap"; then
|
|
566
|
-
echo ""
|
|
567
|
-
echo "✅ gateway 服务恢复成功"
|
|
568
|
-
else
|
|
569
|
-
echo ""
|
|
570
|
-
echo "⚠️ 自动恢复可能失败,请手动执行:"
|
|
571
|
-
echo " openclaw gateway install && openclaw gateway start"
|
|
572
|
-
fi
|
|
600
|
+
echo "✅ gateway 服务恢复成功"
|
|
573
601
|
else
|
|
574
602
|
echo ""
|
|
575
|
-
echo "
|
|
603
|
+
echo "⚠️ 自动恢复可能失败,请手动执行:"
|
|
604
|
+
echo " openclaw gateway install && openclaw gateway start"
|
|
576
605
|
fi
|
|
606
|
+
else
|
|
607
|
+
echo ""
|
|
608
|
+
echo "✅ openclaw 网关已在后台重启"
|
|
577
609
|
fi
|
|
578
610
|
echo ""
|
|
579
611
|
# 等待 gateway 端口就绪
|
|
@@ -591,10 +623,26 @@ case "$start_choice" in
|
|
|
591
623
|
echo ""
|
|
592
624
|
|
|
593
625
|
if [ "$_port_ready" -eq 0 ]; then
|
|
594
|
-
echo "⚠️
|
|
595
|
-
|
|
596
|
-
echo "
|
|
597
|
-
|
|
626
|
+
echo "⚠️ 等待超时,尝试 openclaw doctor --fix 自动修复..."
|
|
627
|
+
_doctor_output=$(openclaw doctor --fix 2>&1) || true
|
|
628
|
+
echo "$_doctor_output"
|
|
629
|
+
# doctor --fix 后再尝试 restart 一次
|
|
630
|
+
echo ""
|
|
631
|
+
echo "doctor 修复后重试 gateway restart..."
|
|
632
|
+
openclaw gateway restart 2>&1 || true
|
|
633
|
+
sleep 5
|
|
634
|
+
if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
635
|
+
echo "✅ doctor --fix 后 gateway 启动成功"
|
|
636
|
+
_port_ready=1
|
|
637
|
+
else
|
|
638
|
+
echo "❌ 仍然无法启动,请手动排查:"
|
|
639
|
+
echo " openclaw doctor"
|
|
640
|
+
echo " tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log"
|
|
641
|
+
fi
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
# 端口就绪后:检查 qqbot 连接 + 跟踪日志
|
|
645
|
+
if [ "$_port_ready" -eq 1 ]; then
|
|
598
646
|
echo "✅ Gateway 端口已就绪"
|
|
599
647
|
echo ""
|
|
600
648
|
# 检查 qqbot WS 是否连接成功(最多等 20 秒)
|
|
@@ -603,7 +651,6 @@ case "$start_choice" in
|
|
|
603
651
|
_restart_ts=$(date +%s)
|
|
604
652
|
_qqbot_ready=0
|
|
605
653
|
for _j in $(seq 1 10); do
|
|
606
|
-
# 只检查在本次重启之后出现的 "Gateway ready" 日志
|
|
607
654
|
if [ -f "$_LOG_FILE" ]; then
|
|
608
655
|
_last_line=$(grep "Gateway ready" "$_LOG_FILE" 2>/dev/null | tail -1 || true)
|
|
609
656
|
if [ -n "$_last_line" ]; then
|
|
@@ -27,7 +27,7 @@ metadata: {"openclaw":{"emoji":"📸","requires":{"config":["channels.qqbot"]}}}
|
|
|
27
27
|
## 规则
|
|
28
28
|
|
|
29
29
|
1. **路径必须是绝对路径**(以 `/` 或 `http` 开头)
|
|
30
|
-
2.
|
|
30
|
+
2. **标签必须用开闭标签包裹路径**:`<qqmedia>路径</qqmedia>`
|
|
31
31
|
3. **文件大小上限 10MB**
|
|
32
32
|
4. **你有能力发送本地图片/文件**——直接用标签包裹路径即可,**不要说"无法发送"**
|
|
33
33
|
5. 发送语音时不要重复语音中已朗读的文字
|
package/src/gateway.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { loadSession, saveSession, clearSession, type SessionState } from "./ses
|
|
|
7
7
|
import { recordKnownUser, flushKnownUsers, listKnownUsers } from "./known-users.js";
|
|
8
8
|
import { getQQBotRuntime } from "./runtime.js";
|
|
9
9
|
import { setRefIndex, getRefIndex, formatRefEntryForAgent, flushRefIndex, type RefAttachmentSummary } from "./ref-index-store.js";
|
|
10
|
-
import { matchSlashCommand, getPluginVersion, type SlashCommandContext, type QueueSnapshot } from "./slash-commands.js";
|
|
10
|
+
import { matchSlashCommand, getPluginVersion, type SlashCommandContext, type SlashCommandFileResult, type QueueSnapshot } from "./slash-commands.js";
|
|
11
|
+
import { triggerUpdateCheck } from "./update-checker.js";
|
|
11
12
|
import { startImageServer, isImageServerRunning, downloadFile, type ImageServerConfig } from "./image-server.js";
|
|
12
13
|
import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize, DEFAULT_IMAGE_SIZE } from "./utils/image-size.js";
|
|
13
14
|
import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload, type CronReminderPayload, type MediaPayload } from "./utils/payload.js";
|
|
@@ -343,6 +344,49 @@ async function ensureImageServer(log?: GatewayContext["log"], publicBaseUrl?: st
|
|
|
343
344
|
}
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
// ============ 启动问候语(首次安装/版本更新 vs 普通重启) ============
|
|
348
|
+
|
|
349
|
+
// 模块级变量:进程生命周期内只有首次为 true
|
|
350
|
+
// 区分 gateway restart(进程重启)和 health-monitor 断线重连
|
|
351
|
+
let isFirstReadyGlobal = true;
|
|
352
|
+
|
|
353
|
+
const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 判断是否为首次安装或版本更新,返回对应的问候语。
|
|
357
|
+
* - 首次安装 / 版本变更 → "Haha,我的'灵魂'已上线,随时等你吩咐。"
|
|
358
|
+
* - 普通重启 → "我重新登上了,有事随时找我。"
|
|
359
|
+
*/
|
|
360
|
+
function getStartupGreeting(): string {
|
|
361
|
+
const currentVersion = getPluginVersion();
|
|
362
|
+
let isFirstOrUpdated = true;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (fs.existsSync(STARTUP_MARKER_FILE)) {
|
|
366
|
+
const data = JSON.parse(fs.readFileSync(STARTUP_MARKER_FILE, "utf8"));
|
|
367
|
+
if (data.version === currentVersion) {
|
|
368
|
+
isFirstOrUpdated = false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
// 文件损坏或不存在,视为首次
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 更新 marker 文件
|
|
376
|
+
try {
|
|
377
|
+
fs.writeFileSync(STARTUP_MARKER_FILE, JSON.stringify({
|
|
378
|
+
version: currentVersion,
|
|
379
|
+
startedAt: new Date().toISOString(),
|
|
380
|
+
}) + "\n");
|
|
381
|
+
} catch {
|
|
382
|
+
// ignore
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return isFirstOrUpdated
|
|
386
|
+
? `Haha,我的'灵魂'已上线,随时等你吩咐。`
|
|
387
|
+
: `我重新登上了,有事随时找我。`;
|
|
388
|
+
}
|
|
389
|
+
|
|
346
390
|
/**
|
|
347
391
|
* 启动 Gateway WebSocket 连接(带自动重连)
|
|
348
392
|
* 支持流式消息发送
|
|
@@ -362,6 +406,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
362
406
|
}
|
|
363
407
|
}
|
|
364
408
|
|
|
409
|
+
// 后台版本检查(detached 子进程,零阻塞)
|
|
410
|
+
triggerUpdateCheck(log);
|
|
411
|
+
|
|
365
412
|
// 初始化 API 配置(markdown 支持)
|
|
366
413
|
initApiConfig({
|
|
367
414
|
markdownSupport: account.markdownSupport,
|
|
@@ -436,6 +483,46 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
436
483
|
let isConnecting = false; // 防止并发连接
|
|
437
484
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; // 重连定时器
|
|
438
485
|
let shouldRefreshToken = false; // 下次连接是否需要刷新 token
|
|
486
|
+
// 使用模块级 isFirstReadyGlobal,确保只有进程级重启才发送问候语
|
|
487
|
+
// health-monitor 重连不会重新初始化为 true
|
|
488
|
+
|
|
489
|
+
/** 异步发送启动问候语(READY 或 RESUMED 时调用) */
|
|
490
|
+
const sendStartupGreetings = (trigger: "READY" | "RESUMED") => {
|
|
491
|
+
(async () => {
|
|
492
|
+
try {
|
|
493
|
+
const greeting = getStartupGreeting();
|
|
494
|
+
log?.info(`[qqbot:${account.accountId}] Sending startup greeting (trigger=${trigger}): "${greeting}"`);
|
|
495
|
+
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
496
|
+
const users = listKnownUsers({ accountId: account.accountId, type: "c2c" });
|
|
497
|
+
for (const user of users) {
|
|
498
|
+
try {
|
|
499
|
+
await sendProactiveC2CMessage(token, user.openid, greeting);
|
|
500
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to c2c:${user.openid}`);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to c2c:${user.openid}: ${err}`);
|
|
503
|
+
}
|
|
504
|
+
await new Promise(r => setTimeout(r, 500));
|
|
505
|
+
}
|
|
506
|
+
const groups = listKnownUsers({ accountId: account.accountId, type: "group" });
|
|
507
|
+
const sentGroups = new Set<string>();
|
|
508
|
+
for (const user of groups) {
|
|
509
|
+
const gid = user.groupOpenid;
|
|
510
|
+
if (!gid || sentGroups.has(gid)) continue;
|
|
511
|
+
sentGroups.add(gid);
|
|
512
|
+
try {
|
|
513
|
+
await sendProactiveGroupMessage(token, gid, greeting);
|
|
514
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to group:${gid}`);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to group:${gid}: ${err}`);
|
|
517
|
+
}
|
|
518
|
+
await new Promise(r => setTimeout(r, 500));
|
|
519
|
+
}
|
|
520
|
+
log?.info(`[qqbot:${account.accountId}] Startup greetings sent (${users.length} c2c, ${sentGroups.size} groups)`);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send startup greetings: ${err}`);
|
|
523
|
+
}
|
|
524
|
+
})();
|
|
525
|
+
};
|
|
439
526
|
|
|
440
527
|
// ============ P1-2: 尝试从持久化存储恢复 Session ============
|
|
441
528
|
// 传入当前 appId,如果 appId 已变更(换了机器人),旧 session 自动失效
|
|
@@ -589,15 +676,40 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
589
676
|
// 命中插件级指令,直接回复
|
|
590
677
|
log?.info(`[qqbot:${account.accountId}] Slash command matched: ${content}, replying directly`);
|
|
591
678
|
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
679
|
+
|
|
680
|
+
// 解析回复:纯文本 or 带文件的结果
|
|
681
|
+
const isFileResult = typeof reply === "object" && reply !== null && "filePath" in reply;
|
|
682
|
+
const replyText = isFileResult ? (reply as SlashCommandFileResult).text : reply as string;
|
|
683
|
+
const replyFile = isFileResult ? (reply as SlashCommandFileResult).filePath : null;
|
|
684
|
+
|
|
685
|
+
// 先发送文本回复
|
|
592
686
|
if (msg.type === "c2c") {
|
|
593
|
-
await sendC2CMessage(token, msg.senderId,
|
|
687
|
+
await sendC2CMessage(token, msg.senderId, replyText, msg.messageId);
|
|
594
688
|
} else if (msg.type === "group" && msg.groupOpenid) {
|
|
595
|
-
await sendGroupMessage(token, msg.groupOpenid,
|
|
689
|
+
await sendGroupMessage(token, msg.groupOpenid, replyText, msg.messageId);
|
|
596
690
|
} else if (msg.channelId) {
|
|
597
|
-
await sendChannelMessage(token, msg.channelId,
|
|
691
|
+
await sendChannelMessage(token, msg.channelId, replyText, msg.messageId);
|
|
598
692
|
} else if (msg.type === "dm") {
|
|
599
|
-
|
|
600
|
-
|
|
693
|
+
await sendC2CMessage(token, msg.senderId, replyText, msg.messageId);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 如果有文件需要发送
|
|
697
|
+
if (replyFile) {
|
|
698
|
+
try {
|
|
699
|
+
const targetType = msg.type === "group" ? "group" : msg.type === "c2c" || msg.type === "dm" ? "c2c" : "channel";
|
|
700
|
+
const targetId = msg.type === "group" ? (msg.groupOpenid || msg.senderId) : msg.type === "c2c" || msg.type === "dm" ? msg.senderId : (msg.channelId || msg.senderId);
|
|
701
|
+
const mediaCtx: MediaTargetContext = {
|
|
702
|
+
targetType,
|
|
703
|
+
targetId,
|
|
704
|
+
account,
|
|
705
|
+
replyToId: msg.messageId,
|
|
706
|
+
logPrefix: `[qqbot:${account.accountId}]`,
|
|
707
|
+
};
|
|
708
|
+
await sendDocument(mediaCtx, replyFile);
|
|
709
|
+
log?.info(`[qqbot:${account.accountId}] Slash command file sent: ${replyFile}`);
|
|
710
|
+
} catch (fileErr) {
|
|
711
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send slash command file: ${fileErr}`);
|
|
712
|
+
}
|
|
601
713
|
}
|
|
602
714
|
} catch (err) {
|
|
603
715
|
log?.error(`[qqbot:${account.accountId}] Slash command error: ${err}`);
|
|
@@ -1061,7 +1173,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
1061
1173
|
];
|
|
1062
1174
|
// TTS 能力声明:仅在启用时告知 AI 可以发语音(与 qqbot-media SKILL.md 互补)
|
|
1063
1175
|
// STT 无需声明:转写结果已在动态上下文的 ASR 行中,AI 自然可见
|
|
1064
|
-
if (hasTTS) staticParts.push("
|
|
1176
|
+
if (hasTTS) staticParts.push("语音合成已启用,发送媒体格式:<qqmedia>路径</qqmedia>");
|
|
1065
1177
|
const staticInstruction = staticParts.join(" | ");
|
|
1066
1178
|
|
|
1067
1179
|
// 静态指引作为 systemPrompts 的首项注入
|
|
@@ -2348,44 +2460,21 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
2348
2460
|
});
|
|
2349
2461
|
onReady?.(d);
|
|
2350
2462
|
|
|
2351
|
-
//
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
await sendProactiveC2CMessage(token, user.openid, greeting);
|
|
2360
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to c2c:${user.openid}`);
|
|
2361
|
-
} catch (err) {
|
|
2362
|
-
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to c2c:${user.openid}: ${err}`);
|
|
2363
|
-
}
|
|
2364
|
-
// 避免频率限制
|
|
2365
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2366
|
-
}
|
|
2367
|
-
const groups = listKnownUsers({ accountId: account.accountId, type: "group" });
|
|
2368
|
-
// 群组去重(同一群只发一次)
|
|
2369
|
-
const sentGroups = new Set<string>();
|
|
2370
|
-
for (const user of groups) {
|
|
2371
|
-
const gid = user.groupOpenid;
|
|
2372
|
-
if (!gid || sentGroups.has(gid)) continue;
|
|
2373
|
-
sentGroups.add(gid);
|
|
2374
|
-
try {
|
|
2375
|
-
await sendProactiveGroupMessage(token, gid, greeting);
|
|
2376
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to group:${gid}`);
|
|
2377
|
-
} catch (err) {
|
|
2378
|
-
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to group:${gid}: ${err}`);
|
|
2379
|
-
}
|
|
2380
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2381
|
-
}
|
|
2382
|
-
log?.info(`[qqbot:${account.accountId}] Startup greetings sent (${users.length} c2c, ${sentGroups.size} groups)`);
|
|
2383
|
-
} catch (err) {
|
|
2384
|
-
log?.error(`[qqbot:${account.accountId}] Failed to send startup greetings: ${err}`);
|
|
2385
|
-
}
|
|
2386
|
-
})();
|
|
2463
|
+
// 仅 startGateway 后的首次 READY 才发送上线通知
|
|
2464
|
+
// ws 断线重连(resume 失败后重新 Identify)产生的 READY 不发送
|
|
2465
|
+
if (!isFirstReadyGlobal) {
|
|
2466
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (reconnect READY, not first startup)`);
|
|
2467
|
+
} else {
|
|
2468
|
+
isFirstReadyGlobal = false;
|
|
2469
|
+
sendStartupGreetings("READY");
|
|
2470
|
+
} // end isFirstReady
|
|
2387
2471
|
} else if (t === "RESUMED") {
|
|
2388
2472
|
log?.info(`[qqbot:${account.accountId}] Session resumed`);
|
|
2473
|
+
// RESUMED 也属于首次启动(gateway restart 通常走 resume)
|
|
2474
|
+
if (isFirstReadyGlobal) {
|
|
2475
|
+
isFirstReadyGlobal = false;
|
|
2476
|
+
sendStartupGreetings("RESUMED");
|
|
2477
|
+
}
|
|
2389
2478
|
// P1-2: 更新 Session 连接时间
|
|
2390
2479
|
if (sessionId) {
|
|
2391
2480
|
saveSession({
|