@qwen-code/channel-dingtalk 0.23.2 → 0.23.3-nightly.20260911.aaa6a32aae

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.
@@ -410,15 +410,6 @@ function formatChatRecord(content, budget = MAX_CHAT_RECORD_CHARS) {
410
410
  }
411
411
  /** Track seen msgIds to deduplicate retried callbacks. */
412
412
  const DEDUP_TTL_MS = 5 * 60 * 1000; // 5 minutes
413
- const BACKGROUND_RESPONSE_AGGREGATION_TIMEOUT_MS = 10 * 60 * 1000;
414
- /**
415
- * A failed aggregation delivery is retried rather than dropped: aggregating
416
- * concentrates a whole turn into one send, and the burst that makes a send
417
- * fail (several agents finishing at once against one chat quota) is exactly
418
- * when the whole result would be lost.
419
- */
420
- const BACKGROUND_RESPONSE_AGGREGATION_RETRY_MS = 30 * 1000;
421
- const BACKGROUND_RESPONSE_AGGREGATION_MAX_RETRIES = 3;
422
413
  const ACK_REACTION_NAME = '👀';
423
414
  const ACK_EMOTION_ID = '2659900';
424
415
  const ACK_EMOTION_BG_ID = 'im_bg_1';
@@ -631,24 +622,6 @@ async function withConnectLoggingSuppressed(connect) {
631
622
  }
632
623
  }
633
624
  }
634
- class ProactiveTextDeliveryError extends Error {
635
- plan;
636
- retryable;
637
- constructor(plan, cause) {
638
- super(cause instanceof Error ? cause.message : String(cause), { cause });
639
- this.plan = plan;
640
- if (cause instanceof DingtalkCardRequestError) {
641
- this.retryable = cause.retryable;
642
- }
643
- }
644
- }
645
- class ReplyTextDeliveryError extends Error {
646
- plan;
647
- constructor(plan, cause) {
648
- super(cause instanceof Error ? cause.message : String(cause), { cause });
649
- this.plan = plan;
650
- }
651
- }
652
625
  export class DingtalkChannel extends ChannelBase {
653
626
  client;
654
627
  atSender;
@@ -680,7 +653,6 @@ export class DingtalkChannel extends ChannelBase {
680
653
  */
681
654
  proactiveToken;
682
655
  interactiveCardConfig;
683
- aggregateBackgroundAgentResponses;
684
656
  interactiveCardClient;
685
657
  statusCardController;
686
658
  questionCardController;
@@ -693,10 +665,6 @@ export class DingtalkChannel extends ChannelBase {
693
665
  // boundary, input requested) mints a fresh segment UUID but the projection
694
666
  // state must survive it, or a marker split across the reset leaks.
695
667
  fileProjectors = new Map();
696
- backgroundResponseAggregations = new Map();
697
- detachedBackgroundResponseAggregations = new Set();
698
- pendingBackgroundResponseTerminals = new Map();
699
- detachedPendingBackgroundResponseTerminals = new Set();
700
668
  constructor(name, config, bridge, options) {
701
669
  super(name, config, bridge, options);
702
670
  this.atSender =
@@ -717,13 +685,6 @@ export class DingtalkChannel extends ChannelBase {
717
685
  this.config.instructions += FILE_INSTRUCTIONS;
718
686
  }
719
687
  this.interactiveCardConfig = parseDingtalkInteractiveCardConfig(config.interactiveCards);
720
- const rawAggregateBackgroundAgentResponses = config.aggregateBackgroundAgentResponses;
721
- if (rawAggregateBackgroundAgentResponses !== undefined &&
722
- typeof rawAggregateBackgroundAgentResponses !== 'boolean') {
723
- throw new Error(`Channel "${name}" aggregateBackgroundAgentResponses must be a boolean.`);
724
- }
725
- this.aggregateBackgroundAgentResponses =
726
- rawAggregateBackgroundAgentResponses === true;
727
688
  if (!config.clientId || !config.clientSecret) {
728
689
  throw new Error(`Channel "${name}" requires clientId and clientSecret for DingTalk.`);
729
690
  }
@@ -1157,14 +1118,11 @@ export class DingtalkChannel extends ChannelBase {
1157
1118
  }
1158
1119
  return replaceImageMarkers(text, markers, replacements);
1159
1120
  }
1160
- async sendReply(chatId, text, atUserId, sourceLabel, prepared = false, failOnHttpError = false) {
1121
+ async sendReply(chatId, text, atUserId, sourceLabel, prepared = false) {
1161
1122
  // chatId is a conversationId — resolve to the latest sessionWebhook.
1162
1123
  const webhook = this.webhooks.get(chatId);
1163
1124
  if (!webhook) {
1164
1125
  process.stderr.write(`[DingTalk:${this.name}] No webhook for chatId ${chatId}, cannot send.\n`);
1165
- if (failOnHttpError) {
1166
- throw new Error('DingTalk session webhook unavailable');
1167
- }
1168
1126
  return;
1169
1127
  }
1170
1128
  const outgoingText = prepared
@@ -1173,12 +1131,7 @@ export class DingtalkChannel extends ChannelBase {
1173
1131
  if (!outgoingText.trim())
1174
1132
  return;
1175
1133
  const plan = this.createReplyTextDelivery(outgoingText, atUserId, sourceLabel);
1176
- try {
1177
- await this.deliverReplyText(chatId, plan, failOnHttpError);
1178
- }
1179
- catch (error) {
1180
- throw new ReplyTextDeliveryError(plan, error);
1181
- }
1134
+ await this.deliverReplyText(chatId, plan);
1182
1135
  }
1183
1136
  createReplyTextDelivery(outgoingText, atUserId, sourceLabel) {
1184
1137
  const mentionPrefix = atUserId ? `@${atUserId}\n\n` : '';
@@ -1197,12 +1150,9 @@ export class DingtalkChannel extends ChannelBase {
1197
1150
  ...(atUserId ? { atUserId } : {}),
1198
1151
  };
1199
1152
  }
1200
- async deliverReplyText(chatId, plan, failOnHttpError = false) {
1153
+ async deliverReplyText(chatId, plan) {
1201
1154
  const webhook = this.webhooks.get(chatId);
1202
1155
  if (!webhook) {
1203
- if (failOnHttpError) {
1204
- throw new Error('DingTalk session webhook unavailable');
1205
- }
1206
1156
  return;
1207
1157
  }
1208
1158
  while (plan.nextChunk < plan.chunks.length) {
@@ -1247,24 +1197,6 @@ export class DingtalkChannel extends ChannelBase {
1247
1197
  if (!resp.ok) {
1248
1198
  const detail = await resp.text().catch(() => '');
1249
1199
  process.stderr.write(`[DingTalk:${this.name}] sendMessage failed: HTTP ${resp.status} ${detail}\n`);
1250
- if (failOnHttpError) {
1251
- throw new Error(`DingTalk reply send failed: HTTP ${resp.status} ${detail}`);
1252
- }
1253
- }
1254
- else if (failOnHttpError) {
1255
- const payload = (await resp
1256
- .clone()
1257
- .json()
1258
- .catch(() => undefined));
1259
- const response = payload && typeof payload === 'object'
1260
- ? payload
1261
- : undefined;
1262
- const value = response?.['errcode'] ?? response?.['code'];
1263
- if (value !== undefined && String(value) !== '0') {
1264
- const detail = sanitizeLogText(String(response?.['errmsg'] ?? response?.['message'] ?? value), 300);
1265
- process.stderr.write(`[DingTalk:${this.name}] sendMessage failed: ${detail}\n`);
1266
- throw new Error(`DingTalk reply send failed: ${detail}`);
1267
- }
1268
1200
  }
1269
1201
  plan.nextChunk++;
1270
1202
  }
@@ -1305,12 +1237,7 @@ export class DingtalkChannel extends ChannelBase {
1305
1237
  const plan = await this.createProactiveTextDelivery(target, text, sourceLabel);
1306
1238
  if (!plan)
1307
1239
  return;
1308
- try {
1309
- await this.deliverProactiveText(target, plan);
1310
- }
1311
- catch (error) {
1312
- throw new ProactiveTextDeliveryError(plan, error);
1313
- }
1240
+ await this.deliverProactiveText(target, plan);
1314
1241
  }
1315
1242
  async createProactiveTextDelivery(target, text, sourceLabel) {
1316
1243
  const outgoingText = await this.prepareFileOutput(text, (file, mediaId) => this.sendProactiveFile(target, file, mediaId));
@@ -1555,7 +1482,6 @@ export class DingtalkChannel extends ChannelBase {
1555
1482
  this.finishReaction(state.chatId, state.messageId, state.sessionId);
1556
1483
  }
1557
1484
  this.statusCardController?.dispose();
1558
- this.drainBackgroundResponseAggregations();
1559
1485
  this.activeReactionKeys.clear();
1560
1486
  this.sessionReactionKeys.clear();
1561
1487
  this.reactionStates.clear();
@@ -1835,11 +1761,6 @@ export class DingtalkChannel extends ChannelBase {
1835
1761
  }
1836
1762
  }
1837
1763
  this.sessionMentionTargets.delete(sessionId);
1838
- // A session dying after segments arrived but before the terminal signal is
1839
- // precisely the case the partial-card fallback exists for; dropping the
1840
- // buffer here would lose text the agent already produced, which the
1841
- // pre-aggregation code always delivered on arrival.
1842
- this.drainBackgroundResponseAggregations(sessionId);
1843
1764
  const cardRunId = this.cardRunBySession.get(sessionId);
1844
1765
  if (cardRunId) {
1845
1766
  this.cardRunBySession.delete(sessionId);
@@ -1875,9 +1796,6 @@ export class DingtalkChannel extends ChannelBase {
1875
1796
  this.cardRuns.delete(runId);
1876
1797
  }
1877
1798
  }
1878
- onSessionRetiring(sessionId) {
1879
- this.drainBackgroundResponseAggregations(sessionId);
1880
- }
1881
1799
  onTaskLifecycle(event) {
1882
1800
  if (event.type === 'started') {
1883
1801
  this.startReaction(event.chatId, event.messageId, event.sessionId);
@@ -2023,636 +1941,15 @@ export class DingtalkChannel extends ChannelBase {
2023
1941
  this.sessionMentionTargets.delete(sessionId);
2024
1942
  this.stopReaction(chatId, messageId, sessionId);
2025
1943
  }
2026
- /** Deliver every Agent segment immediately unless aggregation is enabled. */
2027
1944
  async dispatchBackgroundResponse(sessionId, text, context) {
2028
1945
  const target = this.router.getTarget(sessionId);
2029
1946
  if (!target ||
2030
1947
  target.channelName !== this.name ||
2031
- (context !== undefined && context.kind !== 'agent')) {
1948
+ (context !== undefined && context.kind !== 'agent') ||
1949
+ !text.trim()) {
2032
1950
  return super.dispatchBackgroundResponse(sessionId, text, context);
2033
1951
  }
2034
- const canAggregate = this.aggregateBackgroundAgentResponses &&
2035
- context?.kind === 'agent' &&
2036
- typeof context.turnComplete === 'boolean';
2037
- if (!canAggregate) {
2038
- if (text.trim().length === 0) {
2039
- return super.dispatchBackgroundResponse(sessionId, text, context);
2040
- }
2041
- return super.dispatchBackgroundResponse(sessionId, this.formatBackgroundAgentResponse(text, context?.label), context);
2042
- }
2043
- const key = JSON.stringify([sessionId, context.taskId, context.turnId]);
2044
- let current = this.backgroundResponseAggregations.get(key);
2045
- let parked = this.pendingBackgroundResponseTerminals.get(key);
2046
- if (current?.turnComplete === true) {
2047
- this.detachedBackgroundResponseAggregations.add(current);
2048
- this.backgroundResponseAggregations.delete(key);
2049
- current = undefined;
2050
- }
2051
- if (!current &&
2052
- (parked?.turnEnded === true ||
2053
- (parked?.turnComplete === true &&
2054
- (parked.retryTimer || parked.retryInFlight || parked.resolvers > 0)))) {
2055
- if (!parked.turnEnded) {
2056
- this.detachedPendingBackgroundResponseTerminals.add(parked);
2057
- }
2058
- parked = { sessionId, target, resolvers: 0, held: [] };
2059
- this.pendingBackgroundResponseTerminals.set(key, parked);
2060
- }
2061
- if (!current && text.trim().length === 0) {
2062
- // The first segment's target resolution can suspend (named-session owner
2063
- // lock), so a turn's terminal marker may arrive before the aggregation
2064
- // exists. Park it instead of routing it to the empty-text early return,
2065
- // or the completed turn only surfaces via the bounded wait, mislabeled.
2066
- if (!parked ||
2067
- (parked.resolvers === 0 &&
2068
- !parked.retryTimer &&
2069
- !parked.retryInFlight &&
2070
- !parked.resolutionDropped &&
2071
- !parked.turnComplete)) {
2072
- if (parked)
2073
- this.pendingBackgroundResponseTerminals.delete(key);
2074
- return super.dispatchBackgroundResponse(sessionId, text, context);
2075
- }
2076
- if (context.turnComplete) {
2077
- parked.turnComplete = true;
2078
- parked.status = context.status;
2079
- parked.label = context.label ?? parked.label;
2080
- parked.completionPartial = context.partial === true;
2081
- if (parked.resolvers === 0 &&
2082
- !parked.retryTimer &&
2083
- !parked.retryInFlight &&
2084
- (parked.retryAttempts ?? 0) >=
2085
- BACKGROUND_RESPONSE_AGGREGATION_MAX_RETRIES) {
2086
- parked.turnEnded = true;
2087
- this.pendingBackgroundResponseTerminals.delete(key);
2088
- }
2089
- }
2090
- return;
2091
- }
2092
- if (!current) {
2093
- parked ??= { sessionId, target, resolvers: 0, held: [] };
2094
- this.pendingBackgroundResponseTerminals.set(key, parked);
2095
- this.holdPendingBackgroundResponse(parked, text, context);
2096
- parked.resolvers++;
2097
- try {
2098
- let delivery;
2099
- try {
2100
- delivery = await this.resolveBackgroundResponseDelivery(sessionId);
2101
- }
2102
- catch (error) {
2103
- if (parked.resolvers === 1 &&
2104
- !this.backgroundResponseAggregations.has(key)) {
2105
- this.scheduleBackgroundResponseResolutionRetry(key, sessionId, parked);
2106
- }
2107
- else {
2108
- const existing = this.backgroundResponseAggregations.get(key);
2109
- if (existing) {
2110
- if (parked.held.length > 0) {
2111
- this.applyHeldBackgroundResponses(existing, parked);
2112
- }
2113
- this.applyPendingBackgroundResponseTerminal(existing, parked);
2114
- if (parked.resolvers === 1) {
2115
- if (existing.turnComplete) {
2116
- await this.completeBackgroundResponseAggregation(key, existing);
2117
- }
2118
- else {
2119
- this.scheduleBackgroundResponseAggregationFlush(key, existing);
2120
- }
2121
- }
2122
- }
2123
- }
2124
- throw error;
2125
- }
2126
- if (!delivery || this.router.getTarget(sessionId) !== delivery.target) {
2127
- if (parked.resolvers === 1 &&
2128
- !this.backgroundResponseAggregations.has(key)) {
2129
- this.scheduleBackgroundResponseResolutionRetry(key, sessionId, parked);
2130
- }
2131
- else {
2132
- const existing = this.backgroundResponseAggregations.get(key);
2133
- if (existing) {
2134
- if (parked.held.length > 0) {
2135
- this.applyHeldBackgroundResponses(existing, parked);
2136
- }
2137
- this.applyPendingBackgroundResponseTerminal(existing, parked);
2138
- if (parked.resolvers === 1) {
2139
- if (existing.turnComplete) {
2140
- await this.completeBackgroundResponseAggregation(key, existing);
2141
- }
2142
- else {
2143
- this.scheduleBackgroundResponseAggregationFlush(key, existing);
2144
- }
2145
- }
2146
- }
2147
- }
2148
- return;
2149
- }
2150
- if (parked.retiring ||
2151
- this.pendingBackgroundResponseTerminals.get(key) !== parked) {
2152
- await this.flushDetachedBackgroundResponse(key, sessionId, parked, delivery);
2153
- return;
2154
- }
2155
- if (parked.retryTimer) {
2156
- clearTimeout(parked.retryTimer);
2157
- parked.retryTimer = undefined;
2158
- }
2159
- current =
2160
- this.backgroundResponseAggregations.get(key) ??
2161
- this.createBackgroundResponseAggregation(key, sessionId, parked.held[0]?.context ?? context, delivery.target, delivery.sourceLabel);
2162
- this.applyHeldBackgroundResponses(current, parked);
2163
- this.applyPendingBackgroundResponseTerminal(current, parked);
2164
- if (parked.resolutionDropped) {
2165
- current.resolutionDropped = true;
2166
- parked.resolutionDropped = undefined;
2167
- }
2168
- }
2169
- finally {
2170
- parked.resolvers--;
2171
- if (this.pendingBackgroundResponseTerminals.get(key) === parked &&
2172
- parked.resolvers === 0 &&
2173
- !parked.retryTimer &&
2174
- parked.held.length === 0 &&
2175
- (!parked.resolutionDropped || parked.turnEnded)) {
2176
- this.pendingBackgroundResponseTerminals.delete(key);
2177
- }
2178
- }
2179
- if (!current)
2180
- return;
2181
- if (current.turnComplete && parked.resolvers > 0)
2182
- return;
2183
- if (!current.turnComplete) {
2184
- this.scheduleBackgroundResponseAggregationFlush(key, current);
2185
- }
2186
- else {
2187
- await this.completeBackgroundResponseAggregation(key, current);
2188
- }
2189
- return;
2190
- }
2191
- current.status = context.status;
2192
- current.label = context.label ?? current.label;
2193
- if (text.trim().length > 0)
2194
- current.parts.push(text);
2195
- if (context.turnComplete && parked && parked.resolvers > 0) {
2196
- parked.turnComplete = true;
2197
- parked.status = context.status;
2198
- parked.label = context.label ?? parked.label;
2199
- parked.completionPartial = context.partial === true;
2200
- }
2201
- else if (context.turnComplete) {
2202
- current.turnComplete = true;
2203
- current.completionPartial = context.partial === true;
2204
- }
2205
- else if (parked?.turnComplete && parked.resolvers === 0) {
2206
- current.turnComplete = true;
2207
- current.status = parked.status ?? current.status;
2208
- current.label = parked.label ?? current.label;
2209
- current.completionPartial = parked.completionPartial === true;
2210
- }
2211
- current.resolutionDropped ||= parked?.resolutionDropped;
2212
- if (!current.turnComplete) {
2213
- this.scheduleBackgroundResponseAggregationFlush(key, current);
2214
- return;
2215
- }
2216
- await this.completeBackgroundResponseAggregation(key, current);
2217
- }
2218
- async flushBackgroundResponseAggregation(key, aggregation) {
2219
- if ((this.backgroundResponseAggregations.get(key) !== aggregation &&
2220
- !this.detachedBackgroundResponseAggregations.has(aggregation)) ||
2221
- aggregation.flushing) {
2222
- return;
2223
- }
2224
- this.refreshBackgroundResponseDelivery(aggregation);
2225
- if (aggregation.retryTimer)
2226
- clearTimeout(aggregation.retryTimer);
2227
- aggregation.retryTimer = undefined;
2228
- let delivery = aggregation.delivery;
2229
- if (!delivery) {
2230
- if (aggregation.parts.length === 0) {
2231
- // A turn whose text was already drained by the bounded wait still owes
2232
- // the user its completion: the last card it saw reads `īŧˆéƒ¨åˆ†īŧ‰`.
2233
- if (!this.owesTerminalBackgroundResponseCard(aggregation)) {
2234
- if (aggregation.retiring || aggregation.turnComplete) {
2235
- this.removeBackgroundResponseAggregation(key, aggregation);
2236
- }
2237
- else {
2238
- this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
2239
- }
2240
- return;
2241
- }
2242
- }
2243
- if (aggregation.timeoutTimer)
2244
- clearTimeout(aggregation.timeoutTimer);
2245
- aggregation.timeoutTimer = undefined;
2246
- const parts = aggregation.parts.splice(0);
2247
- delivery = {
2248
- status: aggregation.status,
2249
- label: aggregation.label,
2250
- parts,
2251
- partial: this.isPartialBackgroundResponseDelivery(aggregation, parts.length),
2252
- attempts: 0,
2253
- composedTurnComplete: aggregation.turnComplete === true,
2254
- };
2255
- aggregation.delivery = delivery;
2256
- }
2257
- const body = this.formatBackgroundResponseAggregation(delivery);
2258
- aggregation.flushing = true;
2259
- let error;
2260
- try {
2261
- if (delivery.proactivePlan) {
2262
- await this.deliverProactiveText(aggregation.target, delivery.proactivePlan);
2263
- }
2264
- else if (delivery.replyPlan) {
2265
- await this.deliverReplyText(aggregation.target.chatId, delivery.replyPlan, true);
2266
- }
2267
- else if (this.supportsProactiveSend() &&
2268
- this.supportsProactiveTarget(aggregation.target)) {
2269
- await this.deliverBackgroundResponseToTarget(aggregation.sessionId, body, {
2270
- target: aggregation.target,
2271
- sourceLabel: aggregation.sourceLabel,
2272
- });
2273
- }
2274
- else {
2275
- delivery.preparedReplyBody ??= await this.prepareReplyOutput(aggregation.target.chatId, body);
2276
- await this.deliverBackgroundReply(aggregation.target.chatId, delivery.preparedReplyBody, aggregation.sessionId, aggregation.sourceLabel, true, true);
2277
- }
2278
- }
2279
- catch (caught) {
2280
- error = caught;
2281
- if (caught instanceof ProactiveTextDeliveryError) {
2282
- delivery.proactivePlan = caught.plan;
2283
- }
2284
- else if (caught instanceof ReplyTextDeliveryError) {
2285
- delivery.replyPlan = caught.plan;
2286
- }
2287
- }
2288
- finally {
2289
- aggregation.flushing = false;
2290
- }
2291
- if (error === undefined) {
2292
- aggregation.delivery = undefined;
2293
- aggregation.delivered = true;
2294
- if (delivery.composedTurnComplete === true &&
2295
- aggregation.turnComplete &&
2296
- delivery.partial !== true &&
2297
- !aggregation.retiring) {
2298
- aggregation.completionDelivered = true;
2299
- }
2300
- if (aggregation.retiring || aggregation.turnComplete) {
2301
- await this.flushBackgroundResponseAggregation(key, aggregation);
2302
- }
2303
- else {
2304
- // The turn is still open: keep the entry so its later segments re-join
2305
- // this one (and stay labelled `īŧˆéƒ¨åˆ†īŧ‰`), and re-arm the bounded wait
2306
- // so a silent turn is still reaped.
2307
- this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
2308
- }
2309
- return;
2310
- }
2311
- delivery.attempts++;
2312
- process.stderr.write(`[DingTalk:${this.name}] background response delivery failed (attempt ${delivery.attempts}): ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
2313
- if (delivery.attempts >= BACKGROUND_RESPONSE_AGGREGATION_MAX_RETRIES ||
2314
- // A permanently rejected send (an invalid appkey, a missing app) cannot
2315
- // succeed later; retrying it only spends the chat's send quota.
2316
- (error instanceof ProactiveTextDeliveryError && error.retryable === false)) {
2317
- aggregation.delivery = undefined;
2318
- aggregation.dropped = true;
2319
- if (aggregation.parts.length === 0) {
2320
- if (aggregation.retiring || aggregation.turnComplete) {
2321
- this.removeBackgroundResponseAggregation(key, aggregation);
2322
- }
2323
- else {
2324
- this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
2325
- }
2326
- }
2327
- else if (aggregation.retiring || aggregation.turnComplete) {
2328
- await this.flushBackgroundResponseAggregation(key, aggregation);
2329
- }
2330
- else {
2331
- this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
2332
- }
2333
- return;
2334
- }
2335
- aggregation.retryTimer = setTimeout(() => {
2336
- aggregation.retryTimer = undefined;
2337
- void this.flushBackgroundResponseAggregation(key, aggregation);
2338
- }, BACKGROUND_RESPONSE_AGGREGATION_RETRY_MS);
2339
- aggregation.retryTimer.unref?.();
2340
- }
2341
- /**
2342
- * A card carries `īŧˆéƒ¨åˆ†īŧ‰` whenever it is not the turn's whole output: the
2343
- * turn is still open, earlier text already went out (or was given up on),
2344
- * or the turn itself ended early.
2345
- */
2346
- isPartialBackgroundResponseDelivery(aggregation, partCount) {
2347
- return (partCount > 0 &&
2348
- (aggregation.delivered === true ||
2349
- aggregation.dropped === true ||
2350
- aggregation.resolutionDropped === true ||
2351
- aggregation.retiring === true ||
2352
- aggregation.completionPartial === true ||
2353
- aggregation.turnComplete !== true));
2354
- }
2355
- /**
2356
- * A turn that outran the bounded wait had its text delivered under the
2357
- * `īŧˆéƒ¨åˆ†īŧ‰` label. When it then completes normally with nothing buffered,
2358
- * a header-only card is the only way the chat ever learns it finished.
2359
- */
2360
- owesTerminalBackgroundResponseCard(aggregation) {
2361
- return (aggregation.turnComplete === true &&
2362
- aggregation.delivered === true &&
2363
- aggregation.completionDelivered !== true &&
2364
- aggregation.retiring !== true &&
2365
- aggregation.completionPartial !== true &&
2366
- aggregation.dropped !== true &&
2367
- aggregation.resolutionDropped !== true);
2368
- }
2369
- refreshBackgroundResponseDelivery(aggregation) {
2370
- const delivery = aggregation.delivery;
2371
- if (!delivery || aggregation.flushing)
2372
- return;
2373
- const plan = delivery.proactivePlan ?? delivery.replyPlan;
2374
- if (plan && plan.nextChunk > 0)
2375
- return;
2376
- const body = this.formatBackgroundResponseAggregation(delivery);
2377
- const header = body.split('\n', 1)[0];
2378
- const replaceHeader = (text) => text.replace(/## (?:✅|❌|âšī¸) Agent ¡ [^\n]+/, () => header);
2379
- if (plan) {
2380
- plan.title = extractTitle(body);
2381
- plan.chunks[0] = replaceHeader(plan.chunks[0]);
2382
- }
2383
- if (delivery.preparedReplyBody) {
2384
- delivery.preparedReplyBody = replaceHeader(delivery.preparedReplyBody);
2385
- }
2386
- delivery.composedTurnComplete = aggregation.turnComplete === true;
2387
- }
2388
- removeBackgroundResponseAggregation(key, aggregation) {
2389
- if (this.backgroundResponseAggregations.get(key) === aggregation) {
2390
- this.backgroundResponseAggregations.delete(key);
2391
- }
2392
- this.detachedBackgroundResponseAggregations.delete(aggregation);
2393
- }
2394
- drainBackgroundResponseAggregations(sessionId) {
2395
- for (const [key, pending] of this.pendingBackgroundResponseTerminals) {
2396
- if (sessionId !== undefined && pending.sessionId !== sessionId)
2397
- continue;
2398
- if (pending.retryTimer)
2399
- clearTimeout(pending.retryTimer);
2400
- pending.retryTimer = undefined;
2401
- pending.retiring = true;
2402
- pending.turnComplete = true;
2403
- pending.completionPartial = true;
2404
- this.pendingBackgroundResponseTerminals.delete(key);
2405
- this.detachedPendingBackgroundResponseTerminals.add(pending);
2406
- }
2407
- for (const pending of this.detachedPendingBackgroundResponseTerminals) {
2408
- if (sessionId !== undefined && pending.sessionId !== sessionId)
2409
- continue;
2410
- if (pending.retryTimer)
2411
- clearTimeout(pending.retryTimer);
2412
- pending.retryTimer = undefined;
2413
- pending.retiring = true;
2414
- pending.turnComplete = true;
2415
- pending.completionPartial = true;
2416
- if (pending.held.length > 0 &&
2417
- this.router.getTarget(pending.sessionId) === pending.target) {
2418
- void this.flushDetachedBackgroundResponse('', pending.sessionId, pending, { target: pending.target }).catch((error) => {
2419
- process.stderr.write(`[DingTalk:${this.name}] background response delivery failed during drain: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
2420
- });
2421
- }
2422
- else if (pending.held.length > 0) {
2423
- process.stderr.write(`[DingTalk:${this.name}] background response target unavailable during drain; ${pending.held.length} buffered segment(s) discarded\n`);
2424
- pending.held.length = 0;
2425
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2426
- }
2427
- else if (pending.held.length === 0) {
2428
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2429
- }
2430
- }
2431
- const aggregations = new Set([
2432
- ...this.backgroundResponseAggregations.values(),
2433
- ...this.detachedBackgroundResponseAggregations,
2434
- ]);
2435
- for (const aggregation of aggregations) {
2436
- if (sessionId !== undefined && aggregation.sessionId !== sessionId) {
2437
- continue;
2438
- }
2439
- if (aggregation.timeoutTimer)
2440
- clearTimeout(aggregation.timeoutTimer);
2441
- if (aggregation.retryTimer)
2442
- clearTimeout(aggregation.retryTimer);
2443
- aggregation.timeoutTimer = undefined;
2444
- aggregation.retryTimer = undefined;
2445
- aggregation.retiring = true;
2446
- aggregation.turnComplete = true;
2447
- aggregation.completionPartial = true;
2448
- if (!aggregation.flushing) {
2449
- void this.flushBackgroundResponseAggregation(aggregation.key, aggregation);
2450
- }
2451
- }
2452
- }
2453
- createBackgroundResponseAggregation(key, sessionId, context, target, sourceLabel) {
2454
- const aggregation = {
2455
- key,
2456
- sessionId,
2457
- target,
2458
- sourceLabel,
2459
- status: context.status,
2460
- label: context.label,
2461
- parts: [],
2462
- };
2463
- this.backgroundResponseAggregations.set(key, aggregation);
2464
- return aggregation;
2465
- }
2466
- scheduleBackgroundResponseResolutionRetry(key, sessionId, pending) {
2467
- if (pending.retiring)
2468
- return;
2469
- if (pending.retryTimer)
2470
- return;
2471
- pending.retryAttempts = (pending.retryAttempts ?? 0) + 1;
2472
- if (pending.retryAttempts >= BACKGROUND_RESPONSE_AGGREGATION_MAX_RETRIES) {
2473
- pending.resolutionDropped = true;
2474
- pending.turnEnded = pending.turnComplete === true;
2475
- pending.held.length = 0;
2476
- pending.turnComplete = undefined;
2477
- pending.status = undefined;
2478
- pending.label = undefined;
2479
- pending.completionPartial = undefined;
2480
- if (pending.turnEnded) {
2481
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2482
- }
2483
- return;
2484
- }
2485
- pending.retryTimer = setTimeout(() => {
2486
- pending.retryTimer = undefined;
2487
- pending.retryInFlight = true;
2488
- void this.retryBackgroundResponseResolution(key, sessionId, pending)
2489
- .catch((error) => {
2490
- process.stderr.write(`[DingTalk:${this.name}] background response target resolution failed: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
2491
- })
2492
- .finally(() => {
2493
- pending.retryInFlight = false;
2494
- if (pending.retiring ||
2495
- (!pending.retryTimer && !pending.resolutionDropped)) {
2496
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2497
- }
2498
- if (this.pendingBackgroundResponseTerminals.get(key) === pending &&
2499
- pending.resolvers === 0 &&
2500
- !pending.retryTimer &&
2501
- pending.held.length === 0 &&
2502
- (!pending.resolutionDropped || pending.turnEnded)) {
2503
- this.pendingBackgroundResponseTerminals.delete(key);
2504
- }
2505
- });
2506
- }, BACKGROUND_RESPONSE_AGGREGATION_RETRY_MS);
2507
- pending.retryTimer.unref?.();
2508
- const active = this.pendingBackgroundResponseTerminals.get(key);
2509
- if (!active || active === pending) {
2510
- this.pendingBackgroundResponseTerminals.set(key, pending);
2511
- }
2512
- }
2513
- async retryBackgroundResponseResolution(key, sessionId, pending) {
2514
- let delivery;
2515
- try {
2516
- delivery = await this.resolveBackgroundResponseDelivery(sessionId);
2517
- }
2518
- catch (error) {
2519
- if ((this.pendingBackgroundResponseTerminals.get(key) === pending &&
2520
- !this.backgroundResponseAggregations.has(key)) ||
2521
- this.detachedPendingBackgroundResponseTerminals.has(pending)) {
2522
- this.scheduleBackgroundResponseResolutionRetry(key, sessionId, pending);
2523
- }
2524
- throw error;
2525
- }
2526
- if (!delivery || this.router.getTarget(sessionId) !== delivery.target) {
2527
- if (pending.retiring) {
2528
- if (pending.held.length > 0) {
2529
- process.stderr.write(`[DingTalk:${this.name}] background response target unavailable during drain; ${pending.held.length} buffered segment(s) discarded\n`);
2530
- }
2531
- pending.held.length = 0;
2532
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2533
- return;
2534
- }
2535
- if ((this.pendingBackgroundResponseTerminals.get(key) === pending &&
2536
- !this.backgroundResponseAggregations.has(key)) ||
2537
- this.detachedPendingBackgroundResponseTerminals.has(pending)) {
2538
- this.scheduleBackgroundResponseResolutionRetry(key, sessionId, pending);
2539
- }
2540
- return;
2541
- }
2542
- if (pending.retiring ||
2543
- this.pendingBackgroundResponseTerminals.get(key) !== pending ||
2544
- pending.turnComplete) {
2545
- await this.flushDetachedBackgroundResponse(key, sessionId, pending, delivery);
2546
- return;
2547
- }
2548
- const first = pending.held[0];
2549
- if (!first)
2550
- return;
2551
- const aggregation = this.createBackgroundResponseAggregation(key, sessionId, first.context, delivery.target, delivery.sourceLabel);
2552
- this.applyHeldBackgroundResponses(aggregation, pending);
2553
- if (pending.resolutionDropped) {
2554
- aggregation.resolutionDropped = true;
2555
- pending.resolutionDropped = undefined;
2556
- }
2557
- if (this.pendingBackgroundResponseTerminals.get(key) === pending) {
2558
- this.pendingBackgroundResponseTerminals.delete(key);
2559
- }
2560
- this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
2561
- }
2562
- holdPendingBackgroundResponse(pending, text, context) {
2563
- if (text.trim().length > 0)
2564
- pending.held.push({ text, context });
2565
- if (context.turnComplete) {
2566
- pending.turnComplete = true;
2567
- pending.status = context.status;
2568
- pending.label = context.label ?? pending.label;
2569
- pending.completionPartial = context.partial === true;
2570
- }
2571
- }
2572
- applyHeldBackgroundResponses(aggregation, pending) {
2573
- for (const { text, context } of pending.held.splice(0)) {
2574
- aggregation.status = context.status;
2575
- aggregation.label = context.label ?? aggregation.label;
2576
- aggregation.parts.push(text);
2577
- if (context.turnComplete) {
2578
- aggregation.turnComplete = true;
2579
- aggregation.completionPartial = context.partial === true;
2580
- }
2581
- }
2582
- }
2583
- applyPendingBackgroundResponseTerminal(aggregation, pending) {
2584
- if (!pending.turnComplete)
2585
- return;
2586
- aggregation.turnComplete = true;
2587
- aggregation.status = pending.status ?? aggregation.status;
2588
- aggregation.label = pending.label ?? aggregation.label;
2589
- aggregation.completionPartial = pending.completionPartial === true;
2590
- pending.turnComplete = undefined;
2591
- pending.status = undefined;
2592
- pending.label = undefined;
2593
- pending.completionPartial = undefined;
2594
- }
2595
- async completeBackgroundResponseAggregation(key, aggregation) {
2596
- if (aggregation.delivery) {
2597
- aggregation.delivery.status = aggregation.status;
2598
- aggregation.delivery.label =
2599
- aggregation.label ?? aggregation.delivery.label;
2600
- aggregation.delivery.partial =
2601
- this.isPartialBackgroundResponseDelivery(aggregation, aggregation.delivery.parts.length) || aggregation.parts.length > 0;
2602
- }
2603
- if (aggregation.timeoutTimer)
2604
- clearTimeout(aggregation.timeoutTimer);
2605
- aggregation.timeoutTimer = undefined;
2606
- await this.flushBackgroundResponseAggregation(key, aggregation);
2607
- }
2608
- async flushDetachedBackgroundResponse(key, sessionId, pending, delivery) {
2609
- const first = pending.held[0];
2610
- if (!first) {
2611
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2612
- return;
2613
- }
2614
- const aggregation = {
2615
- key,
2616
- sessionId,
2617
- target: delivery.target,
2618
- sourceLabel: delivery.sourceLabel,
2619
- status: first.context.status,
2620
- label: first.context.label,
2621
- parts: [],
2622
- turnComplete: pending.turnComplete,
2623
- completionPartial: pending.completionPartial,
2624
- resolutionDropped: pending.resolutionDropped,
2625
- };
2626
- this.applyHeldBackgroundResponses(aggregation, pending);
2627
- aggregation.status = pending.status ?? aggregation.status;
2628
- aggregation.label = pending.label ?? aggregation.label;
2629
- if (this.pendingBackgroundResponseTerminals.get(key) === pending) {
2630
- this.pendingBackgroundResponseTerminals.delete(key);
2631
- }
2632
- this.detachedPendingBackgroundResponseTerminals.delete(pending);
2633
- this.detachedBackgroundResponseAggregations.add(aggregation);
2634
- await this.flushBackgroundResponseAggregation(key, aggregation);
2635
- }
2636
- scheduleBackgroundResponseAggregationFlush(key, aggregation) {
2637
- if (aggregation.timeoutTimer || aggregation.delivery)
2638
- return;
2639
- aggregation.timeoutTimer = setTimeout(() => {
2640
- aggregation.timeoutTimer = undefined;
2641
- void this.flushBackgroundResponseAggregation(key, aggregation);
2642
- }, BACKGROUND_RESPONSE_AGGREGATION_TIMEOUT_MS);
2643
- aggregation.timeoutTimer.unref?.();
2644
- }
2645
- formatBackgroundResponseAggregation(delivery) {
2646
- const icon = delivery.status === 'completed'
2647
- ? '✅'
2648
- : delivery.status === 'failed'
2649
- ? '❌'
2650
- : 'âšī¸';
2651
- const label = this.formatBackgroundAgentLabel(delivery.label);
2652
- const header = `## ${icon} Agent ¡ ${label}${delivery.partial ? 'īŧˆéƒ¨åˆ†īŧ‰' : ''}`;
2653
- if (delivery.parts.length === 0)
2654
- return header;
2655
- return `${header}\n\n${delivery.parts.join('\n\n')}`;
1952
+ return super.dispatchBackgroundResponse(sessionId, this.formatBackgroundAgentResponse(text, context?.label), context);
2656
1953
  }
2657
1954
  formatBackgroundAgentResponse(text, label) {
2658
1955
  return `## 🤖 Agent ¡ ${this.formatBackgroundAgentLabel(label)}\n\n${text}`;
@@ -2664,24 +1961,13 @@ export class DingtalkChannel extends ChannelBase {
2664
1961
  .trim();
2665
1962
  return escapeDingTalkMarkdown(normalized || '后台äģģåŠĄ');
2666
1963
  }
2667
- /**
2668
- * Body-identical to the base implementation — this override exists only to
2669
- * widen the signature, which the base seam does not declare. The background
2670
- * aggregation flush passes `prepared` so the body it already projected isn't
2671
- * projected a second time (that would re-upload its files), and
2672
- * `failOnHttpError` so a failed send throws instead of being swallowed,
2673
- * letting the flush capture the delivery plan for the next retry.
2674
- */
2675
- async deliverBackgroundReply(chatId, text, sessionId, sourceLabel, prepared = false, failOnHttpError = false) {
2676
- await this.sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared, failOnHttpError);
2677
- }
2678
- async sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared = false, failOnHttpError = false) {
1964
+ async sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared = false) {
2679
1965
  const atUserId = this.atSender
2680
1966
  ? this.sessionMentionTargets.get(sessionId)
2681
1967
  : undefined;
2682
1968
  if (atUserId)
2683
1969
  this.sessionMentionTargets.delete(sessionId);
2684
- await this.sendReply(chatId, text, atUserId, sourceLabel ?? this.getResponseSourceLabel(sessionId), prepared, failOnHttpError);
1970
+ await this.sendReply(chatId, text, atUserId, sourceLabel ?? this.getResponseSourceLabel(sessionId), prepared);
2685
1971
  }
2686
1972
  async sendFallbackReply(chatId, text, sessionId, sourceLabel) {
2687
1973
  // Mid-run fallbacks must not consume the prompt's mention target: the