@jerryliang122/openclaw-qqbot 1.0.9 → 2.1.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 +21 -29
- package/README.zh.md +21 -29
- package/dist/index.cjs +60 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -9
- package/package.json +1 -1
- package/src/dispatch/body-assembler.ts +4 -2
- package/src/middleware/attachment.ts +30 -32
- package/src/types.ts +11 -9
- package/src/utils/stt.ts +63 -101
package/README.md
CHANGED
|
@@ -810,44 +810,36 @@ The plugin processes messages through a carefully ordered middleware chain:
|
|
|
810
810
|
|
|
811
811
|
#### STT (Speech-to-Text) — Transcribe Incoming Voice Messages
|
|
812
812
|
|
|
813
|
-
STT
|
|
814
|
-
|
|
815
|
-
| Priority | Config Path | Scope |
|
|
816
|
-
|----------|------------|-------|
|
|
817
|
-
| 1 (highest) | `channels.qqbot.stt` | Plugin-specific |
|
|
818
|
-
| 2 (fallback) | `tools.media.audio.models[0]` | Framework-level |
|
|
813
|
+
Transcription runs through the **framework audio-understanding pipeline** (`openclaw/plugin-sdk/media-understanding-runtime`), so STT config comes from the framework-level `tools.media.models` (same as the built-in Telegram channel) — the plugin no longer ships its own OpenAI-compatible HTTP call:
|
|
819
814
|
|
|
820
815
|
```json
|
|
821
816
|
{
|
|
822
|
-
"
|
|
823
|
-
"
|
|
824
|
-
"
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
817
|
+
"tools": {
|
|
818
|
+
"media": {
|
|
819
|
+
"models": [
|
|
820
|
+
{
|
|
821
|
+
"type": "cli",
|
|
822
|
+
"command": "your-asr-cli",
|
|
823
|
+
"args": ["-m", "your-model", "{{AttachmentPath}}"],
|
|
824
|
+
"maxBytes": 52428800,
|
|
825
|
+
"timeoutSeconds": 120,
|
|
826
|
+
"capabilities": ["audio"]
|
|
827
|
+
}
|
|
828
|
+
]
|
|
828
829
|
}
|
|
829
830
|
}
|
|
830
831
|
}
|
|
831
832
|
```
|
|
832
833
|
|
|
833
|
-
- `provider
|
|
834
|
-
- Set `enabled: false` to disable
|
|
835
|
-
- When configured, incoming voice messages are automatically converted (SILK→WAV) and transcribed
|
|
836
|
-
- `asrFallback` — platform ASR (`asr_refer_text`) participation switch. Unless explicitly set to `true`, QQ's built-in platform transcript is **discarded in all cases**: not used as a fallback when your STT fails or returns empty, and not used as the sole source when STT is not configured at all (voice messages then render as `[Voice message - transcription unavailable]`; the audio URL is still referenced via the `- Voice:` line). The flag is read from `channels.qqbot.stt.asrFallback` regardless of whether STT credentials resolve — `stt: { "asrFallback": true }` alone restores the legacy platform-transcript behavior:
|
|
834
|
+
A provider-form entry also works (e.g. `{ "provider": "openai", "model": "whisper-1", "capabilities": ["audio"] }`). **`capabilities` must include `"audio"`** — the shared model list is selected by capability tags; untagged entries never take part in voice transcription.
|
|
837
835
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
"asrFallback": true
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
```
|
|
836
|
+
Voice message handling order (hardcoded since 2026-10; no plugin-level switches):
|
|
837
|
+
|
|
838
|
+
1. **Framework STT configured** (a `tools.media.models` entry whose `capabilities` includes `"audio"`) → voice is downloaded, converted (SILK→WAV), and transcribed through the framework pipeline. **The framework is trusted strictly** — on failure or empty transcript a placeholder (`[Voice message - transcription failed]`) is emitted; there is **no** fallback to the platform transcript.
|
|
839
|
+
2. **Framework STT not configured** → the **QQ platform transcript (`asr_refer_text`)** — QQ auto-STTs voice messages and ships the text in the event JSON — is used directly as the sole source, no download and no external call needed.
|
|
840
|
+
3. Not configured and no platform transcript → placeholder text (`[Voice message - transcription unavailable]`); the audio URL is still referenced via the `- Voice:` line.
|
|
841
|
+
|
|
842
|
+
The whole `channels.qqbot.stt` block is **deprecated and ignored** (legacy credential keys plus the historical `enabled`/`asrFallback` switches, removed 2026-10-04); a one-time migration notice is logged if any key is present. STT is controlled solely by the framework config — remove the audio entry from `tools.media.models` (or set `tools.media.audio.enabled: false`) to disable framework transcription.
|
|
851
843
|
|
|
852
844
|
#### TTS (Text-to-Speech) — Send Voice Messages
|
|
853
845
|
|
package/README.zh.md
CHANGED
|
@@ -631,44 +631,36 @@ openclaw message send --channel "qqbot" \
|
|
|
631
631
|
|
|
632
632
|
#### STT(语音转文字)— 自动转录用户发来的语音消息
|
|
633
633
|
|
|
634
|
-
STT
|
|
635
|
-
|
|
636
|
-
| 优先级 | 配置路径 | 作用域 |
|
|
637
|
-
|--------|----------|--------|
|
|
638
|
-
| 1(highest) | `channels.qqbot.stt` | 插件专属 |
|
|
639
|
-
| 2(fallback) | `tools.media.audio.models[0]` | 框架级 |
|
|
634
|
+
转录统一走**框架音频理解管线**(`openclaw/plugin-sdk/media-understanding-runtime`),STT 配置只认框架级 `tools.media.models`(与内置 Telegram 通道一致)——插件不再自带 OpenAI 兼容 HTTP 调用:
|
|
640
635
|
|
|
641
636
|
```json
|
|
642
637
|
{
|
|
643
|
-
"
|
|
644
|
-
"
|
|
645
|
-
"
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
638
|
+
"tools": {
|
|
639
|
+
"media": {
|
|
640
|
+
"models": [
|
|
641
|
+
{
|
|
642
|
+
"type": "cli",
|
|
643
|
+
"command": "your-asr-cli",
|
|
644
|
+
"args": ["-m", "your-model", "{{AttachmentPath}}"],
|
|
645
|
+
"maxBytes": 52428800,
|
|
646
|
+
"timeoutSeconds": 120,
|
|
647
|
+
"capabilities": ["audio"]
|
|
648
|
+
}
|
|
649
|
+
]
|
|
649
650
|
}
|
|
650
651
|
}
|
|
651
652
|
}
|
|
652
653
|
```
|
|
653
654
|
|
|
654
|
-
|
|
655
|
-
- 设置 `enabled: false` 可禁用
|
|
656
|
-
- 配置后,用户发来的语音消息会自动转换(SILK→WAV)并转录为文字
|
|
657
|
-
- `asrFallback` — 平台转写(`asr_refer_text`)参与开关。**未显式设为 `true` 时,平台转写在所有场景下都被丢弃**:自有 STT 失败或返回空时不作兜底,STT 未配置时也不作为唯一来源(此时语音消息渲染为 `[Voice message - transcription unavailable]` 占位文本,音频 URL 仍通过 `- Voice:` 行引用)。该开关从 `channels.qqbot.stt.asrFallback` 读取,与 STT 凭证是否解析成功无关——仅写 `stt: { "asrFallback": true }` 即可恢复平台转写参与旧行为:
|
|
655
|
+
也支持 provider 形态(如 `{ "provider": "openai", "model": "whisper-1", "capabilities": ["audio"] }`)。**`capabilities` 必须包含 `"audio"`**——模型列表按能力标签选择,无标签条目不参与语音转录。
|
|
658
656
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
"asrFallback": true
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
```
|
|
657
|
+
语音消息处理顺序(2026-10 起硬编码,无插件级开关):
|
|
658
|
+
|
|
659
|
+
1. **框架 STT 已配置**(`tools.media.models` 存在 `capabilities` 含 `"audio"` 的条目)→ 下载语音、转换(SILK→WAV)后经框架管线转录;**严格信框架**——转录失败或为空时输出占位文本(`[Voice message - transcription failed]`),不回退平台转写。
|
|
660
|
+
2. **框架 STT 未配置** → **直接采用 QQ 平台转写**(`asr_refer_text`,QQ 平台对语音消息自动 STT 并随事件 JSON 下发)作为唯一来源——无需下载、不发起任何外部调用。
|
|
661
|
+
3. 未配置且无平台转写 → 占位文本(`[Voice message - transcription unavailable]`),音频 URL 仍通过 `- Voice:` 行引用。
|
|
662
|
+
|
|
663
|
+
`channels.qqbot.stt` 整块**已废弃并被忽略**(旧凭证键 + 历史 `enabled`/`asrFallback` 开关,2026-10-04 移除);检测到任何键时会打一次性迁移提示日志。STT 启停只由框架配置控制——删掉 `tools.media.models` 的 audio 条目、或设 `tools.media.audio.enabled: false` 即关闭框架转录。
|
|
672
664
|
|
|
673
665
|
#### TTS(文字转语音)— 机器人发送语音消息
|
|
674
666
|
|
package/dist/index.cjs
CHANGED
|
@@ -8259,7 +8259,7 @@ var init_outbound_service = __esm({
|
|
|
8259
8259
|
|
|
8260
8260
|
// src/utils/pkg-version.ts
|
|
8261
8261
|
function getPackageVersion() {
|
|
8262
|
-
return true ? "1.0
|
|
8262
|
+
return true ? "2.1.0" : "unknown";
|
|
8263
8263
|
}
|
|
8264
8264
|
function getOpenclawVersion(runtimeVersion) {
|
|
8265
8265
|
return runtimeVersion && runtimeVersion !== "unknown" ? runtimeVersion : "unknown";
|
|
@@ -12904,70 +12904,41 @@ init_webhook_server_node();
|
|
|
12904
12904
|
init_webhook_verify();
|
|
12905
12905
|
|
|
12906
12906
|
// src/utils/stt.ts
|
|
12907
|
-
var fs18 = __toESM(require("fs"), 1);
|
|
12908
12907
|
var path16 = __toESM(require("path"), 1);
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
const qqbot = asRecord(channels?.qqbot);
|
|
12912
|
-
return asRecord(qqbot?.stt)?.asrFallback === true;
|
|
12913
|
-
}
|
|
12914
|
-
function resolveSTTConfig(cfg) {
|
|
12915
|
-
const channels = asRecord(cfg.channels);
|
|
12916
|
-
const qqbot = asRecord(channels?.qqbot);
|
|
12917
|
-
const sttCfg = asRecord(qqbot?.stt);
|
|
12918
|
-
if (sttCfg?.enabled === false) {
|
|
12919
|
-
return null;
|
|
12920
|
-
}
|
|
12921
|
-
const models = asRecord(cfg.models);
|
|
12922
|
-
const providers = asRecord(models?.providers);
|
|
12923
|
-
if (sttCfg) {
|
|
12924
|
-
const providerId = readString(sttCfg, "provider") ?? "openai";
|
|
12925
|
-
const providerCfg = asRecord(providers?.[providerId]);
|
|
12926
|
-
const baseUrl = readString(sttCfg, "baseUrl") ?? readString(providerCfg, "baseUrl");
|
|
12927
|
-
const apiKey = readString(sttCfg, "apiKey") ?? readString(providerCfg, "apiKey");
|
|
12928
|
-
const model = readString(sttCfg, "model") ?? "whisper-1";
|
|
12929
|
-
if (baseUrl && apiKey) {
|
|
12930
|
-
return { enabled: true, baseUrl: baseUrl.replace(/\/+$/, ""), apiKey, model };
|
|
12931
|
-
}
|
|
12932
|
-
}
|
|
12908
|
+
var import_media_understanding_runtime = require("openclaw/plugin-sdk/media-understanding-runtime");
|
|
12909
|
+
function isFrameworkSttConfigured(cfg) {
|
|
12933
12910
|
const tools = asRecord(cfg.tools);
|
|
12934
12911
|
const media = asRecord(tools?.media);
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
const audioModelEntry = Array.isArray(audioModels) ? asRecord(audioModels[0]) : void 0;
|
|
12938
|
-
if (audioModelEntry) {
|
|
12939
|
-
const providerId = readString(audioModelEntry, "provider") ?? "openai";
|
|
12940
|
-
const providerCfg = asRecord(providers?.[providerId]);
|
|
12941
|
-
const baseUrl = readString(audioModelEntry, "baseUrl") ?? readString(providerCfg, "baseUrl");
|
|
12942
|
-
const apiKey = readString(audioModelEntry, "apiKey") ?? readString(providerCfg, "apiKey");
|
|
12943
|
-
const model = readString(audioModelEntry, "model") ?? "whisper-1";
|
|
12944
|
-
if (baseUrl && apiKey) {
|
|
12945
|
-
return { enabled: true, baseUrl: baseUrl.replace(/\/+$/, ""), apiKey, model };
|
|
12946
|
-
}
|
|
12912
|
+
if (asRecord(media?.audio)?.enabled === false) {
|
|
12913
|
+
return false;
|
|
12947
12914
|
}
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
const sttCfg = resolveSTTConfig(cfg);
|
|
12952
|
-
if (!sttCfg) {
|
|
12953
|
-
return null;
|
|
12915
|
+
const models = media?.models;
|
|
12916
|
+
if (!Array.isArray(models)) {
|
|
12917
|
+
return false;
|
|
12954
12918
|
}
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
const
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12919
|
+
return models.some((entry) => {
|
|
12920
|
+
const capabilities = asRecord(entry)?.capabilities;
|
|
12921
|
+
return Array.isArray(capabilities) && capabilities.includes("audio");
|
|
12922
|
+
});
|
|
12923
|
+
}
|
|
12924
|
+
function hasLegacySttConfig(cfg) {
|
|
12925
|
+
const channels = asRecord(cfg.channels);
|
|
12926
|
+
const qqbot = asRecord(channels?.qqbot);
|
|
12927
|
+
const stt = asRecord(qqbot?.stt);
|
|
12928
|
+
if (!stt) return false;
|
|
12929
|
+
const legacyKeys = ["provider", "baseUrl", "apiKey", "model", "enabled", "asrFallback"];
|
|
12930
|
+
return legacyKeys.some((key2) => {
|
|
12931
|
+
const value = stt[key2];
|
|
12932
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
12933
|
+
return value != null;
|
|
12934
|
+
});
|
|
12935
|
+
}
|
|
12936
|
+
async function transcribeAudioViaFramework(audioPath, cfg) {
|
|
12937
|
+
const result = await (0, import_media_understanding_runtime.transcribeAudioFile)({
|
|
12938
|
+
filePath: audioPath,
|
|
12939
|
+
cfg,
|
|
12940
|
+
mime: guessMimeType(audioPath)
|
|
12965
12941
|
});
|
|
12966
|
-
if (!resp.ok) {
|
|
12967
|
-
const detail = await resp.text().catch(() => "");
|
|
12968
|
-
throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
|
|
12969
|
-
}
|
|
12970
|
-
const result = await resp.json();
|
|
12971
12942
|
return result.text?.trim() || null;
|
|
12972
12943
|
}
|
|
12973
12944
|
function asRecord(value) {
|
|
@@ -12976,16 +12947,6 @@ function asRecord(value) {
|
|
|
12976
12947
|
}
|
|
12977
12948
|
return void 0;
|
|
12978
12949
|
}
|
|
12979
|
-
function readString(obj, key2) {
|
|
12980
|
-
const val = obj?.[key2];
|
|
12981
|
-
if (typeof val === "string" && val.trim()) {
|
|
12982
|
-
return val.trim();
|
|
12983
|
-
}
|
|
12984
|
-
return void 0;
|
|
12985
|
-
}
|
|
12986
|
-
function sanitizeFileName2(name) {
|
|
12987
|
-
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
12988
|
-
}
|
|
12989
12950
|
function guessMimeType(fileName) {
|
|
12990
12951
|
const ext = path16.extname(fileName).toLowerCase();
|
|
12991
12952
|
const mimeMap = {
|
|
@@ -13061,8 +13022,8 @@ function attachmentProcessor(opts) {
|
|
|
13061
13022
|
};
|
|
13062
13023
|
}
|
|
13063
13024
|
async function processAttachments(attachments, cfg, log5) {
|
|
13064
|
-
const
|
|
13065
|
-
|
|
13025
|
+
const sttConfigured = isFrameworkSttConfigured(cfg);
|
|
13026
|
+
warnLegacySttConfig(cfg, log5);
|
|
13066
13027
|
const audioPolicy = resolveAudioPolicy(cfg);
|
|
13067
13028
|
const imageUrls = [];
|
|
13068
13029
|
const otherParts = [];
|
|
@@ -13080,7 +13041,7 @@ async function processAttachments(attachments, cfg, log5) {
|
|
|
13080
13041
|
return { type: "image", localPath, url, contentType: att.content_type ?? "image/png", filename: att.filename };
|
|
13081
13042
|
}
|
|
13082
13043
|
if (isVoice) {
|
|
13083
|
-
const transcript = await processVoiceAttachment(att,
|
|
13044
|
+
const transcript = await processVoiceAttachment(att, cfg, sttConfigured, audioPolicy, log5);
|
|
13084
13045
|
return { type: "voice", transcript };
|
|
13085
13046
|
}
|
|
13086
13047
|
if (url) {
|
|
@@ -13153,22 +13114,26 @@ function kindFromContentType(contentType) {
|
|
|
13153
13114
|
if (contentType?.startsWith("audio/")) return "audio";
|
|
13154
13115
|
return "document";
|
|
13155
13116
|
}
|
|
13156
|
-
|
|
13157
|
-
|
|
13158
|
-
|
|
13117
|
+
var legacySttWarned = false;
|
|
13118
|
+
function warnLegacySttConfig(cfg, log5) {
|
|
13119
|
+
if (!legacySttWarned && hasLegacySttConfig(cfg)) {
|
|
13120
|
+
legacySttWarned = true;
|
|
13121
|
+
log5?.info(
|
|
13122
|
+
"Voice: channels.qqbot.stt is deprecated and ignored entirely (credentials + enabled/asrFallback); configure an audio-capable tools.media.models entry for STT \u2014 platform asr_refer_text is used only when framework STT is absent"
|
|
13123
|
+
);
|
|
13124
|
+
}
|
|
13125
|
+
}
|
|
13126
|
+
async function processVoiceAttachment(att, cfg, sttConfigured, audioPolicy, log5) {
|
|
13159
13127
|
const remoteUrl = normalizeUrl(att.voice_wav_url) || normalizeUrl(att.url) || void 0;
|
|
13160
|
-
if (!
|
|
13161
|
-
|
|
13162
|
-
log5?.info(`Voice: STT not configured; platform asr_refer_text discarded (asrFallback not enabled)`);
|
|
13163
|
-
}
|
|
13128
|
+
if (!sttConfigured) {
|
|
13129
|
+
const asrReferText = att.asr_refer_text?.trim() || void 0;
|
|
13164
13130
|
if (asrReferText) {
|
|
13165
|
-
log5?.debug?.(`Voice: using asr_refer_text (STT not configured
|
|
13131
|
+
log5?.debug?.(`Voice: using platform asr_refer_text (framework STT not configured)`);
|
|
13166
13132
|
return { text: asrReferText, source: "asr", asrReferText, remoteUrl };
|
|
13167
13133
|
}
|
|
13168
13134
|
return {
|
|
13169
13135
|
text: "[Voice message - transcription unavailable]",
|
|
13170
13136
|
source: "fallback",
|
|
13171
|
-
asrReferText,
|
|
13172
13137
|
remoteUrl
|
|
13173
13138
|
};
|
|
13174
13139
|
}
|
|
@@ -13209,25 +13174,21 @@ async function processVoiceAttachment(att, sttCfg, usePlatformAsr, audioPolicy,
|
|
|
13209
13174
|
}
|
|
13210
13175
|
if (localPath) {
|
|
13211
13176
|
try {
|
|
13212
|
-
const transcript = await
|
|
13177
|
+
const transcript = await transcribeAudioViaFramework(localPath, cfg);
|
|
13213
13178
|
if (transcript) {
|
|
13214
|
-
log5?.debug?.(`Voice STT: ${transcript.slice(0, 80)}...`);
|
|
13215
|
-
return { text: transcript, source: "stt", duration, localPath, remoteUrl
|
|
13179
|
+
log5?.debug?.(`Voice STT (framework): ${transcript.slice(0, 80)}...`);
|
|
13180
|
+
return { text: transcript, source: "stt", duration, localPath, remoteUrl };
|
|
13216
13181
|
}
|
|
13217
13182
|
} catch (err) {
|
|
13218
|
-
log5?.error(`Voice STT failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
13183
|
+
log5?.error(`Voice STT (framework) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
13219
13184
|
}
|
|
13220
13185
|
}
|
|
13221
|
-
if (asrReferText) {
|
|
13222
|
-
return { text: asrReferText, source: "asr", duration, localPath, remoteUrl, asrReferText };
|
|
13223
|
-
}
|
|
13224
13186
|
return {
|
|
13225
13187
|
text: "[Voice message - transcription failed]",
|
|
13226
13188
|
source: "fallback",
|
|
13227
13189
|
duration,
|
|
13228
13190
|
localPath,
|
|
13229
|
-
remoteUrl
|
|
13230
|
-
asrReferText
|
|
13191
|
+
remoteUrl
|
|
13231
13192
|
};
|
|
13232
13193
|
}
|
|
13233
13194
|
function resolveAudioPolicy(cfg) {
|
|
@@ -13248,9 +13209,6 @@ function normalizeFormats(formats) {
|
|
|
13248
13209
|
return lower.startsWith(".") ? lower : `.${lower}`;
|
|
13249
13210
|
});
|
|
13250
13211
|
}
|
|
13251
|
-
function cfg2stt(sttCfg) {
|
|
13252
|
-
return { channels: { qqbot: { stt: sttCfg } } };
|
|
13253
|
-
}
|
|
13254
13212
|
function normalizeUrl(url) {
|
|
13255
13213
|
if (!url) return "";
|
|
13256
13214
|
return url.startsWith("//") ? `https:${url}` : url;
|
|
@@ -13352,7 +13310,7 @@ function buildDynamicCtx(processed, msg, quote) {
|
|
|
13352
13310
|
lines.push(`- Voice: ${voiceRefs.join(", ")}`);
|
|
13353
13311
|
}
|
|
13354
13312
|
const asrTexts = unique(
|
|
13355
|
-
transcripts.
|
|
13313
|
+
transcripts.filter((t) => t.source === "asr").map((t) => t.text).filter(isNonEmpty)
|
|
13356
13314
|
);
|
|
13357
13315
|
if (asrTexts.length > 0) {
|
|
13358
13316
|
lines.push(`- ASR: ${asrTexts.join(" | ")}`);
|
|
@@ -15891,7 +15849,7 @@ function getQQBotApprovalCapability() {
|
|
|
15891
15849
|
|
|
15892
15850
|
// src/features/proactive.ts
|
|
15893
15851
|
init_plugin_logger();
|
|
15894
|
-
var
|
|
15852
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
15895
15853
|
var path20 = __toESM(require("path"), 1);
|
|
15896
15854
|
init_bot_instance();
|
|
15897
15855
|
init_config();
|
|
@@ -15902,8 +15860,8 @@ var KNOWN_USERS_FILE = path20.join(STORAGE_DIR, "known-users.json");
|
|
|
15902
15860
|
var knownUsersCache = null;
|
|
15903
15861
|
var cacheLastModified = 0;
|
|
15904
15862
|
function ensureStorageDir() {
|
|
15905
|
-
if (!
|
|
15906
|
-
|
|
15863
|
+
if (!fs18.existsSync(STORAGE_DIR)) {
|
|
15864
|
+
fs18.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
15907
15865
|
}
|
|
15908
15866
|
}
|
|
15909
15867
|
function getUserKey(type, openid, accountId) {
|
|
@@ -15912,7 +15870,7 @@ function getUserKey(type, openid, accountId) {
|
|
|
15912
15870
|
function loadKnownUsers() {
|
|
15913
15871
|
if (knownUsersCache !== null) {
|
|
15914
15872
|
try {
|
|
15915
|
-
const stat =
|
|
15873
|
+
const stat = fs18.statSync(KNOWN_USERS_FILE);
|
|
15916
15874
|
if (stat.mtimeMs <= cacheLastModified) {
|
|
15917
15875
|
return knownUsersCache;
|
|
15918
15876
|
}
|
|
@@ -15922,14 +15880,14 @@ function loadKnownUsers() {
|
|
|
15922
15880
|
}
|
|
15923
15881
|
const users = /* @__PURE__ */ new Map();
|
|
15924
15882
|
try {
|
|
15925
|
-
if (
|
|
15926
|
-
const data =
|
|
15883
|
+
if (fs18.existsSync(KNOWN_USERS_FILE)) {
|
|
15884
|
+
const data = fs18.readFileSync(KNOWN_USERS_FILE, "utf-8");
|
|
15927
15885
|
const parsed = JSON.parse(data);
|
|
15928
15886
|
for (const user of parsed) {
|
|
15929
15887
|
const key2 = getUserKey(user.type, user.openid, user.accountId);
|
|
15930
15888
|
users.set(key2, user);
|
|
15931
15889
|
}
|
|
15932
|
-
cacheLastModified =
|
|
15890
|
+
cacheLastModified = fs18.statSync(KNOWN_USERS_FILE).mtimeMs;
|
|
15933
15891
|
}
|
|
15934
15892
|
} catch (err) {
|
|
15935
15893
|
log3.error(`Failed to load known users: ${err}`);
|
|
@@ -15941,7 +15899,7 @@ function saveKnownUsers(users) {
|
|
|
15941
15899
|
try {
|
|
15942
15900
|
ensureStorageDir();
|
|
15943
15901
|
const data = Array.from(users.values());
|
|
15944
|
-
|
|
15902
|
+
fs18.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
15945
15903
|
cacheLastModified = Date.now();
|
|
15946
15904
|
knownUsersCache = users;
|
|
15947
15905
|
} catch (err) {
|