@openclaw-china/qqbot 2026.3.21 → 2026.3.29

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.js CHANGED
@@ -4238,6 +4238,7 @@ var QQBotAccountSchema = external_exports.object({
4238
4238
  enabled: external_exports.boolean().optional(),
4239
4239
  appId: optionalCoercedString,
4240
4240
  clientSecret: optionalCoercedString,
4241
+ streaming: external_exports.boolean().optional().default(false),
4241
4242
  displayAliases: displayAliasesSchema,
4242
4243
  asr: external_exports.object({
4243
4244
  enabled: external_exports.boolean().optional().default(false),
@@ -4318,6 +4319,9 @@ function resolveQQBotTypingHeartbeatIntervalMs(config) {
4318
4319
  function resolveQQBotTypingInputSeconds(config) {
4319
4320
  return config?.typingInputSeconds ?? DEFAULT_QQBOT_TYPING_INPUT_SECONDS;
4320
4321
  }
4322
+ function resolveQQBotStreaming(config) {
4323
+ return config?.streaming === true;
4324
+ }
4321
4325
  function resolveInboundMediaTempDir() {
4322
4326
  return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
4323
4327
  }
@@ -7502,6 +7506,9 @@ function nextMsgSeq(sequenceKey) {
7502
7506
  }
7503
7507
  return MSG_SEQ_BASE + next;
7504
7508
  }
7509
+ function allocateMsgSeq(sequenceKey) {
7510
+ return nextMsgSeq(sequenceKey);
7511
+ }
7505
7512
  function resolveMsgSeqKey(messageId, eventId) {
7506
7513
  if (messageId) return `msg:${messageId}`;
7507
7514
  if (eventId) return `event:${eventId}`;
@@ -7609,6 +7616,16 @@ async function getGatewayUrl(accessToken) {
7609
7616
  const data = await apiGet(accessToken, "/gateway", { timeout: 15e3 });
7610
7617
  return data.url;
7611
7618
  }
7619
+ var QQBotStreamInputMode = {
7620
+ REPLACE: "replace"
7621
+ };
7622
+ var QQBotStreamInputState = {
7623
+ GENERATING: 1,
7624
+ DONE: 10
7625
+ };
7626
+ var QQBotStreamContentType = {
7627
+ MARKDOWN: "markdown"
7628
+ };
7612
7629
  function buildMessageBody(params) {
7613
7630
  const body = buildTextMessageBody({
7614
7631
  content: params.content,
@@ -7766,6 +7783,24 @@ async function sendC2CMediaMessage(params) {
7766
7783
  })
7767
7784
  });
7768
7785
  }
7786
+ async function sendC2CStreamMessage(params) {
7787
+ const body = {
7788
+ input_mode: params.request.input_mode,
7789
+ input_state: params.request.input_state,
7790
+ content_type: params.request.content_type,
7791
+ content_raw: params.request.content_raw,
7792
+ event_id: params.request.event_id,
7793
+ msg_id: params.request.msg_id,
7794
+ msg_seq: params.request.msg_seq,
7795
+ index: params.request.index
7796
+ };
7797
+ if (params.request.stream_msg_id) {
7798
+ body.stream_msg_id = params.request.stream_msg_id;
7799
+ }
7800
+ return apiPost(params.accessToken, `/v2/users/${params.openid}/stream_messages`, body, {
7801
+ timeout: 15e3
7802
+ });
7803
+ }
7769
7804
  async function sendGroupMediaMessage(params) {
7770
7805
  return postPassiveMessage({
7771
7806
  accessToken: params.accessToken,
@@ -9282,6 +9317,263 @@ function getQQBotRuntime() {
9282
9317
  }
9283
9318
  return runtime;
9284
9319
  }
9320
+
9321
+ // src/streaming.ts
9322
+ var DEFAULT_THROTTLE_MS = 500;
9323
+ var DEFAULT_MIN_THROTTLE_MS = 300;
9324
+ var QQBotStreamingController = class {
9325
+ params;
9326
+ throttleMs;
9327
+ chain = Promise.resolve();
9328
+ flushTimer = null;
9329
+ startPromise = null;
9330
+ latestText = "";
9331
+ lastSentText = "";
9332
+ streamMsgId;
9333
+ msgSeq;
9334
+ index = 0;
9335
+ lastPartialLength = 0;
9336
+ lastSendAt = 0;
9337
+ sessionSentChunkCount = 0;
9338
+ sessionShouldFallbackToStatic = false;
9339
+ firstChunkNotified = false;
9340
+ replyOrdinal = 0;
9341
+ disposed = false;
9342
+ constructor(params) {
9343
+ this.params = params;
9344
+ const throttle = params.throttleMs ?? DEFAULT_THROTTLE_MS;
9345
+ const minThrottle = params.minThrottleMs ?? DEFAULT_MIN_THROTTLE_MS;
9346
+ this.throttleMs = Math.max(throttle, minThrottle);
9347
+ }
9348
+ get hasSuccessfulChunk() {
9349
+ return this.sessionSentChunkCount > 0;
9350
+ }
9351
+ get shouldFallbackToStatic() {
9352
+ return this.sessionShouldFallbackToStatic && this.sessionSentChunkCount === 0;
9353
+ }
9354
+ get hasObservedPartial() {
9355
+ return this.lastPartialLength > 0;
9356
+ }
9357
+ async onPartialReply(text) {
9358
+ await this.enqueue(async () => {
9359
+ if (this.disposed) return;
9360
+ if (this.lastPartialLength > 0 && text.length < this.lastPartialLength) {
9361
+ this.logInfo(
9362
+ `reply boundary detected (${text.length} < ${this.lastPartialLength}), starting new stream session`
9363
+ );
9364
+ await this.finalizeCurrentReply();
9365
+ this.resetReplyState();
9366
+ }
9367
+ this.lastPartialLength = text.length;
9368
+ this.latestText = text;
9369
+ if (!text.trim() || this.sessionShouldFallbackToStatic) {
9370
+ return;
9371
+ }
9372
+ if (!this.streamMsgId) {
9373
+ await this.ensureStreamingStarted();
9374
+ return;
9375
+ }
9376
+ this.scheduleFlush();
9377
+ });
9378
+ }
9379
+ async finalize() {
9380
+ await this.enqueue(async () => {
9381
+ await this.finalizeCurrentReply();
9382
+ });
9383
+ }
9384
+ dispose() {
9385
+ this.disposed = true;
9386
+ this.clearFlushTimer();
9387
+ }
9388
+ enqueue(task) {
9389
+ this.chain = this.chain.then(task, async (err) => {
9390
+ this.logError(`stream queue recovered after error: ${String(err)}`);
9391
+ await task();
9392
+ });
9393
+ return this.chain;
9394
+ }
9395
+ async ensureStreamingStarted() {
9396
+ if (this.disposed || this.streamMsgId || this.sessionShouldFallbackToStatic) {
9397
+ return;
9398
+ }
9399
+ if (this.startPromise) {
9400
+ await this.startPromise;
9401
+ return;
9402
+ }
9403
+ this.startPromise = this.startStreaming();
9404
+ try {
9405
+ await this.startPromise;
9406
+ } finally {
9407
+ this.startPromise = null;
9408
+ }
9409
+ }
9410
+ async startStreaming() {
9411
+ if (!this.latestText.trim()) {
9412
+ return;
9413
+ }
9414
+ try {
9415
+ this.msgSeq ??= allocateMsgSeq(`stream:${this.params.messageId}:${this.replyOrdinal}`);
9416
+ const response = await this.sendChunk({
9417
+ content: this.latestText,
9418
+ inputState: QQBotStreamInputState.GENERATING
9419
+ });
9420
+ if (!response.id) {
9421
+ throw new Error("QQ stream response missing stream message id");
9422
+ }
9423
+ this.streamMsgId = response.id;
9424
+ this.lastSentText = this.latestText;
9425
+ this.lastSendAt = Date.now();
9426
+ this.sessionSentChunkCount += 1;
9427
+ this.index += 1;
9428
+ await this.notifyFirstChunk();
9429
+ if (this.latestText !== this.lastSentText) {
9430
+ this.scheduleFlush();
9431
+ }
9432
+ } catch (err) {
9433
+ this.sessionShouldFallbackToStatic = true;
9434
+ this.logWarn(`failed to start stream session, falling back to static: ${String(err)}`);
9435
+ }
9436
+ }
9437
+ async flushNow() {
9438
+ if (this.disposed || !this.streamMsgId || this.sessionShouldFallbackToStatic || !this.latestText.trim() || this.latestText === this.lastSentText) {
9439
+ return;
9440
+ }
9441
+ try {
9442
+ await this.sendChunk({
9443
+ content: this.latestText,
9444
+ inputState: QQBotStreamInputState.GENERATING
9445
+ });
9446
+ this.lastSentText = this.latestText;
9447
+ this.lastSendAt = Date.now();
9448
+ this.sessionSentChunkCount += 1;
9449
+ this.index += 1;
9450
+ await this.notifyFirstChunk();
9451
+ } catch (err) {
9452
+ this.logWarn(`failed to flush stream chunk: ${String(err)}`);
9453
+ }
9454
+ }
9455
+ scheduleFlush() {
9456
+ if (this.disposed || this.flushTimer || !this.streamMsgId || this.sessionShouldFallbackToStatic) {
9457
+ return;
9458
+ }
9459
+ const elapsed = Date.now() - this.lastSendAt;
9460
+ if (elapsed >= this.throttleMs) {
9461
+ void this.enqueue(async () => {
9462
+ await this.flushNow();
9463
+ });
9464
+ return;
9465
+ }
9466
+ const waitMs = this.throttleMs - elapsed;
9467
+ this.flushTimer = setTimeout(() => {
9468
+ this.flushTimer = null;
9469
+ void this.enqueue(async () => {
9470
+ await this.flushNow();
9471
+ });
9472
+ }, waitMs);
9473
+ this.flushTimer.unref?.();
9474
+ }
9475
+ async finalizeCurrentReply() {
9476
+ this.clearFlushTimer();
9477
+ if (this.startPromise) {
9478
+ await this.startPromise;
9479
+ }
9480
+ if (!this.streamMsgId) {
9481
+ return;
9482
+ }
9483
+ const finalText = (this.latestText || this.lastSentText).trim() ? this.latestText || this.lastSentText : this.lastSentText;
9484
+ if (!finalText) {
9485
+ this.resetStreamSession();
9486
+ return;
9487
+ }
9488
+ try {
9489
+ await this.sendChunk({
9490
+ content: finalText,
9491
+ inputState: QQBotStreamInputState.DONE
9492
+ });
9493
+ this.lastSentText = finalText;
9494
+ this.lastSendAt = Date.now();
9495
+ this.sessionSentChunkCount += 1;
9496
+ this.index += 1;
9497
+ } catch (err) {
9498
+ this.logWarn(`failed to finalize stream session: ${String(err)}`);
9499
+ } finally {
9500
+ this.resetStreamSession();
9501
+ }
9502
+ }
9503
+ async sendChunk(params) {
9504
+ const msgSeq = this.msgSeq ?? allocateMsgSeq(`stream:${this.params.messageId}:${this.replyOrdinal}`);
9505
+ this.msgSeq = msgSeq;
9506
+ const accessToken = await getAccessToken(this.params.appId, this.params.clientSecret);
9507
+ const response = await sendC2CStreamMessage({
9508
+ accessToken,
9509
+ openid: this.params.openid,
9510
+ request: {
9511
+ input_mode: QQBotStreamInputMode.REPLACE,
9512
+ input_state: params.inputState,
9513
+ content_type: QQBotStreamContentType.MARKDOWN,
9514
+ content_raw: params.content,
9515
+ event_id: this.params.eventId,
9516
+ msg_id: this.params.messageId,
9517
+ msg_seq: msgSeq,
9518
+ index: this.index,
9519
+ ...this.streamMsgId ? { stream_msg_id: this.streamMsgId } : {}
9520
+ }
9521
+ });
9522
+ if (response.code && response.code > 0) {
9523
+ throw new Error(`QQ stream API error ${response.code}: ${response.message ?? "unknown error"}`);
9524
+ }
9525
+ return {
9526
+ id: typeof response.id === "string" ? response.id : void 0
9527
+ };
9528
+ }
9529
+ async notifyFirstChunk() {
9530
+ if (this.firstChunkNotified) {
9531
+ return;
9532
+ }
9533
+ this.firstChunkNotified = true;
9534
+ try {
9535
+ await this.params.onFirstChunk?.();
9536
+ } catch (err) {
9537
+ this.logWarn(`onFirstChunk hook failed: ${String(err)}`);
9538
+ }
9539
+ }
9540
+ clearFlushTimer() {
9541
+ if (!this.flushTimer) {
9542
+ return;
9543
+ }
9544
+ clearTimeout(this.flushTimer);
9545
+ this.flushTimer = null;
9546
+ }
9547
+ resetReplyState() {
9548
+ this.replyOrdinal += 1;
9549
+ this.lastPartialLength = 0;
9550
+ this.latestText = "";
9551
+ this.lastSentText = "";
9552
+ this.sessionSentChunkCount = 0;
9553
+ this.sessionShouldFallbackToStatic = false;
9554
+ this.firstChunkNotified = false;
9555
+ this.resetStreamSession();
9556
+ }
9557
+ resetStreamSession() {
9558
+ this.streamMsgId = void 0;
9559
+ this.msgSeq = void 0;
9560
+ this.index = 0;
9561
+ this.lastSendAt = 0;
9562
+ this.clearFlushTimer();
9563
+ }
9564
+ logInfo(message) {
9565
+ const next = `${this.params.logPrefix ?? "[qqbot:streaming]"} ${message}`;
9566
+ this.params.logger?.info?.(next);
9567
+ }
9568
+ logWarn(message) {
9569
+ const next = `${this.params.logPrefix ?? "[qqbot:streaming]"} ${message}`;
9570
+ (this.params.logger?.warn ?? this.params.logger?.info)?.(next);
9571
+ }
9572
+ logError(message) {
9573
+ const next = `${this.params.logPrefix ?? "[qqbot:streaming]"} ${message}`;
9574
+ this.params.logger?.error?.(next);
9575
+ }
9576
+ };
9285
9577
  var sessionDispatchQueue = /* @__PURE__ */ new Map();
9286
9578
  var QQBOT_ABORT_TRIGGERS = /* @__PURE__ */ new Set([
9287
9579
  "stop",
@@ -11408,6 +11700,14 @@ function looksLikeStructuredMarkdown(text) {
11408
11700
  }
11409
11701
  return normalized.includes("\n\n") || lines.some((line) => MARKDOWN_ATX_HEADING_RE.test(line)) || lines.some((line) => MARKDOWN_BLOCKQUOTE_RE.test(line)) || lines.some((line) => MARKDOWN_FENCE_RE.test(line)) || lines.some((line) => MARKDOWN_THEMATIC_BREAK_RE.test(line)) || lines.some((line) => MARKDOWN_LIST_ITEM_RE.test(line)) || MARKDOWN_INLINE_STRUCTURE_RE.test(normalized);
11410
11702
  }
11703
+ function looksLikeQQBotStreamingIneligibleMarkdown(text) {
11704
+ const normalized = normalizeQQBotMarkdownSegment(text);
11705
+ if (!normalized) {
11706
+ return false;
11707
+ }
11708
+ const lines = normalized.split("\n");
11709
+ return hasQQBotMarkdownTable(normalized) || lines.some((line) => MARKDOWN_ATX_HEADING_RE.test(line)) || lines.some((line) => MARKDOWN_BLOCKQUOTE_RE.test(line)) || lines.some((line) => MARKDOWN_FENCE_RE.test(line)) || lines.some((line) => MARKDOWN_THEMATIC_BREAK_RE.test(line)) || lines.some((line) => MARKDOWN_LIST_ITEM_RE.test(line));
11710
+ }
11411
11711
  function chunkC2CMarkdownText(params) {
11412
11712
  const normalized = params.text.trim();
11413
11713
  if (!normalized) {
@@ -11798,10 +12098,26 @@ async function dispatchToAgent(params) {
11798
12098
  const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11799
12099
  const c2cMarkdownSafeChunkByteLimit = resolveQQBotC2CMarkdownSafeChunkByteLimit(qqCfg);
11800
12100
  const isC2CTarget = isQQBotC2CTarget(target.to);
12101
+ const streamingEnabled = isC2CTarget && !replyFinalOnly && resolveQQBotStreaming(qqCfg);
11801
12102
  const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
11802
12103
  let bufferedC2CMarkdownTexts = [];
11803
12104
  let bufferedC2CMarkdownMediaUrls = [];
11804
12105
  const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
12106
+ const streamingCredentials = streamingEnabled ? resolveQQBotCredentials(qqCfg) : void 0;
12107
+ const streamingController = streamingEnabled && inbound.c2cOpenid && streamingCredentials ? new QQBotStreamingController({
12108
+ appId: streamingCredentials.appId,
12109
+ clientSecret: streamingCredentials.clientSecret,
12110
+ openid: inbound.c2cOpenid,
12111
+ messageId: inbound.messageId,
12112
+ eventId: inbound.eventId ?? inbound.messageId,
12113
+ logger,
12114
+ logPrefix: `[qqbot:${outboundAccountId}:streaming]`,
12115
+ onFirstChunk: async () => {
12116
+ markVisibleOutboundStarted();
12117
+ markReplyDelivered();
12118
+ typingHeartbeat?.stop();
12119
+ }
12120
+ }) : null;
11805
12121
  const hasBufferedC2CMarkdownReply = () => bufferedC2CMarkdownTexts.length > 0 || bufferedC2CMarkdownMediaUrls.length > 0;
11806
12122
  const bufferC2CMarkdownMedia = (url) => {
11807
12123
  const next = url?.trim();
@@ -11809,6 +12125,28 @@ async function dispatchToAgent(params) {
11809
12125
  bufferedC2CMarkdownMediaSeen.add(next);
11810
12126
  bufferedC2CMarkdownMediaUrls.push(next);
11811
12127
  };
12128
+ const handleStreamingPartialReply = async (payload) => {
12129
+ if (!streamingController || shouldSuppressVisibleReplies()) {
12130
+ return;
12131
+ }
12132
+ const rawText = payload.text ?? "";
12133
+ if (!rawText.trim()) {
12134
+ return;
12135
+ }
12136
+ const extractedTextMedia = extractQQBotReplyMedia({
12137
+ text: rawText,
12138
+ logger,
12139
+ autoSendLocalPathMedia: resolveQQBotAutoSendLocalPathMedia(qqCfg)
12140
+ });
12141
+ const cleanedText = sanitizeQQBotOutboundText(extractedTextMedia.text);
12142
+ if (!cleanedText) {
12143
+ return;
12144
+ }
12145
+ if (!streamingController.hasSuccessfulChunk && (extractedTextMedia.mediaUrls.length > 0 || looksLikeQQBotStreamingIneligibleMarkdown(cleanedText))) {
12146
+ return;
12147
+ }
12148
+ await streamingController.onPartialReply(cleanedText);
12149
+ };
11812
12150
  const sendC2CMarkdownTransportPayload = async (params2) => {
11813
12151
  if (shouldSuppressVisibleReplies()) {
11814
12152
  return;
@@ -11946,8 +12284,14 @@ async function dispatchToAgent(params) {
11946
12284
  });
11947
12285
  if (deliveryDecision.skipDelivery) return;
11948
12286
  const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(extractedTextMedia.text, cleanedText);
11949
- const suppressText = deliveryDecision.suppressText || suppressEchoText;
12287
+ const streamingOwnsAssistantText = info?.kind !== "tool" && Boolean(
12288
+ streamingController && streamingController.hasSuccessfulChunk && !streamingController.shouldFallbackToStatic
12289
+ );
12290
+ const suppressText = deliveryDecision.suppressText || suppressEchoText || streamingOwnsAssistantText;
11950
12291
  const textToSend = suppressText ? "" : cleanedText;
12292
+ if (streamingOwnsAssistantText && mediaQueue.length === 0 && !textToSend) {
12293
+ return;
12294
+ }
11951
12295
  if (useC2CMarkdownTransport) {
11952
12296
  const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
11953
12297
  const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && (hasBufferedC2CMarkdownReply() || looksLikeStructuredMarkdown(textToSend));
@@ -12032,82 +12376,92 @@ async function dispatchToAgent(params) {
12032
12376
  const dispatchDirect = replyApi.dispatchReplyWithDispatcher;
12033
12377
  const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
12034
12378
  const streamingReplyOptions = isC2CTarget && !replyFinalOnly ? {
12035
- disableBlockStreaming: false
12379
+ disableBlockStreaming: false,
12380
+ ...streamingController ? {
12381
+ onPartialReply: handleStreamingPartialReply
12382
+ } : {}
12036
12383
  } : void 0;
12037
- if (isC2CTarget && !replyFinalOnly && dispatchDirect) {
12038
- logger.debug(`[dispatch] mode=direct session=${routeSessionKey} to=${target.to}`);
12039
- await dispatchDirect({
12040
- ctx: finalCtx,
12041
- cfg,
12042
- dispatcherOptions: {
12043
- deliver,
12044
- humanDelay,
12045
- onError: (err, info) => {
12046
- logger.error(`${info.kind} reply failed: ${String(err)}`);
12047
- },
12048
- onSkip: (_payload, info) => {
12049
- if (info.reason !== "silent") {
12050
- logger.info(`reply skipped: ${info.reason}`);
12384
+ try {
12385
+ if (isC2CTarget && !replyFinalOnly && dispatchDirect) {
12386
+ logger.debug(`[dispatch] mode=direct session=${routeSessionKey} to=${target.to}`);
12387
+ await dispatchDirect({
12388
+ ctx: finalCtx,
12389
+ cfg,
12390
+ dispatcherOptions: {
12391
+ deliver,
12392
+ humanDelay,
12393
+ onError: (err, info) => {
12394
+ logger.error(`${info.kind} reply failed: ${String(err)}`);
12395
+ },
12396
+ onSkip: (_payload, info) => {
12397
+ if (info.reason !== "silent") {
12398
+ logger.info(`reply skipped: ${info.reason}`);
12399
+ }
12051
12400
  }
12052
- }
12053
- },
12054
- replyOptions: streamingReplyOptions
12055
- });
12056
- await flushBufferedC2CMarkdownReply();
12057
- } else if (dispatchBuffered) {
12058
- logger.debug(`[dispatch] mode=buffered session=${routeSessionKey} to=${target.to}`);
12059
- await dispatchBuffered({
12060
- ctx: finalCtx,
12061
- cfg,
12062
- dispatcherOptions: {
12063
- deliver,
12064
- humanDelay,
12065
- onError: (err, info) => {
12066
- logger.error(`${info.kind} reply failed: ${String(err)}`);
12067
12401
  },
12068
- onSkip: (_payload, info) => {
12069
- if (info.reason !== "silent") {
12070
- logger.info(`reply skipped: ${info.reason}`);
12402
+ replyOptions: streamingReplyOptions
12403
+ });
12404
+ await flushBufferedC2CMarkdownReply();
12405
+ } else if (dispatchBuffered) {
12406
+ logger.debug(`[dispatch] mode=buffered session=${routeSessionKey} to=${target.to}`);
12407
+ await dispatchBuffered({
12408
+ ctx: finalCtx,
12409
+ cfg,
12410
+ dispatcherOptions: {
12411
+ deliver,
12412
+ humanDelay,
12413
+ onError: (err, info) => {
12414
+ logger.error(`${info.kind} reply failed: ${String(err)}`);
12415
+ },
12416
+ onSkip: (_payload, info) => {
12417
+ if (info.reason !== "silent") {
12418
+ logger.info(`reply skipped: ${info.reason}`);
12419
+ }
12071
12420
  }
12072
- }
12073
- },
12074
- replyOptions: streamingReplyOptions
12075
- });
12076
- await flushBufferedC2CMarkdownReply();
12077
- } else {
12078
- logger.debug(`[dispatch] mode=legacy session=${routeSessionKey} to=${target.to}`);
12079
- const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
12080
- deliver,
12081
- humanDelay,
12082
- onError: (err, info) => {
12083
- logger.error(`${info.kind} reply failed: ${String(err)}`);
12084
- }
12085
- }) : {
12086
- dispatcher: replyApi.createReplyDispatcher?.({
12421
+ },
12422
+ replyOptions: streamingReplyOptions
12423
+ });
12424
+ await flushBufferedC2CMarkdownReply();
12425
+ } else {
12426
+ logger.debug(`[dispatch] mode=legacy session=${routeSessionKey} to=${target.to}`);
12427
+ const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
12087
12428
  deliver,
12088
12429
  humanDelay,
12089
12430
  onError: (err, info) => {
12090
12431
  logger.error(`${info.kind} reply failed: ${String(err)}`);
12091
12432
  }
12092
- }),
12093
- replyOptions: {},
12094
- markDispatchIdle: () => void 0
12095
- };
12096
- if (!dispatcherResult.dispatcher || !replyApi.dispatchReplyFromConfig) {
12097
- logger.warn("dispatcher not available, skipping reply");
12098
- return;
12099
- }
12100
- await replyApi.dispatchReplyFromConfig({
12101
- ctx: finalCtx,
12102
- cfg,
12103
- dispatcher: dispatcherResult.dispatcher,
12104
- replyOptions: {
12105
- ...typeof dispatcherResult.replyOptions === "object" && dispatcherResult.replyOptions ? dispatcherResult.replyOptions : {},
12106
- ...streamingReplyOptions ?? {}
12433
+ }) : {
12434
+ dispatcher: replyApi.createReplyDispatcher?.({
12435
+ deliver,
12436
+ humanDelay,
12437
+ onError: (err, info) => {
12438
+ logger.error(`${info.kind} reply failed: ${String(err)}`);
12439
+ }
12440
+ }),
12441
+ replyOptions: {},
12442
+ markDispatchIdle: () => void 0
12443
+ };
12444
+ if (!dispatcherResult.dispatcher || !replyApi.dispatchReplyFromConfig) {
12445
+ logger.warn("dispatcher not available, skipping reply");
12446
+ return;
12107
12447
  }
12108
- });
12109
- dispatcherResult.markDispatchIdle?.();
12110
- await flushBufferedC2CMarkdownReply();
12448
+ await replyApi.dispatchReplyFromConfig({
12449
+ ctx: finalCtx,
12450
+ cfg,
12451
+ dispatcher: dispatcherResult.dispatcher,
12452
+ replyOptions: {
12453
+ ...typeof dispatcherResult.replyOptions === "object" && dispatcherResult.replyOptions ? dispatcherResult.replyOptions : {},
12454
+ ...streamingReplyOptions ?? {}
12455
+ }
12456
+ });
12457
+ dispatcherResult.markDispatchIdle?.();
12458
+ await flushBufferedC2CMarkdownReply();
12459
+ }
12460
+ } finally {
12461
+ if (streamingController) {
12462
+ await streamingController.finalize();
12463
+ streamingController.dispose();
12464
+ }
12111
12465
  }
12112
12466
  const noReplyFallback = resolveQQBotNoReplyFallback({
12113
12467
  inbound,
@@ -12632,6 +12986,7 @@ function resolveQQBotAccount(params) {
12632
12986
  enabled,
12633
12987
  configured,
12634
12988
  appId: credentials?.appId,
12989
+ streaming: merged.streaming === true,
12635
12990
  markdownSupport: merged.markdownSupport ?? true,
12636
12991
  c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only",
12637
12992
  c2cMarkdownChunkStrategy: merged.c2cMarkdownChunkStrategy ?? "markdown-block"
@@ -12712,6 +13067,7 @@ var qqbotPlugin = {
12712
13067
  defaultAccount: { type: "string" },
12713
13068
  appId: { type: ["string", "number"] },
12714
13069
  clientSecret: { type: "string" },
13070
+ streaming: { type: "boolean" },
12715
13071
  displayAliases: {
12716
13072
  type: "object",
12717
13073
  additionalProperties: { type: "string" }
@@ -12766,6 +13122,7 @@ var qqbotPlugin = {
12766
13122
  enabled: { type: "boolean" },
12767
13123
  appId: { type: ["string", "number"] },
12768
13124
  clientSecret: { type: "string" },
13125
+ streaming: { type: "boolean" },
12769
13126
  displayAliases: {
12770
13127
  type: "object",
12771
13128
  additionalProperties: { type: "string" }
@@ -12979,6 +13336,7 @@ var plugin = {
12979
13336
  defaultAccount: { type: "string" },
12980
13337
  appId: { type: ["string", "number"] },
12981
13338
  clientSecret: { type: "string" },
13339
+ streaming: { type: "boolean" },
12982
13340
  displayAliases: {
12983
13341
  type: "object",
12984
13342
  additionalProperties: { type: "string" }
@@ -13033,6 +13391,7 @@ var plugin = {
13033
13391
  enabled: { type: "boolean" },
13034
13392
  appId: { type: ["string", "number"] },
13035
13393
  clientSecret: { type: "string" },
13394
+ streaming: { type: "boolean" },
13036
13395
  displayAliases: {
13037
13396
  type: "object",
13038
13397
  additionalProperties: { type: "string" }