@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.d.ts +35 -3
- package/dist/index.js +426 -67
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +2 -0
- package/package.json +2 -2
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
|
|
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
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
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
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
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
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
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
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
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
|
-
|
|
12110
|
-
|
|
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" }
|