@openclaw-china/qqbot 0.1.2 → 0.1.4
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 +315 -4
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +30 -17
- 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";
|
|
@@ -5624,6 +5798,8 @@ function parseTextWithAttachments(payload) {
|
|
|
5624
5798
|
attachments
|
|
5625
5799
|
};
|
|
5626
5800
|
}
|
|
5801
|
+
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";
|
|
5802
|
+
var VOICE_EXTENSIONS = [".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex"];
|
|
5627
5803
|
function isHttpUrl2(value) {
|
|
5628
5804
|
return /^https?:\/\//i.test(value);
|
|
5629
5805
|
}
|
|
@@ -5641,6 +5817,22 @@ function isImageAttachment(att) {
|
|
|
5641
5817
|
return false;
|
|
5642
5818
|
}
|
|
5643
5819
|
}
|
|
5820
|
+
function isVoiceAttachment(att) {
|
|
5821
|
+
const contentType = att.contentType?.trim().toLowerCase() ?? "";
|
|
5822
|
+
if (contentType === "voice" || contentType.startsWith("audio/")) {
|
|
5823
|
+
return true;
|
|
5824
|
+
}
|
|
5825
|
+
const lowerName = att.filename?.trim().toLowerCase() ?? "";
|
|
5826
|
+
if (VOICE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
|
5827
|
+
return true;
|
|
5828
|
+
}
|
|
5829
|
+
try {
|
|
5830
|
+
const pathname = new URL(att.url).pathname.toLowerCase();
|
|
5831
|
+
return VOICE_EXTENSIONS.some((ext) => pathname.endsWith(ext));
|
|
5832
|
+
} catch {
|
|
5833
|
+
return false;
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5644
5836
|
function scheduleTempCleanup(filePath) {
|
|
5645
5837
|
const timer = setTimeout(() => {
|
|
5646
5838
|
void cleanupFileSafe(filePath);
|
|
@@ -5650,11 +5842,20 @@ function scheduleTempCleanup(filePath) {
|
|
|
5650
5842
|
async function resolveInboundAttachmentsForAgent(params) {
|
|
5651
5843
|
const { attachments, qqCfg, logger } = params;
|
|
5652
5844
|
const list = attachments ?? [];
|
|
5653
|
-
if (list.length === 0)
|
|
5845
|
+
if (list.length === 0) {
|
|
5846
|
+
return {
|
|
5847
|
+
attachments: [],
|
|
5848
|
+
hasVoiceAttachment: false,
|
|
5849
|
+
hasVoiceTranscript: false
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5654
5852
|
const timeout = qqCfg.mediaTimeoutMs ?? 3e4;
|
|
5655
5853
|
const maxFileSizeMB = qqCfg.maxFileSizeMB ?? 100;
|
|
5656
5854
|
const maxSize = Math.floor(maxFileSizeMB * 1024 * 1024);
|
|
5855
|
+
const asrCredentials = resolveQQBotASRCredentials(qqCfg);
|
|
5657
5856
|
const resolved = [];
|
|
5857
|
+
let hasVoiceAttachment = false;
|
|
5858
|
+
let hasVoiceTranscript = false;
|
|
5658
5859
|
for (const att of list) {
|
|
5659
5860
|
const next = { attachment: att };
|
|
5660
5861
|
if (isImageAttachment(att) && isHttpUrl2(att.url)) {
|
|
@@ -5672,15 +5873,64 @@ async function resolveInboundAttachmentsForAgent(params) {
|
|
|
5672
5873
|
logger.warn(`failed to download inbound attachment: ${String(err)}`);
|
|
5673
5874
|
}
|
|
5674
5875
|
}
|
|
5876
|
+
if (isVoiceAttachment(att)) {
|
|
5877
|
+
hasVoiceAttachment = true;
|
|
5878
|
+
if (!qqCfg.asr?.enabled) {
|
|
5879
|
+
logger.info("voice attachment received but ASR is disabled");
|
|
5880
|
+
} else if (!asrCredentials) {
|
|
5881
|
+
logger.warn("voice ASR enabled but credentials are missing or invalid");
|
|
5882
|
+
} else if (!isHttpUrl2(att.url)) {
|
|
5883
|
+
logger.warn("voice ASR skipped: attachment URL is not an HTTP URL");
|
|
5884
|
+
} else {
|
|
5885
|
+
try {
|
|
5886
|
+
const media = await fetchMediaFromUrl(att.url, {
|
|
5887
|
+
timeout,
|
|
5888
|
+
maxSize
|
|
5889
|
+
});
|
|
5890
|
+
const transcript = await transcribeTencentFlash({
|
|
5891
|
+
audio: media.buffer,
|
|
5892
|
+
config: {
|
|
5893
|
+
appId: asrCredentials.appId,
|
|
5894
|
+
secretId: asrCredentials.secretId,
|
|
5895
|
+
secretKey: asrCredentials.secretKey,
|
|
5896
|
+
timeoutMs: timeout
|
|
5897
|
+
}
|
|
5898
|
+
});
|
|
5899
|
+
if (transcript.trim()) {
|
|
5900
|
+
next.voiceTranscript = transcript.trim();
|
|
5901
|
+
hasVoiceTranscript = true;
|
|
5902
|
+
logger.info(
|
|
5903
|
+
`[voice-asr] transcript: ${next.voiceTranscript}${att.filename ? ` (file: ${att.filename})` : ""}`
|
|
5904
|
+
);
|
|
5905
|
+
}
|
|
5906
|
+
} catch (err) {
|
|
5907
|
+
if (err instanceof ASRError) {
|
|
5908
|
+
logger.warn(
|
|
5909
|
+
`voice ASR failed: kind=${err.kind} provider=${err.provider} retryable=${err.retryable} message=${err.message}`
|
|
5910
|
+
);
|
|
5911
|
+
} else {
|
|
5912
|
+
logger.warn(`voice ASR failed: ${String(err)}`);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5675
5917
|
resolved.push(next);
|
|
5676
5918
|
}
|
|
5677
|
-
return
|
|
5919
|
+
return {
|
|
5920
|
+
attachments: resolved,
|
|
5921
|
+
hasVoiceAttachment,
|
|
5922
|
+
hasVoiceTranscript
|
|
5923
|
+
};
|
|
5678
5924
|
}
|
|
5679
5925
|
function buildInboundContentWithAttachments(params) {
|
|
5680
5926
|
const { content, attachments } = params;
|
|
5681
5927
|
const list = attachments ?? [];
|
|
5682
5928
|
if (list.length === 0) return content;
|
|
5683
5929
|
const imageRefs = list.map((item) => item.localImagePath).filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => `[Image: source: ${value}]`);
|
|
5930
|
+
const voiceTranscripts = list.filter((item) => typeof item.voiceTranscript === "string" && item.voiceTranscript.trim()).map((item, index) => {
|
|
5931
|
+
const filename = item.attachment.filename?.trim() || `voice-${index + 1}`;
|
|
5932
|
+
return `- ${filename}: ${item.voiceTranscript}`;
|
|
5933
|
+
});
|
|
5684
5934
|
const lines = list.map((item, index) => {
|
|
5685
5935
|
const att = item.attachment;
|
|
5686
5936
|
const filename = att.filename?.trim() ? att.filename.trim() : `attachment-${index + 1}`;
|
|
@@ -5692,9 +5942,30 @@ function buildInboundContentWithAttachments(params) {
|
|
|
5692
5942
|
const parts = [];
|
|
5693
5943
|
if (content) parts.push(content);
|
|
5694
5944
|
if (imageRefs.length > 0) parts.push(imageRefs.join("\n"));
|
|
5945
|
+
if (voiceTranscripts.length > 0) {
|
|
5946
|
+
parts.push(["[QQ voice transcripts]", ...voiceTranscripts].join("\n"));
|
|
5947
|
+
}
|
|
5695
5948
|
parts.push(block);
|
|
5696
5949
|
return parts.join("\n\n");
|
|
5697
5950
|
}
|
|
5951
|
+
function resolveInboundLogContent(params) {
|
|
5952
|
+
const text = params.content.trim();
|
|
5953
|
+
if (text) return text;
|
|
5954
|
+
const attachments = params.attachments ?? [];
|
|
5955
|
+
if (attachments.some((att) => isVoiceAttachment(att))) {
|
|
5956
|
+
return "\u3010\u8BED\u97F3\u3011";
|
|
5957
|
+
}
|
|
5958
|
+
if (attachments.some((att) => isImageAttachment(att))) {
|
|
5959
|
+
return "\u3010\u56FE\u7247\u3011";
|
|
5960
|
+
}
|
|
5961
|
+
if (attachments.length > 0) {
|
|
5962
|
+
return "\u3010\u9644\u4EF6\u3011";
|
|
5963
|
+
}
|
|
5964
|
+
return "\u3010\u7A7A\u6D88\u606F\u3011";
|
|
5965
|
+
}
|
|
5966
|
+
function sanitizeInboundLogText(text) {
|
|
5967
|
+
return text.replace(/\r?\n/g, "\\n");
|
|
5968
|
+
}
|
|
5698
5969
|
function parseC2CMessage(data) {
|
|
5699
5970
|
const payload = data;
|
|
5700
5971
|
const { text, attachments } = parseTextWithAttachments(payload);
|
|
@@ -5950,11 +6221,24 @@ async function dispatchToAgent(params) {
|
|
|
5950
6221
|
);
|
|
5951
6222
|
const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
|
|
5952
6223
|
const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
|
|
5953
|
-
const
|
|
6224
|
+
const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
|
|
5954
6225
|
attachments: inbound.attachments,
|
|
5955
6226
|
qqCfg,
|
|
5956
6227
|
logger
|
|
5957
6228
|
});
|
|
6229
|
+
if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
|
|
6230
|
+
const fallback = await qqbotOutbound.sendText({
|
|
6231
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
6232
|
+
to: target.to,
|
|
6233
|
+
text: VOICE_ASR_FALLBACK_TEXT,
|
|
6234
|
+
replyToId: inbound.messageId
|
|
6235
|
+
});
|
|
6236
|
+
if (fallback.error) {
|
|
6237
|
+
logger.error(`sendText ASR fallback failed: ${fallback.error}`);
|
|
6238
|
+
}
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
const resolvedAttachments = resolvedAttachmentResult.attachments;
|
|
5958
6242
|
const localImageCount = resolvedAttachments.filter((item) => Boolean(item.localImagePath)).length;
|
|
5959
6243
|
if (localImageCount > 0) {
|
|
5960
6244
|
logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
|
|
@@ -6213,10 +6497,17 @@ async function handleQQBotDispatch(params) {
|
|
|
6213
6497
|
logger.info("qqbot disabled, ignoring inbound message");
|
|
6214
6498
|
return;
|
|
6215
6499
|
}
|
|
6500
|
+
const content = inbound.content.trim();
|
|
6501
|
+
const inboundLogContent = sanitizeInboundLogText(
|
|
6502
|
+
resolveInboundLogContent({
|
|
6503
|
+
content,
|
|
6504
|
+
attachments: inbound.attachments
|
|
6505
|
+
})
|
|
6506
|
+
);
|
|
6507
|
+
logger.info(`[inbound-user] senderId=${inbound.senderId} content=${inboundLogContent}`);
|
|
6216
6508
|
if (!shouldHandleMessage(inbound, qqCfg, logger)) {
|
|
6217
6509
|
return;
|
|
6218
6510
|
}
|
|
6219
|
-
const content = inbound.content.trim();
|
|
6220
6511
|
const attachmentCount = inbound.attachments?.length ?? 0;
|
|
6221
6512
|
if (attachmentCount > 0) {
|
|
6222
6513
|
logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
|
|
@@ -6593,6 +6884,16 @@ var qqbotPlugin = {
|
|
|
6593
6884
|
enabled: { type: "boolean" },
|
|
6594
6885
|
appId: { type: "string" },
|
|
6595
6886
|
clientSecret: { type: "string" },
|
|
6887
|
+
asr: {
|
|
6888
|
+
type: "object",
|
|
6889
|
+
additionalProperties: false,
|
|
6890
|
+
properties: {
|
|
6891
|
+
enabled: { type: "boolean" },
|
|
6892
|
+
appId: { type: "string" },
|
|
6893
|
+
secretId: { type: "string" },
|
|
6894
|
+
secretKey: { type: "string" }
|
|
6895
|
+
}
|
|
6896
|
+
},
|
|
6596
6897
|
markdownSupport: { type: "boolean" },
|
|
6597
6898
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
6598
6899
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
@@ -6709,6 +7010,16 @@ var plugin = {
|
|
|
6709
7010
|
enabled: { type: "boolean" },
|
|
6710
7011
|
appId: { type: "string" },
|
|
6711
7012
|
clientSecret: { type: "string" },
|
|
7013
|
+
asr: {
|
|
7014
|
+
type: "object",
|
|
7015
|
+
additionalProperties: false,
|
|
7016
|
+
properties: {
|
|
7017
|
+
enabled: { type: "boolean" },
|
|
7018
|
+
appId: { type: "string" },
|
|
7019
|
+
secretId: { type: "string" },
|
|
7020
|
+
secretKey: { type: "string" }
|
|
7021
|
+
}
|
|
7022
|
+
},
|
|
6712
7023
|
markdownSupport: { type: "boolean" },
|
|
6713
7024
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
6714
7025
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|