@jerryliang122/openclaw-qqbot 1.0.9 → 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 CHANGED
@@ -810,44 +810,31 @@ 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 supports two-level configuration with priority fallback:
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 credentials come from the framework-level config (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
- "channels": {
823
- "qqbot": {
824
- "stt": {
825
- "provider": "your-provider",
826
- "model": "your-stt-model"
817
+ "tools": {
818
+ "media": {
819
+ "audio": {
820
+ "models": [{ "provider": "your-provider", "model": "your-stt-model" }]
827
821
  }
828
822
  }
829
823
  }
830
824
  }
831
825
  ```
832
826
 
833
- - `provider` — references a key in `models.providers` to inherit `baseUrl` and `apiKey`
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:
827
+ Voice message handling order:
837
828
 
838
- ```json
839
- {
840
- "channels": {
841
- "qqbot": {
842
- "stt": {
843
- "provider": "your-provider",
844
- "model": "your-stt-model",
845
- "asrFallback": true
846
- }
847
- }
848
- }
849
- }
850
- ```
829
+ 1. **Framework STT configured** (`tools.media.audio.models` non-empty) → voice is downloaded, converted (SILK→WAV), and transcribed through the framework pipeline. On failure or empty transcript, falls back to the platform transcript.
830
+ 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.
831
+ 3. Neither available → placeholder text (`[Voice message - transcription unavailable]`); the audio URL is still referenced via the `- Voice:` line.
832
+
833
+ Plugin-level behavior switches under `channels.qqbot.stt`:
834
+
835
+ - `enabled: false` — never call external STT; use only the platform transcript (or the placeholder)
836
+ - `asrFallback: false` — strict mode: discard the platform transcript in **all** cases (restores the pre-2026-10 behavior)
837
+ - `provider` / `baseUrl` / `apiKey` / `model` — **deprecated and ignored** (2026-10); a one-time migration notice is logged if still present. Move credentials to `tools.media.audio.models`.
851
838
 
852
839
  #### TTS (Text-to-Speech) — Send Voice Messages
853
840
 
package/README.zh.md CHANGED
@@ -631,44 +631,31 @@ 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 凭证只认框架级配置(与内置 Telegram 通道一致)——插件不再自带 OpenAI 兼容 HTTP 调用:
640
635
 
641
636
  ```json
642
637
  {
643
- "channels": {
644
- "qqbot": {
645
- "stt": {
646
- "provider": "your-provider",
647
- "model": "your-stt-model"
638
+ "tools": {
639
+ "media": {
640
+ "audio": {
641
+ "models": [{ "provider": "your-provider", "model": "your-stt-model" }]
648
642
  }
649
643
  }
650
644
  }
651
645
  }
652
646
  ```
653
647
 
654
- - `provider` — 引用 `models.providers` 中的 key,自动继承 `baseUrl` 和 `apiKey`
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 }` 即可恢复平台转写参与旧行为:
648
+ 语音消息处理顺序:
658
649
 
659
- ```json
660
- {
661
- "channels": {
662
- "qqbot": {
663
- "stt": {
664
- "provider": "your-provider",
665
- "model": "your-stt-model",
666
- "asrFallback": true
667
- }
668
- }
669
- }
670
- }
671
- ```
650
+ 1. **框架 STT 已配置**(`tools.media.audio.models` 非空)→ 下载语音、转换(SILK→WAV)后经框架管线转录;转录失败或为空时兜底用平台转写。
651
+ 2. **框架 STT 未配置** → **直接采用 QQ 平台转写**(`asr_refer_text`,QQ 平台对语音消息自动 STT 并随事件 JSON 下发)作为唯一来源——无需下载、不发起任何外部调用。
652
+ 3. 两者都不可用 → 占位文本(`[Voice message - transcription unavailable]`),音频 URL 仍通过 `- Voice:` 行引用。
653
+
654
+ `channels.qqbot.stt` 下仅保留行为开关:
655
+
656
+ - `enabled: false` — 不调用外部 STT,语音只用平台转写(或无转写时占位文本)
657
+ - `asrFallback: false` — 严格模式:所有场景丢弃平台转写(恢复 2026-10 之前的旧行为)
658
+ - `provider` / `baseUrl` / `apiKey` / `model` — **已废弃并被忽略**(2026-10);检测到仍配置时会打一次性迁移提示日志,请把凭证迁移到 `tools.media.audio.models`。
672
659
 
673
660
  #### TTS(文字转语音)— 机器人发送语音消息
674
661
 
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.9" : "unknown";
8262
+ return true ? "2.0.0" : "unknown";
8263
8263
  }
8264
8264
  function getOpenclawVersion(runtimeVersion) {
8265
8265
  return runtimeVersion && runtimeVersion !== "unknown" ? runtimeVersion : "unknown";
@@ -12904,70 +12904,42 @@ 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);
12908
+ var import_media_understanding_runtime = require("openclaw/plugin-sdk/media-understanding-runtime");
12909
12909
  function shouldUsePlatformAsr(cfg) {
12910
12910
  const channels = asRecord(cfg.channels);
12911
12911
  const qqbot = asRecord(channels?.qqbot);
12912
- return asRecord(qqbot?.stt)?.asrFallback === true;
12912
+ return asRecord(qqbot?.stt)?.asrFallback !== false;
12913
12913
  }
12914
- function resolveSTTConfig(cfg) {
12914
+ function isFrameworkSttConfigured(cfg) {
12915
12915
  const channels = asRecord(cfg.channels);
12916
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
- }
12917
+ if (asRecord(qqbot?.stt)?.enabled === false) {
12918
+ return false;
12932
12919
  }
12933
12920
  const tools = asRecord(cfg.tools);
12934
12921
  const media = asRecord(tools?.media);
12935
12922
  const audio = asRecord(media?.audio);
12936
- const audioModels = audio?.models;
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
- }
12923
+ if (!audio || audio.enabled === false) {
12924
+ return false;
12947
12925
  }
12948
- return null;
12926
+ return Array.isArray(audio.models) && audio.models.length > 0;
12949
12927
  }
12950
- async function transcribeAudio(audioPath, cfg) {
12951
- const sttCfg = resolveSTTConfig(cfg);
12952
- if (!sttCfg) {
12953
- return null;
12954
- }
12955
- const fileBuffer = fs18.readFileSync(audioPath);
12956
- const fileName = sanitizeFileName2(path16.basename(audioPath));
12957
- const mime = guessMimeType(fileName);
12958
- const form = new FormData();
12959
- form.append("file", new Blob([fileBuffer], { type: mime }), fileName);
12960
- form.append("model", sttCfg.model);
12961
- const resp = await fetch(`${sttCfg.baseUrl}/audio/transcriptions`, {
12962
- method: "POST",
12963
- headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
12964
- body: form
12928
+ function hasLegacySttCredentials(cfg) {
12929
+ const channels = asRecord(cfg.channels);
12930
+ const qqbot = asRecord(channels?.qqbot);
12931
+ const stt = asRecord(qqbot?.stt);
12932
+ if (!stt) return false;
12933
+ return ["provider", "baseUrl", "apiKey", "model"].some(
12934
+ (key2) => typeof stt[key2] === "string" && stt[key2].trim().length > 0
12935
+ );
12936
+ }
12937
+ async function transcribeAudioViaFramework(audioPath, cfg) {
12938
+ const result = await (0, import_media_understanding_runtime.transcribeAudioFile)({
12939
+ filePath: audioPath,
12940
+ cfg,
12941
+ mime: guessMimeType(audioPath)
12965
12942
  });
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
12943
  return result.text?.trim() || null;
12972
12944
  }
12973
12945
  function asRecord(value) {
@@ -12976,16 +12948,6 @@ function asRecord(value) {
12976
12948
  }
12977
12949
  return void 0;
12978
12950
  }
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
12951
  function guessMimeType(fileName) {
12990
12952
  const ext = path16.extname(fileName).toLowerCase();
12991
12953
  const mimeMap = {
@@ -13061,8 +13023,9 @@ function attachmentProcessor(opts) {
13061
13023
  };
13062
13024
  }
13063
13025
  async function processAttachments(attachments, cfg, log5) {
13064
- const sttCfg = resolveSTTConfig(cfg);
13065
13026
  const usePlatformAsr = shouldUsePlatformAsr(cfg);
13027
+ const sttConfigured = isFrameworkSttConfigured(cfg);
13028
+ warnLegacySttCredentials(cfg, log5);
13066
13029
  const audioPolicy = resolveAudioPolicy(cfg);
13067
13030
  const imageUrls = [];
13068
13031
  const otherParts = [];
@@ -13080,7 +13043,7 @@ async function processAttachments(attachments, cfg, log5) {
13080
13043
  return { type: "image", localPath, url, contentType: att.content_type ?? "image/png", filename: att.filename };
13081
13044
  }
13082
13045
  if (isVoice) {
13083
- const transcript = await processVoiceAttachment(att, sttCfg, usePlatformAsr, audioPolicy, log5);
13046
+ const transcript = await processVoiceAttachment(att, cfg, usePlatformAsr, sttConfigured, audioPolicy, log5);
13084
13047
  return { type: "voice", transcript };
13085
13048
  }
13086
13049
  if (url) {
@@ -13153,16 +13116,25 @@ function kindFromContentType(contentType) {
13153
13116
  if (contentType?.startsWith("audio/")) return "audio";
13154
13117
  return "document";
13155
13118
  }
13156
- async function processVoiceAttachment(att, sttCfg, usePlatformAsr, audioPolicy, log5) {
13119
+ var legacySttWarned = false;
13120
+ function warnLegacySttCredentials(cfg, log5) {
13121
+ if (!legacySttWarned && hasLegacySttCredentials(cfg)) {
13122
+ legacySttWarned = true;
13123
+ log5?.info(
13124
+ "Voice: channels.qqbot.stt credentials (provider/baseUrl/apiKey/model) are deprecated and ignored; configure tools.media.audio.models instead \u2014 platform asr_refer_text is used when framework STT is absent"
13125
+ );
13126
+ }
13127
+ }
13128
+ async function processVoiceAttachment(att, cfg, usePlatformAsr, sttConfigured, audioPolicy, log5) {
13157
13129
  const rawAsrText = att.asr_refer_text?.trim() || void 0;
13158
13130
  const asrReferText = usePlatformAsr ? rawAsrText : void 0;
13159
13131
  const remoteUrl = normalizeUrl(att.voice_wav_url) || normalizeUrl(att.url) || void 0;
13160
- if (!sttCfg) {
13132
+ if (!sttConfigured) {
13161
13133
  if (!usePlatformAsr && rawAsrText) {
13162
- log5?.info(`Voice: STT not configured; platform asr_refer_text discarded (asrFallback not enabled)`);
13134
+ log5?.info(`Voice: framework STT not configured; platform asr_refer_text discarded (asrFallback: false)`);
13163
13135
  }
13164
13136
  if (asrReferText) {
13165
- log5?.debug?.(`Voice: using asr_refer_text (STT not configured, asrFallback enabled)`);
13137
+ log5?.debug?.(`Voice: using platform asr_refer_text (framework STT not configured)`);
13166
13138
  return { text: asrReferText, source: "asr", asrReferText, remoteUrl };
13167
13139
  }
13168
13140
  return {
@@ -13209,16 +13181,17 @@ async function processVoiceAttachment(att, sttCfg, usePlatformAsr, audioPolicy,
13209
13181
  }
13210
13182
  if (localPath) {
13211
13183
  try {
13212
- const transcript = await transcribeAudio(localPath, cfg2stt(sttCfg));
13184
+ const transcript = await transcribeAudioViaFramework(localPath, cfg);
13213
13185
  if (transcript) {
13214
- log5?.debug?.(`Voice STT: ${transcript.slice(0, 80)}...`);
13186
+ log5?.debug?.(`Voice STT (framework): ${transcript.slice(0, 80)}...`);
13215
13187
  return { text: transcript, source: "stt", duration, localPath, remoteUrl, asrReferText };
13216
13188
  }
13217
13189
  } catch (err) {
13218
- log5?.error(`Voice STT failed: ${err instanceof Error ? err.message : String(err)}`);
13190
+ log5?.error(`Voice STT (framework) failed: ${err instanceof Error ? err.message : String(err)}`);
13219
13191
  }
13220
13192
  }
13221
13193
  if (asrReferText) {
13194
+ log5?.debug?.(`Voice: falling back to platform asr_refer_text after framework STT failure`);
13222
13195
  return { text: asrReferText, source: "asr", duration, localPath, remoteUrl, asrReferText };
13223
13196
  }
13224
13197
  return {
@@ -13248,9 +13221,6 @@ function normalizeFormats(formats) {
13248
13221
  return lower.startsWith(".") ? lower : `.${lower}`;
13249
13222
  });
13250
13223
  }
13251
- function cfg2stt(sttCfg) {
13252
- return { channels: { qqbot: { stt: sttCfg } } };
13253
- }
13254
13224
  function normalizeUrl(url) {
13255
13225
  if (!url) return "";
13256
13226
  return url.startsWith("//") ? `https:${url}` : url;
@@ -15891,7 +15861,7 @@ function getQQBotApprovalCapability() {
15891
15861
 
15892
15862
  // src/features/proactive.ts
15893
15863
  init_plugin_logger();
15894
- var fs19 = __toESM(require("fs"), 1);
15864
+ var fs18 = __toESM(require("fs"), 1);
15895
15865
  var path20 = __toESM(require("path"), 1);
15896
15866
  init_bot_instance();
15897
15867
  init_config();
@@ -15902,8 +15872,8 @@ var KNOWN_USERS_FILE = path20.join(STORAGE_DIR, "known-users.json");
15902
15872
  var knownUsersCache = null;
15903
15873
  var cacheLastModified = 0;
15904
15874
  function ensureStorageDir() {
15905
- if (!fs19.existsSync(STORAGE_DIR)) {
15906
- fs19.mkdirSync(STORAGE_DIR, { recursive: true });
15875
+ if (!fs18.existsSync(STORAGE_DIR)) {
15876
+ fs18.mkdirSync(STORAGE_DIR, { recursive: true });
15907
15877
  }
15908
15878
  }
15909
15879
  function getUserKey(type, openid, accountId) {
@@ -15912,7 +15882,7 @@ function getUserKey(type, openid, accountId) {
15912
15882
  function loadKnownUsers() {
15913
15883
  if (knownUsersCache !== null) {
15914
15884
  try {
15915
- const stat = fs19.statSync(KNOWN_USERS_FILE);
15885
+ const stat = fs18.statSync(KNOWN_USERS_FILE);
15916
15886
  if (stat.mtimeMs <= cacheLastModified) {
15917
15887
  return knownUsersCache;
15918
15888
  }
@@ -15922,14 +15892,14 @@ function loadKnownUsers() {
15922
15892
  }
15923
15893
  const users = /* @__PURE__ */ new Map();
15924
15894
  try {
15925
- if (fs19.existsSync(KNOWN_USERS_FILE)) {
15926
- const data = fs19.readFileSync(KNOWN_USERS_FILE, "utf-8");
15895
+ if (fs18.existsSync(KNOWN_USERS_FILE)) {
15896
+ const data = fs18.readFileSync(KNOWN_USERS_FILE, "utf-8");
15927
15897
  const parsed = JSON.parse(data);
15928
15898
  for (const user of parsed) {
15929
15899
  const key2 = getUserKey(user.type, user.openid, user.accountId);
15930
15900
  users.set(key2, user);
15931
15901
  }
15932
- cacheLastModified = fs19.statSync(KNOWN_USERS_FILE).mtimeMs;
15902
+ cacheLastModified = fs18.statSync(KNOWN_USERS_FILE).mtimeMs;
15933
15903
  }
15934
15904
  } catch (err) {
15935
15905
  log3.error(`Failed to load known users: ${err}`);
@@ -15941,7 +15911,7 @@ function saveKnownUsers(users) {
15941
15911
  try {
15942
15912
  ensureStorageDir();
15943
15913
  const data = Array.from(users.values());
15944
- fs19.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(data, null, 2), "utf-8");
15914
+ fs18.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(data, null, 2), "utf-8");
15945
15915
  cacheLastModified = Date.now();
15946
15916
  knownUsersCache = users;
15947
15917
  } catch (err) {