@qwen-code/channel-dingtalk 0.22.3 → 0.23.0
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/DingtalkAdapter.d.ts +13 -4
- package/dist/DingtalkAdapter.d.ts.map +1 -1
- package/dist/DingtalkAdapter.js +354 -45
- package/dist/DingtalkAdapter.js.map +1 -1
- package/dist/interaction-presenter.d.ts +5 -2
- package/dist/interaction-presenter.d.ts.map +1 -1
- package/dist/interaction-presenter.js +53 -14
- package/dist/interaction-presenter.js.map +1 -1
- package/dist/interactive-card-client.d.ts +6 -0
- package/dist/interactive-card-client.d.ts.map +1 -1
- package/dist/interactive-card-client.js +40 -16
- package/dist/interactive-card-client.js.map +1 -1
- package/dist/markdown.d.ts +4 -2
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +33 -9
- package/dist/markdown.js.map +1 -1
- package/dist/outbound-file.d.ts +12 -4
- package/dist/outbound-file.d.ts.map +1 -1
- package/dist/outbound-file.js +122 -4
- package/dist/outbound-file.js.map +1 -1
- package/dist/question-card-controller.d.ts +2 -1
- package/dist/question-card-controller.d.ts.map +1 -1
- package/dist/question-card-controller.js +16 -7
- package/dist/question-card-controller.js.map +1 -1
- package/dist/status-card-controller.d.ts +24 -1
- package/dist/status-card-controller.d.ts.map +1 -1
- package/dist/status-card-controller.js +314 -84
- package/dist/status-card-controller.js.map +1 -1
- package/package.json +2 -2
|
@@ -15,6 +15,7 @@ export declare class StatusCardController {
|
|
|
15
15
|
private readonly recordsByOutTrack;
|
|
16
16
|
private readonly segmentIdsByRun;
|
|
17
17
|
private readonly terminalSegmentIds;
|
|
18
|
+
private disposed;
|
|
18
19
|
constructor(options: StatusCardControllerOptions);
|
|
19
20
|
ensure(segment: ChannelOutputSegmentContext, target: {
|
|
20
21
|
chatId: string;
|
|
@@ -28,7 +29,9 @@ export declare class StatusCardController {
|
|
|
28
29
|
* Whether a created, still-running status card is displaying content for
|
|
29
30
|
* this segment. Awaits the in-flight creation so a boundary decision made
|
|
30
31
|
* while creation is pending does not race it. A latched stream failure
|
|
31
|
-
* means the card can never show further content, so it is not live.
|
|
32
|
+
* means the card can never show further content, so it is not live. A
|
|
33
|
+
* creation that is backing off for a retry is not awaited either (see
|
|
34
|
+
* `awaitDelivery`).
|
|
32
35
|
*/
|
|
33
36
|
isCardLive(segmentId: string): Promise<boolean>;
|
|
34
37
|
/**
|
|
@@ -38,15 +41,35 @@ export declare class StatusCardController {
|
|
|
38
41
|
* caller can fall back instead of claiming delivery.
|
|
39
42
|
*/
|
|
40
43
|
flushPending(segmentId: string): Promise<boolean>;
|
|
44
|
+
abandon(segmentId: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Resolves once the card is known to be delivered (or not) without waiting
|
|
47
|
+
* through a creation backoff: `creationAttempt` is the failed attempt until
|
|
48
|
+
* the retry actually starts, so a boundary reached mid-backoff falls back
|
|
49
|
+
* to text while the retry keeps running for later output.
|
|
50
|
+
*/
|
|
51
|
+
private awaitDelivery;
|
|
41
52
|
private createRecord;
|
|
42
53
|
complete(segmentId: string, text: string, retainedContent?: (content: string) => string): Promise<boolean>;
|
|
43
54
|
fail(segmentId: string, error: string): void;
|
|
44
55
|
cancelRun(runId: string, reason: ChannelTaskCancellationReason): void;
|
|
45
56
|
claimStop(outTrackId: string, actorId: string): DingtalkCardCallbackResult;
|
|
46
57
|
private create;
|
|
58
|
+
/**
|
|
59
|
+
* Sleeps for the next creation backoff. Resolves false when finalization or
|
|
60
|
+
* disposal abandons the creation so `ready` settles without waiting.
|
|
61
|
+
*/
|
|
62
|
+
private waitForCreationRetry;
|
|
47
63
|
private scheduleFlush;
|
|
48
64
|
private flush;
|
|
49
65
|
private finalize;
|
|
66
|
+
private attemptFinalization;
|
|
67
|
+
private scheduleStreamRetry;
|
|
68
|
+
private scheduleTerminalRetry;
|
|
69
|
+
private handleStreamFailure;
|
|
70
|
+
private retryDelay;
|
|
71
|
+
private removeRecord;
|
|
72
|
+
dispose(): void;
|
|
50
73
|
private statusLine;
|
|
51
74
|
private updateRunningStatus;
|
|
52
75
|
private scheduleStatusRefresh;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-card-controller.d.ts","sourceRoot":"","sources":["../src/status-card-controller.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,2BAA2B,EAC3B,6BAA6B,EAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"status-card-controller.d.ts","sourceRoot":"","sources":["../src/status-card-controller.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,2BAA2B,EAC3B,6BAA6B,EAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAU9E,eAAO,MAAM,aAAa,QAAS,CAAC;AACpC,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAkDhE,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,6BAA6B,CAAC;IACtC,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnD;AASD,qBAAa,oBAAoB;IAOnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmC;IACpE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAmC;IACrE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkC;IAClE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAS;gBAEI,OAAO,EAAE,2BAA2B;IAEjE,MAAM,CACJ,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAC3C,IAAI;IAQP,OAAO,CACL,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAC5C,OAAO,EAAE,MAAM,GACd,IAAI;IAgBP;;;;;;;OAOG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOrD;;;;;OAKG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAuBvD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAqBhC;;;;;OAKG;YACW,aAAa;IAK3B,OAAO,CAAC,YAAY;IA8CpB,QAAQ,CACN,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAC5C,OAAO,CAAC,OAAO,CAAC;IAUnB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,6BAA6B,GAAG,IAAI;IAcrE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,0BAA0B;YAoC5D,MAAM;IAqDpB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAc5B,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,KAAK;YA4CC,QAAQ;YA0CR,mBAAmB;IA0DjC,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,mBAAmB;IAoB3B,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,YAAY;IAuBpB,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,UAAU;YAeJ,mBAAmB;IAiCjC,OAAO,CAAC,qBAAqB;CAiC9B"}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { STATUS_CARD_TEMPLATE_ID, } from './interactive-card-client.js';
|
|
2
|
+
import { STATUS_CARD_TEMPLATE_ID, isRetryableDingtalkCardError, } from './interactive-card-client.js';
|
|
3
3
|
import { sanitizeStreamingImageMarkers } from './outbound-image.js';
|
|
4
4
|
const FLUSH_INTERVAL_MS = 500;
|
|
5
5
|
const STATUS_REFRESH_INTERVAL_MS = 1_000;
|
|
6
|
+
const CONTENT_SYNC_INTERVAL_SECONDS = 5;
|
|
6
7
|
const BREAKER_PROBE_INTERVAL_MS = 30_000;
|
|
7
8
|
const MAX_CONSECUTIVE_STATUS_FAILURES = 3;
|
|
9
|
+
const INITIAL_RETRY_INTERVAL_MS = 1_000;
|
|
10
|
+
const MAX_RETRY_INTERVAL_MS = 30_000;
|
|
8
11
|
export const CONTENT_LIMIT = 20_000;
|
|
9
12
|
export const TRUNCATION_MARKER = '[Earlier output truncated]\n';
|
|
10
13
|
function boundContent(content) {
|
|
@@ -18,10 +21,13 @@ export class StatusCardController {
|
|
|
18
21
|
recordsByOutTrack = new Map();
|
|
19
22
|
segmentIdsByRun = new Map();
|
|
20
23
|
terminalSegmentIds = new Set();
|
|
24
|
+
disposed = false;
|
|
21
25
|
constructor(options) {
|
|
22
26
|
this.options = options;
|
|
23
27
|
}
|
|
24
28
|
ensure(segment, target) {
|
|
29
|
+
if (this.disposed)
|
|
30
|
+
return;
|
|
25
31
|
if (this.terminalSegmentIds.has(segment.segmentId))
|
|
26
32
|
return;
|
|
27
33
|
if (!this.recordsBySegment.has(segment.segmentId)) {
|
|
@@ -29,6 +35,8 @@ export class StatusCardController {
|
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
replace(segment, target, content) {
|
|
38
|
+
if (this.disposed)
|
|
39
|
+
return;
|
|
32
40
|
if (this.terminalSegmentIds.has(segment.segmentId))
|
|
33
41
|
return;
|
|
34
42
|
const record = this.recordsBySegment.get(segment.segmentId);
|
|
@@ -39,22 +47,27 @@ export class StatusCardController {
|
|
|
39
47
|
if (record.terminal)
|
|
40
48
|
return;
|
|
41
49
|
record.content = boundContent(content);
|
|
50
|
+
record.contentVersion++;
|
|
42
51
|
if (record.streamFailed)
|
|
43
52
|
return;
|
|
44
|
-
record.
|
|
53
|
+
record.hasPendingWrite = true;
|
|
45
54
|
this.scheduleFlush(record);
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
57
|
* Whether a created, still-running status card is displaying content for
|
|
49
58
|
* this segment. Awaits the in-flight creation so a boundary decision made
|
|
50
59
|
* while creation is pending does not race it. A latched stream failure
|
|
51
|
-
* means the card can never show further content, so it is not live.
|
|
60
|
+
* means the card can never show further content, so it is not live. A
|
|
61
|
+
* creation that is backing off for a retry is not awaited either (see
|
|
62
|
+
* `awaitDelivery`).
|
|
52
63
|
*/
|
|
53
64
|
async isCardLive(segmentId) {
|
|
65
|
+
if (this.disposed)
|
|
66
|
+
return false;
|
|
54
67
|
const record = this.recordsBySegment.get(segmentId);
|
|
55
68
|
if (!record || record.terminal || record.streamFailed)
|
|
56
69
|
return false;
|
|
57
|
-
return record
|
|
70
|
+
return this.awaitDelivery(record);
|
|
58
71
|
}
|
|
59
72
|
/**
|
|
60
73
|
* Drain any pending snapshot so callers can treat the card's current
|
|
@@ -63,23 +76,65 @@ export class StatusCardController {
|
|
|
63
76
|
* caller can fall back instead of claiming delivery.
|
|
64
77
|
*/
|
|
65
78
|
async flushPending(segmentId) {
|
|
79
|
+
if (this.disposed)
|
|
80
|
+
return false;
|
|
66
81
|
const record = this.recordsBySegment.get(segmentId);
|
|
67
82
|
if (!record || record.terminal)
|
|
68
83
|
return false;
|
|
69
|
-
if (!(await record
|
|
84
|
+
if (!(await this.awaitDelivery(record)))
|
|
70
85
|
return false;
|
|
71
86
|
while (!record.terminal && !record.streamFailed) {
|
|
72
87
|
if (record.flushTimer) {
|
|
73
88
|
clearTimeout(record.flushTimer);
|
|
74
89
|
record.flushTimer = undefined;
|
|
75
90
|
}
|
|
91
|
+
if (record.streamRetryTimer) {
|
|
92
|
+
clearTimeout(record.streamRetryTimer);
|
|
93
|
+
record.streamRetryTimer = undefined;
|
|
94
|
+
}
|
|
95
|
+
const failureVersion = record.streamFailureVersion;
|
|
76
96
|
this.flush(record);
|
|
77
97
|
await record.writeChain;
|
|
78
|
-
if (record.
|
|
98
|
+
if (record.streamFailureVersion !== failureVersion)
|
|
99
|
+
return false;
|
|
100
|
+
if (!record.hasPendingWrite)
|
|
79
101
|
break;
|
|
80
102
|
}
|
|
81
103
|
return !record.terminal && !record.streamFailed;
|
|
82
104
|
}
|
|
105
|
+
abandon(segmentId) {
|
|
106
|
+
const record = this.recordsBySegment.get(segmentId);
|
|
107
|
+
if (!record || record.terminal)
|
|
108
|
+
return;
|
|
109
|
+
record.terminal = true;
|
|
110
|
+
record.hasPendingWrite = false;
|
|
111
|
+
void record.ready.then((ready) => {
|
|
112
|
+
if (!ready)
|
|
113
|
+
return;
|
|
114
|
+
void this.options.client
|
|
115
|
+
.updateInstance({
|
|
116
|
+
outTrackId: record.outTrackId,
|
|
117
|
+
cardParamMap: {
|
|
118
|
+
flowStatus: 3,
|
|
119
|
+
hasAction: 'false',
|
|
120
|
+
stop_action: 'false',
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
.catch(() => { });
|
|
124
|
+
});
|
|
125
|
+
this.removeRecord(record);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolves once the card is known to be delivered (or not) without waiting
|
|
129
|
+
* through a creation backoff: `creationAttempt` is the failed attempt until
|
|
130
|
+
* the retry actually starts, so a boundary reached mid-backoff falls back
|
|
131
|
+
* to text while the retry keeps running for later output.
|
|
132
|
+
*/
|
|
133
|
+
async awaitDelivery(record) {
|
|
134
|
+
if (!(await record.creationAttempt))
|
|
135
|
+
return false;
|
|
136
|
+
return record.ready;
|
|
137
|
+
}
|
|
83
138
|
createRecord(segment, target, initialContent = '') {
|
|
84
139
|
const outTrackId = `qwen-status-${randomUUID()}`;
|
|
85
140
|
const record = {
|
|
@@ -92,13 +147,21 @@ export class StatusCardController {
|
|
|
92
147
|
content: boundContent(initialContent),
|
|
93
148
|
startedAt: Date.now(),
|
|
94
149
|
lastStatusSecond: 0,
|
|
150
|
+
lastContentSyncSecond: 0,
|
|
95
151
|
ready: Promise.resolve(false),
|
|
152
|
+
creationAttempt: Promise.resolve(false),
|
|
96
153
|
terminal: false,
|
|
97
154
|
streamFailed: false,
|
|
155
|
+
streamFailureVersion: 0,
|
|
156
|
+
createRetryAttempt: 0,
|
|
157
|
+
streamRetryAttempt: 0,
|
|
158
|
+
terminalRetryAttempt: 0,
|
|
98
159
|
consecutiveStatusFailures: 0,
|
|
99
160
|
stopClaimed: false,
|
|
100
161
|
forbiddenActors: new Set(),
|
|
101
162
|
lastWriteAt: Date.now(),
|
|
163
|
+
contentVersion: 0,
|
|
164
|
+
hasPendingWrite: false,
|
|
102
165
|
writeChain: Promise.resolve(),
|
|
103
166
|
};
|
|
104
167
|
this.recordsBySegment.set(record.segmentId, record);
|
|
@@ -120,6 +183,8 @@ export class StatusCardController {
|
|
|
120
183
|
void this.finalize(segmentId, boundContent(error), 'Failed', true);
|
|
121
184
|
}
|
|
122
185
|
cancelRun(runId, reason) {
|
|
186
|
+
if (this.disposed)
|
|
187
|
+
return;
|
|
123
188
|
for (const segmentId of [...(this.segmentIdsByRun.get(runId) ?? [])]) {
|
|
124
189
|
const record = this.recordsBySegment.get(segmentId);
|
|
125
190
|
if (!record)
|
|
@@ -128,6 +193,8 @@ export class StatusCardController {
|
|
|
128
193
|
}
|
|
129
194
|
}
|
|
130
195
|
claimStop(outTrackId, actorId) {
|
|
196
|
+
if (this.disposed)
|
|
197
|
+
return { kind: 'ignored', actorId };
|
|
131
198
|
const record = this.recordsByOutTrack.get(outTrackId);
|
|
132
199
|
if (!record || record.terminal || record.stopClaimed) {
|
|
133
200
|
return { kind: 'ignored', actorId };
|
|
@@ -157,46 +224,79 @@ export class StatusCardController {
|
|
|
157
224
|
};
|
|
158
225
|
}
|
|
159
226
|
async create(record, target) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
outTrackId: record.outTrackId,
|
|
165
|
-
target,
|
|
166
|
-
cardParamMap: {
|
|
167
|
-
content: initialContent,
|
|
168
|
-
flowStatus: 2,
|
|
169
|
-
statusLine: this.statusLine(record, 'Running').text,
|
|
170
|
-
hasAction: 'true',
|
|
171
|
-
stop_action: 'true',
|
|
172
|
-
},
|
|
227
|
+
for (;;) {
|
|
228
|
+
let settleAttempt;
|
|
229
|
+
record.creationAttempt = new Promise((resolve) => {
|
|
230
|
+
settleAttempt = resolve;
|
|
173
231
|
});
|
|
232
|
+
try {
|
|
233
|
+
await this.options.client.createAndDeliver({
|
|
234
|
+
templateId: STATUS_CARD_TEMPLATE_ID,
|
|
235
|
+
outTrackId: record.outTrackId,
|
|
236
|
+
target,
|
|
237
|
+
cardParamMap: {
|
|
238
|
+
content: sanitizeStreamingImageMarkers(record.content),
|
|
239
|
+
flowStatus: 2,
|
|
240
|
+
statusLine: this.statusLine(record, 'Running').text,
|
|
241
|
+
hasAction: 'true',
|
|
242
|
+
stop_action: 'true',
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
settleAttempt(true);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
settleAttempt(false);
|
|
250
|
+
this.options.onError?.('status card creation', error);
|
|
251
|
+
if (this.disposed ||
|
|
252
|
+
record.terminal ||
|
|
253
|
+
!isRetryableDingtalkCardError(error)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (!(await this.waitForCreationRetry(record)))
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (this.disposed)
|
|
261
|
+
return false;
|
|
262
|
+
if (this.recordsBySegment.get(record.segmentId) !== record)
|
|
263
|
+
return true;
|
|
264
|
+
try {
|
|
174
265
|
await this.options.client.openOrUpdateStream({
|
|
175
266
|
outTrackId: record.outTrackId,
|
|
176
267
|
key: 'content',
|
|
177
|
-
content:
|
|
268
|
+
content: sanitizeStreamingImageMarkers(record.content),
|
|
178
269
|
finalize: false,
|
|
179
270
|
});
|
|
180
|
-
return true;
|
|
181
271
|
}
|
|
182
272
|
catch (error) {
|
|
183
|
-
this.
|
|
184
|
-
await this.options.client
|
|
185
|
-
.updateInstance({
|
|
186
|
-
outTrackId: record.outTrackId,
|
|
187
|
-
cardParamMap: {
|
|
188
|
-
flowStatus: 3,
|
|
189
|
-
statusLine: 'Unavailable',
|
|
190
|
-
hasAction: 'false',
|
|
191
|
-
stop_action: 'false',
|
|
192
|
-
},
|
|
193
|
-
})
|
|
194
|
-
.catch(() => { });
|
|
195
|
-
return false;
|
|
273
|
+
this.handleStreamFailure(record, error);
|
|
196
274
|
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Sleeps for the next creation backoff. Resolves false when finalization or
|
|
279
|
+
* disposal abandons the creation so `ready` settles without waiting.
|
|
280
|
+
*/
|
|
281
|
+
waitForCreationRetry(record) {
|
|
282
|
+
const delay = this.retryDelay(record.createRetryAttempt++);
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
const settle = (resumed) => {
|
|
285
|
+
if (record.createRetryTimer)
|
|
286
|
+
clearTimeout(record.createRetryTimer);
|
|
287
|
+
record.createRetryTimer = undefined;
|
|
288
|
+
record.abandonCreation = undefined;
|
|
289
|
+
resolve(resumed);
|
|
290
|
+
};
|
|
291
|
+
record.abandonCreation = () => settle(false);
|
|
292
|
+
record.createRetryTimer = setTimeout(() => settle(true), delay);
|
|
293
|
+
});
|
|
197
294
|
}
|
|
198
295
|
scheduleFlush(record) {
|
|
199
|
-
if (record.flushTimer ||
|
|
296
|
+
if (record.flushTimer ||
|
|
297
|
+
record.streamRetryTimer ||
|
|
298
|
+
record.inFlight ||
|
|
299
|
+
record.terminal)
|
|
200
300
|
return;
|
|
201
301
|
const delay = Math.max(0, FLUSH_INTERVAL_MS - (Date.now() - record.lastWriteAt));
|
|
202
302
|
record.flushTimer = setTimeout(() => {
|
|
@@ -205,46 +305,50 @@ export class StatusCardController {
|
|
|
205
305
|
}, delay);
|
|
206
306
|
}
|
|
207
307
|
flush(record) {
|
|
208
|
-
if (
|
|
308
|
+
if (this.disposed ||
|
|
309
|
+
record.terminal ||
|
|
310
|
+
record.streamFailed ||
|
|
209
311
|
record.inFlight ||
|
|
210
|
-
record.
|
|
312
|
+
!record.hasPendingWrite)
|
|
211
313
|
return;
|
|
212
|
-
|
|
213
|
-
|
|
314
|
+
record.hasPendingWrite = false;
|
|
315
|
+
let sentVersion;
|
|
316
|
+
let contentWritten = false;
|
|
214
317
|
const write = record.writeChain
|
|
215
318
|
.then(async () => {
|
|
216
319
|
const ready = await record.ready;
|
|
217
|
-
if (!ready || record.terminal)
|
|
320
|
+
if (!ready || record.terminal || record.streamFailed)
|
|
218
321
|
return;
|
|
322
|
+
sentVersion = record.contentVersion;
|
|
219
323
|
await this.options.client.openOrUpdateStream({
|
|
220
324
|
outTrackId: record.outTrackId,
|
|
221
325
|
key: 'content',
|
|
222
|
-
content,
|
|
326
|
+
content: sanitizeStreamingImageMarkers(record.content),
|
|
223
327
|
finalize: false,
|
|
224
328
|
});
|
|
329
|
+
contentWritten = true;
|
|
330
|
+
record.streamRetryAttempt = 0;
|
|
225
331
|
await this.updateRunningStatus(record);
|
|
226
332
|
})
|
|
227
|
-
.catch((error) =>
|
|
228
|
-
record.streamFailed = true;
|
|
229
|
-
record.pendingSnapshot = undefined;
|
|
230
|
-
if (record.statusTimer) {
|
|
231
|
-
clearTimeout(record.statusTimer);
|
|
232
|
-
record.statusTimer = undefined;
|
|
233
|
-
}
|
|
234
|
-
this.options.onError?.('status card streaming', error);
|
|
235
|
-
});
|
|
333
|
+
.catch((error) => this.handleStreamFailure(record, error));
|
|
236
334
|
const tracked = write.finally(() => {
|
|
237
335
|
if (record.inFlight === tracked) {
|
|
238
336
|
record.inFlight = undefined;
|
|
239
337
|
}
|
|
240
338
|
record.lastWriteAt = Date.now();
|
|
241
|
-
if (record.
|
|
339
|
+
if (contentWritten && record.contentVersion === sentVersion) {
|
|
340
|
+
record.hasPendingWrite = false;
|
|
341
|
+
}
|
|
342
|
+
if (record.hasPendingWrite && record.contentVersion !== sentVersion) {
|
|
242
343
|
this.scheduleFlush(record);
|
|
344
|
+
}
|
|
243
345
|
});
|
|
244
346
|
record.inFlight = tracked;
|
|
245
347
|
record.writeChain = tracked;
|
|
246
348
|
}
|
|
247
349
|
async finalize(segmentId, content, state, isError, retainedContent) {
|
|
350
|
+
if (this.disposed)
|
|
351
|
+
return false;
|
|
248
352
|
const record = this.recordsBySegment.get(segmentId);
|
|
249
353
|
if (!record || record.terminal)
|
|
250
354
|
return false;
|
|
@@ -259,61 +363,175 @@ export class StatusCardController {
|
|
|
259
363
|
if (record.flushTimer)
|
|
260
364
|
clearTimeout(record.flushTimer);
|
|
261
365
|
record.flushTimer = undefined;
|
|
366
|
+
if (record.streamRetryTimer)
|
|
367
|
+
clearTimeout(record.streamRetryTimer);
|
|
368
|
+
record.streamRetryTimer = undefined;
|
|
262
369
|
if (record.statusTimer)
|
|
263
370
|
clearTimeout(record.statusTimer);
|
|
264
371
|
record.statusTimer = undefined;
|
|
265
|
-
record.
|
|
372
|
+
record.hasPendingWrite = false;
|
|
373
|
+
record.abandonCreation?.();
|
|
374
|
+
if (!(await record.ready)) {
|
|
375
|
+
this.removeRecord(record);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
await record.writeChain;
|
|
379
|
+
const retained = content ||
|
|
380
|
+
(retainedContent ? retainedContent(record.content) : record.content);
|
|
381
|
+
record.terminalIntent = {
|
|
382
|
+
content: boundContent(sanitizeStreamingImageMarkers(retained)),
|
|
383
|
+
isError,
|
|
384
|
+
statusLine: this.statusLine(record, state).text,
|
|
385
|
+
streamFinalizeSettled: false,
|
|
386
|
+
};
|
|
387
|
+
return this.attemptFinalization(record);
|
|
388
|
+
}
|
|
389
|
+
async attemptFinalization(record) {
|
|
390
|
+
if (this.disposed)
|
|
391
|
+
return false;
|
|
392
|
+
if (this.recordsBySegment.get(record.segmentId) !== record)
|
|
393
|
+
return false;
|
|
394
|
+
const intent = record.terminalIntent;
|
|
395
|
+
if (!intent)
|
|
396
|
+
return false;
|
|
397
|
+
if (!intent.streamFinalizeSettled) {
|
|
398
|
+
try {
|
|
399
|
+
await this.options.client.openOrUpdateStream({
|
|
400
|
+
outTrackId: record.outTrackId,
|
|
401
|
+
key: 'content',
|
|
402
|
+
content: '',
|
|
403
|
+
finalize: true,
|
|
404
|
+
isError: intent.isError,
|
|
405
|
+
});
|
|
406
|
+
intent.streamFinalizeSettled = true;
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (!isRetryableDingtalkCardError(error)) {
|
|
410
|
+
intent.streamFinalizeSettled = true;
|
|
411
|
+
}
|
|
412
|
+
this.options.onError?.('status card finalization', error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (this.disposed)
|
|
416
|
+
return false;
|
|
266
417
|
try {
|
|
267
|
-
if (!(await record.ready))
|
|
268
|
-
return false;
|
|
269
|
-
await record.writeChain;
|
|
270
|
-
const retained = content ||
|
|
271
|
-
(retainedContent ? retainedContent(record.content) : record.content);
|
|
272
|
-
const finalContent = boundContent(sanitizeStreamingImageMarkers(retained));
|
|
273
|
-
await this.options.client.openOrUpdateStream({
|
|
274
|
-
outTrackId: record.outTrackId,
|
|
275
|
-
key: 'content',
|
|
276
|
-
content: '',
|
|
277
|
-
finalize: true,
|
|
278
|
-
isError,
|
|
279
|
-
});
|
|
280
418
|
await this.options.client.updateInstance({
|
|
281
419
|
outTrackId: record.outTrackId,
|
|
282
420
|
cardParamMap: {
|
|
283
421
|
blockList: JSON.stringify([
|
|
284
422
|
{
|
|
285
423
|
type: 0,
|
|
286
|
-
markdown:
|
|
424
|
+
markdown: intent.content,
|
|
287
425
|
},
|
|
288
426
|
]),
|
|
289
|
-
content:
|
|
290
|
-
copy_content:
|
|
427
|
+
content: intent.content,
|
|
428
|
+
copy_content: intent.content,
|
|
291
429
|
flowStatus: 3,
|
|
292
|
-
statusLine:
|
|
430
|
+
statusLine: intent.statusLine,
|
|
293
431
|
hasAction: 'false',
|
|
294
432
|
stop_action: 'false',
|
|
295
433
|
},
|
|
296
434
|
});
|
|
297
435
|
record.content = '';
|
|
436
|
+
this.removeRecord(record);
|
|
298
437
|
return true;
|
|
299
438
|
}
|
|
300
439
|
catch (error) {
|
|
301
440
|
this.options.onError?.('status card finalization', error);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (this.recordsBySegment.get(segmentId) === record) {
|
|
306
|
-
this.recordsBySegment.delete(segmentId);
|
|
441
|
+
if (isRetryableDingtalkCardError(error)) {
|
|
442
|
+
this.scheduleTerminalRetry(record);
|
|
443
|
+
return true;
|
|
307
444
|
}
|
|
308
|
-
|
|
309
|
-
this.
|
|
445
|
+
else {
|
|
446
|
+
this.removeRecord(record);
|
|
310
447
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
scheduleStreamRetry(record) {
|
|
452
|
+
if (this.disposed ||
|
|
453
|
+
record.terminal ||
|
|
454
|
+
record.streamFailed ||
|
|
455
|
+
record.streamRetryTimer) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const delay = this.retryDelay(record.streamRetryAttempt++);
|
|
459
|
+
record.streamRetryTimer = setTimeout(() => {
|
|
460
|
+
record.streamRetryTimer = undefined;
|
|
461
|
+
if (this.disposed || record.terminal || record.streamFailed)
|
|
462
|
+
return;
|
|
463
|
+
this.flush(record);
|
|
464
|
+
}, delay);
|
|
465
|
+
}
|
|
466
|
+
scheduleTerminalRetry(record) {
|
|
467
|
+
if (this.disposed || !record.terminalIntent || record.terminalRetryTimer) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const delay = this.retryDelay(record.terminalRetryAttempt++);
|
|
471
|
+
record.terminalRetryTimer = setTimeout(() => {
|
|
472
|
+
record.terminalRetryTimer = undefined;
|
|
473
|
+
if (this.disposed)
|
|
474
|
+
return;
|
|
475
|
+
void this.attemptFinalization(record);
|
|
476
|
+
}, delay);
|
|
477
|
+
}
|
|
478
|
+
handleStreamFailure(record, error) {
|
|
479
|
+
record.streamFailureVersion++;
|
|
480
|
+
if (!this.disposed &&
|
|
481
|
+
!record.terminal &&
|
|
482
|
+
isRetryableDingtalkCardError(error)) {
|
|
483
|
+
record.hasPendingWrite = true;
|
|
484
|
+
this.scheduleStreamRetry(record);
|
|
485
|
+
}
|
|
486
|
+
else if (!record.terminal) {
|
|
487
|
+
record.streamFailed = true;
|
|
488
|
+
record.hasPendingWrite = false;
|
|
489
|
+
if (record.statusTimer) {
|
|
490
|
+
clearTimeout(record.statusTimer);
|
|
491
|
+
record.statusTimer = undefined;
|
|
315
492
|
}
|
|
316
493
|
}
|
|
494
|
+
this.options.onError?.('status card streaming', error);
|
|
495
|
+
}
|
|
496
|
+
retryDelay(attempt) {
|
|
497
|
+
return Math.min(INITIAL_RETRY_INTERVAL_MS * 2 ** Math.min(attempt, 10), MAX_RETRY_INTERVAL_MS);
|
|
498
|
+
}
|
|
499
|
+
removeRecord(record) {
|
|
500
|
+
record.abandonCreation?.();
|
|
501
|
+
if (record.flushTimer)
|
|
502
|
+
clearTimeout(record.flushTimer);
|
|
503
|
+
if (record.streamRetryTimer)
|
|
504
|
+
clearTimeout(record.streamRetryTimer);
|
|
505
|
+
if (record.terminalRetryTimer)
|
|
506
|
+
clearTimeout(record.terminalRetryTimer);
|
|
507
|
+
if (record.statusTimer)
|
|
508
|
+
clearTimeout(record.statusTimer);
|
|
509
|
+
record.flushTimer = undefined;
|
|
510
|
+
record.streamRetryTimer = undefined;
|
|
511
|
+
record.terminalRetryTimer = undefined;
|
|
512
|
+
record.statusTimer = undefined;
|
|
513
|
+
if (this.recordsBySegment.get(record.segmentId) === record) {
|
|
514
|
+
this.recordsBySegment.delete(record.segmentId);
|
|
515
|
+
}
|
|
516
|
+
if (this.recordsByOutTrack.get(record.outTrackId) === record) {
|
|
517
|
+
this.recordsByOutTrack.delete(record.outTrackId);
|
|
518
|
+
}
|
|
519
|
+
const segmentIds = this.segmentIdsByRun.get(record.runId);
|
|
520
|
+
segmentIds?.delete(record.segmentId);
|
|
521
|
+
if (segmentIds?.size === 0) {
|
|
522
|
+
this.segmentIdsByRun.delete(record.runId);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
dispose() {
|
|
526
|
+
if (this.disposed)
|
|
527
|
+
return;
|
|
528
|
+
this.disposed = true;
|
|
529
|
+
for (const record of [...this.recordsBySegment.values()]) {
|
|
530
|
+
record.terminal = true;
|
|
531
|
+
record.hasPendingWrite = false;
|
|
532
|
+
this.removeRecord(record);
|
|
533
|
+
}
|
|
534
|
+
this.terminalSegmentIds.clear();
|
|
317
535
|
}
|
|
318
536
|
statusLine(record, state) {
|
|
319
537
|
const second = Math.max(0, Math.floor((Date.now() - record.startedAt) / 1000));
|
|
@@ -324,17 +542,26 @@ export class StatusCardController {
|
|
|
324
542
|
};
|
|
325
543
|
}
|
|
326
544
|
async updateRunningStatus(record) {
|
|
327
|
-
if (record.terminal || record.streamFailed)
|
|
545
|
+
if (this.disposed || record.terminal || record.streamFailed)
|
|
328
546
|
return;
|
|
329
547
|
const status = this.statusLine(record, 'Running');
|
|
330
548
|
if (status.second === record.lastStatusSecond)
|
|
331
549
|
return;
|
|
550
|
+
const syncContent = status.second - record.lastContentSyncSecond >=
|
|
551
|
+
CONTENT_SYNC_INTERVAL_SECONDS;
|
|
332
552
|
try {
|
|
333
553
|
await this.options.client.updateInstance({
|
|
334
554
|
outTrackId: record.outTrackId,
|
|
335
|
-
cardParamMap: {
|
|
555
|
+
cardParamMap: {
|
|
556
|
+
...(syncContent
|
|
557
|
+
? { content: sanitizeStreamingImageMarkers(record.content) }
|
|
558
|
+
: {}),
|
|
559
|
+
statusLine: status.text,
|
|
560
|
+
},
|
|
336
561
|
});
|
|
337
562
|
record.lastStatusSecond = status.second;
|
|
563
|
+
if (syncContent)
|
|
564
|
+
record.lastContentSyncSecond = status.second;
|
|
338
565
|
record.consecutiveStatusFailures = 0;
|
|
339
566
|
// A success revives the per-second chain even when only a low-frequency
|
|
340
567
|
// breaker probe is scheduled.
|
|
@@ -350,13 +577,16 @@ export class StatusCardController {
|
|
|
350
577
|
}
|
|
351
578
|
}
|
|
352
579
|
scheduleStatusRefresh(record, intervalMs = STATUS_REFRESH_INTERVAL_MS) {
|
|
353
|
-
if (
|
|
580
|
+
if (this.disposed ||
|
|
581
|
+
record.terminal ||
|
|
582
|
+
record.streamFailed ||
|
|
583
|
+
record.statusTimer)
|
|
354
584
|
return;
|
|
355
585
|
const elapsed = Math.max(0, Date.now() - record.startedAt);
|
|
356
586
|
const delay = Math.max(50, intervalMs - (elapsed % intervalMs));
|
|
357
587
|
record.statusTimer = setTimeout(() => {
|
|
358
588
|
record.statusTimer = undefined;
|
|
359
|
-
if (record.terminal || record.streamFailed)
|
|
589
|
+
if (this.disposed || record.terminal || record.streamFailed)
|
|
360
590
|
return;
|
|
361
591
|
const refresh = record.writeChain.then(() => this.updateRunningStatus(record));
|
|
362
592
|
record.writeChain = refresh;
|