@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/README.md +17 -12
- package/README.zh.md +17 -12
- package/dist/index.cjs +27 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -19
- package/package.json +1 -1
- package/src/dispatch/body-assembler.ts +4 -2
- package/src/middleware/attachment.ts +14 -27
- package/src/types.ts +7 -19
- package/src/utils/stt.ts +42 -37
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
|
|
295
|
-
*
|
|
296
|
-
*
|
|
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
|
@@ -164,10 +164,12 @@ function buildDynamicCtx(
|
|
|
164
164
|
lines.push(`- Voice: ${voiceRefs.join(', ')}`);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// ASR
|
|
167
|
+
// ASR:仅平台转写来源(source==='asr')——严格信框架后 stt/fallback
|
|
168
|
+
// transcript 不再携带 asrReferText
|
|
168
169
|
const asrTexts = unique(
|
|
169
170
|
transcripts
|
|
170
|
-
.
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
211
|
+
/** 废弃 channels.qqbot.stt 配置块的一次性迁移提示(每进程一条,避免每条语音刷屏) */
|
|
213
212
|
let legacySttWarned = false;
|
|
214
213
|
|
|
215
|
-
function
|
|
216
|
-
if (!legacySttWarned &&
|
|
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
|
|
220
|
-
'configure tools.media.
|
|
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 未配置 →
|
|
234
|
+
// 框架 STT 未配置 → 平台转写(asr_refer_text,QQ 平台自动 STT 随事件
|
|
235
|
+
// JSON 下发)直接作为唯一来源(无下载、零外部调用);无平台转写 → 占位。
|
|
242
236
|
if (!sttConfigured) {
|
|
243
|
-
|
|
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
|
|
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
|
|
307
|
-
*
|
|
308
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* 均由框架维护),插件不自带 OpenAI 兼容 HTTP 调用;STT 配置只认框架级
|
|
7
|
+
* `tools.media.models` 的 audio 能力条目(与内置 telegram 通道一致)。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
46
|
-
if (!audio || audio.enabled === false) {
|
|
39
|
+
if (asRecord(media?.audio)?.enabled === false) {
|
|
47
40
|
return false;
|
|
48
41
|
}
|
|
49
|
-
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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,
|