@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 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) return [];
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 resolved;
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 resolvedAttachments = await resolveInboundAttachmentsForAgent({
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"] },