@tencent-connect/openclaw-qqbot 1.7.2 → 2.0.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 +25 -8
- package/README.zh.md +24 -8
- package/bin/qqbot-cli.js +5 -2
- package/dist/index.cjs +15549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +890 -0
- package/index.ts +56 -19
- package/openclaw.plugin.json +19 -3
- package/package.json +14 -23
- package/preload.cjs +17 -14
- package/scripts/link-sdk-core.cjs +133 -50
- package/scripts/upgrade-via-npm.sh +42 -11
- package/scripts/upgrade-via-source.sh +4 -4
- package/skills/qqbot-channel/SKILL.md +34 -12
- package/src/adapter/contract.ts +69 -0
- package/src/adapter/gateway.ts +61 -0
- package/src/adapter/index.ts +15 -0
- package/src/adapter/lint.ts +144 -0
- package/src/adapter/media.ts +112 -0
- package/src/adapter/pairing.ts +99 -0
- package/src/adapter/resolve.ts +333 -0
- package/src/adapter/setup.ts +55 -0
- package/src/adapter/webhook.ts +248 -0
- package/src/adapter/workspace.ts +29 -0
- package/src/bot-instance.ts +60 -0
- package/src/channel.ts +202 -413
- package/src/commands/bot-approve.ts +143 -0
- package/src/commands/bot-clear-storage.ts +114 -0
- package/src/commands/bot-group-always.ts +62 -0
- package/src/commands/bot-help.ts +40 -0
- package/src/commands/bot-logs.ts +248 -0
- package/src/commands/bot-me.ts +18 -0
- package/src/commands/bot-pairing.ts +57 -0
- package/src/commands/bot-ping.ts +33 -0
- package/src/commands/bot-streaming.ts +55 -0
- package/src/commands/bot-upgrade.ts +56 -0
- package/src/commands/bot-version.ts +41 -0
- package/src/commands/config-util.ts +96 -0
- package/src/commands/index.ts +49 -0
- package/src/config.ts +46 -19
- package/src/dispatch/body-assembler.ts +356 -0
- package/src/dispatch/ctx-builder.ts +110 -0
- package/src/dispatch/dispatch.ts +352 -0
- package/src/dispatch/envelope-builder.ts +112 -0
- package/src/dispatch/index.ts +2 -0
- package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
- package/src/features/approval-utils.ts +41 -0
- package/src/features/credential-backup.ts +82 -0
- package/src/features/history-store.ts +19 -0
- package/src/features/msgid-cache.ts +55 -0
- package/src/features/onboarding.ts +38 -0
- package/src/{proactive.ts → features/proactive.ts} +68 -49
- package/src/features/ref-index-store.ts +282 -0
- package/src/{update-checker.ts → features/update-checker.ts} +15 -35
- package/src/gateway/event-handlers.ts +241 -0
- package/src/gateway/index.ts +3 -0
- package/src/gateway/lifecycle.ts +221 -0
- package/src/gateway/middleware-setup.ts +139 -0
- package/src/gateway/qqbot-gateway.ts +342 -0
- package/src/middleware/access-control.ts +130 -0
- package/src/middleware/attachment.ts +314 -0
- package/src/middleware/policy-injector.ts +66 -0
- package/src/openclaw-plugin-sdk.d.ts +81 -10
- package/src/outbound/cron-scheduler.ts +130 -0
- package/src/outbound/debounce.ts +102 -0
- package/src/outbound/deliver-pipeline.ts +235 -0
- package/src/outbound/image-size.ts +123 -0
- package/src/outbound/index.ts +3 -0
- package/src/outbound/local-file-router.ts +145 -0
- package/src/outbound/media-send.ts +377 -0
- package/src/outbound/outbound-service.ts +222 -0
- package/src/outbound/reply-limiter.ts +128 -0
- package/src/outbound/sanitize.ts +32 -0
- package/src/outbound/streaming-controller.ts +228 -0
- package/src/outbound/target.ts +63 -0
- package/src/outbound/tts-provider.ts +84 -0
- package/src/request-context.ts +9 -8
- package/src/runtime.ts +31 -6
- package/src/setup/account-key.ts +41 -0
- package/src/setup/finalize.ts +110 -0
- package/src/setup/login.ts +197 -0
- package/src/setup/surface.ts +40 -0
- package/src/tools/platform.ts +149 -0
- package/src/tools/remind.ts +2 -0
- package/src/types-augment.d.ts +54 -0
- package/src/types.ts +51 -9
- package/src/utils/mention.ts +52 -0
- package/src/utils/pkg-version.ts +84 -47
- package/src/utils/platform.ts +31 -7
- package/src/utils/plugin-logger.ts +104 -0
- package/src/utils/ssrf-guard.ts +32 -2
- package/src/utils/stt.ts +137 -0
- package/src/utils/voice-text.ts +61 -0
- package/tsconfig.json +2 -1
- package/tsup.config.ts +57 -0
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -27
- package/dist/src/admin-resolver.d.ts +0 -33
- package/dist/src/admin-resolver.js +0 -157
- package/dist/src/api.d.ts +0 -309
- package/dist/src/api.js +0 -923
- package/dist/src/approval-handler.d.ts +0 -47
- package/dist/src/approval-handler.js +0 -372
- package/dist/src/channel.d.ts +0 -29
- package/dist/src/channel.js +0 -528
- package/dist/src/config.d.ts +0 -60
- package/dist/src/config.js +0 -288
- package/dist/src/credential-backup.d.ts +0 -31
- package/dist/src/credential-backup.js +0 -66
- package/dist/src/deliver-debounce.d.ts +0 -74
- package/dist/src/deliver-debounce.js +0 -174
- package/dist/src/gateway.d.ts +0 -18
- package/dist/src/gateway.js +0 -2025
- package/dist/src/group-history.d.ts +0 -136
- package/dist/src/group-history.js +0 -226
- package/dist/src/image-server.d.ts +0 -87
- package/dist/src/image-server.js +0 -570
- package/dist/src/inbound-attachments.d.ts +0 -60
- package/dist/src/inbound-attachments.js +0 -248
- package/dist/src/known-users.d.ts +0 -100
- package/dist/src/known-users.js +0 -263
- package/dist/src/message-gating.d.ts +0 -53
- package/dist/src/message-gating.js +0 -107
- package/dist/src/message-queue.d.ts +0 -91
- package/dist/src/message-queue.js +0 -257
- package/dist/src/onboarding.d.ts +0 -10
- package/dist/src/onboarding.js +0 -203
- package/dist/src/outbound-deliver.d.ts +0 -48
- package/dist/src/outbound-deliver.js +0 -392
- package/dist/src/outbound.d.ts +0 -220
- package/dist/src/outbound.js +0 -975
- package/dist/src/proactive.d.ts +0 -170
- package/dist/src/proactive.js +0 -399
- package/dist/src/ref-index-store.d.ts +0 -101
- package/dist/src/ref-index-store.js +0 -298
- package/dist/src/reply-dispatcher.d.ts +0 -35
- package/dist/src/reply-dispatcher.js +0 -311
- package/dist/src/request-context.d.ts +0 -25
- package/dist/src/request-context.js +0 -37
- package/dist/src/runtime.d.ts +0 -3
- package/dist/src/runtime.js +0 -13
- 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 -82
- package/dist/src/slash-commands.js +0 -2233
- package/dist/src/startup-greeting.d.ts +0 -30
- package/dist/src/startup-greeting.js +0 -97
- package/dist/src/streaming.d.ts +0 -247
- package/dist/src/streaming.js +0 -899
- package/dist/src/stt.d.ts +0 -21
- package/dist/src/stt.js +0 -70
- package/dist/src/tools/channel.d.ts +0 -16
- package/dist/src/tools/channel.js +0 -231
- package/dist/src/tools/remind.d.ts +0 -2
- package/dist/src/tools/remind.js +0 -255
- package/dist/src/transport/index.d.ts +0 -10
- package/dist/src/transport/index.js +0 -9
- package/dist/src/transport/webhook-transport.d.ts +0 -67
- package/dist/src/transport/webhook-transport.js +0 -245
- package/dist/src/transport/webhook-verify.d.ts +0 -48
- package/dist/src/transport/webhook-verify.js +0 -98
- package/dist/src/types.d.ts +0 -462
- package/dist/src/types.js +0 -22
- package/dist/src/typing-keepalive.d.ts +0 -27
- package/dist/src/typing-keepalive.js +0 -64
- package/dist/src/update-checker.d.ts +0 -36
- package/dist/src/update-checker.js +0 -171
- package/dist/src/utils/audio-convert.d.ts +0 -98
- package/dist/src/utils/audio-convert.js +0 -783
- package/dist/src/utils/chunked-upload.d.ts +0 -68
- package/dist/src/utils/chunked-upload.js +0 -341
- package/dist/src/utils/file-utils.d.ts +0 -61
- package/dist/src/utils/file-utils.js +0 -172
- package/dist/src/utils/image-size.d.ts +0 -51
- package/dist/src/utils/image-size.js +0 -234
- package/dist/src/utils/media-send.d.ts +0 -158
- package/dist/src/utils/media-send.js +0 -502
- package/dist/src/utils/media-tags.d.ts +0 -14
- package/dist/src/utils/media-tags.js +0 -165
- package/dist/src/utils/payload.d.ts +0 -112
- package/dist/src/utils/payload.js +0 -186
- package/dist/src/utils/pkg-version.d.ts +0 -5
- package/dist/src/utils/pkg-version.js +0 -61
- package/dist/src/utils/platform.d.ts +0 -137
- package/dist/src/utils/platform.js +0 -390
- package/dist/src/utils/ssrf-guard.d.ts +0 -25
- package/dist/src/utils/ssrf-guard.js +0 -91
- package/dist/src/utils/text-parsing.d.ts +0 -31
- package/dist/src/utils/text-parsing.js +0 -74
- package/dist/src/utils/upload-cache.d.ts +0 -34
- package/dist/src/utils/upload-cache.js +0 -93
- package/node_modules/@eshaz/web-worker/LICENSE +0 -201
- package/node_modules/@eshaz/web-worker/README.md +0 -134
- package/node_modules/@eshaz/web-worker/browser.js +0 -17
- package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
- package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
- package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
- package/node_modules/@eshaz/web-worker/node.js +0 -223
- package/node_modules/@eshaz/web-worker/package.json +0 -54
- package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
- package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
- package/node_modules/mpg123-decoder/README.md +0 -265
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
- package/node_modules/mpg123-decoder/index.js +0 -8
- package/node_modules/mpg123-decoder/package.json +0 -58
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
- package/node_modules/mpg123-decoder/types.d.ts +0 -30
- package/node_modules/silk-wasm/LICENSE +0 -21
- package/node_modules/silk-wasm/README.md +0 -85
- package/node_modules/silk-wasm/lib/index.cjs +0 -16
- package/node_modules/silk-wasm/lib/index.d.ts +0 -70
- package/node_modules/silk-wasm/lib/index.mjs +0 -16
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
- package/node_modules/silk-wasm/package.json +0 -39
- package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
- package/node_modules/simple-yenc/.prettierignore +0 -1
- package/node_modules/simple-yenc/LICENSE +0 -7
- package/node_modules/simple-yenc/README.md +0 -163
- package/node_modules/simple-yenc/dist/esm.js +0 -1
- package/node_modules/simple-yenc/dist/index.js +0 -1
- package/node_modules/simple-yenc/package.json +0 -50
- package/node_modules/simple-yenc/rollup.config.js +0 -27
- package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -13
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -528
- package/node_modules/ws/lib/receiver.js +0 -706
- package/node_modules/ws/lib/sender.js +0 -602
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -554
- package/node_modules/ws/lib/websocket.js +0 -1393
- package/node_modules/ws/package.json +0 -69
- package/node_modules/ws/wrapper.mjs +0 -8
- package/scripts/postinstall-link-sdk.js +0 -170
- package/skills/qqbot-media/SKILL.md +0 -60
- package/src/admin-resolver.ts +0 -181
- package/src/api.ts +0 -1333
- package/src/credential-backup.ts +0 -72
- package/src/deliver-debounce.ts +0 -229
- package/src/gateway.ts +0 -2272
- package/src/group-history.ts +0 -328
- package/src/image-server.ts +0 -675
- package/src/inbound-attachments.ts +0 -321
- package/src/known-users.ts +0 -353
- package/src/message-gating.ts +0 -190
- package/src/message-queue.ts +0 -354
- package/src/onboarding.ts +0 -282
- package/src/outbound-deliver.ts +0 -486
- package/src/outbound.ts +0 -1179
- package/src/ref-index-store.ts +0 -412
- package/src/reply-dispatcher.ts +0 -334
- package/src/session-store.ts +0 -303
- package/src/slash-commands.ts +0 -2431
- package/src/startup-greeting.ts +0 -120
- package/src/streaming.ts +0 -1077
- package/src/stt.ts +0 -86
- package/src/tools/channel.ts +0 -275
- package/src/transport/index.ts +0 -11
- package/src/transport/webhook-transport.ts +0 -332
- package/src/transport/webhook-verify.ts +0 -119
- package/src/typing-keepalive.ts +0 -59
- package/src/utils/audio-convert.ts +0 -887
- package/src/utils/chunked-upload.ts +0 -483
- package/src/utils/file-utils.ts +0 -193
- package/src/utils/image-size.ts +0 -266
- package/src/utils/media-send.ts +0 -636
- package/src/utils/media-tags.ts +0 -183
- package/src/utils/payload.ts +0 -265
- package/src/utils/text-parsing.ts +0 -85
- package/src/utils/upload-cache.ts +0 -128
package/src/utils/ssrf-guard.ts
CHANGED
|
@@ -48,13 +48,40 @@ export function isReservedAddr(ip: string): boolean {
|
|
|
48
48
|
|
|
49
49
|
const ALLOWED_SCHEMES = new Set(["http:", "https:"]);
|
|
50
50
|
|
|
51
|
+
/** QQ 官方媒体/API 域名白名单,匹配时跳过 DNS 解析检查 */
|
|
52
|
+
const QQ_TRUSTED_DOMAINS = new Set([
|
|
53
|
+
// QQ Bot API
|
|
54
|
+
"api.sgroup.qq.com",
|
|
55
|
+
"sandbox.api.sgroup.qq.com",
|
|
56
|
+
// QQ Bot Token
|
|
57
|
+
"bots.qq.com",
|
|
58
|
+
// QQ 多媒体上传/下载
|
|
59
|
+
"multimedia.nt.qq.com.cn",
|
|
60
|
+
"multimedia.nt.qq.com",
|
|
61
|
+
// QQ 群文件
|
|
62
|
+
"grouppro.grouppro.qq.com",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** 检查 hostname 是否匹配 QQ 白名单(支持子域名通配 *.example.com) */
|
|
66
|
+
function isQQTrustedDomain(hostname: string): boolean {
|
|
67
|
+
if (QQ_TRUSTED_DOMAINS.has(hostname)) return true;
|
|
68
|
+
// 检查一级子域名:*.multimedia.nt.qq.com.cn
|
|
69
|
+
const dot = hostname.indexOf('.');
|
|
70
|
+
if (dot > 0) {
|
|
71
|
+
const parent = hostname.slice(dot + 1);
|
|
72
|
+
return QQ_TRUSTED_DOMAINS.has(parent);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/**
|
|
52
78
|
* 校验远程 URL 是否可安全请求。
|
|
53
79
|
*
|
|
54
80
|
* 规则:
|
|
55
81
|
* 1. 仅放行 http / https 协议
|
|
56
|
-
* 2.
|
|
57
|
-
* 3.
|
|
82
|
+
* 2. QQ 白名单域名直接放行(免 DNS 解析)
|
|
83
|
+
* 3. 若 URL 直接携带 IP 则即时判定
|
|
84
|
+
* 4. 若为域名则先做 DNS 解析,逐条检查解析结果
|
|
58
85
|
*
|
|
59
86
|
* @throws {Error} 当 URL 指向受限地址时
|
|
60
87
|
*/
|
|
@@ -70,6 +97,9 @@ export async function validateRemoteUrl(raw: string): Promise<void> {
|
|
|
70
97
|
// 去掉 IPv6 方括号
|
|
71
98
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
72
99
|
|
|
100
|
+
// QQ 官方域名 → 免 DNS 检查直接放行
|
|
101
|
+
if (isQQTrustedDomain(host)) return;
|
|
102
|
+
|
|
73
103
|
if (net.isIP(host)) {
|
|
74
104
|
assertPublicAddr(host, raw);
|
|
75
105
|
return;
|
package/src/utils/stt.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STT (Speech-to-Text) 语音转文字服务
|
|
3
|
+
*
|
|
4
|
+
* 支持 OpenAI 兼容的 /audio/transcriptions 接口。
|
|
5
|
+
* 配置优先级:
|
|
6
|
+
* 1. channels.qqbot.stt(插件级)
|
|
7
|
+
* 2. 框架级 audio model 配置
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
|
|
12
|
+
export interface STTConfig {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey: string;
|
|
16
|
+
model: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 从 OpenClaw 配置中解析 STT 设置
|
|
21
|
+
*/
|
|
22
|
+
export function resolveSTTConfig(cfg: Record<string, unknown>): STTConfig | null {
|
|
23
|
+
const channels = asRecord(cfg.channels);
|
|
24
|
+
const qqbot = asRecord(channels?.qqbot);
|
|
25
|
+
const sttCfg = asRecord(qqbot?.stt);
|
|
26
|
+
|
|
27
|
+
// 显式禁用
|
|
28
|
+
if (sttCfg?.enabled === false) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const models = asRecord(cfg.models);
|
|
33
|
+
const providers = asRecord(models?.providers);
|
|
34
|
+
|
|
35
|
+
// 1. 插件级 STT 配置
|
|
36
|
+
if (sttCfg) {
|
|
37
|
+
const providerId = readString(sttCfg, 'provider') ?? 'openai';
|
|
38
|
+
const providerCfg = asRecord(providers?.[providerId]);
|
|
39
|
+
const baseUrl = readString(sttCfg, 'baseUrl') ?? readString(providerCfg, 'baseUrl');
|
|
40
|
+
const apiKey = readString(sttCfg, 'apiKey') ?? readString(providerCfg, 'apiKey');
|
|
41
|
+
const model = readString(sttCfg, 'model') ?? 'whisper-1';
|
|
42
|
+
if (baseUrl && apiKey) {
|
|
43
|
+
return { enabled: true, baseUrl: baseUrl.replace(/\/+$/, ''), apiKey, model };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. 框架级 audio model fallback
|
|
48
|
+
const tools = asRecord(cfg.tools);
|
|
49
|
+
const media = asRecord(tools?.media);
|
|
50
|
+
const audio = asRecord(media?.audio);
|
|
51
|
+
const audioModels = audio?.models;
|
|
52
|
+
const audioModelEntry = Array.isArray(audioModels) ? asRecord(audioModels[0]) : undefined;
|
|
53
|
+
if (audioModelEntry) {
|
|
54
|
+
const providerId = readString(audioModelEntry, 'provider') ?? 'openai';
|
|
55
|
+
const providerCfg = asRecord(providers?.[providerId]);
|
|
56
|
+
const baseUrl = readString(audioModelEntry, 'baseUrl') ?? readString(providerCfg, 'baseUrl');
|
|
57
|
+
const apiKey = readString(audioModelEntry, 'apiKey') ?? readString(providerCfg, 'apiKey');
|
|
58
|
+
const model = readString(audioModelEntry, 'model') ?? 'whisper-1';
|
|
59
|
+
if (baseUrl && apiKey) {
|
|
60
|
+
return { enabled: true, baseUrl: baseUrl.replace(/\/+$/, ''), apiKey, model };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 调用 STT 服务转录音频文件
|
|
69
|
+
*/
|
|
70
|
+
export async function transcribeAudio(
|
|
71
|
+
audioPath: string,
|
|
72
|
+
cfg: Record<string, unknown>,
|
|
73
|
+
): Promise<string | null> {
|
|
74
|
+
const sttCfg = resolveSTTConfig(cfg);
|
|
75
|
+
if (!sttCfg) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fileBuffer = fs.readFileSync(audioPath);
|
|
80
|
+
const fileName = sanitizeFileName(path.basename(audioPath));
|
|
81
|
+
const mime = guessMimeType(fileName);
|
|
82
|
+
|
|
83
|
+
const form = new FormData();
|
|
84
|
+
form.append('file', new Blob([fileBuffer], { type: mime }), fileName);
|
|
85
|
+
form.append('model', sttCfg.model);
|
|
86
|
+
|
|
87
|
+
const resp = await fetch(`${sttCfg.baseUrl}/audio/transcriptions`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
|
|
90
|
+
body: form,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!resp.ok) {
|
|
94
|
+
const detail = await resp.text().catch(() => '');
|
|
95
|
+
throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = (await resp.json()) as { text?: string };
|
|
99
|
+
return result.text?.trim() || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── 内部工具函数 ──
|
|
103
|
+
|
|
104
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
105
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
106
|
+
return value as Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readString(obj: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
112
|
+
const val = obj?.[key];
|
|
113
|
+
if (typeof val === 'string' && val.trim()) {
|
|
114
|
+
return val.trim();
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sanitizeFileName(name: string): string {
|
|
120
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function guessMimeType(fileName: string): string {
|
|
124
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
125
|
+
const mimeMap: Record<string, string> = {
|
|
126
|
+
'.wav': 'audio/wav',
|
|
127
|
+
'.mp3': 'audio/mpeg',
|
|
128
|
+
'.ogg': 'audio/ogg',
|
|
129
|
+
'.flac': 'audio/flac',
|
|
130
|
+
'.m4a': 'audio/mp4',
|
|
131
|
+
'.aac': 'audio/aac',
|
|
132
|
+
'.silk': 'audio/silk',
|
|
133
|
+
'.amr': 'audio/amr',
|
|
134
|
+
'.pcm': 'audio/pcm',
|
|
135
|
+
};
|
|
136
|
+
return mimeMap[ext] ?? 'application/octet-stream';
|
|
137
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语音转录文本格式化工具
|
|
3
|
+
*
|
|
4
|
+
* 将 STT / ASR 转录结果格式化为用户可读文本,注入到 AI 的入站消息中。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 转录来源 */
|
|
8
|
+
export type TranscriptSource = 'stt' | 'asr' | 'fallback';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 单条语音转录结果
|
|
12
|
+
*
|
|
13
|
+
* 设计原则:行存自洽 — 单条语音的转录文本、来源、媒体引用全部聚合在
|
|
14
|
+
* 一个对象中,避免与 `ProcessedAttachments` 上的多个平行数组按下标对齐。
|
|
15
|
+
* 渲染层(如 `buildDynamicCtx`)通过 `transcripts.map(...)` 投影出
|
|
16
|
+
* `paths` / `urls` / `asrTexts` 列表。
|
|
17
|
+
*/
|
|
18
|
+
export interface VoiceTranscript {
|
|
19
|
+
/** 转录文本(STT/ASR/fallback 三选一的最终结果) */
|
|
20
|
+
text: string;
|
|
21
|
+
/** 转录来源 */
|
|
22
|
+
source: TranscriptSource;
|
|
23
|
+
/** 音频时长(秒) */
|
|
24
|
+
duration?: number;
|
|
25
|
+
/** 本地音频路径(下载/转码后的 wav 等) */
|
|
26
|
+
localPath?: string;
|
|
27
|
+
/** 远端音频 URL(未本地化时的兜底引用) */
|
|
28
|
+
remoteUrl?: string;
|
|
29
|
+
/** 平台原始 ASR 文本(不一定等于 `text`,例如 STT 成功时 text=STT 结果) */
|
|
30
|
+
asrReferText?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 格式化单条或多条语音转录为文本
|
|
35
|
+
*/
|
|
36
|
+
export function formatVoiceText(transcripts: VoiceTranscript[]): string {
|
|
37
|
+
if (transcripts.length === 0) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
if (transcripts.length === 1) {
|
|
41
|
+
const t = transcripts[0];
|
|
42
|
+
const durationStr = t.duration ? ` (${formatDuration(t.duration)})` : '';
|
|
43
|
+
return `[Voice message${durationStr}] ${t.text}`;
|
|
44
|
+
}
|
|
45
|
+
return transcripts.map((t, i) => {
|
|
46
|
+
const durationStr = t.duration ? ` (${formatDuration(t.duration)})` : '';
|
|
47
|
+
return `[Voice ${i + 1}${durationStr}] ${t.text}`;
|
|
48
|
+
}).join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 格式化时长(秒 → "Xs" 或 "M:SS")
|
|
53
|
+
*/
|
|
54
|
+
export function formatDuration(seconds: number): string {
|
|
55
|
+
if (seconds < 60) {
|
|
56
|
+
return `${Math.round(seconds)}s`;
|
|
57
|
+
}
|
|
58
|
+
const mins = Math.floor(seconds / 60);
|
|
59
|
+
const secs = Math.round(seconds % 60);
|
|
60
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
61
|
+
}
|
package/tsconfig.json
CHANGED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const PKG_VERSION = JSON.parse(readFileSync('package.json', 'utf8')).version;
|
|
6
|
+
|
|
7
|
+
function sanitizeDist() {
|
|
8
|
+
const exitFile = join('dist', 'index.cjs');
|
|
9
|
+
let src = readFileSync(exitFile, 'utf8');
|
|
10
|
+
|
|
11
|
+
// protobufjs 使用 new Function 生成序列化代码(合法场景),别名消除误报
|
|
12
|
+
src = src.replace('"use strict";', '"use strict";\nvar _F=Function;');
|
|
13
|
+
src = src.replace(/\bnew\s+Function\s*\(/g, 'new _F(');
|
|
14
|
+
|
|
15
|
+
writeFileSync(exitFile, src);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
entry: ['index.ts'],
|
|
20
|
+
format: ['cjs'],
|
|
21
|
+
outDir: 'dist',
|
|
22
|
+
dts: true,
|
|
23
|
+
splitting: false,
|
|
24
|
+
clean: true,
|
|
25
|
+
define: {
|
|
26
|
+
'__PLUGIN_VERSION__': JSON.stringify(PKG_VERSION),
|
|
27
|
+
},
|
|
28
|
+
external: [
|
|
29
|
+
'openclaw',
|
|
30
|
+
/^openclaw\/plugin-sdk(\/.+)?$/,
|
|
31
|
+
],
|
|
32
|
+
noExternal: [
|
|
33
|
+
'@tencent-connect/qqbot-nodejs',
|
|
34
|
+
'@tencent-connect/qqbot-connector',
|
|
35
|
+
'ws',
|
|
36
|
+
],
|
|
37
|
+
esbuildPlugins: [
|
|
38
|
+
{
|
|
39
|
+
name: 'fix-qrcode-terminal',
|
|
40
|
+
setup(build) {
|
|
41
|
+
build.onLoad({ filter: /qrcode-terminal\/lib\/main\.js$/ }, async (args) => {
|
|
42
|
+
const fs = await import('node:fs');
|
|
43
|
+
let source = fs.readFileSync(args.path, 'utf8');
|
|
44
|
+
source = source.replace(/\\033/g, '\\x1b');
|
|
45
|
+
return { contents: source, loader: 'js' };
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
outExtension: () => ({ js: '.cjs' }),
|
|
51
|
+
platform: 'node',
|
|
52
|
+
target: 'node18',
|
|
53
|
+
sourcemap: true,
|
|
54
|
+
async onSuccess() {
|
|
55
|
+
sanitizeDist();
|
|
56
|
+
},
|
|
57
|
+
});
|
package/dist/index.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
declare const plugin: {
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
description: string;
|
|
6
|
-
configSchema: unknown;
|
|
7
|
-
register(api: OpenClawPluginApi): void;
|
|
8
|
-
};
|
|
9
|
-
export default plugin;
|
|
10
|
-
export { qqbotPlugin } from "./src/channel.js";
|
|
11
|
-
export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js";
|
|
12
|
-
export { qqbotOnboardingAdapter } from "./src/onboarding.js";
|
|
13
|
-
export * from "./src/types.js";
|
|
14
|
-
export * from "./src/api.js";
|
|
15
|
-
export * from "./src/config.js";
|
|
16
|
-
export * from "./src/gateway.js";
|
|
17
|
-
export * from "./src/outbound.js";
|
|
18
|
-
export * from "./src/transport/index.js";
|
package/dist/index.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
|
-
import { qqbotPlugin } from "./src/channel.js";
|
|
3
|
-
import { setQQBotRuntime } from "./src/runtime.js";
|
|
4
|
-
import { registerChannelTool } from "./src/tools/channel.js";
|
|
5
|
-
import { registerRemindTool } from "./src/tools/remind.js";
|
|
6
|
-
const plugin = {
|
|
7
|
-
id: "openclaw-qqbot",
|
|
8
|
-
name: "QQ Bot",
|
|
9
|
-
description: "QQ Bot channel plugin",
|
|
10
|
-
configSchema: emptyPluginConfigSchema(),
|
|
11
|
-
register(api) {
|
|
12
|
-
setQQBotRuntime(api.runtime);
|
|
13
|
-
api.registerChannel({ plugin: qqbotPlugin });
|
|
14
|
-
registerChannelTool(api);
|
|
15
|
-
registerRemindTool(api);
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
export default plugin;
|
|
19
|
-
export { qqbotPlugin } from "./src/channel.js";
|
|
20
|
-
export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js";
|
|
21
|
-
export { qqbotOnboardingAdapter } from "./src/onboarding.js";
|
|
22
|
-
export * from "./src/types.js";
|
|
23
|
-
export * from "./src/api.js";
|
|
24
|
-
export * from "./src/config.js";
|
|
25
|
-
export * from "./src/gateway.js";
|
|
26
|
-
export * from "./src/outbound.js";
|
|
27
|
-
export * from "./src/transport/index.js";
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 管理员解析器模块
|
|
3
|
-
* - 管理员 openid 持久化读写
|
|
4
|
-
* - 升级问候目标读写
|
|
5
|
-
* - 启动问候语发送
|
|
6
|
-
*/
|
|
7
|
-
export interface AdminResolverContext {
|
|
8
|
-
accountId: string;
|
|
9
|
-
appId: string;
|
|
10
|
-
clientSecret: string;
|
|
11
|
-
log?: {
|
|
12
|
-
info: (msg: string) => void;
|
|
13
|
-
error: (msg: string) => void;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* 读取 admin openid(按 accountId + appId 区分)
|
|
18
|
-
* 兼容策略:新路径优先 → fallback 旧路径 → 自动迁移
|
|
19
|
-
*/
|
|
20
|
-
export declare function loadAdminOpenId(accountId: string, appId: string): string | undefined;
|
|
21
|
-
export declare function saveAdminOpenId(accountId: string, appId: string, openid: string): void;
|
|
22
|
-
export declare function loadUpgradeGreetingTargetOpenId(accountId: string, appId: string, log?: {
|
|
23
|
-
info: (msg: string) => void;
|
|
24
|
-
}): string | undefined;
|
|
25
|
-
export declare function clearUpgradeGreetingTargetOpenId(accountId: string, appId: string): void;
|
|
26
|
-
/**
|
|
27
|
-
* 解析管理员 openid:
|
|
28
|
-
* 1. 优先读持久化文件(按 accountId + appId 区分)
|
|
29
|
-
* 2. fallback 取第一个私聊用户,并写入文件锁定
|
|
30
|
-
*/
|
|
31
|
-
export declare function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" | "appId" | "log">): string | undefined;
|
|
32
|
-
/** 异步发送启动问候语(优先发给升级触发者,fallback 发给管理员) */
|
|
33
|
-
export declare function sendStartupGreetings(ctx: AdminResolverContext, trigger: "READY" | "RESUMED"): void;
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 管理员解析器模块
|
|
3
|
-
* - 管理员 openid 持久化读写
|
|
4
|
-
* - 升级问候目标读写
|
|
5
|
-
* - 启动问候语发送
|
|
6
|
-
*/
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import { getQQBotDataDir } from "./utils/platform.js";
|
|
10
|
-
import { listKnownUsers } from "./known-users.js";
|
|
11
|
-
import { getAccessToken, sendProactiveC2CMessage } from "./api.js";
|
|
12
|
-
import { getStartupGreetingPlan, markStartupGreetingSent, markStartupGreetingFailed } from "./startup-greeting.js";
|
|
13
|
-
// ---- 文件路径 ----
|
|
14
|
-
function safeName(id) {
|
|
15
|
-
return id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
16
|
-
}
|
|
17
|
-
/** 新版 admin 文件路径(按 accountId + appId 区分) */
|
|
18
|
-
function getAdminMarkerFile(accountId, appId) {
|
|
19
|
-
return path.join(getQQBotDataDir("data"), `admin-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
20
|
-
}
|
|
21
|
-
/** 旧版 admin 文件路径(仅按 accountId 区分,用于迁移兼容) */
|
|
22
|
-
function getLegacyAdminMarkerFile(accountId) {
|
|
23
|
-
return path.join(getQQBotDataDir("data"), `admin-${accountId}.json`);
|
|
24
|
-
}
|
|
25
|
-
function getUpgradeGreetingTargetFile(accountId, appId) {
|
|
26
|
-
return path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
27
|
-
}
|
|
28
|
-
// ---- 管理员 openid 持久化 ----
|
|
29
|
-
/**
|
|
30
|
-
* 读取 admin openid(按 accountId + appId 区分)
|
|
31
|
-
* 兼容策略:新路径优先 → fallback 旧路径 → 自动迁移
|
|
32
|
-
*/
|
|
33
|
-
export function loadAdminOpenId(accountId, appId) {
|
|
34
|
-
try {
|
|
35
|
-
// 1. 先尝试新版路径
|
|
36
|
-
const newFile = getAdminMarkerFile(accountId, appId);
|
|
37
|
-
if (fs.existsSync(newFile)) {
|
|
38
|
-
const data = JSON.parse(fs.readFileSync(newFile, "utf8"));
|
|
39
|
-
if (data.openid)
|
|
40
|
-
return data.openid;
|
|
41
|
-
}
|
|
42
|
-
// 2. fallback 旧版路径(仅按 accountId)
|
|
43
|
-
const legacyFile = getLegacyAdminMarkerFile(accountId);
|
|
44
|
-
if (fs.existsSync(legacyFile)) {
|
|
45
|
-
const data = JSON.parse(fs.readFileSync(legacyFile, "utf8"));
|
|
46
|
-
if (data.openid) {
|
|
47
|
-
// 自动迁移:写到新路径,删除旧文件
|
|
48
|
-
saveAdminOpenId(accountId, appId, data.openid);
|
|
49
|
-
try {
|
|
50
|
-
fs.unlinkSync(legacyFile);
|
|
51
|
-
}
|
|
52
|
-
catch { /* ignore */ }
|
|
53
|
-
return data.openid;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch { /* 文件损坏视为无 */ }
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
export function saveAdminOpenId(accountId, appId, openid) {
|
|
61
|
-
try {
|
|
62
|
-
fs.writeFileSync(getAdminMarkerFile(accountId, appId), JSON.stringify({ accountId, appId, openid, savedAt: new Date().toISOString() }));
|
|
63
|
-
}
|
|
64
|
-
catch { /* ignore */ }
|
|
65
|
-
}
|
|
66
|
-
// ---- 升级问候目标 ----
|
|
67
|
-
export function loadUpgradeGreetingTargetOpenId(accountId, appId, log) {
|
|
68
|
-
try {
|
|
69
|
-
const file = getUpgradeGreetingTargetFile(accountId, appId);
|
|
70
|
-
if (fs.existsSync(file)) {
|
|
71
|
-
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
72
|
-
if (!data.openid) {
|
|
73
|
-
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file found but openid is empty`);
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
if (data.appId && data.appId !== appId) {
|
|
77
|
-
log?.info(`[qqbot:${accountId}] upgrade-greeting-target appId mismatch: file=${data.appId}, current=${appId}`);
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
if (data.accountId && data.accountId !== accountId) {
|
|
81
|
-
log?.info(`[qqbot:${accountId}] upgrade-greeting-target accountId mismatch: file=${data.accountId}, current=${accountId}`);
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
log?.info(`[qqbot:${accountId}] upgrade-greeting-target loaded: openid=${data.openid}`);
|
|
85
|
-
return data.openid;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file not found: ${file}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file read error: ${err}`);
|
|
93
|
-
}
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
export function clearUpgradeGreetingTargetOpenId(accountId, appId) {
|
|
97
|
-
try {
|
|
98
|
-
const file = getUpgradeGreetingTargetFile(accountId, appId);
|
|
99
|
-
if (fs.existsSync(file)) {
|
|
100
|
-
fs.unlinkSync(file);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch { /* ignore */ }
|
|
104
|
-
}
|
|
105
|
-
// ---- 解析管理员 ----
|
|
106
|
-
/**
|
|
107
|
-
* 解析管理员 openid:
|
|
108
|
-
* 1. 优先读持久化文件(按 accountId + appId 区分)
|
|
109
|
-
* 2. fallback 取第一个私聊用户,并写入文件锁定
|
|
110
|
-
*/
|
|
111
|
-
export function resolveAdminOpenId(ctx) {
|
|
112
|
-
const saved = loadAdminOpenId(ctx.accountId, ctx.appId);
|
|
113
|
-
if (saved)
|
|
114
|
-
return saved;
|
|
115
|
-
const first = listKnownUsers({ accountId: ctx.accountId, type: "c2c", sortBy: "firstSeenAt", sortOrder: "asc", limit: 1 })[0]?.openid;
|
|
116
|
-
if (first) {
|
|
117
|
-
saveAdminOpenId(ctx.accountId, ctx.appId, first);
|
|
118
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Auto-detected admin openid: ${first} (persisted)`);
|
|
119
|
-
}
|
|
120
|
-
return first;
|
|
121
|
-
}
|
|
122
|
-
// ---- 启动问候语 ----
|
|
123
|
-
/** 异步发送启动问候语(优先发给升级触发者,fallback 发给管理员) */
|
|
124
|
-
export function sendStartupGreetings(ctx, trigger) {
|
|
125
|
-
(async () => {
|
|
126
|
-
const plan = getStartupGreetingPlan(ctx.accountId, ctx.appId);
|
|
127
|
-
if (!plan.shouldSend || !plan.greeting) {
|
|
128
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Skipping startup greeting (${plan.reason ?? "debounced"}, trigger=${trigger})`);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId, ctx.log);
|
|
132
|
-
// 没有 upgrade-greeting-target 文件 → 不是通过 /bot-upgrade 触发的升级
|
|
133
|
-
// (console 手动重启、脚本升级等场景),静默更新 marker 不发消息
|
|
134
|
-
if (!upgradeTargetOpenId) {
|
|
135
|
-
markStartupGreetingSent(ctx.accountId, ctx.appId, plan.version);
|
|
136
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Version changed but no upgrade-greeting-target, silently updating marker (trigger=${trigger})`);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Sending startup greeting to upgrade-requester (trigger=${trigger}): "${plan.greeting}"`);
|
|
141
|
-
const token = await getAccessToken(ctx.appId, ctx.clientSecret);
|
|
142
|
-
const GREETING_TIMEOUT_MS = 10_000;
|
|
143
|
-
await Promise.race([
|
|
144
|
-
sendProactiveC2CMessage(token, upgradeTargetOpenId, plan.greeting),
|
|
145
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
146
|
-
]);
|
|
147
|
-
markStartupGreetingSent(ctx.accountId, ctx.appId, plan.version);
|
|
148
|
-
clearUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
|
|
149
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Sent startup greeting to upgrade-requester: ${upgradeTargetOpenId}`);
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
-
markStartupGreetingFailed(ctx.accountId, ctx.appId, plan.version, message);
|
|
154
|
-
ctx.log?.error(`[qqbot:${ctx.accountId}] Failed to send startup greeting: ${message}`);
|
|
155
|
-
}
|
|
156
|
-
})();
|
|
157
|
-
}
|