@openclaw-china/qqbot 0.1.3 → 0.1.5
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.ts +64 -0
- package/dist/index.js +339 -10
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +44 -31
- package/package.json +2 -4
- package/clawdbot.plugin.json +0 -27
- package/moltbot.plugin.json +0 -27
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,22 @@ declare const QQBotConfigSchema: z.ZodObject<{
|
|
|
4
4
|
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
5
5
|
appId: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
|
|
6
6
|
clientSecret: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
|
|
7
|
+
asr: z.ZodOptional<z.ZodObject<{
|
|
8
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
9
|
+
appId: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
|
|
10
|
+
secretId: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
|
|
11
|
+
secretKey: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
appId?: string | undefined;
|
|
15
|
+
secretId?: string | undefined;
|
|
16
|
+
secretKey?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
enabled?: boolean | undefined;
|
|
19
|
+
appId?: unknown;
|
|
20
|
+
secretId?: unknown;
|
|
21
|
+
secretKey?: unknown;
|
|
22
|
+
}>>;
|
|
7
23
|
markdownSupport: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
24
|
dmPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "pairing", "allowlist"]>>>;
|
|
9
25
|
groupPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "allowlist", "disabled"]>>>;
|
|
@@ -28,12 +44,24 @@ declare const QQBotConfigSchema: z.ZodObject<{
|
|
|
28
44
|
mediaTimeoutMs: number;
|
|
29
45
|
appId?: string | undefined;
|
|
30
46
|
clientSecret?: string | undefined;
|
|
47
|
+
asr?: {
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
appId?: string | undefined;
|
|
50
|
+
secretId?: string | undefined;
|
|
51
|
+
secretKey?: string | undefined;
|
|
52
|
+
} | undefined;
|
|
31
53
|
allowFrom?: string[] | undefined;
|
|
32
54
|
groupAllowFrom?: string[] | undefined;
|
|
33
55
|
}, {
|
|
34
56
|
enabled?: boolean | undefined;
|
|
35
57
|
appId?: unknown;
|
|
36
58
|
clientSecret?: unknown;
|
|
59
|
+
asr?: {
|
|
60
|
+
enabled?: boolean | undefined;
|
|
61
|
+
appId?: unknown;
|
|
62
|
+
secretId?: unknown;
|
|
63
|
+
secretKey?: unknown;
|
|
64
|
+
} | undefined;
|
|
37
65
|
markdownSupport?: boolean | undefined;
|
|
38
66
|
dmPolicy?: "open" | "pairing" | "allowlist" | undefined;
|
|
39
67
|
groupPolicy?: "open" | "allowlist" | "disabled" | undefined;
|
|
@@ -126,6 +154,24 @@ declare const qqbotPlugin: {
|
|
|
126
154
|
clientSecret: {
|
|
127
155
|
type: string;
|
|
128
156
|
};
|
|
157
|
+
asr: {
|
|
158
|
+
type: string;
|
|
159
|
+
additionalProperties: boolean;
|
|
160
|
+
properties: {
|
|
161
|
+
enabled: {
|
|
162
|
+
type: string;
|
|
163
|
+
};
|
|
164
|
+
appId: {
|
|
165
|
+
type: string;
|
|
166
|
+
};
|
|
167
|
+
secretId: {
|
|
168
|
+
type: string;
|
|
169
|
+
};
|
|
170
|
+
secretKey: {
|
|
171
|
+
type: string;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
};
|
|
129
175
|
markdownSupport: {
|
|
130
176
|
type: string;
|
|
131
177
|
};
|
|
@@ -406,6 +452,24 @@ declare const plugin: {
|
|
|
406
452
|
clientSecret: {
|
|
407
453
|
type: string;
|
|
408
454
|
};
|
|
455
|
+
asr: {
|
|
456
|
+
type: string;
|
|
457
|
+
additionalProperties: boolean;
|
|
458
|
+
properties: {
|
|
459
|
+
enabled: {
|
|
460
|
+
type: string;
|
|
461
|
+
};
|
|
462
|
+
appId: {
|
|
463
|
+
type: string;
|
|
464
|
+
};
|
|
465
|
+
secretId: {
|
|
466
|
+
type: string;
|
|
467
|
+
};
|
|
468
|
+
secretKey: {
|
|
469
|
+
type: string;
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
};
|
|
409
473
|
markdownSupport: {
|
|
410
474
|
type: string;
|
|
411
475
|
};
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import * as fsPromises from 'fs/promises';
|
|
6
|
+
import { createHmac } from 'crypto';
|
|
6
7
|
import WebSocket from 'ws';
|
|
7
8
|
|
|
8
9
|
var __defProp = Object.defineProperty;
|
|
@@ -4064,6 +4065,12 @@ var QQBotConfigSchema = external_exports.object({
|
|
|
4064
4065
|
enabled: external_exports.boolean().optional().default(true),
|
|
4065
4066
|
appId: optionalCoercedString,
|
|
4066
4067
|
clientSecret: optionalCoercedString,
|
|
4068
|
+
asr: external_exports.object({
|
|
4069
|
+
enabled: external_exports.boolean().optional().default(false),
|
|
4070
|
+
appId: optionalCoercedString,
|
|
4071
|
+
secretId: optionalCoercedString,
|
|
4072
|
+
secretKey: optionalCoercedString
|
|
4073
|
+
}).optional(),
|
|
4067
4074
|
markdownSupport: external_exports.boolean().optional().default(false),
|
|
4068
4075
|
dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
|
|
4069
4076
|
groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
|
|
@@ -4083,6 +4090,16 @@ function resolveQQBotCredentials(config) {
|
|
|
4083
4090
|
if (!config?.appId || !config?.clientSecret) return void 0;
|
|
4084
4091
|
return { appId: config.appId, clientSecret: config.clientSecret };
|
|
4085
4092
|
}
|
|
4093
|
+
function resolveQQBotASRCredentials(config) {
|
|
4094
|
+
const asr = config?.asr;
|
|
4095
|
+
if (!asr?.enabled) return void 0;
|
|
4096
|
+
if (!asr.appId || !asr.secretId || !asr.secretKey) return void 0;
|
|
4097
|
+
return {
|
|
4098
|
+
appId: asr.appId,
|
|
4099
|
+
secretId: asr.secretId,
|
|
4100
|
+
secretKey: asr.secretKey
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4086
4103
|
|
|
4087
4104
|
// ../../packages/shared/src/logger/logger.ts
|
|
4088
4105
|
function createLogger(prefix, opts) {
|
|
@@ -5085,6 +5102,163 @@ function appendCronHiddenPrompt(text) {
|
|
|
5085
5102
|
${CRON_HIDDEN_PROMPT}`;
|
|
5086
5103
|
}
|
|
5087
5104
|
|
|
5105
|
+
// ../../packages/shared/src/asr/errors.ts
|
|
5106
|
+
var ASRError = class extends Error {
|
|
5107
|
+
constructor(message, kind, provider, retryable = false) {
|
|
5108
|
+
super(message);
|
|
5109
|
+
this.kind = kind;
|
|
5110
|
+
this.provider = provider;
|
|
5111
|
+
this.retryable = retryable;
|
|
5112
|
+
this.name = "ASRError";
|
|
5113
|
+
}
|
|
5114
|
+
};
|
|
5115
|
+
var ASRTimeoutError = class extends ASRError {
|
|
5116
|
+
constructor(provider, timeoutMs) {
|
|
5117
|
+
super(`ASR request timeout after ${timeoutMs}ms`, "timeout", provider, true);
|
|
5118
|
+
this.timeoutMs = timeoutMs;
|
|
5119
|
+
this.name = "ASRTimeoutError";
|
|
5120
|
+
}
|
|
5121
|
+
};
|
|
5122
|
+
var ASRAuthError = class extends ASRError {
|
|
5123
|
+
constructor(provider, message, status) {
|
|
5124
|
+
super(message, "auth", provider, false);
|
|
5125
|
+
this.status = status;
|
|
5126
|
+
this.name = "ASRAuthError";
|
|
5127
|
+
}
|
|
5128
|
+
};
|
|
5129
|
+
var ASRRequestError = class extends ASRError {
|
|
5130
|
+
constructor(provider, message, status) {
|
|
5131
|
+
super(message, "request", provider, true);
|
|
5132
|
+
this.status = status;
|
|
5133
|
+
this.name = "ASRRequestError";
|
|
5134
|
+
}
|
|
5135
|
+
};
|
|
5136
|
+
var ASRResponseParseError = class extends ASRError {
|
|
5137
|
+
constructor(provider, bodySnippet) {
|
|
5138
|
+
super("ASR response is not valid JSON", "response_parse", provider, false);
|
|
5139
|
+
this.bodySnippet = bodySnippet;
|
|
5140
|
+
this.name = "ASRResponseParseError";
|
|
5141
|
+
}
|
|
5142
|
+
};
|
|
5143
|
+
var ASRServiceError = class extends ASRError {
|
|
5144
|
+
constructor(provider, message, serviceCode) {
|
|
5145
|
+
super(message, "service", provider, false);
|
|
5146
|
+
this.serviceCode = serviceCode;
|
|
5147
|
+
this.name = "ASRServiceError";
|
|
5148
|
+
}
|
|
5149
|
+
};
|
|
5150
|
+
var ASREmptyResultError = class extends ASRError {
|
|
5151
|
+
constructor(provider) {
|
|
5152
|
+
super("ASR returned empty transcript", "empty_result", provider, false);
|
|
5153
|
+
this.name = "ASREmptyResultError";
|
|
5154
|
+
}
|
|
5155
|
+
};
|
|
5156
|
+
|
|
5157
|
+
// ../../packages/shared/src/asr/tencent-flash.ts
|
|
5158
|
+
var ASR_FLASH_HOST = "asr.cloud.tencent.com";
|
|
5159
|
+
var ASR_FLASH_PATH_PREFIX = "/asr/flash/v1";
|
|
5160
|
+
var ASR_FLASH_URL_PREFIX = `https://${ASR_FLASH_HOST}${ASR_FLASH_PATH_PREFIX}`;
|
|
5161
|
+
var ASR_PROVIDER = "tencent-flash";
|
|
5162
|
+
function encodeQueryValue(value) {
|
|
5163
|
+
return encodeURIComponent(value).replace(/%20/g, "+").replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
5164
|
+
}
|
|
5165
|
+
function buildSignedQuery(params) {
|
|
5166
|
+
return Object.entries(params).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeQueryValue(value)}`).join("&");
|
|
5167
|
+
}
|
|
5168
|
+
function extractTranscript(payload) {
|
|
5169
|
+
const items = Array.isArray(payload.flash_result) ? payload.flash_result : [];
|
|
5170
|
+
const lines = [];
|
|
5171
|
+
for (const item of items) {
|
|
5172
|
+
if (typeof item?.text === "string" && item.text.trim()) {
|
|
5173
|
+
lines.push(item.text.trim());
|
|
5174
|
+
continue;
|
|
5175
|
+
}
|
|
5176
|
+
const sentenceList = Array.isArray(item?.sentence_list) ? item.sentence_list : [];
|
|
5177
|
+
for (const sentence of sentenceList) {
|
|
5178
|
+
if (typeof sentence?.text === "string" && sentence.text.trim()) {
|
|
5179
|
+
lines.push(sentence.text.trim());
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
return lines.join("\n").trim();
|
|
5184
|
+
}
|
|
5185
|
+
async function transcribeTencentFlash(params) {
|
|
5186
|
+
const { audio, config } = params;
|
|
5187
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
5188
|
+
const engineType = config.engineType ?? "16k_zh";
|
|
5189
|
+
const voiceFormat = config.voiceFormat ?? "silk";
|
|
5190
|
+
const query = buildSignedQuery({
|
|
5191
|
+
engine_type: engineType,
|
|
5192
|
+
secretid: config.secretId,
|
|
5193
|
+
timestamp,
|
|
5194
|
+
voice_format: voiceFormat
|
|
5195
|
+
});
|
|
5196
|
+
const signText = `POST${ASR_FLASH_HOST}${ASR_FLASH_PATH_PREFIX}/${config.appId}?${query}`;
|
|
5197
|
+
const authorization = createHmac("sha1", config.secretKey).update(signText).digest("base64");
|
|
5198
|
+
const url = `${ASR_FLASH_URL_PREFIX}/${config.appId}?${query}`;
|
|
5199
|
+
const timeoutMs = config.timeoutMs ?? 3e4;
|
|
5200
|
+
const controller = new AbortController();
|
|
5201
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
5202
|
+
try {
|
|
5203
|
+
const response = await fetch(url, {
|
|
5204
|
+
method: "POST",
|
|
5205
|
+
headers: {
|
|
5206
|
+
Authorization: authorization,
|
|
5207
|
+
"Content-Type": "application/octet-stream"
|
|
5208
|
+
},
|
|
5209
|
+
body: audio,
|
|
5210
|
+
signal: controller.signal
|
|
5211
|
+
});
|
|
5212
|
+
const bodyText = await response.text();
|
|
5213
|
+
let payload;
|
|
5214
|
+
try {
|
|
5215
|
+
payload = JSON.parse(bodyText);
|
|
5216
|
+
} catch {
|
|
5217
|
+
throw new ASRResponseParseError(ASR_PROVIDER, bodyText.slice(0, 300));
|
|
5218
|
+
}
|
|
5219
|
+
if (!response.ok) {
|
|
5220
|
+
const message = payload.message ?? `HTTP ${response.status}`;
|
|
5221
|
+
if (response.status === 401 || response.status === 403) {
|
|
5222
|
+
throw new ASRAuthError(
|
|
5223
|
+
ASR_PROVIDER,
|
|
5224
|
+
`Tencent Flash ASR authentication failed: ${message}`,
|
|
5225
|
+
response.status
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
throw new ASRRequestError(
|
|
5229
|
+
ASR_PROVIDER,
|
|
5230
|
+
`Tencent Flash ASR request failed: ${message}`,
|
|
5231
|
+
response.status
|
|
5232
|
+
);
|
|
5233
|
+
}
|
|
5234
|
+
if (payload.code !== 0) {
|
|
5235
|
+
throw new ASRServiceError(
|
|
5236
|
+
ASR_PROVIDER,
|
|
5237
|
+
`Tencent Flash ASR failed: ${payload.message ?? "unknown error"} (code=${payload.code})`,
|
|
5238
|
+
payload.code
|
|
5239
|
+
);
|
|
5240
|
+
}
|
|
5241
|
+
const transcript = extractTranscript(payload);
|
|
5242
|
+
if (!transcript) {
|
|
5243
|
+
throw new ASREmptyResultError(ASR_PROVIDER);
|
|
5244
|
+
}
|
|
5245
|
+
return transcript;
|
|
5246
|
+
} catch (error) {
|
|
5247
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
5248
|
+
throw new ASRTimeoutError(ASR_PROVIDER, timeoutMs);
|
|
5249
|
+
}
|
|
5250
|
+
if (error instanceof ASRResponseParseError || error instanceof ASRAuthError || error instanceof ASRRequestError || error instanceof ASRServiceError || error instanceof ASREmptyResultError || error instanceof ASRTimeoutError) {
|
|
5251
|
+
throw error;
|
|
5252
|
+
}
|
|
5253
|
+
throw new ASRRequestError(
|
|
5254
|
+
ASR_PROVIDER,
|
|
5255
|
+
`Tencent Flash ASR request failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5256
|
+
);
|
|
5257
|
+
} finally {
|
|
5258
|
+
clearTimeout(timeoutId);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5088
5262
|
// src/client.ts
|
|
5089
5263
|
var API_BASE = "https://api.sgroup.qq.com";
|
|
5090
5264
|
var TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
|
|
@@ -5192,7 +5366,8 @@ async function sendGroupMessage(params) {
|
|
|
5192
5366
|
messageId: params.messageId,
|
|
5193
5367
|
markdown: params.markdown
|
|
5194
5368
|
});
|
|
5195
|
-
|
|
5369
|
+
const groupOpenidLower = params.groupOpenid.toLowerCase();
|
|
5370
|
+
return apiPost(params.accessToken, `/v2/groups/${groupOpenidLower}/messages`, body, {
|
|
5196
5371
|
timeout: 15e3
|
|
5197
5372
|
});
|
|
5198
5373
|
}
|
|
@@ -5201,7 +5376,8 @@ async function sendChannelMessage(params) {
|
|
|
5201
5376
|
if (params.messageId) {
|
|
5202
5377
|
body.msg_id = params.messageId;
|
|
5203
5378
|
}
|
|
5204
|
-
|
|
5379
|
+
const channelIdLower = params.channelId.toLowerCase();
|
|
5380
|
+
return apiPost(params.accessToken, `/channels/${channelIdLower}/messages`, body, {
|
|
5205
5381
|
timeout: 15e3
|
|
5206
5382
|
});
|
|
5207
5383
|
}
|
|
@@ -5624,6 +5800,9 @@ function parseTextWithAttachments(payload) {
|
|
|
5624
5800
|
attachments
|
|
5625
5801
|
};
|
|
5626
5802
|
}
|
|
5803
|
+
var VOICE_ASR_FALLBACK_TEXT = "\u5F53\u524D\u8BED\u97F3\u529F\u80FD\u672A\u542F\u52A8\u6216\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
5804
|
+
var VOICE_EXTENSIONS = [".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex"];
|
|
5805
|
+
var VOICE_ASR_ERROR_MAX_LENGTH = 500;
|
|
5627
5806
|
function isHttpUrl2(value) {
|
|
5628
5807
|
return /^https?:\/\//i.test(value);
|
|
5629
5808
|
}
|
|
@@ -5641,20 +5820,58 @@ function isImageAttachment(att) {
|
|
|
5641
5820
|
return false;
|
|
5642
5821
|
}
|
|
5643
5822
|
}
|
|
5823
|
+
function isVoiceAttachment(att) {
|
|
5824
|
+
const contentType = att.contentType?.trim().toLowerCase() ?? "";
|
|
5825
|
+
if (contentType === "voice" || contentType.startsWith("audio/")) {
|
|
5826
|
+
return true;
|
|
5827
|
+
}
|
|
5828
|
+
const lowerName = att.filename?.trim().toLowerCase() ?? "";
|
|
5829
|
+
if (VOICE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
|
5830
|
+
return true;
|
|
5831
|
+
}
|
|
5832
|
+
try {
|
|
5833
|
+
const pathname = new URL(att.url).pathname.toLowerCase();
|
|
5834
|
+
return VOICE_EXTENSIONS.some((ext) => pathname.endsWith(ext));
|
|
5835
|
+
} catch {
|
|
5836
|
+
return false;
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5644
5839
|
function scheduleTempCleanup(filePath) {
|
|
5645
5840
|
const timer = setTimeout(() => {
|
|
5646
5841
|
void cleanupFileSafe(filePath);
|
|
5647
5842
|
}, 20 * 60 * 1e3);
|
|
5648
5843
|
timer.unref?.();
|
|
5649
5844
|
}
|
|
5845
|
+
function trimTextForReply(text, maxLength) {
|
|
5846
|
+
if (text.length <= maxLength) return text;
|
|
5847
|
+
return `${text.slice(0, maxLength)}...`;
|
|
5848
|
+
}
|
|
5849
|
+
function buildVoiceASRFallbackReply(errorMessage) {
|
|
5850
|
+
const detail = errorMessage?.trim();
|
|
5851
|
+
if (!detail) return VOICE_ASR_FALLBACK_TEXT;
|
|
5852
|
+
return `${VOICE_ASR_FALLBACK_TEXT}
|
|
5853
|
+
|
|
5854
|
+
\u63A5\u53E3\u9519\u8BEF\uFF1A${trimTextForReply(detail, VOICE_ASR_ERROR_MAX_LENGTH)}`;
|
|
5855
|
+
}
|
|
5650
5856
|
async function resolveInboundAttachmentsForAgent(params) {
|
|
5651
5857
|
const { attachments, qqCfg, logger } = params;
|
|
5652
5858
|
const list = attachments ?? [];
|
|
5653
|
-
if (list.length === 0)
|
|
5859
|
+
if (list.length === 0) {
|
|
5860
|
+
return {
|
|
5861
|
+
attachments: [],
|
|
5862
|
+
hasVoiceAttachment: false,
|
|
5863
|
+
hasVoiceTranscript: false,
|
|
5864
|
+
asrErrorMessage: void 0
|
|
5865
|
+
};
|
|
5866
|
+
}
|
|
5654
5867
|
const timeout = qqCfg.mediaTimeoutMs ?? 3e4;
|
|
5655
5868
|
const maxFileSizeMB = qqCfg.maxFileSizeMB ?? 100;
|
|
5656
5869
|
const maxSize = Math.floor(maxFileSizeMB * 1024 * 1024);
|
|
5870
|
+
const asrCredentials = resolveQQBotASRCredentials(qqCfg);
|
|
5657
5871
|
const resolved = [];
|
|
5872
|
+
let hasVoiceAttachment = false;
|
|
5873
|
+
let hasVoiceTranscript = false;
|
|
5874
|
+
let asrErrorMessage;
|
|
5658
5875
|
for (const att of list) {
|
|
5659
5876
|
const next = { attachment: att };
|
|
5660
5877
|
if (isImageAttachment(att) && isHttpUrl2(att.url)) {
|
|
@@ -5672,15 +5889,66 @@ async function resolveInboundAttachmentsForAgent(params) {
|
|
|
5672
5889
|
logger.warn(`failed to download inbound attachment: ${String(err)}`);
|
|
5673
5890
|
}
|
|
5674
5891
|
}
|
|
5892
|
+
if (isVoiceAttachment(att)) {
|
|
5893
|
+
hasVoiceAttachment = true;
|
|
5894
|
+
if (!qqCfg.asr?.enabled) {
|
|
5895
|
+
logger.info("voice attachment received but ASR is disabled");
|
|
5896
|
+
} else if (!asrCredentials) {
|
|
5897
|
+
logger.warn("voice ASR enabled but credentials are missing or invalid");
|
|
5898
|
+
} else if (!isHttpUrl2(att.url)) {
|
|
5899
|
+
logger.warn("voice ASR skipped: attachment URL is not an HTTP URL");
|
|
5900
|
+
} else {
|
|
5901
|
+
try {
|
|
5902
|
+
const media = await fetchMediaFromUrl(att.url, {
|
|
5903
|
+
timeout,
|
|
5904
|
+
maxSize
|
|
5905
|
+
});
|
|
5906
|
+
const transcript = await transcribeTencentFlash({
|
|
5907
|
+
audio: media.buffer,
|
|
5908
|
+
config: {
|
|
5909
|
+
appId: asrCredentials.appId,
|
|
5910
|
+
secretId: asrCredentials.secretId,
|
|
5911
|
+
secretKey: asrCredentials.secretKey,
|
|
5912
|
+
timeoutMs: timeout
|
|
5913
|
+
}
|
|
5914
|
+
});
|
|
5915
|
+
if (transcript.trim()) {
|
|
5916
|
+
next.voiceTranscript = transcript.trim();
|
|
5917
|
+
hasVoiceTranscript = true;
|
|
5918
|
+
logger.info(
|
|
5919
|
+
`[voice-asr] transcript: ${next.voiceTranscript}${att.filename ? ` (file: ${att.filename})` : ""}`
|
|
5920
|
+
);
|
|
5921
|
+
}
|
|
5922
|
+
} catch (err) {
|
|
5923
|
+
if (err instanceof ASRError) {
|
|
5924
|
+
logger.warn(
|
|
5925
|
+
`voice ASR failed: kind=${err.kind} provider=${err.provider} retryable=${err.retryable} message=${err.message}`
|
|
5926
|
+
);
|
|
5927
|
+
asrErrorMessage ??= err.message.trim() || void 0;
|
|
5928
|
+
} else {
|
|
5929
|
+
logger.warn(`voice ASR failed: ${String(err)}`);
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5675
5934
|
resolved.push(next);
|
|
5676
5935
|
}
|
|
5677
|
-
return
|
|
5936
|
+
return {
|
|
5937
|
+
attachments: resolved,
|
|
5938
|
+
hasVoiceAttachment,
|
|
5939
|
+
hasVoiceTranscript,
|
|
5940
|
+
asrErrorMessage
|
|
5941
|
+
};
|
|
5678
5942
|
}
|
|
5679
5943
|
function buildInboundContentWithAttachments(params) {
|
|
5680
5944
|
const { content, attachments } = params;
|
|
5681
5945
|
const list = attachments ?? [];
|
|
5682
5946
|
if (list.length === 0) return content;
|
|
5683
5947
|
const imageRefs = list.map((item) => item.localImagePath).filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => `[Image: source: ${value}]`);
|
|
5948
|
+
const voiceTranscripts = list.filter((item) => typeof item.voiceTranscript === "string" && item.voiceTranscript.trim()).map((item, index) => {
|
|
5949
|
+
const filename = item.attachment.filename?.trim() || `voice-${index + 1}`;
|
|
5950
|
+
return `- ${filename}: ${item.voiceTranscript}`;
|
|
5951
|
+
});
|
|
5684
5952
|
const lines = list.map((item, index) => {
|
|
5685
5953
|
const att = item.attachment;
|
|
5686
5954
|
const filename = att.filename?.trim() ? att.filename.trim() : `attachment-${index + 1}`;
|
|
@@ -5692,9 +5960,30 @@ function buildInboundContentWithAttachments(params) {
|
|
|
5692
5960
|
const parts = [];
|
|
5693
5961
|
if (content) parts.push(content);
|
|
5694
5962
|
if (imageRefs.length > 0) parts.push(imageRefs.join("\n"));
|
|
5963
|
+
if (voiceTranscripts.length > 0) {
|
|
5964
|
+
parts.push(["[QQ voice transcripts]", ...voiceTranscripts].join("\n"));
|
|
5965
|
+
}
|
|
5695
5966
|
parts.push(block);
|
|
5696
5967
|
return parts.join("\n\n");
|
|
5697
5968
|
}
|
|
5969
|
+
function resolveInboundLogContent(params) {
|
|
5970
|
+
const text = params.content.trim();
|
|
5971
|
+
if (text) return text;
|
|
5972
|
+
const attachments = params.attachments ?? [];
|
|
5973
|
+
if (attachments.some((att) => isVoiceAttachment(att))) {
|
|
5974
|
+
return "\u3010\u8BED\u97F3\u3011";
|
|
5975
|
+
}
|
|
5976
|
+
if (attachments.some((att) => isImageAttachment(att))) {
|
|
5977
|
+
return "\u3010\u56FE\u7247\u3011";
|
|
5978
|
+
}
|
|
5979
|
+
if (attachments.length > 0) {
|
|
5980
|
+
return "\u3010\u9644\u4EF6\u3011";
|
|
5981
|
+
}
|
|
5982
|
+
return "\u3010\u7A7A\u6D88\u606F\u3011";
|
|
5983
|
+
}
|
|
5984
|
+
function sanitizeInboundLogText(text) {
|
|
5985
|
+
return text.replace(/\r?\n/g, "\\n");
|
|
5986
|
+
}
|
|
5698
5987
|
function parseC2CMessage(data) {
|
|
5699
5988
|
const payload = data;
|
|
5700
5989
|
const { text, attachments } = parseTextWithAttachments(payload);
|
|
@@ -5796,7 +6085,7 @@ function resolveInbound(eventType, data) {
|
|
|
5796
6085
|
}
|
|
5797
6086
|
function resolveChatTarget(event) {
|
|
5798
6087
|
if (event.type === "group") {
|
|
5799
|
-
const group = event.groupOpenid ?? "";
|
|
6088
|
+
const group = (event.groupOpenid ?? "").toLowerCase();
|
|
5800
6089
|
return {
|
|
5801
6090
|
to: `group:${group}`,
|
|
5802
6091
|
peerId: `group:${group}`,
|
|
@@ -5804,7 +6093,7 @@ function resolveChatTarget(event) {
|
|
|
5804
6093
|
};
|
|
5805
6094
|
}
|
|
5806
6095
|
if (event.type === "channel") {
|
|
5807
|
-
const channel = event.channelId ?? "";
|
|
6096
|
+
const channel = (event.channelId ?? "").toLowerCase();
|
|
5808
6097
|
return {
|
|
5809
6098
|
to: `channel:${channel}`,
|
|
5810
6099
|
peerId: `channel:${channel}`,
|
|
@@ -5819,10 +6108,10 @@ function resolveChatTarget(event) {
|
|
|
5819
6108
|
}
|
|
5820
6109
|
function resolveEnvelopeFrom(event) {
|
|
5821
6110
|
if (event.type === "group") {
|
|
5822
|
-
return `group:${event.groupOpenid ?? "unknown"}`;
|
|
6111
|
+
return `group:${(event.groupOpenid ?? "unknown").toLowerCase()}`;
|
|
5823
6112
|
}
|
|
5824
6113
|
if (event.type === "channel") {
|
|
5825
|
-
return `channel:${event.channelId ?? "unknown"}`;
|
|
6114
|
+
return `channel:${(event.channelId ?? "unknown").toLowerCase()}`;
|
|
5826
6115
|
}
|
|
5827
6116
|
return event.senderName?.trim() || event.senderId;
|
|
5828
6117
|
}
|
|
@@ -5950,11 +6239,24 @@ async function dispatchToAgent(params) {
|
|
|
5950
6239
|
);
|
|
5951
6240
|
const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
|
|
5952
6241
|
const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
|
|
5953
|
-
const
|
|
6242
|
+
const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
|
|
5954
6243
|
attachments: inbound.attachments,
|
|
5955
6244
|
qqCfg,
|
|
5956
6245
|
logger
|
|
5957
6246
|
});
|
|
6247
|
+
if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
|
|
6248
|
+
const fallback = await qqbotOutbound.sendText({
|
|
6249
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
6250
|
+
to: target.to,
|
|
6251
|
+
text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
|
|
6252
|
+
replyToId: inbound.messageId
|
|
6253
|
+
});
|
|
6254
|
+
if (fallback.error) {
|
|
6255
|
+
logger.error(`sendText ASR fallback failed: ${fallback.error}`);
|
|
6256
|
+
}
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
const resolvedAttachments = resolvedAttachmentResult.attachments;
|
|
5958
6260
|
const localImageCount = resolvedAttachments.filter((item) => Boolean(item.localImagePath)).length;
|
|
5959
6261
|
if (localImageCount > 0) {
|
|
5960
6262
|
logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
|
|
@@ -6213,10 +6515,17 @@ async function handleQQBotDispatch(params) {
|
|
|
6213
6515
|
logger.info("qqbot disabled, ignoring inbound message");
|
|
6214
6516
|
return;
|
|
6215
6517
|
}
|
|
6518
|
+
const content = inbound.content.trim();
|
|
6519
|
+
const inboundLogContent = sanitizeInboundLogText(
|
|
6520
|
+
resolveInboundLogContent({
|
|
6521
|
+
content,
|
|
6522
|
+
attachments: inbound.attachments
|
|
6523
|
+
})
|
|
6524
|
+
);
|
|
6525
|
+
logger.info(`[inbound-user] senderId=${inbound.senderId} content=${inboundLogContent}`);
|
|
6216
6526
|
if (!shouldHandleMessage(inbound, qqCfg, logger)) {
|
|
6217
6527
|
return;
|
|
6218
6528
|
}
|
|
6219
|
-
const content = inbound.content.trim();
|
|
6220
6529
|
const attachmentCount = inbound.attachments?.length ?? 0;
|
|
6221
6530
|
if (attachmentCount > 0) {
|
|
6222
6531
|
logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
|
|
@@ -6593,6 +6902,16 @@ var qqbotPlugin = {
|
|
|
6593
6902
|
enabled: { type: "boolean" },
|
|
6594
6903
|
appId: { type: "string" },
|
|
6595
6904
|
clientSecret: { type: "string" },
|
|
6905
|
+
asr: {
|
|
6906
|
+
type: "object",
|
|
6907
|
+
additionalProperties: false,
|
|
6908
|
+
properties: {
|
|
6909
|
+
enabled: { type: "boolean" },
|
|
6910
|
+
appId: { type: "string" },
|
|
6911
|
+
secretId: { type: "string" },
|
|
6912
|
+
secretKey: { type: "string" }
|
|
6913
|
+
}
|
|
6914
|
+
},
|
|
6596
6915
|
markdownSupport: { type: "boolean" },
|
|
6597
6916
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
6598
6917
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
@@ -6709,6 +7028,16 @@ var plugin = {
|
|
|
6709
7028
|
enabled: { type: "boolean" },
|
|
6710
7029
|
appId: { type: "string" },
|
|
6711
7030
|
clientSecret: { type: "string" },
|
|
7031
|
+
asr: {
|
|
7032
|
+
type: "object",
|
|
7033
|
+
additionalProperties: false,
|
|
7034
|
+
properties: {
|
|
7035
|
+
enabled: { type: "boolean" },
|
|
7036
|
+
appId: { type: "string" },
|
|
7037
|
+
secretId: { type: "string" },
|
|
7038
|
+
secretKey: { type: "string" }
|
|
7039
|
+
}
|
|
7040
|
+
},
|
|
6712
7041
|
markdownSupport: { type: "boolean" },
|
|
6713
7042
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
6714
7043
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|