@ryantest/openclaw-qqbot 0.0.3 → 1.6.6-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/README.md +2 -15
- package/README.zh.md +3 -16
- package/dist/src/admin-resolver.d.ts +12 -6
- package/dist/src/admin-resolver.js +69 -34
- package/dist/src/api.d.ts +105 -1
- package/dist/src/api.js +164 -15
- package/dist/src/channel.js +13 -0
- package/dist/src/config.js +3 -10
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.js +450 -248
- package/dist/src/image-server.d.ts +27 -8
- package/dist/src/image-server.js +179 -71
- package/dist/src/inbound-attachments.d.ts +3 -1
- package/dist/src/inbound-attachments.js +28 -14
- package/dist/src/outbound-deliver.js +77 -148
- package/dist/src/outbound.d.ts +6 -4
- package/dist/src/outbound.js +266 -442
- package/dist/src/reply-dispatcher.js +4 -4
- package/dist/src/request-context.d.ts +18 -0
- package/dist/src/request-context.js +30 -0
- package/dist/src/slash-commands.js +277 -32
- package/dist/src/startup-greeting.d.ts +5 -5
- package/dist/src/startup-greeting.js +32 -13
- package/dist/src/streaming.d.ts +244 -0
- package/dist/src/streaming.js +907 -0
- package/dist/src/tools/remind.js +11 -10
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.js +17 -1
- package/dist/src/update-checker.js +2 -8
- package/dist/src/utils/audio-convert.d.ts +9 -0
- package/dist/src/utils/audio-convert.js +51 -0
- package/dist/src/utils/chunked-upload.d.ts +59 -0
- package/dist/src/utils/chunked-upload.js +289 -0
- package/dist/src/utils/file-utils.d.ts +7 -1
- package/dist/src/utils/file-utils.js +24 -2
- package/dist/src/utils/media-send.d.ts +147 -0
- package/dist/src/utils/media-send.js +434 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/node_modules/ws/index.js +15 -6
- package/node_modules/ws/lib/permessage-deflate.js +6 -6
- package/node_modules/ws/lib/websocket-server.js +5 -5
- package/node_modules/ws/lib/websocket.js +6 -6
- package/node_modules/ws/package.json +4 -3
- package/node_modules/ws/wrapper.mjs +14 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +11 -22
- package/scripts/postinstall-link-sdk.js +113 -0
- package/scripts/upgrade-via-npm.ps1 +161 -6
- package/scripts/upgrade-via-npm.sh +311 -104
- package/scripts/upgrade-via-source.sh +117 -0
- package/skills/qqbot-media/SKILL.md +9 -5
- package/skills/qqbot-remind/SKILL.md +3 -3
- package/src/admin-resolver.ts +76 -35
- package/src/api.ts +284 -12
- package/src/channel.ts +12 -0
- package/src/config.ts +3 -10
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +277 -67
- package/src/image-server.ts +213 -77
- package/src/inbound-attachments.ts +32 -15
- package/src/outbound-deliver.ts +77 -157
- package/src/outbound.ts +304 -451
- package/src/reply-dispatcher.ts +4 -4
- package/src/request-context.ts +39 -0
- package/src/slash-commands.ts +303 -33
- package/src/startup-greeting.ts +35 -13
- package/src/streaming.ts +1096 -0
- package/src/tools/remind.ts +15 -11
- package/src/types.ts +111 -0
- package/src/update-checker.ts +2 -7
- package/src/utils/audio-convert.ts +56 -0
- package/src/utils/chunked-upload.ts +419 -0
- package/src/utils/file-utils.ts +28 -2
- package/src/utils/media-send.ts +563 -0
- package/src/utils/pkg-version.ts +54 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/clawdbot.plugin.json +0 -16
- package/dist/src/user-messages.d.ts +0 -8
- package/dist/src/user-messages.js +0 -8
- package/moltbot.plugin.json +0 -16
- package/scripts/upgrade-via-alt-pkg.sh +0 -307
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +0 -46
- package/src/gateway.log +0 -43
- package/src/openclaw-2026-03-21.log +0 -3729
- package/src/user-messages.ts +0 -7
|
@@ -25,6 +25,7 @@ cd "$PROJ_DIR"
|
|
|
25
25
|
APPID=""
|
|
26
26
|
SECRET=""
|
|
27
27
|
MARKDOWN=""
|
|
28
|
+
STREAMING=""
|
|
28
29
|
|
|
29
30
|
while [[ $# -gt 0 ]]; do
|
|
30
31
|
case $1 in
|
|
@@ -40,6 +41,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
40
41
|
MARKDOWN="$2"
|
|
41
42
|
shift 2
|
|
42
43
|
;;
|
|
44
|
+
--streaming)
|
|
45
|
+
STREAMING="$2"
|
|
46
|
+
shift 2
|
|
47
|
+
;;
|
|
43
48
|
-h|--help)
|
|
44
49
|
echo "用法: $0 [选项]"
|
|
45
50
|
echo ""
|
|
@@ -47,6 +52,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
47
52
|
echo " --appid <appid> QQ机器人 appid"
|
|
48
53
|
echo " --secret <secret> QQ机器人 secret"
|
|
49
54
|
echo " --markdown <yes|no> 是否启用 markdown 消息格式(默认: no)"
|
|
55
|
+
echo " --streaming <yes|no> 是否启用流式消息(默认: no,仅 C2C 私聊)"
|
|
50
56
|
echo " -h, --help 显示帮助信息"
|
|
51
57
|
echo ""
|
|
52
58
|
echo "也可以通过环境变量设置:"
|
|
@@ -54,6 +60,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
54
60
|
echo " QQBOT_SECRET QQ机器人 secret"
|
|
55
61
|
echo " QQBOT_TOKEN QQ机器人 token (appid:secret)"
|
|
56
62
|
echo " QQBOT_MARKDOWN 是否启用 markdown(yes/no)"
|
|
63
|
+
echo " QQBOT_STREAMING 是否启用流式消息(yes/no)"
|
|
57
64
|
echo ""
|
|
58
65
|
echo "不带参数时,将使用已有配置直接启动。"
|
|
59
66
|
echo ""
|
|
@@ -72,6 +79,7 @@ done
|
|
|
72
79
|
APPID="${APPID:-$QQBOT_APPID}"
|
|
73
80
|
SECRET="${SECRET:-$QQBOT_SECRET}"
|
|
74
81
|
MARKDOWN="${MARKDOWN:-$QQBOT_MARKDOWN}"
|
|
82
|
+
STREAMING="${STREAMING:-$QQBOT_STREAMING}"
|
|
75
83
|
|
|
76
84
|
echo "========================================="
|
|
77
85
|
echo " qqbot 一键更新启动脚本"
|
|
@@ -131,6 +139,29 @@ if [ -z "$SAVED_QQBOT_TOKEN" ] && [ -d "$HOME/.openclaw" ]; then
|
|
|
131
139
|
fi
|
|
132
140
|
fi
|
|
133
141
|
|
|
142
|
+
# 备份 streaming 配置(升级后恢复)
|
|
143
|
+
SAVED_STREAMING=""
|
|
144
|
+
for _app in openclaw clawdbot moltbot; do
|
|
145
|
+
_cfg="$HOME/.$_app/$_app.json"
|
|
146
|
+
if [ -f "$_cfg" ]; then
|
|
147
|
+
SAVED_STREAMING=$(node -e "
|
|
148
|
+
const cfg = JSON.parse(require('fs').readFileSync('$_cfg', 'utf8'));
|
|
149
|
+
const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
|
|
150
|
+
for (const key of keys) {
|
|
151
|
+
const ch = cfg.channels && cfg.channels[key];
|
|
152
|
+
if (ch && typeof ch.streaming === 'boolean') {
|
|
153
|
+
process.stdout.write(String(ch.streaming));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
" 2>/dev/null || true)
|
|
158
|
+
[ -n "$SAVED_STREAMING" ] && break
|
|
159
|
+
fi
|
|
160
|
+
done
|
|
161
|
+
if [ -n "$SAVED_STREAMING" ]; then
|
|
162
|
+
echo "已备份 streaming 配置: $SAVED_STREAMING"
|
|
163
|
+
fi
|
|
164
|
+
|
|
134
165
|
# 2. 移除老版本
|
|
135
166
|
echo ""
|
|
136
167
|
echo "[2/6] 移除老版本..."
|
|
@@ -444,6 +475,27 @@ else
|
|
|
444
475
|
# gateway 已在安装前 stop,此时不会有自动 restart 的问题
|
|
445
476
|
# 所有配置写入完成后,在 Step 6 统一启动
|
|
446
477
|
|
|
478
|
+
# 确保 openclaw/plugin-sdk 可解析:
|
|
479
|
+
# openclaw plugins install 不会执行 npm lifecycle scripts,
|
|
480
|
+
# 需要手动调用 postinstall-link-sdk.js 创建 node_modules/openclaw → 全局 openclaw 的符号链接。
|
|
481
|
+
# 必须在 peerDeps 清理之后执行,否则 symlink 会被清理逻辑删除。
|
|
482
|
+
for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
|
|
483
|
+
_postinstall="$HOME/.openclaw/extensions/$_candidate_name/scripts/postinstall-link-sdk.js"
|
|
484
|
+
if [ -f "$_postinstall" ]; then
|
|
485
|
+
echo " 执行 postinstall-link-sdk..."
|
|
486
|
+
if node "$_postinstall" 2>&1; then
|
|
487
|
+
echo " ✅ plugin-sdk 链接就绪"
|
|
488
|
+
else
|
|
489
|
+
echo " ⚠️ postinstall-link-sdk 失败,插件可能无法加载"
|
|
490
|
+
fi
|
|
491
|
+
break
|
|
492
|
+
fi
|
|
493
|
+
done
|
|
494
|
+
|
|
495
|
+
# 清理 openclaw CLI install 留下的 backup 目录,
|
|
496
|
+
# 避免 gateway 发现两个同 id 插件不断刷 duplicate plugin id 警告
|
|
497
|
+
find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-qqbot-backup-*" -exec rm -rf {} + 2>/dev/null || true
|
|
498
|
+
|
|
447
499
|
# 记录更新后的 qqbot 插件版本
|
|
448
500
|
NEW_QQBOT_VERSION=$(node -e '
|
|
449
501
|
try {
|
|
@@ -525,6 +577,8 @@ if [ -n "$DESIRED_QQBOT_TOKEN" ]; then
|
|
|
525
577
|
# 由 channel 插件热重载处理,通常 <1 秒完成,无需长时间等待。
|
|
526
578
|
sleep 1
|
|
527
579
|
fi
|
|
580
|
+
|
|
581
|
+
|
|
528
582
|
else
|
|
529
583
|
# 未提供任何可用 token 时,检查是否已有可用配置
|
|
530
584
|
_has_channel=0
|
|
@@ -612,6 +666,69 @@ else
|
|
|
612
666
|
echo "未指定 markdown 选项,使用已有配置"
|
|
613
667
|
fi
|
|
614
668
|
|
|
669
|
+
# 5.5. 配置 streaming 选项
|
|
670
|
+
echo ""
|
|
671
|
+
echo "[5.5/6] 配置 streaming(流式消息)选项..."
|
|
672
|
+
|
|
673
|
+
# 确定目标 streaming 值:命令行参数 > 备份值
|
|
674
|
+
STREAMING_VALUE=""
|
|
675
|
+
if [ -n "$STREAMING" ]; then
|
|
676
|
+
if [ "$STREAMING" = "yes" ] || [ "$STREAMING" = "y" ] || [ "$STREAMING" = "true" ]; then
|
|
677
|
+
STREAMING_VALUE="true"
|
|
678
|
+
echo "启用流式消息..."
|
|
679
|
+
else
|
|
680
|
+
STREAMING_VALUE="false"
|
|
681
|
+
echo "禁用流式消息..."
|
|
682
|
+
fi
|
|
683
|
+
elif [ -n "$SAVED_STREAMING" ]; then
|
|
684
|
+
STREAMING_VALUE="$SAVED_STREAMING"
|
|
685
|
+
echo "从备份恢复 streaming 配置: $SAVED_STREAMING"
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
if [ -n "$STREAMING_VALUE" ]; then
|
|
689
|
+
CURRENT_STREAMING_VALUE=$(node -e "
|
|
690
|
+
const fs = require('fs');
|
|
691
|
+
const path = require('path');
|
|
692
|
+
const home = process.env.HOME;
|
|
693
|
+
for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
|
|
694
|
+
const f = path.join(home, '.' + app, app + '.json');
|
|
695
|
+
if (!fs.existsSync(f)) continue;
|
|
696
|
+
try {
|
|
697
|
+
const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
698
|
+
const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
|
|
699
|
+
for (const key of keys) {
|
|
700
|
+
const ch = cfg.channels && cfg.channels[key];
|
|
701
|
+
if (!ch) continue;
|
|
702
|
+
if (typeof ch.streaming === 'boolean') { process.stdout.write(String(ch.streaming)); process.exit(0); }
|
|
703
|
+
}
|
|
704
|
+
} catch {}
|
|
705
|
+
}
|
|
706
|
+
" 2>/dev/null || true)
|
|
707
|
+
|
|
708
|
+
if [ "$CURRENT_STREAMING_VALUE" = "$STREAMING_VALUE" ]; then
|
|
709
|
+
echo "✅ streaming 配置已是目标值,跳过写入"
|
|
710
|
+
else
|
|
711
|
+
OPENCLAW_CONFIG="$HOME/.openclaw/openclaw.json"
|
|
712
|
+
if [ -f "$OPENCLAW_CONFIG" ] && node -e "
|
|
713
|
+
const fs = require('fs');
|
|
714
|
+
const cfg = JSON.parse(fs.readFileSync('$OPENCLAW_CONFIG', 'utf-8'));
|
|
715
|
+
if (!cfg.channels) cfg.channels = {};
|
|
716
|
+
if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
|
|
717
|
+
const target = $STREAMING_VALUE;
|
|
718
|
+
if (cfg.channels.qqbot.streaming === target) process.exit(0);
|
|
719
|
+
cfg.channels.qqbot.streaming = target;
|
|
720
|
+
fs.writeFileSync('$OPENCLAW_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
|
|
721
|
+
" 2>&1; then
|
|
722
|
+
echo "✅ streaming 配置成功"
|
|
723
|
+
_config_changed=1
|
|
724
|
+
else
|
|
725
|
+
echo "⚠️ streaming 配置设置失败,不影响后续运行"
|
|
726
|
+
fi
|
|
727
|
+
fi
|
|
728
|
+
else
|
|
729
|
+
echo "未指定 streaming 选项且无备份值,使用默认配置"
|
|
730
|
+
fi
|
|
731
|
+
|
|
615
732
|
# 6. 启动 openclaw
|
|
616
733
|
echo ""
|
|
617
734
|
echo "[6/6] 启动 openclaw..."
|
|
@@ -28,11 +28,15 @@ metadata: {"openclaw":{"emoji":"📸","requires":{"config":["channels.qqbot"]}}}
|
|
|
28
28
|
|
|
29
29
|
1. **路径必须是绝对路径**(以 `/` 或 `http` 开头)
|
|
30
30
|
2. **标签必须用开闭标签包裹路径**:`<qqmedia>路径</qqmedia>`
|
|
31
|
-
3.
|
|
32
|
-
4.
|
|
33
|
-
5.
|
|
34
|
-
6.
|
|
35
|
-
7.
|
|
31
|
+
3. **你有能力发送本地图片/文件**——直接用标签包裹路径即可,**不要说"无法发送"**
|
|
32
|
+
4. 发送语音时不要重复语音中已朗读的文字
|
|
33
|
+
5. 多个媒体用多个标签
|
|
34
|
+
6. 以会话上下文中的能力说明为准(如未启用语音则不要发语音)
|
|
35
|
+
7. **发送前需检查文件大小**,当文件超限时告知用户文件太大,QQBot 发送文件大小规则如下:
|
|
36
|
+
- 图片:最大 **30MB**
|
|
37
|
+
- 语音:最大 **20MB**
|
|
38
|
+
- 视频:最大 **200MB**
|
|
39
|
+
- 文件:最大 **200MB**
|
|
36
40
|
|
|
37
41
|
## 示例
|
|
38
42
|
|
|
@@ -22,7 +22,7 @@ metadata: {"openclaw":{"emoji":"⏰","requires":{"config":["channels.qqbot"]}}}
|
|
|
22
22
|
|------|------|------|
|
|
23
23
|
| `action` | 操作类型 | `"add"` / `"list"` / `"remove"` |
|
|
24
24
|
| `content` | 提醒内容 | `"喝水"` |
|
|
25
|
-
| `to` |
|
|
25
|
+
| `to` | 目标地址(可选,系统自动获取,通常无需填写) | — |
|
|
26
26
|
| `time` | 时间(相对时间或 cron 表达式) | `"5m"` / `"1h30m"` / `"0 8 * * *"` |
|
|
27
27
|
| `jobId` | 任务 ID(仅 remove) | `"xxx"` |
|
|
28
28
|
|
|
@@ -34,7 +34,7 @@ metadata: {"openclaw":{"emoji":"⏰","requires":{"config":["channels.qqbot"]}}}
|
|
|
34
34
|
|
|
35
35
|
用户说:"5分钟后提醒我喝水"
|
|
36
36
|
|
|
37
|
-
1. 调用 `qqbot_remind`:`{ "action": "add", "content": "喝水", "
|
|
37
|
+
1. 调用 `qqbot_remind`:`{ "action": "add", "content": "喝水", "time": "5m" }`
|
|
38
38
|
2. 收到返回的 `cronParams` → 立即调用 `cron` 工具,参数为该 `cronParams`
|
|
39
39
|
3. 回复用户:`⏰ 好的,5分钟后提醒你喝水~`
|
|
40
40
|
|
|
@@ -56,7 +56,7 @@ metadata: {"openclaw":{"emoji":"⏰","requires":{"config":["channels.qqbot"]}}}
|
|
|
56
56
|
| `payload.kind` | `"agentTurn"` | `systemEvent` 不会发 QQ 消息 |
|
|
57
57
|
| `payload.deliver` | `true` | 否则不投递 |
|
|
58
58
|
| `payload.channel` | `"qqbot"` | QQ 通道标识 |
|
|
59
|
-
| `payload.to` | 用户 openid |
|
|
59
|
+
| `payload.to` | 用户 openid | 从 `To` 字段获取 |
|
|
60
60
|
| `sessionTarget` | `"isolated"` | 隔离会话避免污染 |
|
|
61
61
|
|
|
62
62
|
> `schedule.atMs` 必须是**绝对毫秒时间戳**(如 `1770733800000`),不支持 `"5m"` 等相对字符串。
|
package/src/admin-resolver.ts
CHANGED
|
@@ -26,48 +26,90 @@ export interface AdminResolverContext {
|
|
|
26
26
|
|
|
27
27
|
// ---- 文件路径 ----
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
function safeName(id: string): string {
|
|
30
|
+
return id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 新版 admin 文件路径(按 accountId + appId 区分) */
|
|
34
|
+
function getAdminMarkerFile(accountId: string, appId: string): string {
|
|
35
|
+
return path.join(getQQBotDataDir("data"), `admin-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 旧版 admin 文件路径(仅按 accountId 区分,用于迁移兼容) */
|
|
39
|
+
function getLegacyAdminMarkerFile(accountId: string): string {
|
|
30
40
|
return path.join(getQQBotDataDir("data"), `admin-${accountId}.json`);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
function getUpgradeGreetingTargetFile(accountId: string, appId: string): string {
|
|
34
|
-
|
|
35
|
-
const safeAppId = appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
36
|
-
return path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
|
|
44
|
+
return path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
// ---- 管理员 openid 持久化 ----
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 读取 admin openid(按 accountId + appId 区分)
|
|
51
|
+
* 兼容策略:新路径优先 → fallback 旧路径 → 自动迁移
|
|
52
|
+
*/
|
|
53
|
+
export function loadAdminOpenId(accountId: string, appId: string): string | undefined {
|
|
42
54
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
// 1. 先尝试新版路径
|
|
56
|
+
const newFile = getAdminMarkerFile(accountId, appId);
|
|
57
|
+
if (fs.existsSync(newFile)) {
|
|
58
|
+
const data = JSON.parse(fs.readFileSync(newFile, "utf8"));
|
|
46
59
|
if (data.openid) return data.openid;
|
|
47
60
|
}
|
|
61
|
+
|
|
62
|
+
// 2. fallback 旧版路径(仅按 accountId)
|
|
63
|
+
const legacyFile = getLegacyAdminMarkerFile(accountId);
|
|
64
|
+
if (fs.existsSync(legacyFile)) {
|
|
65
|
+
const data = JSON.parse(fs.readFileSync(legacyFile, "utf8"));
|
|
66
|
+
if (data.openid) {
|
|
67
|
+
// 自动迁移:写到新路径,删除旧文件
|
|
68
|
+
saveAdminOpenId(accountId, appId, data.openid);
|
|
69
|
+
try { fs.unlinkSync(legacyFile); } catch { /* ignore */ }
|
|
70
|
+
return data.openid;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
48
73
|
} catch { /* 文件损坏视为无 */ }
|
|
49
74
|
return undefined;
|
|
50
75
|
}
|
|
51
76
|
|
|
52
|
-
export function saveAdminOpenId(accountId: string, openid: string): void {
|
|
77
|
+
export function saveAdminOpenId(accountId: string, appId: string, openid: string): void {
|
|
53
78
|
try {
|
|
54
|
-
fs.writeFileSync(
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
getAdminMarkerFile(accountId, appId),
|
|
81
|
+
JSON.stringify({ accountId, appId, openid, savedAt: new Date().toISOString() }),
|
|
82
|
+
);
|
|
55
83
|
} catch { /* ignore */ }
|
|
56
84
|
}
|
|
57
85
|
|
|
58
86
|
// ---- 升级问候目标 ----
|
|
59
87
|
|
|
60
|
-
export function loadUpgradeGreetingTargetOpenId(accountId: string, appId: string): string | undefined {
|
|
88
|
+
export function loadUpgradeGreetingTargetOpenId(accountId: string, appId: string, log?: { info: (msg: string) => void }): string | undefined {
|
|
61
89
|
try {
|
|
62
90
|
const file = getUpgradeGreetingTargetFile(accountId, appId);
|
|
63
91
|
if (fs.existsSync(file)) {
|
|
64
92
|
const data = JSON.parse(fs.readFileSync(file, "utf8")) as { accountId?: string; appId?: string; openid?: string };
|
|
65
|
-
if (!data.openid)
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
if (!data.openid) {
|
|
94
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file found but openid is empty`);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (data.appId && data.appId !== appId) {
|
|
98
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target appId mismatch: file=${data.appId}, current=${appId}`);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (data.accountId && data.accountId !== accountId) {
|
|
102
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target accountId mismatch: file=${data.accountId}, current=${accountId}`);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target loaded: openid=${data.openid}`);
|
|
68
106
|
return data.openid;
|
|
107
|
+
} else {
|
|
108
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file not found: ${file}`);
|
|
69
109
|
}
|
|
70
|
-
} catch {
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file read error: ${err}`);
|
|
112
|
+
}
|
|
71
113
|
return undefined;
|
|
72
114
|
}
|
|
73
115
|
|
|
@@ -84,15 +126,15 @@ export function clearUpgradeGreetingTargetOpenId(accountId: string, appId: strin
|
|
|
84
126
|
|
|
85
127
|
/**
|
|
86
128
|
* 解析管理员 openid:
|
|
87
|
-
* 1.
|
|
129
|
+
* 1. 优先读持久化文件(按 accountId + appId 区分)
|
|
88
130
|
* 2. fallback 取第一个私聊用户,并写入文件锁定
|
|
89
131
|
*/
|
|
90
|
-
export function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" | "log">): string | undefined {
|
|
91
|
-
const saved = loadAdminOpenId(ctx.accountId);
|
|
132
|
+
export function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" | "appId" | "log">): string | undefined {
|
|
133
|
+
const saved = loadAdminOpenId(ctx.accountId, ctx.appId);
|
|
92
134
|
if (saved) return saved;
|
|
93
135
|
const first = listKnownUsers({ accountId: ctx.accountId, type: "c2c", sortBy: "firstSeenAt", sortOrder: "asc", limit: 1 })[0]?.openid;
|
|
94
136
|
if (first) {
|
|
95
|
-
saveAdminOpenId(ctx.accountId, first);
|
|
137
|
+
saveAdminOpenId(ctx.accountId, ctx.appId, first);
|
|
96
138
|
ctx.log?.info(`[qqbot:${ctx.accountId}] Auto-detected admin openid: ${first} (persisted)`);
|
|
97
139
|
}
|
|
98
140
|
return first;
|
|
@@ -100,40 +142,39 @@ export function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" |
|
|
|
100
142
|
|
|
101
143
|
// ---- 启动问候语 ----
|
|
102
144
|
|
|
103
|
-
/**
|
|
145
|
+
/** 异步发送启动问候语(优先发给升级触发者,fallback 发给管理员) */
|
|
104
146
|
export function sendStartupGreetings(ctx: AdminResolverContext, trigger: "READY" | "RESUMED"): void {
|
|
105
147
|
(async () => {
|
|
106
|
-
const plan = getStartupGreetingPlan();
|
|
148
|
+
const plan = getStartupGreetingPlan(ctx.accountId, ctx.appId);
|
|
107
149
|
if (!plan.shouldSend || !plan.greeting) {
|
|
108
150
|
ctx.log?.info(`[qqbot:${ctx.accountId}] Skipping startup greeting (${plan.reason ?? "debounced"}, trigger=${trigger})`);
|
|
109
151
|
return;
|
|
110
152
|
}
|
|
111
153
|
|
|
112
|
-
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
154
|
+
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId, ctx.log);
|
|
155
|
+
|
|
156
|
+
// 没有 upgrade-greeting-target 文件 → 不是通过 /bot-upgrade 触发的升级
|
|
157
|
+
// (console 手动重启、脚本升级等场景),静默更新 marker 不发消息
|
|
158
|
+
if (!upgradeTargetOpenId) {
|
|
159
|
+
markStartupGreetingSent(ctx.accountId, ctx.appId, plan.version);
|
|
160
|
+
ctx.log?.info(`[qqbot:${ctx.accountId}] Version changed but no upgrade-greeting-target, silently updating marker (trigger=${trigger})`);
|
|
117
161
|
return;
|
|
118
162
|
}
|
|
119
163
|
|
|
120
164
|
try {
|
|
121
|
-
|
|
122
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Sending startup greeting to ${receiverType} (trigger=${trigger}): "${plan.greeting}"`);
|
|
165
|
+
ctx.log?.info(`[qqbot:${ctx.accountId}] Sending startup greeting to upgrade-requester (trigger=${trigger}): "${plan.greeting}"`);
|
|
123
166
|
const token = await getAccessToken(ctx.appId, ctx.clientSecret);
|
|
124
167
|
const GREETING_TIMEOUT_MS = 10_000;
|
|
125
168
|
await Promise.race([
|
|
126
|
-
sendProactiveC2CMessage(token,
|
|
169
|
+
sendProactiveC2CMessage(token, upgradeTargetOpenId, plan.greeting),
|
|
127
170
|
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
128
171
|
]);
|
|
129
|
-
markStartupGreetingSent(plan.version);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Sent startup greeting to ${receiverType}: ${targetOpenId}`);
|
|
172
|
+
markStartupGreetingSent(ctx.accountId, ctx.appId, plan.version);
|
|
173
|
+
clearUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
|
|
174
|
+
ctx.log?.info(`[qqbot:${ctx.accountId}] Sent startup greeting to upgrade-requester: ${upgradeTargetOpenId}`);
|
|
134
175
|
} catch (err) {
|
|
135
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
136
|
-
markStartupGreetingFailed(plan.version, message);
|
|
177
|
+
markStartupGreetingFailed(ctx.accountId, ctx.appId, plan.version, message);
|
|
137
178
|
ctx.log?.error(`[qqbot:${ctx.accountId}] Failed to send startup greeting: ${message}`);
|
|
138
179
|
}
|
|
139
180
|
})();
|