@jerryliang122/openclaw-qqbot 2.0.0 → 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/dist/index.d.cts CHANGED
@@ -203,9 +203,7 @@ interface QQBotAccountConfig {
203
203
  sendMode?: 'stream' | 'static';
204
204
  };
205
205
  /**
206
- * STT (语音转文字) 行为开关
207
- * 转录凭证统一走框架 tools.media.audio.models;
208
- * 框架未配置时直接使用 QQ 平台转写(asr_refer_text)
206
+ * STT (语音转文字) 历史遗留配置块(整块被忽略,详见 STTChannelConfig)
209
207
  */
210
208
  stt?: STTChannelConfig;
211
209
  /**
@@ -289,25 +287,15 @@ interface AudioFormatPolicy {
289
287
  transcodeEnabled?: boolean;
290
288
  }
291
289
  /**
292
- * STT (语音转文字) 配置
290
+ * STT (语音转文字) 配置块——历史遗留形状
293
291
  *
294
- * 2026-10 起转录统一走框架音频理解管线(tools.media.audio.models 凭证),
295
- * 本块只保留行为开关;旧凭证键已废弃(检测到会打迁移提示日志)。
296
- * 框架 STT 未配置时,QQ 平台转写(asr_refer_text)直接作为唯一来源。
292
+ * 2026-10-04 起整块被忽略(含历史行为开关 enabled/asrFallback 与旧凭证
293
+ * 键):STT 启停只由框架 `tools.media.models`(capabilities 含 "audio" 的
294
+ * 条目)+ `tools.media.audio.enabled` 控制。本接口仅为遗留检测
295
+ * (hasLegacySttConfig + 一次性迁移提示)保留键的类型形状。
297
296
  */
298
297
  interface STTChannelConfig {
299
- /**
300
- * 是否启用框架 STT 转录(默认 true)。
301
- * false = 不调用外部 STT,语音只用平台转写(或无转写时占位文本)。
302
- */
303
- enabled?: boolean;
304
- /**
305
- * 平台转写(asr_refer_text)参与开关。默认参与:
306
- * 框架 STT 未配置时直接作为唯一来源,STT 失败/为空时兜底。
307
- * 设为 false 恢复严格模式——所有场景丢弃平台转写。
308
- */
309
- asrFallback?: boolean;
310
- /** @deprecated 2026-10 起忽略——STT 凭证统一配置在框架 tools.media.audio.models */
298
+ /** @deprecated 2026-10 起忽略——STT 配置统一走框架 tools.media.models */
311
299
  provider?: string;
312
300
  /** @deprecated 同上 */
313
301
  baseUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jerryliang122/openclaw-qqbot",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "QQ Bot channel plugin for OpenClaw (independently maintained fork)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -164,10 +164,12 @@ function buildDynamicCtx(
164
164
  lines.push(`- Voice: ${voiceRefs.join(', ')}`);
165
165
  }
166
166
 
167
- // ASR:source==='asr' 的 text,或任意 transcript 上的 asrReferText
167
+ // ASR:仅平台转写来源(source==='asr')——严格信框架后 stt/fallback
168
+ // transcript 不再携带 asrReferText
168
169
  const asrTexts = unique(
169
170
  transcripts
170
- .map((t) => (t.source === 'asr' ? t.text : t.asrReferText))
171
+ .filter((t) => t.source === 'asr')
172
+ .map((t) => t.text)
171
173
  .filter(isNonEmpty),
172
174
  );
173
175
  if (asrTexts.length > 0) {
@@ -15,7 +15,7 @@ import {
15
15
  isVoiceAttachment,
16
16
  } from '@tencent-connect/qqbot-nodejs/protocol';
17
17
  import type { MessageAttachment } from '../types.js';
18
- import { shouldUsePlatformAsr, isFrameworkSttConfigured, hasLegacySttCredentials, transcribeAudioViaFramework } from '../utils/stt.js';
18
+ import { isFrameworkSttConfigured, hasLegacySttConfig, transcribeAudioViaFramework } from '../utils/stt.js';
19
19
  import { formatVoiceText, formatDuration, type VoiceTranscript, type TranscriptSource } from '../utils/voice-text.js';
20
20
  import { downloadRemoteMedia } from '../adapter/media.js';
21
21
  import { getAdapters } from '../adapter/resolve.js';
@@ -96,9 +96,8 @@ export async function processAttachments(
96
96
  cfg: Record<string, unknown>,
97
97
  log?: Log,
98
98
  ): Promise<ProcessedAttachments> {
99
- const usePlatformAsr = shouldUsePlatformAsr(cfg);
100
99
  const sttConfigured = isFrameworkSttConfigured(cfg);
101
- warnLegacySttCredentials(cfg, log);
100
+ warnLegacySttConfig(cfg, log);
102
101
  const audioPolicy = resolveAudioPolicy(cfg);
103
102
 
104
103
  const imageUrls: string[] = [];
@@ -121,7 +120,7 @@ export async function processAttachments(
121
120
  }
122
121
 
123
122
  if (isVoice) {
124
- const transcript = await processVoiceAttachment(att, cfg, usePlatformAsr, sttConfigured, audioPolicy, log);
123
+ const transcript = await processVoiceAttachment(att, cfg, sttConfigured, audioPolicy, log);
125
124
  return { type: 'voice' as const, transcript };
126
125
  }
127
126
 
@@ -209,15 +208,15 @@ function kindFromContentType(contentType: string | undefined): InboundMediaEntry
209
208
 
210
209
  // ── 语音处理 ──
211
210
 
212
- /** 废弃插件级 STT 凭证的一次性迁移提示(每进程一条,避免每条语音刷屏) */
211
+ /** 废弃 channels.qqbot.stt 配置块的一次性迁移提示(每进程一条,避免每条语音刷屏) */
213
212
  let legacySttWarned = false;
214
213
 
215
- function warnLegacySttCredentials(cfg: Record<string, unknown>, log?: Log): void {
216
- if (!legacySttWarned && hasLegacySttCredentials(cfg)) {
214
+ function warnLegacySttConfig(cfg: Record<string, unknown>, log?: Log): void {
215
+ if (!legacySttWarned && hasLegacySttConfig(cfg)) {
217
216
  legacySttWarned = true;
218
217
  log?.info(
219
- 'Voice: channels.qqbot.stt credentials (provider/baseUrl/apiKey/model) are deprecated and ignored; ' +
220
- 'configure tools.media.audio.models instead — platform asr_refer_text is used when framework STT is absent',
218
+ 'Voice: channels.qqbot.stt is deprecated and ignored entirely (credentials + enabled/asrFallback); ' +
219
+ 'configure an audio-capable tools.media.models entry for STT — platform asr_refer_text is used only when framework STT is absent',
221
220
  );
222
221
  }
223
222
  }
@@ -225,24 +224,17 @@ function warnLegacySttCredentials(cfg: Record<string, unknown>, log?: Log): void
225
224
  async function processVoiceAttachment(
226
225
  att: MessageAttachment,
227
226
  cfg: Record<string, unknown>,
228
- usePlatformAsr: boolean,
229
227
  sttConfigured: boolean,
230
228
  audioPolicy: AudioPolicyResolved,
231
229
  log?: Log,
232
230
  ): Promise<VoiceTranscript> {
233
- // 平台转写(asr_refer_text,QQ 平台自动 STT 随事件 JSON 下发):
234
- // 默认参与——框架 STT 未配置时直接作为唯一来源,STT 失败时兜底;
235
- // 仅 asrFallback: false(严格模式)时在所有场景丢弃。
236
- const rawAsrText = att.asr_refer_text?.trim() || undefined;
237
- const asrReferText = usePlatformAsr ? rawAsrText : undefined;
238
231
  // 远端 URL 兜底:优先 wav_url,其次原始 url
239
232
  const remoteUrl = normalizeUrl(att.voice_wav_url) || normalizeUrl(att.url) || undefined;
240
233
 
241
- // 框架 STT 未配置 → 平台转写直接作为 transcript(无下载、无外部调用)
234
+ // 框架 STT 未配置 → 平台转写(asr_refer_text,QQ 平台自动 STT 随事件
235
+ // JSON 下发)直接作为唯一来源(无下载、零外部调用);无平台转写 → 占位。
242
236
  if (!sttConfigured) {
243
- if (!usePlatformAsr && rawAsrText) {
244
- log?.info(`Voice: framework STT not configured; platform asr_refer_text discarded (asrFallback: false)`);
245
- }
237
+ const asrReferText = att.asr_refer_text?.trim() || undefined;
246
238
  if (asrReferText) {
247
239
  log?.debug?.(`Voice: using platform asr_refer_text (framework STT not configured)`);
248
240
  return { text: asrReferText, source: 'asr', asrReferText, remoteUrl };
@@ -250,11 +242,12 @@ async function processVoiceAttachment(
250
242
  return {
251
243
  text: '[Voice message - transcription unavailable]',
252
244
  source: 'fallback',
253
- asrReferText,
254
245
  remoteUrl,
255
246
  };
256
247
  }
257
248
 
249
+ // 框架 STT 已配置 → 严格信框架:下载/转码后提交框架转录,
250
+ // 失败/为空/下载失败一律占位文本,不回退平台转写。
258
251
  let localPath: string | undefined;
259
252
  let duration: number | undefined;
260
253
 
@@ -298,25 +291,19 @@ async function processVoiceAttachment(
298
291
  const transcript = await transcribeAudioViaFramework(localPath, cfg);
299
292
  if (transcript) {
300
293
  log?.debug?.(`Voice STT (framework): ${transcript.slice(0, 80)}...`);
301
- return { text: transcript, source: 'stt', duration, localPath, remoteUrl, asrReferText };
294
+ return { text: transcript, source: 'stt', duration, localPath, remoteUrl };
302
295
  }
303
296
  } catch (err) {
304
297
  log?.error(`Voice STT (framework) failed: ${err instanceof Error ? err.message : String(err)}`);
305
298
  }
306
299
  }
307
300
 
308
- if (asrReferText) {
309
- log?.debug?.(`Voice: falling back to platform asr_refer_text after framework STT failure`);
310
- return { text: asrReferText, source: 'asr', duration, localPath, remoteUrl, asrReferText };
311
- }
312
-
313
301
  return {
314
302
  text: '[Voice message - transcription failed]',
315
303
  source: 'fallback',
316
304
  duration,
317
305
  localPath,
318
306
  remoteUrl,
319
- asrReferText,
320
307
  };
321
308
  }
322
309
 
package/src/types.ts CHANGED
@@ -211,9 +211,7 @@ export interface QQBotAccountConfig {
211
211
  sendMode?: 'stream' | 'static';
212
212
  };
213
213
  /**
214
- * STT (语音转文字) 行为开关
215
- * 转录凭证统一走框架 tools.media.audio.models;
216
- * 框架未配置时直接使用 QQ 平台转写(asr_refer_text)
214
+ * STT (语音转文字) 历史遗留配置块(整块被忽略,详见 STTChannelConfig)
217
215
  */
218
216
  stt?: STTChannelConfig;
219
217
  /**
@@ -301,25 +299,15 @@ export interface AudioFormatPolicy {
301
299
  }
302
300
 
303
301
  /**
304
- * STT (语音转文字) 配置
302
+ * STT (语音转文字) 配置块——历史遗留形状
305
303
  *
306
- * 2026-10 起转录统一走框架音频理解管线(tools.media.audio.models 凭证),
307
- * 本块只保留行为开关;旧凭证键已废弃(检测到会打迁移提示日志)。
308
- * 框架 STT 未配置时,QQ 平台转写(asr_refer_text)直接作为唯一来源。
304
+ * 2026-10-04 起整块被忽略(含历史行为开关 enabled/asrFallback 与旧凭证
305
+ * 键):STT 启停只由框架 `tools.media.models`(capabilities 含 "audio" 的
306
+ * 条目)+ `tools.media.audio.enabled` 控制。本接口仅为遗留检测
307
+ * (hasLegacySttConfig + 一次性迁移提示)保留键的类型形状。
309
308
  */
310
309
  export interface STTChannelConfig {
311
- /**
312
- * 是否启用框架 STT 转录(默认 true)。
313
- * false = 不调用外部 STT,语音只用平台转写(或无转写时占位文本)。
314
- */
315
- enabled?: boolean;
316
- /**
317
- * 平台转写(asr_refer_text)参与开关。默认参与:
318
- * 框架 STT 未配置时直接作为唯一来源,STT 失败/为空时兜底。
319
- * 设为 false 恢复严格模式——所有场景丢弃平台转写。
320
- */
321
- asrFallback?: boolean;
322
- /** @deprecated 2026-10 起忽略——STT 凭证统一配置在框架 tools.media.audio.models */
310
+ /** @deprecated 2026-10 起忽略——STT 配置统一走框架 tools.media.models */
323
311
  provider?: string;
324
312
  /** @deprecated 同上 */
325
313
  baseUrl?: string;
package/src/utils/stt.ts CHANGED
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * 转录统一委托给 openclaw/plugin-sdk/media-understanding-runtime 的
5
5
  * `transcribeAudioFile`(provider 注册表、附件缓存、SSRF 策略与错误语义
6
- * 均由框架维护),插件不再自带 OpenAI 兼容 HTTP 调用;STT 凭证只认
7
- * 框架级 `tools.media.audio.models` 配置(与内置 telegram 通道一致)。
6
+ * 均由框架维护),插件不自带 OpenAI 兼容 HTTP 调用;STT 配置只认框架级
7
+ * `tools.media.models` 的 audio 能力条目(与内置 telegram 通道一致)。
8
8
  *
9
- * 平台转写(asr_refer_text):QQ 平台对语音消息自动 STT 并随事件 JSON 下发。
10
- * 默认策略——框架 STT 未配置时**直接采用平台转写**;已配置时平台转写作为
11
- * 自有转录失败/为空的兜底。`channels.qqbot.stt.asrFallback: false` 可整体
12
- * 禁用平台转写(严格模式,恢复 2026-08-17 的丢弃行为)。
9
+ * 硬编码两分支策略(2026-10-04 起,无插件级开关):
10
+ * - 框架 STT 未配置 → QQ 平台转写(asr_refer_text,随事件 JSON 下发)直
11
+ * 接作为唯一来源(零下载、零外部调用);无平台转写 → 占位文本。
12
+ * - 框架 STT 已配置 → 下载语音提交框架转录;**严格信框架**——失败/为空/
13
+ * 下载失败一律占位文本,不回退平台转写。
13
14
  */
14
15
  import * as path from 'node:path';
15
16
  import { transcribeAudioFile } from 'openclaw/plugin-sdk/media-understanding-runtime';
@@ -17,57 +18,61 @@ import { transcribeAudioFile } from 'openclaw/plugin-sdk/media-understanding-run
17
18
  type TranscribeParams = Parameters<typeof transcribeAudioFile>[0];
18
19
 
19
20
  /**
20
- * 平台转写(asr_refer_text)是否参与(独立于框架 STT 配置读取)。
21
- * 默认 true;显式 `channels.qqbot.stt.asrFallback: false` 时关闭(严格模式)。
22
- */
23
- export function shouldUsePlatformAsr(cfg: Record<string, unknown>): boolean {
24
- const channels = asRecord(cfg.channels);
25
- const qqbot = asRecord(channels?.qqbot);
26
- return asRecord(qqbot?.stt)?.asrFallback !== false;
27
- }
28
-
29
- /**
30
- * 框架 STT(tools.media.audio)是否可用:
31
- * - `channels.qqbot.stt.enabled === false` → 插件级显式关闭(只用平台转写)
32
- * - `tools.media.audio.enabled === false` → 框架级关闭
33
- * - `models` 为空 → 未配置
21
+ * 框架 STT(语音转录)是否可用:
22
+ * - `tools.media.audio.enabled === false` → 框架级 per-capability 关闭
23
+ * - `tools.media.models` 无显式 `capabilities` 含 `"audio"` 的条目 → 未配置
34
24
  *
35
- * 仅做存在性探测控制流程;provider 解析与实际调用由 transcribeAudioFile 完成。
25
+ * 规范路径是 `tools.media.models`——openclaw 2026.9.1 schema 中模型列表只
26
+ * 存在于此(`tools.media.audio` 块的类型为 `Omit<…, "models">`,不含
27
+ * models 键)。**只认显式 `capabilities` 标签**(有意保守):无标签 CLI
28
+ * 条目按框架语义本就不参与共享列表的 audio 匹配;无标签 provider 条目框
29
+ * 架会从 provider 注册表推断能力,但插件侧无法廉价复刻注册表——宁可漏判
30
+ * (降级走平台转写,功能仍可用)也不误判(严格模式下误判会变成彻底无转
31
+ * 写)。
32
+ *
33
+ * 仅做存在性探测控制流程;provider 解析与实际调用由 transcribeAudioFile
34
+ * 完成(错误在调用点捕获处理)。
36
35
  */
37
36
  export function isFrameworkSttConfigured(cfg: Record<string, unknown>): boolean {
38
- const channels = asRecord(cfg.channels);
39
- const qqbot = asRecord(channels?.qqbot);
40
- if (asRecord(qqbot?.stt)?.enabled === false) {
41
- return false;
42
- }
43
37
  const tools = asRecord(cfg.tools);
44
38
  const media = asRecord(tools?.media);
45
- const audio = asRecord(media?.audio);
46
- if (!audio || audio.enabled === false) {
39
+ if (asRecord(media?.audio)?.enabled === false) {
47
40
  return false;
48
41
  }
49
- return Array.isArray(audio.models) && audio.models.length > 0;
42
+ const models = media?.models;
43
+ if (!Array.isArray(models)) {
44
+ return false;
45
+ }
46
+ return models.some((entry) => {
47
+ const capabilities = asRecord(entry)?.capabilities;
48
+ return Array.isArray(capabilities) && capabilities.includes('audio');
49
+ });
50
50
  }
51
51
 
52
52
  /**
53
- * 检测已废弃的插件级 STT 凭证(channels.qqbot.stt.provider/baseUrl/apiKey/model)。
54
- * 2026-10 起凭证统一走框架 `tools.media.audio.models`,旧键被忽略;
55
- * 返回 true 时调用方打一次性迁移提示。
53
+ * 检测已废弃的 `channels.qqbot.stt` 配置块:旧凭证键
54
+ * (provider/baseUrl/apiKey/model)与历史行为开关(enabled/asrFallback)。
55
+ * 2026-10-04 起整块被忽略——STT 启停只由框架 `tools.media.models`(audio
56
+ * 能力条目)+ `tools.media.audio.enabled` 控制;返回 true 时调用方打一次
57
+ * 性迁移提示。
56
58
  */
57
- export function hasLegacySttCredentials(cfg: Record<string, unknown>): boolean {
59
+ export function hasLegacySttConfig(cfg: Record<string, unknown>): boolean {
58
60
  const channels = asRecord(cfg.channels);
59
61
  const qqbot = asRecord(channels?.qqbot);
60
62
  const stt = asRecord(qqbot?.stt);
61
63
  if (!stt) return false;
62
- return ['provider', 'baseUrl', 'apiKey', 'model'].some(
63
- (key) => typeof stt[key] === 'string' && (stt[key] as string).trim().length > 0,
64
- );
64
+ const legacyKeys = ['provider', 'baseUrl', 'apiKey', 'model', 'enabled', 'asrFallback'] as const;
65
+ return legacyKeys.some((key) => {
66
+ const value = stt[key];
67
+ if (typeof value === 'string') return value.trim().length > 0;
68
+ return value != null;
69
+ });
65
70
  }
66
71
 
67
72
  /**
68
73
  * 经框架音频理解管线转录本地音频文件。
69
74
  * 返回修剪后的转录文本;无文本返回 null。
70
- * provider 缺失/调用失败会抛错,由调用方捕获后走平台转写兜底。
75
+ * provider 缺失/调用失败会抛错,由调用方捕获后输出失败占位文本。
71
76
  */
72
77
  export async function transcribeAudioViaFramework(
73
78
  audioPath: string,