codeksei 0.1.0 → 0.1.1
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/LICENSE +661 -661
- package/README.en.md +109 -47
- package/README.md +79 -58
- package/bin/cyberboss.js +1 -1
- package/package.json +86 -86
- package/scripts/open_shared_wechat_thread.sh +77 -77
- package/scripts/open_wechat_thread.sh +108 -108
- package/scripts/shared-common.js +144 -144
- package/scripts/shared-open.js +14 -14
- package/scripts/shared-start.js +5 -5
- package/scripts/shared-status.js +27 -27
- package/scripts/show_shared_status.sh +45 -45
- package/scripts/start_shared_app_server.sh +52 -52
- package/scripts/start_shared_wechat.sh +94 -94
- package/scripts/timeline-screenshot.sh +14 -14
- package/src/adapters/channel/weixin/account-store.js +99 -99
- package/src/adapters/channel/weixin/api-v2.js +50 -50
- package/src/adapters/channel/weixin/api.js +169 -169
- package/src/adapters/channel/weixin/context-token-store.js +84 -84
- package/src/adapters/channel/weixin/index.js +618 -604
- package/src/adapters/channel/weixin/legacy.js +579 -566
- package/src/adapters/channel/weixin/media-mime.js +22 -22
- package/src/adapters/channel/weixin/media-receive.js +370 -370
- package/src/adapters/channel/weixin/media-send.js +102 -102
- package/src/adapters/channel/weixin/message-utils-v2.js +282 -282
- package/src/adapters/channel/weixin/message-utils.js +199 -199
- package/src/adapters/channel/weixin/redact.js +41 -41
- package/src/adapters/channel/weixin/reminder-queue-store.js +101 -101
- package/src/adapters/channel/weixin/sync-buffer-store.js +35 -35
- package/src/adapters/runtime/codex/events.js +215 -215
- package/src/adapters/runtime/codex/index.js +109 -104
- package/src/adapters/runtime/codex/message-utils.js +95 -95
- package/src/adapters/runtime/codex/model-catalog.js +106 -106
- package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -75
- package/src/adapters/runtime/codex/rpc-client.js +339 -339
- package/src/adapters/runtime/codex/session-store.js +286 -286
- package/src/app/channel-send-file-cli.js +57 -57
- package/src/app/diary-write-cli.js +236 -88
- package/src/app/note-sync-cli.js +2 -2
- package/src/app/reminder-write-cli.js +215 -210
- package/src/app/review-cli.js +7 -5
- package/src/app/system-checkin-poller.js +64 -64
- package/src/app/system-send-cli.js +129 -129
- package/src/app/timeline-event-cli.js +28 -25
- package/src/app/timeline-screenshot-cli.js +103 -100
- package/src/core/app.js +1763 -1763
- package/src/core/branding.js +2 -1
- package/src/core/command-registry.js +381 -369
- package/src/core/config.js +30 -14
- package/src/core/default-targets.js +163 -163
- package/src/core/durable-note-schema.js +9 -8
- package/src/core/instructions-template.js +17 -16
- package/src/core/note-sync.js +8 -7
- package/src/core/path-utils.js +54 -0
- package/src/core/project-radar.js +11 -10
- package/src/core/review.js +48 -50
- package/src/core/stream-delivery.js +1162 -983
- package/src/core/system-message-dispatcher.js +68 -68
- package/src/core/system-message-queue-store.js +128 -128
- package/src/core/thread-state-store.js +96 -96
- package/src/core/timeline-screenshot-queue-store.js +134 -134
- package/src/core/timezone.js +436 -0
- package/src/core/workspace-bootstrap.js +9 -1
- package/src/index.js +148 -146
- package/src/integrations/timeline/index.js +130 -74
- package/src/integrations/timeline/state-sync.js +240 -0
- package/templates/weixin-instructions.md +12 -38
- package/templates/weixin-operations.md +29 -31
|
@@ -1,990 +1,1169 @@
|
|
|
1
|
-
const crypto = require("crypto");
|
|
2
|
-
const { sanitizeProtocolLeakText } = require("../adapters/runtime/codex/protocol-leak-monitor");
|
|
3
|
-
const { normalizeAssistantPhase } = require("../adapters/runtime/codex/message-utils");
|
|
4
|
-
|
|
5
|
-
const RECENT_WEIXIN_DELIVERY_TTL_MS = 30_000;
|
|
6
|
-
const STREAM_PROGRESS_MAX_CHARS = 120;
|
|
7
|
-
const STREAM_PROGRESS_MAX_LINES = 2;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
queue.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
if (!
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const { sanitizeProtocolLeakText } = require("../adapters/runtime/codex/protocol-leak-monitor");
|
|
3
|
+
const { normalizeAssistantPhase } = require("../adapters/runtime/codex/message-utils");
|
|
4
|
+
|
|
5
|
+
const RECENT_WEIXIN_DELIVERY_TTL_MS = 30_000;
|
|
6
|
+
const STREAM_PROGRESS_MAX_CHARS = 120;
|
|
7
|
+
const STREAM_PROGRESS_MAX_LINES = 2;
|
|
8
|
+
const STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS = 40;
|
|
9
|
+
|
|
10
|
+
class StreamDelivery {
|
|
11
|
+
constructor({
|
|
12
|
+
channelAdapter,
|
|
13
|
+
sessionStore,
|
|
14
|
+
weixinReplyMode = "settled",
|
|
15
|
+
deliveryTraceEnabled = false,
|
|
16
|
+
onDeliveryFailure = null,
|
|
17
|
+
}) {
|
|
18
|
+
this.channelAdapter = channelAdapter;
|
|
19
|
+
this.sessionStore = sessionStore;
|
|
20
|
+
this.weixinReplyMode = normalizeWeixinReplyMode(weixinReplyMode);
|
|
21
|
+
this.deliveryTraceEnabled = Boolean(deliveryTraceEnabled);
|
|
22
|
+
this.onDeliveryFailure = typeof onDeliveryFailure === "function" ? onDeliveryFailure : null;
|
|
23
|
+
this.replyTargetByBindingKey = new Map();
|
|
24
|
+
this.pendingReplyTargetsByThreadId = new Map();
|
|
25
|
+
this.stateByRunKey = new Map();
|
|
26
|
+
this.ignoredRunKeys = new Set();
|
|
27
|
+
this.recentSettledWeixinDeliveries = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setReplyTarget(bindingKey, target) {
|
|
31
|
+
if (!bindingKey || !target?.userId || !target?.contextToken) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.replyTargetByBindingKey.set(bindingKey, {
|
|
35
|
+
userId: String(target.userId).trim(),
|
|
36
|
+
contextToken: String(target.contextToken).trim(),
|
|
37
|
+
provider: normalizeText(target.provider),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
queueReplyTargetForThread(threadId, target) {
|
|
42
|
+
const normalizedThreadId = normalizeText(threadId);
|
|
43
|
+
if (!normalizedThreadId || !target?.userId || !target?.contextToken) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const queue = this.pendingReplyTargetsByThreadId.get(normalizedThreadId) || [];
|
|
47
|
+
queue.push({
|
|
48
|
+
userId: String(target.userId).trim(),
|
|
49
|
+
contextToken: String(target.contextToken).trim(),
|
|
50
|
+
provider: normalizeText(target.provider),
|
|
51
|
+
});
|
|
52
|
+
this.pendingReplyTargetsByThreadId.set(normalizedThreadId, queue);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async handleRuntimeEvent(event) {
|
|
56
|
+
const threadId = normalizeText(event?.payload?.threadId);
|
|
57
|
+
const turnId = normalizeText(event?.payload?.turnId);
|
|
58
|
+
if (!threadId) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const ignoredRunKey = turnId ? buildRunKey(threadId, turnId) : "";
|
|
62
|
+
if (ignoredRunKey && this.ignoredRunKeys.has(ignoredRunKey)) {
|
|
63
|
+
if (event.type === "runtime.turn.completed" || event.type === "runtime.turn.failed") {
|
|
64
|
+
this.ignoredRunKeys.delete(ignoredRunKey);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case "runtime.turn.started": {
|
|
71
|
+
this.disposeSupersededAbandonedRuns(threadId, turnId);
|
|
72
|
+
const state = this.ensureRunState(threadId, turnId);
|
|
73
|
+
state.turnId = turnId || state.turnId;
|
|
74
|
+
state.abandonedAt = 0;
|
|
75
|
+
this.attachReplyTarget(state);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case "runtime.reply.delta": {
|
|
79
|
+
const state = this.ensureRunState(threadId, turnId);
|
|
80
|
+
state.abandonedAt = 0;
|
|
81
|
+
this.upsertItem(state, {
|
|
82
|
+
itemId: normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`,
|
|
83
|
+
text: normalizeLineEndings(event.payload.text),
|
|
84
|
+
completed: false,
|
|
85
|
+
phase: normalizeAssistantPhase(event.payload.phase),
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case "runtime.reply.completed": {
|
|
90
|
+
const state = this.ensureRunState(threadId, turnId);
|
|
91
|
+
state.abandonedAt = 0;
|
|
92
|
+
const itemId = normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`;
|
|
93
|
+
const phase = normalizeAssistantPhase(event.payload.phase);
|
|
94
|
+
this.upsertItem(state, {
|
|
95
|
+
itemId,
|
|
96
|
+
text: normalizeLineEndings(event.payload.text),
|
|
97
|
+
completed: true,
|
|
98
|
+
phase,
|
|
99
|
+
});
|
|
100
|
+
await this.flush(state, {
|
|
101
|
+
force: false,
|
|
102
|
+
trigger: {
|
|
103
|
+
source: event.type,
|
|
104
|
+
itemId,
|
|
105
|
+
phase,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
case "runtime.turn.completed": {
|
|
111
|
+
const state = this.ensureRunState(threadId, turnId);
|
|
112
|
+
state.turnId = turnId || state.turnId;
|
|
113
|
+
state.abandonedAt = 0;
|
|
114
|
+
await this.flush(state, {
|
|
115
|
+
force: true,
|
|
116
|
+
trigger: { source: event.type },
|
|
117
|
+
});
|
|
118
|
+
this.disposeRunState(state.runKey);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case "runtime.turn.failed":
|
|
122
|
+
this.disposeRunState(buildRunKey(threadId, turnId));
|
|
123
|
+
return;
|
|
124
|
+
default:
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async finishTurn({ threadId, finalText }) {
|
|
130
|
+
const normalizedThreadId = normalizeText(threadId);
|
|
131
|
+
const normalizedFinalText = normalizeLineEndings(finalText);
|
|
132
|
+
if (!normalizedThreadId || !normalizedFinalText) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const state = this.ensureRunState(normalizedThreadId, "");
|
|
137
|
+
this.attachReplyTarget(state);
|
|
138
|
+
if (!state.itemOrder.length) {
|
|
139
|
+
this.upsertItem(state, {
|
|
140
|
+
itemId: "final",
|
|
141
|
+
text: normalizedFinalText,
|
|
142
|
+
completed: true,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
const itemId = state.itemOrder[state.itemOrder.length - 1] || "final";
|
|
146
|
+
this.setItemText(state, itemId, normalizedFinalText, true);
|
|
147
|
+
for (const candidateId of state.itemOrder) {
|
|
148
|
+
const item = state.items.get(candidateId);
|
|
149
|
+
if (item) {
|
|
150
|
+
item.currentText = item.completedText || item.currentText;
|
|
151
|
+
item.completed = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await this.flush(state, {
|
|
157
|
+
force: true,
|
|
158
|
+
trigger: {
|
|
159
|
+
source: "finishTurn",
|
|
160
|
+
itemId: state.itemOrder[state.itemOrder.length - 1] || "final",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
this.disposeRunState(state.runKey);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async finalizeAbandonedTurn({ threadId, turnId = "", trailingText = "" }) {
|
|
167
|
+
const normalizedThreadId = normalizeText(threadId);
|
|
168
|
+
const normalizedTurnId = normalizeText(turnId);
|
|
169
|
+
const normalizedTrailingText = normalizeLineEndings(trailingText).trim();
|
|
170
|
+
if (!normalizedThreadId) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const state = this.findRunState(normalizedThreadId, normalizedTurnId);
|
|
175
|
+
if (!state) {
|
|
176
|
+
// If this exact turn is already gone, do not resurrect a new pending run
|
|
177
|
+
// just to send the watchdog suffix again. That creates duplicate "tail"
|
|
178
|
+
// messages after delivery failure or other local terminal states.
|
|
179
|
+
if (normalizedTurnId) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (normalizedTrailingText) {
|
|
183
|
+
await this.finishTurn({
|
|
184
|
+
threadId: normalizedThreadId,
|
|
185
|
+
finalText: normalizedTrailingText,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.attachReplyTarget(state);
|
|
192
|
+
if (normalizedTrailingText) {
|
|
193
|
+
this.upsertItem(state, {
|
|
194
|
+
itemId: "__watchdog__",
|
|
195
|
+
text: normalizedTrailingText,
|
|
196
|
+
completed: true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
await this.flush(state, {
|
|
200
|
+
force: true,
|
|
201
|
+
trigger: {
|
|
202
|
+
source: "finalizeAbandonedTurn",
|
|
203
|
+
itemId: normalizedTrailingText ? "__watchdog__" : "",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
removeStateItem(state, "__watchdog__");
|
|
207
|
+
state.sentText = buildCurrentSafeReplyText(state, { force: true });
|
|
208
|
+
state.abandonedAt = Date.now();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
ensureRunState(threadId, turnId = "") {
|
|
212
|
+
const runKey = buildRunKey(threadId, turnId);
|
|
213
|
+
const existing = this.stateByRunKey.get(runKey);
|
|
214
|
+
if (existing) {
|
|
215
|
+
return existing;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const created = {
|
|
219
|
+
runKey,
|
|
220
|
+
threadId,
|
|
221
|
+
bindingKey: "",
|
|
222
|
+
replyTarget: null,
|
|
223
|
+
turnId: normalizeText(turnId),
|
|
224
|
+
itemOrder: [],
|
|
225
|
+
items: new Map(),
|
|
226
|
+
weixinReplyMode: this.weixinReplyMode,
|
|
227
|
+
sentText: "",
|
|
228
|
+
sendChain: Promise.resolve(),
|
|
229
|
+
flushPromise: null,
|
|
230
|
+
abandonedAt: 0,
|
|
231
|
+
};
|
|
232
|
+
this.stateByRunKey.set(runKey, created);
|
|
233
|
+
this.attachReplyTarget(created);
|
|
234
|
+
return created;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
findRunState(threadId, turnId = "") {
|
|
238
|
+
const normalizedThreadId = normalizeText(threadId);
|
|
239
|
+
const normalizedTurnId = normalizeText(turnId);
|
|
240
|
+
if (!normalizedThreadId) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (normalizedTurnId) {
|
|
244
|
+
const exact = this.stateByRunKey.get(buildRunKey(normalizedThreadId, normalizedTurnId));
|
|
245
|
+
if (exact) {
|
|
246
|
+
return exact;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const pending = this.stateByRunKey.get(buildRunKey(normalizedThreadId, ""));
|
|
250
|
+
if (pending && (!normalizedTurnId || !pending.turnId || pending.turnId === normalizedTurnId)) {
|
|
251
|
+
return pending;
|
|
252
|
+
}
|
|
253
|
+
for (const candidate of this.stateByRunKey.values()) {
|
|
254
|
+
if (candidate.threadId !== normalizedThreadId) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (!normalizedTurnId || candidate.turnId === normalizedTurnId) {
|
|
258
|
+
return candidate;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
attachReplyTarget(state) {
|
|
265
|
+
if (!state.replyTarget) {
|
|
266
|
+
const queue = this.pendingReplyTargetsByThreadId.get(state.threadId) || [];
|
|
267
|
+
if (queue.length) {
|
|
268
|
+
state.replyTarget = queue.shift();
|
|
269
|
+
if (queue.length) {
|
|
270
|
+
this.pendingReplyTargetsByThreadId.set(state.threadId, queue);
|
|
271
|
+
} else {
|
|
272
|
+
this.pendingReplyTargetsByThreadId.delete(state.threadId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const linked = this.sessionStore.findBindingForThreadId(state.threadId);
|
|
277
|
+
if (!linked?.bindingKey) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
state.bindingKey = linked.bindingKey;
|
|
281
|
+
if (!state.replyTarget) {
|
|
282
|
+
const target = this.replyTargetByBindingKey.get(linked.bindingKey);
|
|
283
|
+
state.replyTarget = target;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
upsertItem(state, { itemId, text, completed, phase = "" }) {
|
|
288
|
+
if (!text) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!state.items.has(itemId)) {
|
|
292
|
+
state.itemOrder.push(itemId);
|
|
293
|
+
state.items.set(itemId, {
|
|
294
|
+
currentText: "",
|
|
295
|
+
completedText: "",
|
|
296
|
+
completed: false,
|
|
297
|
+
phase: "",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const current = state.items.get(itemId);
|
|
302
|
+
const normalizedPhase = normalizeAssistantPhase(phase);
|
|
303
|
+
if (normalizedPhase) {
|
|
304
|
+
current.phase = normalizedPhase;
|
|
305
|
+
}
|
|
306
|
+
if (completed) {
|
|
307
|
+
const merged = mergeCompletedItemText(current.currentText, text);
|
|
308
|
+
current.currentText = merged;
|
|
309
|
+
current.completedText = merged;
|
|
310
|
+
current.completed = true;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
current.currentText = appendStreamingText(current.currentText, text);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setItemText(state, itemId, text, completed) {
|
|
318
|
+
if (!text) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!state.items.has(itemId)) {
|
|
322
|
+
state.itemOrder.push(itemId);
|
|
323
|
+
state.items.set(itemId, {
|
|
324
|
+
currentText: "",
|
|
325
|
+
completedText: "",
|
|
326
|
+
completed: false,
|
|
327
|
+
phase: "",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const current = state.items.get(itemId);
|
|
332
|
+
current.currentText = text;
|
|
333
|
+
if (completed) {
|
|
334
|
+
current.completedText = text;
|
|
335
|
+
}
|
|
336
|
+
current.completed = Boolean(completed);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async flush(state, { force, trigger = null }) {
|
|
340
|
+
const previous = state.flushPromise || Promise.resolve();
|
|
341
|
+
const current = previous
|
|
342
|
+
.catch(() => {})
|
|
343
|
+
.then(() => this.flushNow(state, { force, trigger }));
|
|
344
|
+
const tracked = current.finally(() => {
|
|
345
|
+
const latestState = this.stateByRunKey.get(state.runKey);
|
|
346
|
+
if (latestState && latestState.flushPromise === tracked) {
|
|
347
|
+
latestState.flushPromise = null;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
state.flushPromise = tracked;
|
|
351
|
+
await tracked;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async flushNow(state, { force, trigger = null }) {
|
|
355
|
+
this.attachReplyTarget(state);
|
|
356
|
+
if (!state.replyTarget) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// WeChat reply mode is explicit. `settled` waits for the terminal snapshot;
|
|
360
|
+
// `stream` ships user-visible assistant message blocks as they complete.
|
|
361
|
+
// Keep the modes distinct here so future "optimize the stream" tweaks do
|
|
362
|
+
// not accidentally reintroduce token-level spam or repeated block sends.
|
|
363
|
+
if (!force && prefersSettledDelivery(state)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const plainText = markdownToPlainText(buildReplyText(state, {
|
|
368
|
+
completedOnly: !force,
|
|
369
|
+
preferLatestMessage: prefersSettledDelivery(state),
|
|
370
|
+
force,
|
|
371
|
+
}));
|
|
372
|
+
const sanitized = sanitizeReplyText(state.replyTarget, plainText);
|
|
373
|
+
if (sanitized.suppress) {
|
|
374
|
+
state.sentText = sanitized.text;
|
|
375
|
+
console.log(
|
|
367
376
|
`[codeksei] suppressed system reply `
|
|
368
|
-
+ `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
|
|
369
|
-
+ `preview=${JSON.stringify(plainText.slice(0, 80))}`
|
|
370
|
-
);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
const safeText = sanitized.text;
|
|
374
|
-
if (!safeText || safeText === state.sentText) {
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (state.sentText && !safeText.startsWith(state.sentText)) {
|
|
377
|
+
+ `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
|
|
378
|
+
+ `preview=${JSON.stringify(plainText.slice(0, 80))}`
|
|
379
|
+
);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const safeText = sanitized.text;
|
|
383
|
+
if (!safeText || safeText === state.sentText) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (state.sentText && !safeText.startsWith(state.sentText)) {
|
|
379
388
|
console.warn(`[codeksei] skip non-monotonic reply thread=${state.threadId}`);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const delta = normalizeDeliveryDelta(
|
|
384
|
-
safeText.slice(state.sentText.length),
|
|
385
|
-
{ streaming: prefersStreamingDelivery(state) }
|
|
386
|
-
);
|
|
387
|
-
if (!delta) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (!delta.trim()) {
|
|
392
|
-
state.sentText = safeText;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const deliveryDedupKey = buildSettledWeixinDeliveryKey(state, safeText);
|
|
397
|
-
if (deliveryDedupKey && this.wasRecentlyDelivered(deliveryDedupKey)) {
|
|
398
|
-
state.sentText = safeText;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const delta = normalizeDeliveryDelta(
|
|
393
|
+
safeText.slice(state.sentText.length),
|
|
394
|
+
{ streaming: prefersStreamingDelivery(state) }
|
|
395
|
+
);
|
|
396
|
+
if (!delta) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!delta.trim()) {
|
|
401
|
+
state.sentText = safeText;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const deliveryDedupKey = buildSettledWeixinDeliveryKey(state, safeText);
|
|
406
|
+
if (deliveryDedupKey && this.wasRecentlyDelivered(deliveryDedupKey)) {
|
|
407
|
+
state.sentText = safeText;
|
|
399
408
|
console.warn(`[codeksei] suppress duplicate weixin delivery thread=${state.threadId}`);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const settledWechatDelivery = prefersSettledDelivery(state);
|
|
404
|
-
const tracePayload = buildDeliveryTracePayload(state, {
|
|
405
|
-
force,
|
|
406
|
-
trigger,
|
|
407
|
-
traceId: this.deliveryTraceEnabled ? crypto.randomUUID().slice(0, 8) : "",
|
|
408
|
-
safeText,
|
|
409
|
-
delta,
|
|
410
|
-
});
|
|
411
|
-
state.sendChain = state.sendChain.then(async () => {
|
|
412
|
-
this.logDeliveryTrace("attempt", tracePayload);
|
|
413
|
-
await this.channelAdapter.sendText({
|
|
414
|
-
userId: state.replyTarget.userId,
|
|
415
|
-
text: delta,
|
|
416
|
-
contextToken: state.replyTarget.contextToken,
|
|
417
|
-
preserveBlock: settledWechatDelivery,
|
|
418
|
-
trace: this.deliveryTraceEnabled
|
|
419
|
-
? {
|
|
420
|
-
...tracePayload,
|
|
421
|
-
origin: "stream-delivery",
|
|
422
|
-
preserveBlock: settledWechatDelivery,
|
|
423
|
-
}
|
|
424
|
-
: null,
|
|
425
|
-
});
|
|
426
|
-
state.sentText = safeText;
|
|
427
|
-
this.logDeliveryTrace("delivered", tracePayload);
|
|
428
|
-
if (deliveryDedupKey) {
|
|
429
|
-
this.rememberRecentDelivery(deliveryDedupKey);
|
|
430
|
-
console.log(
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const settledWechatDelivery = prefersSettledDelivery(state);
|
|
413
|
+
const tracePayload = buildDeliveryTracePayload(state, {
|
|
414
|
+
force,
|
|
415
|
+
trigger,
|
|
416
|
+
traceId: this.deliveryTraceEnabled ? crypto.randomUUID().slice(0, 8) : "",
|
|
417
|
+
safeText,
|
|
418
|
+
delta,
|
|
419
|
+
});
|
|
420
|
+
state.sendChain = state.sendChain.then(async () => {
|
|
421
|
+
this.logDeliveryTrace("attempt", tracePayload);
|
|
422
|
+
await this.channelAdapter.sendText({
|
|
423
|
+
userId: state.replyTarget.userId,
|
|
424
|
+
text: delta,
|
|
425
|
+
contextToken: state.replyTarget.contextToken,
|
|
426
|
+
preserveBlock: settledWechatDelivery,
|
|
427
|
+
trace: this.deliveryTraceEnabled
|
|
428
|
+
? {
|
|
429
|
+
...tracePayload,
|
|
430
|
+
origin: "stream-delivery",
|
|
431
|
+
preserveBlock: settledWechatDelivery,
|
|
432
|
+
}
|
|
433
|
+
: null,
|
|
434
|
+
});
|
|
435
|
+
state.sentText = safeText;
|
|
436
|
+
this.logDeliveryTrace("delivered", tracePayload);
|
|
437
|
+
if (deliveryDedupKey) {
|
|
438
|
+
this.rememberRecentDelivery(deliveryDedupKey);
|
|
439
|
+
console.log(
|
|
431
440
|
`[codeksei] delivered weixin reply `
|
|
432
|
-
+ `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
|
|
433
|
-
+ `chars=${safeText.length} hash=${hashReplyText(safeText)}`
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
}).catch((error) => {
|
|
437
|
-
this.logDeliveryTrace("failed", tracePayload, error);
|
|
441
|
+
+ `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
|
|
442
|
+
+ `chars=${safeText.length} hash=${hashReplyText(safeText)}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}).catch((error) => {
|
|
446
|
+
this.logDeliveryTrace("failed", tracePayload, error);
|
|
438
447
|
console.error(`[codeksei] failed to deliver reply thread=${state.threadId}: ${error.message}`);
|
|
439
|
-
this.handleDeliveryFailure(state, error);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
await state.sendChain;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
handleDeliveryFailure(state, error) {
|
|
446
|
-
if (!state?.runKey) {
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
// Once WeChat delivery has exhausted its retries for this run, continuing
|
|
450
|
-
// to stream later deltas only creates repeated send failures and fake typing.
|
|
451
|
-
this.ignoredRunKeys.add(state.runKey);
|
|
452
|
-
this.disposeRunState(state.runKey);
|
|
453
|
-
if (!this.onDeliveryFailure) {
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
Promise.resolve(this.onDeliveryFailure({
|
|
457
|
-
threadId: state.threadId,
|
|
458
|
-
turnId: state.turnId,
|
|
459
|
-
bindingKey: state.bindingKey,
|
|
460
|
-
error,
|
|
461
|
-
sentText: state.sentText,
|
|
462
|
-
replyTarget: state.replyTarget ? { ...state.replyTarget } : null,
|
|
463
|
-
})).catch((callbackError) => {
|
|
448
|
+
this.handleDeliveryFailure(state, error);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await state.sendChain;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
handleDeliveryFailure(state, error) {
|
|
455
|
+
if (!state?.runKey) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Once WeChat delivery has exhausted its retries for this run, continuing
|
|
459
|
+
// to stream later deltas only creates repeated send failures and fake typing.
|
|
460
|
+
this.ignoredRunKeys.add(state.runKey);
|
|
461
|
+
this.disposeRunState(state.runKey);
|
|
462
|
+
if (!this.onDeliveryFailure) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
Promise.resolve(this.onDeliveryFailure({
|
|
466
|
+
threadId: state.threadId,
|
|
467
|
+
turnId: state.turnId,
|
|
468
|
+
bindingKey: state.bindingKey,
|
|
469
|
+
error,
|
|
470
|
+
sentText: state.sentText,
|
|
471
|
+
replyTarget: state.replyTarget ? { ...state.replyTarget } : null,
|
|
472
|
+
})).catch((callbackError) => {
|
|
464
473
|
console.error(`[codeksei] delivery failure callback crashed thread=${state.threadId}: ${callbackError.message}`);
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
disposeSupersededAbandonedRuns(threadId, activeTurnId = "") {
|
|
478
|
+
const normalizedThreadId = normalizeText(threadId);
|
|
479
|
+
const normalizedActiveTurnId = normalizeText(activeTurnId);
|
|
480
|
+
if (!normalizedThreadId) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
for (const candidate of this.stateByRunKey.values()) {
|
|
484
|
+
if (candidate.threadId !== normalizedThreadId || !candidate.abandonedAt) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (normalizedActiveTurnId && candidate.turnId === normalizedActiveTurnId) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
this.ignoredRunKeys.add(candidate.runKey);
|
|
491
|
+
this.disposeRunState(candidate.runKey);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
disposeRunState(runKey) {
|
|
496
|
+
const normalizedRunKey = normalizeText(runKey);
|
|
497
|
+
if (!normalizedRunKey) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.stateByRunKey.delete(normalizedRunKey);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
wasRecentlyDelivered(key) {
|
|
504
|
+
this.pruneRecentDeliveries();
|
|
505
|
+
const deliveredAt = this.recentSettledWeixinDeliveries.get(key);
|
|
506
|
+
return Number.isFinite(deliveredAt) && (Date.now() - deliveredAt) <= RECENT_WEIXIN_DELIVERY_TTL_MS;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
rememberRecentDelivery(key) {
|
|
510
|
+
this.pruneRecentDeliveries();
|
|
511
|
+
this.recentSettledWeixinDeliveries.set(key, Date.now());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
pruneRecentDeliveries() {
|
|
515
|
+
const now = Date.now();
|
|
516
|
+
for (const [key, deliveredAt] of this.recentSettledWeixinDeliveries.entries()) {
|
|
517
|
+
if (!Number.isFinite(deliveredAt) || (now - deliveredAt) > RECENT_WEIXIN_DELIVERY_TTL_MS) {
|
|
518
|
+
this.recentSettledWeixinDeliveries.delete(key);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
logDeliveryTrace(stage, payload, error = null) {
|
|
524
|
+
if (!this.deliveryTraceEnabled || !payload) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const parts = [
|
|
501
528
|
`[codeksei] weixin delivery trace stage=${stage}`,
|
|
502
|
-
`pid=${process.pid}`,
|
|
503
|
-
`trace=${payload.traceId || "(none)"}`,
|
|
504
|
-
`thread=${payload.threadId}`,
|
|
505
|
-
`turn=${payload.turnId || "(pending)"}`,
|
|
506
|
-
`mode=${payload.mode}`,
|
|
507
|
-
`force=${payload.force ? "1" : "0"}`,
|
|
508
|
-
payload.trigger ? `trigger=${payload.trigger}` : "",
|
|
509
|
-
`sentCharsBefore=${payload.sentCharsBefore}`,
|
|
510
|
-
`safeChars=${payload.safeChars}`,
|
|
511
|
-
`deltaChars=${payload.deltaChars}`,
|
|
512
|
-
`safeHash=${payload.safeHash}`,
|
|
513
|
-
`deltaHash=${payload.deltaHash}`,
|
|
514
|
-
].filter(Boolean);
|
|
515
|
-
if (error) {
|
|
516
|
-
parts.push(`error=${JSON.stringify(String(error?.message || error || ""))}`);
|
|
517
|
-
console.error(parts.join(" "));
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
console.log(parts.join(" "));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function buildRunKey(threadId, turnId = "") {
|
|
525
|
-
const normalizedThreadId = normalizeText(threadId);
|
|
526
|
-
const normalizedTurnId = normalizeText(turnId);
|
|
527
|
-
return normalizedTurnId
|
|
528
|
-
? `${normalizedThreadId}:${normalizedTurnId}`
|
|
529
|
-
: `${normalizedThreadId}:pending`;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function buildReplyText(state, { completedOnly, preferLatestMessage = false, force = false }) {
|
|
533
|
-
if (force && hasWatchdogTail(state, { completedOnly })) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
return
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (!
|
|
740
|
-
return
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
if (!
|
|
903
|
-
return
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
529
|
+
`pid=${process.pid}`,
|
|
530
|
+
`trace=${payload.traceId || "(none)"}`,
|
|
531
|
+
`thread=${payload.threadId}`,
|
|
532
|
+
`turn=${payload.turnId || "(pending)"}`,
|
|
533
|
+
`mode=${payload.mode}`,
|
|
534
|
+
`force=${payload.force ? "1" : "0"}`,
|
|
535
|
+
payload.trigger ? `trigger=${payload.trigger}` : "",
|
|
536
|
+
`sentCharsBefore=${payload.sentCharsBefore}`,
|
|
537
|
+
`safeChars=${payload.safeChars}`,
|
|
538
|
+
`deltaChars=${payload.deltaChars}`,
|
|
539
|
+
`safeHash=${payload.safeHash}`,
|
|
540
|
+
`deltaHash=${payload.deltaHash}`,
|
|
541
|
+
].filter(Boolean);
|
|
542
|
+
if (error) {
|
|
543
|
+
parts.push(`error=${JSON.stringify(String(error?.message || error || ""))}`);
|
|
544
|
+
console.error(parts.join(" "));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
console.log(parts.join(" "));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function buildRunKey(threadId, turnId = "") {
|
|
552
|
+
const normalizedThreadId = normalizeText(threadId);
|
|
553
|
+
const normalizedTurnId = normalizeText(turnId);
|
|
554
|
+
return normalizedTurnId
|
|
555
|
+
? `${normalizedThreadId}:${normalizedTurnId}`
|
|
556
|
+
: `${normalizedThreadId}:pending`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function buildReplyText(state, { completedOnly, preferLatestMessage = false, force = false }) {
|
|
560
|
+
if (force && hasWatchdogTail(state, { completedOnly })) {
|
|
561
|
+
if (prefersStreamingDelivery(state)) {
|
|
562
|
+
return buildStreamingWatchdogReplyText(state, { completedOnly });
|
|
563
|
+
}
|
|
564
|
+
return buildSettledReplyText(state, { completedOnly });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (preferLatestMessage) {
|
|
568
|
+
return buildSettledReplyText(state, { completedOnly });
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (prefersStreamingDelivery(state)) {
|
|
572
|
+
return buildStreamingReplyText(state, { completedOnly, force });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return buildAllVisibleReplyText(state, { completedOnly });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function buildStreamingReplyText(state, { completedOnly, force }) {
|
|
579
|
+
const visibleItems = collectVisibleItems(state, { completedOnly });
|
|
580
|
+
const parts = [];
|
|
581
|
+
const seenParts = new Set();
|
|
582
|
+
const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
|
|
583
|
+
|
|
584
|
+
for (let index = 0; index < visibleItems.length; index += 1) {
|
|
585
|
+
const item = visibleItems[index];
|
|
586
|
+
if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
rememberVisiblePart(parts, seenParts, item.text);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!force) {
|
|
593
|
+
return parts.join("\n\n");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const terminal = findStreamingTerminalReplyText(visibleItems);
|
|
597
|
+
if (terminal) {
|
|
598
|
+
// `stream` means "ship completed user-visible blocks early when safe", not
|
|
599
|
+
// "stitch every unseen progress block onto the terminal answer". If no
|
|
600
|
+
// user-visible text has actually gone out yet, collapsing to the terminal
|
|
601
|
+
// block avoids the historical failure mode where several brief progress
|
|
602
|
+
// items get welded onto the final answer as one duplicated mega-bubble.
|
|
603
|
+
const terminalWasHeldBack = !shouldStreamImmediately(terminal, {
|
|
604
|
+
isTerminalVisibleItem: true,
|
|
605
|
+
});
|
|
606
|
+
if (!normalizeVisibleStreamingText(state.sentText) && terminalWasHeldBack) {
|
|
607
|
+
return terminal.text;
|
|
608
|
+
}
|
|
609
|
+
rememberVisiblePart(parts, seenParts, terminal.text);
|
|
610
|
+
}
|
|
611
|
+
return parts.join("\n\n");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function buildStreamingWatchdogReplyText(state, { completedOnly }) {
|
|
615
|
+
const visible = buildStreamingReplyText(state, { completedOnly, force: true });
|
|
616
|
+
const tail = readStateItemText(state, "__watchdog__", { completedOnly });
|
|
617
|
+
return [visible, tail].filter(Boolean).join("\n\n");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function buildCurrentSafeReplyText(state, { force = false, completedOnly = false } = {}) {
|
|
621
|
+
const plainText = markdownToPlainText(buildReplyText(state, {
|
|
622
|
+
completedOnly,
|
|
623
|
+
preferLatestMessage: prefersSettledDelivery(state),
|
|
624
|
+
force,
|
|
625
|
+
}));
|
|
626
|
+
return sanitizeReplyText(state.replyTarget, plainText).text;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function buildSettledReplyText(state, { completedOnly }) {
|
|
630
|
+
const tail = readStateItemText(state, "__watchdog__", { completedOnly });
|
|
631
|
+
if (!tail) {
|
|
632
|
+
// Codex can emit several assistant messages inside one turn. In settled
|
|
633
|
+
// WeChat delivery we only want the latest user-facing reply, otherwise all
|
|
634
|
+
// intermediate progress updates get stitched onto the final answer.
|
|
635
|
+
return findLatestVisibleReplyText(state, { completedOnly });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// When the watchdog has to rescue a stalled turn, dumping every visible item
|
|
639
|
+
// back to WeChat recreates the historical failure mode where long internal
|
|
640
|
+
// commentary or task cards leak as one giant assistant bubble. Keep only the
|
|
641
|
+
// latest safe visible block, then append the watchdog tail.
|
|
642
|
+
const visible = findLatestWatchdogVisibleReplyText(state, { completedOnly });
|
|
643
|
+
return [visible, tail].filter(Boolean).join("\n\n");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function buildAllVisibleReplyText(
|
|
647
|
+
state,
|
|
648
|
+
{ completedOnly, skipItemIds = null, collapseDuplicateVisibleItems = false }
|
|
649
|
+
) {
|
|
650
|
+
const parts = [];
|
|
651
|
+
const seenVisibleParts = collapseDuplicateVisibleItems ? new Set() : null;
|
|
652
|
+
for (const item of collectVisibleItems(state, { completedOnly, skipItemIds })) {
|
|
653
|
+
if (!seenVisibleParts) {
|
|
654
|
+
parts.push(item.text);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
rememberVisiblePart(parts, seenVisibleParts, item.text);
|
|
658
|
+
}
|
|
659
|
+
return parts.join("\n\n");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function findLatestVisibleReplyText(state, { completedOnly }) {
|
|
663
|
+
const visibleItems = collectVisibleItems(state, { completedOnly });
|
|
664
|
+
for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
|
|
665
|
+
if (visibleItems[index].itemId !== "__watchdog__") {
|
|
666
|
+
return visibleItems[index].text;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return "";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function findLatestWatchdogVisibleReplyText(state, { completedOnly }) {
|
|
673
|
+
const visibleItems = collectVisibleItems(state, {
|
|
674
|
+
completedOnly,
|
|
675
|
+
skipItemIds: new Set(["__watchdog__"]),
|
|
676
|
+
});
|
|
677
|
+
const candidate = findLatestWatchdogVisibleReply(visibleItems);
|
|
678
|
+
return candidate?.text || "";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function findLatestWatchdogVisibleReply(visibleItems) {
|
|
682
|
+
for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
|
|
683
|
+
const item = visibleItems[index];
|
|
684
|
+
if (!item || item.itemId === "__watchdog__") {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (item.phase === "final") {
|
|
689
|
+
return item;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (item.phase === "commentary") {
|
|
693
|
+
if (item.completed && isBriefStreamingProgressText(item.text)) {
|
|
694
|
+
return item;
|
|
695
|
+
}
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (item.completed || isBriefStreamingProgressText(item.text)) {
|
|
700
|
+
return item;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function findStreamingTerminalReplyText(visibleItems) {
|
|
707
|
+
const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
|
|
708
|
+
if (lastVisibleReplyIndex < 0) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
|
|
712
|
+
const item = visibleItems[index];
|
|
713
|
+
if (item.itemId === "__watchdog__") {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
|
|
717
|
+
return item;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return visibleItems[lastVisibleReplyIndex] || null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function findLastVisibleReplyIndex(visibleItems) {
|
|
724
|
+
for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
|
|
725
|
+
if (visibleItems[index]?.itemId !== "__watchdog__") {
|
|
726
|
+
return index;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return -1;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function readStateItemText(state, itemId, { completedOnly }) {
|
|
733
|
+
const item = state.items.get(itemId);
|
|
734
|
+
if (!item) {
|
|
735
|
+
return "";
|
|
736
|
+
}
|
|
737
|
+
const sourceText = completedOnly
|
|
738
|
+
? (item.completed ? item.completedText : "")
|
|
739
|
+
: (item.completed ? item.completedText : item.currentText);
|
|
740
|
+
return trimOuterBlankLines(sourceText);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function collectVisibleItems(state, { completedOnly, skipItemIds = null }) {
|
|
744
|
+
const items = [];
|
|
745
|
+
for (const itemId of state.itemOrder) {
|
|
746
|
+
if (skipItemIds?.has(itemId)) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const text = readStateItemText(state, itemId, { completedOnly });
|
|
750
|
+
if (!text) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const item = state.items.get(itemId);
|
|
754
|
+
items.push({
|
|
755
|
+
itemId,
|
|
756
|
+
text,
|
|
757
|
+
completed: Boolean(item?.completed),
|
|
758
|
+
phase: normalizeAssistantPhase(item?.phase),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return items;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function removeStateItem(state, itemId) {
|
|
765
|
+
const normalizedItemId = normalizeText(itemId);
|
|
766
|
+
if (!normalizedItemId || !state?.items?.has(normalizedItemId)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
state.items.delete(normalizedItemId);
|
|
770
|
+
state.itemOrder = state.itemOrder.filter((candidateId) => candidateId !== normalizedItemId);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function markdownToPlainText(text) {
|
|
774
|
+
let result = normalizeLineEndings(text);
|
|
775
|
+
result = result.replace(/```([^\n]*)\n?([\s\S]*?)```/g, (_, language, code) => {
|
|
776
|
+
const label = String(language || "").trim();
|
|
777
|
+
const body = indentBlock(String(code || ""));
|
|
778
|
+
return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
|
|
779
|
+
});
|
|
780
|
+
result = result.replace(/```([^\n]*)\n?([\s\S]*)$/g, (_, language, code) => {
|
|
781
|
+
const label = String(language || "").trim();
|
|
782
|
+
const body = indentBlock(String(code || ""));
|
|
783
|
+
return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
|
|
784
|
+
});
|
|
785
|
+
result = result.replace(/!\[[^\]]*]\([^)]*\)/g, "");
|
|
786
|
+
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
787
|
+
result = result.replace(/`([^`]+)`/g, "$1");
|
|
788
|
+
result = result.replace(/^#{1,6}\s*(.+)$/gm, "$1");
|
|
789
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "$1");
|
|
790
|
+
result = result.replace(/\*([^*]+)\*/g, "$1");
|
|
791
|
+
result = result.replace(/^>\s?/gm, "> ");
|
|
792
|
+
result = result.replace(/^\|[\s:|-]+\|$/gm, "");
|
|
793
|
+
result = result.replace(/^\|(.+)\|$/gm, (_, inner) =>
|
|
794
|
+
String(inner || "").split("|").map((cell) => cell.trim()).join(" ")
|
|
795
|
+
);
|
|
796
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
797
|
+
return trimOuterBlankLines(result);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function appendStreamingText(current, next) {
|
|
801
|
+
const base = String(current || "");
|
|
802
|
+
const incoming = String(next || "");
|
|
803
|
+
if (!incoming) {
|
|
804
|
+
return base;
|
|
805
|
+
}
|
|
806
|
+
if (!base) {
|
|
807
|
+
return incoming;
|
|
808
|
+
}
|
|
809
|
+
if (base.endsWith(incoming)) {
|
|
810
|
+
return base;
|
|
811
|
+
}
|
|
812
|
+
if (incoming.startsWith(base)) {
|
|
813
|
+
return incoming;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const replacement = chooseStreamingSnapshotReplacement(base, incoming);
|
|
817
|
+
if (replacement) {
|
|
818
|
+
return replacement;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const maxOverlap = Math.min(base.length, incoming.length);
|
|
822
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
823
|
+
if (base.slice(-size) === incoming.slice(0, size)) {
|
|
824
|
+
return `${base}${incoming.slice(size)}`;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return `${base}${incoming}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function prefersSettledDelivery(state) {
|
|
832
|
+
return normalizeText(state?.replyTarget?.provider) === "weixin"
|
|
833
|
+
&& normalizeWeixinReplyMode(state?.weixinReplyMode) === "settled";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function prefersStreamingDelivery(state) {
|
|
837
|
+
return normalizeText(state?.replyTarget?.provider) === "weixin"
|
|
838
|
+
&& normalizeWeixinReplyMode(state?.weixinReplyMode) === "stream";
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function buildSettledWeixinDeliveryKey(state, safeText) {
|
|
842
|
+
if (!prefersSettledDelivery(state)) {
|
|
843
|
+
return "";
|
|
844
|
+
}
|
|
845
|
+
const threadId = normalizeText(state?.threadId);
|
|
846
|
+
const userId = normalizeText(state?.replyTarget?.userId);
|
|
847
|
+
const contextToken = normalizeText(state?.replyTarget?.contextToken);
|
|
848
|
+
const text = normalizeLineEndings(safeText).trim();
|
|
849
|
+
if (!threadId || !userId || !contextToken || !text) {
|
|
850
|
+
return "";
|
|
851
|
+
}
|
|
852
|
+
// Keep the scope narrow: only suppress the same settled payload on the same
|
|
853
|
+
// WeChat thread shortly after a successful send, which is where we see
|
|
854
|
+
// accidental duplicate terminal deliveries.
|
|
855
|
+
return `${threadId}|${userId}|${contextToken}|${text}`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function buildDeliveryMode(state) {
|
|
859
|
+
if (prefersSettledDelivery(state)) {
|
|
860
|
+
return "settled";
|
|
861
|
+
}
|
|
862
|
+
if (prefersStreamingDelivery(state)) {
|
|
863
|
+
return "stream";
|
|
864
|
+
}
|
|
865
|
+
return normalizeText(state?.replyTarget?.provider) || "unknown";
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function buildDeliveryTracePayload(state, {
|
|
869
|
+
force = false,
|
|
870
|
+
trigger = null,
|
|
871
|
+
traceId = "",
|
|
872
|
+
safeText = "",
|
|
873
|
+
delta = "",
|
|
874
|
+
} = {}) {
|
|
875
|
+
return {
|
|
876
|
+
traceId: normalizeText(traceId),
|
|
877
|
+
threadId: normalizeText(state?.threadId),
|
|
878
|
+
turnId: normalizeText(state?.turnId),
|
|
879
|
+
mode: buildDeliveryMode(state),
|
|
880
|
+
force: Boolean(force),
|
|
881
|
+
trigger: formatDeliveryTrigger(trigger),
|
|
882
|
+
sentCharsBefore: String(state?.sentText || "").length,
|
|
883
|
+
safeChars: String(safeText || "").length,
|
|
884
|
+
deltaChars: String(delta || "").length,
|
|
885
|
+
safeHash: hashReplyText(safeText),
|
|
886
|
+
deltaHash: hashReplyText(delta),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function formatDeliveryTrigger(trigger) {
|
|
891
|
+
if (!trigger || typeof trigger !== "object") {
|
|
892
|
+
return "";
|
|
893
|
+
}
|
|
894
|
+
return [
|
|
895
|
+
normalizeText(trigger.source),
|
|
896
|
+
normalizeText(trigger.itemId),
|
|
897
|
+
normalizeText(trigger.phase),
|
|
898
|
+
].filter(Boolean).join("/");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function hashReplyText(text) {
|
|
902
|
+
return crypto.createHash("sha1").update(String(text || ""), "utf8").digest("hex").slice(0, 12);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function mergeCompletedItemText(current, completed) {
|
|
906
|
+
const streamed = String(current || "");
|
|
907
|
+
const finalized = String(completed || "");
|
|
908
|
+
if (!finalized) {
|
|
909
|
+
return streamed;
|
|
910
|
+
}
|
|
911
|
+
if (!streamed) {
|
|
912
|
+
return finalized;
|
|
913
|
+
}
|
|
914
|
+
// Upstream can resend the same final item with only whitespace / formatting
|
|
915
|
+
// differences between delta and completed snapshots. Treat those as the same
|
|
916
|
+
// semantic block so we do not concatenate two copies of the same answer.
|
|
917
|
+
if (normalizeVisibleStreamingText(streamed) === normalizeVisibleStreamingText(finalized)) {
|
|
918
|
+
return finalized;
|
|
919
|
+
}
|
|
920
|
+
const finalizedReplacement = chooseCompletedSnapshotReplacement(streamed, finalized);
|
|
921
|
+
if (finalizedReplacement) {
|
|
922
|
+
return finalizedReplacement;
|
|
923
|
+
}
|
|
924
|
+
return appendStreamingText(streamed, finalized);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function indentBlock(text) {
|
|
928
|
+
const normalized = trimOuterBlankLines(normalizeLineEndings(text));
|
|
929
|
+
if (!normalized) {
|
|
930
|
+
return "";
|
|
931
|
+
}
|
|
932
|
+
return normalized.split("\n").map((line) => ` ${line}`).join("\n");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function normalizeText(value) {
|
|
936
|
+
return typeof value === "string" ? value.trim() : "";
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function normalizeWeixinReplyMode(value) {
|
|
940
|
+
return normalizeText(value).toLowerCase() === "settled" ? "settled" : "stream";
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function normalizeLineEndings(value) {
|
|
944
|
+
return String(value || "").replace(/\r\n/g, "\n");
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function normalizeDeliveryDelta(delta, { streaming = false } = {}) {
|
|
948
|
+
const normalized = String(delta || "");
|
|
949
|
+
if (!streaming) {
|
|
950
|
+
return normalized;
|
|
951
|
+
}
|
|
952
|
+
// Stream mode ships completed assistant items one message at a time. The
|
|
953
|
+
// snapshot diff can therefore start with the joiner's blank lines; trim only
|
|
954
|
+
// that transport artifact so the next item lands as a clean standalone send.
|
|
955
|
+
return normalized.replace(/^\n+/u, "");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function buildVisibleItemDedupKey(text) {
|
|
959
|
+
return trimOuterBlankLines(markdownToPlainText(normalizeLineEndings(text)));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function normalizeVisibleStreamingText(text) {
|
|
963
|
+
return buildVisibleItemDedupKey(text).replace(/\s+/gu, " ").trim();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function normalizeStreamingSnapshotSemanticText(text) {
|
|
967
|
+
return normalizeVisibleStreamingText(text)
|
|
968
|
+
.replace(/[\p{P}\p{S}]+/gu, "")
|
|
969
|
+
.replace(/\s+/gu, " ")
|
|
970
|
+
.trim();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function chooseStreamingSnapshotReplacement(base, incoming) {
|
|
974
|
+
const baseVisible = normalizeStreamingSnapshotSemanticText(base);
|
|
975
|
+
const incomingVisible = normalizeStreamingSnapshotSemanticText(incoming);
|
|
976
|
+
if (
|
|
977
|
+
!baseVisible
|
|
978
|
+
|| !incomingVisible
|
|
979
|
+
|| baseVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
|
|
980
|
+
|| incomingVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
|
|
981
|
+
) {
|
|
982
|
+
return "";
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Codex can resend the whole in-flight assistant item after reformatting an
|
|
986
|
+
// earlier region (for example, once a fenced code block becomes plain text in
|
|
987
|
+
// a later snapshot). Raw prefix checks then fail even though the newer
|
|
988
|
+
// snapshot semantically contains the older one. Prefer the newer snapshot so
|
|
989
|
+
// we do not weld two whole copies of the same answer together.
|
|
990
|
+
if (incomingVisible.startsWith(baseVisible)) {
|
|
991
|
+
return incoming;
|
|
992
|
+
}
|
|
993
|
+
// Some upstream snapshots briefly regress to a shorter normalized view while
|
|
994
|
+
// the same item is still being built. Keep the richer text we already have
|
|
995
|
+
// instead of treating that shorter resend as a new block to append.
|
|
996
|
+
if (baseVisible.startsWith(incomingVisible)) {
|
|
997
|
+
return base;
|
|
998
|
+
}
|
|
999
|
+
return "";
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function chooseCompletedSnapshotReplacement(streamed, finalized) {
|
|
1003
|
+
const streamedVisible = normalizeStreamingSnapshotSemanticText(streamed);
|
|
1004
|
+
const finalizedVisible = normalizeStreamingSnapshotSemanticText(finalized);
|
|
1005
|
+
if (
|
|
1006
|
+
!streamedVisible
|
|
1007
|
+
|| !finalizedVisible
|
|
1008
|
+
|| streamedVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
|
|
1009
|
+
|| finalizedVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
|
|
1010
|
+
) {
|
|
1011
|
+
return "";
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// The completed item is the authoritative final snapshot. If the streamed
|
|
1015
|
+
// buffer already contains the same normalized answer as a prefix/substring,
|
|
1016
|
+
// it usually means earlier delta snapshots were stitched together after a
|
|
1017
|
+
// formatting rewrite. Collapse back to the completed text instead of sending
|
|
1018
|
+
// a duplicated mega-bubble.
|
|
1019
|
+
if (streamedVisible.startsWith(finalizedVisible) || streamedVisible.includes(finalizedVisible)) {
|
|
1020
|
+
return finalized;
|
|
1021
|
+
}
|
|
1022
|
+
if (finalizedVisible.startsWith(streamedVisible)) {
|
|
1023
|
+
return finalized;
|
|
1024
|
+
}
|
|
1025
|
+
return "";
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function hasWatchdogTail(state, { completedOnly }) {
|
|
1029
|
+
return Boolean(readStateItemText(state, "__watchdog__", { completedOnly }));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function rememberVisiblePart(parts, seenParts, text) {
|
|
1033
|
+
const dedupeKey = buildVisibleItemDedupKey(text);
|
|
1034
|
+
if (dedupeKey && seenParts.has(dedupeKey)) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (dedupeKey) {
|
|
1038
|
+
seenParts.add(dedupeKey);
|
|
1039
|
+
}
|
|
1040
|
+
parts.push(text);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function shouldStreamImmediately(item, { isTerminalVisibleItem = false } = {}) {
|
|
1044
|
+
if (!item?.text || item.itemId === "__watchdog__") {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
const phase = normalizeAssistantPhase(item.phase);
|
|
1048
|
+
if (phase === "final") {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
if (!isBriefStreamingProgressText(item.text)) {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
if (phase === "commentary") {
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
// When phase is missing, the current terminal short block could still be the
|
|
1058
|
+
// user's final answer. Hold only that trailing block until another visible
|
|
1059
|
+
// item arrives or the turn completes, so we do not leak a short final reply
|
|
1060
|
+
// early just because upstream omitted phase metadata once.
|
|
1061
|
+
return !isTerminalVisibleItem;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function isBriefStreamingProgressText(text) {
|
|
1065
|
+
const raw = trimOuterBlankLines(normalizeLineEndings(text));
|
|
1066
|
+
if (!raw) {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
if (
|
|
1070
|
+
raw.includes("```")
|
|
1071
|
+
|| raw.includes("\n\n")
|
|
1072
|
+
|| /\[[^\]]+\]\([^)]+\)/u.test(raw)
|
|
1073
|
+
|| /^\s*(?:[-*]|\d+\.)\s/mu.test(raw)
|
|
1074
|
+
|| raw.includes("【继续任务】")
|
|
1075
|
+
|| raw.includes("【当前状态】")
|
|
1076
|
+
|| raw.includes("【执行前检查】")
|
|
1077
|
+
) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
const plain = buildVisibleItemDedupKey(raw);
|
|
1081
|
+
if (!plain || plain.length > STREAM_PROGRESS_MAX_CHARS) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
const lineCount = plain.split("\n").filter(Boolean).length;
|
|
1085
|
+
return lineCount > 0 && lineCount <= STREAM_PROGRESS_MAX_LINES;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function trimOuterBlankLines(text) {
|
|
1089
|
+
return String(text || "")
|
|
1090
|
+
.replace(/^\s*\n+/g, "")
|
|
1091
|
+
.replace(/\n+\s*$/g, "");
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function shouldSuppressSystemReply(replyTarget, plainReplyText) {
|
|
1095
|
+
if (replyTarget?.provider !== "system") {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
const normalized = normalizeLineEndings(String(plainReplyText || ""));
|
|
1099
|
+
const compact = normalized.trim();
|
|
1100
|
+
if (!compact) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
const sentinelNormalized = normalizeSilentSentinelText(compact);
|
|
1104
|
+
if (compact === "CB_SILENT" || compact === "__SILENT__" || compact === "SILENT") {
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
if (containsStructuredSilentSignal(normalized)) {
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
if (compact.toUpperCase().includes("CB_SILENT") || compact.toUpperCase().includes("__SILENT__")) {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
if (sentinelNormalized.includes("CB_SILENT") || sentinelNormalized.includes("__SILENT__") || sentinelNormalized.includes("SILENT")) {
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
return normalized
|
|
1117
|
+
.split("\n")
|
|
1118
|
+
.map((line) => normalizeSilentSentinelText(line.trim()))
|
|
1119
|
+
.some((line) => line === "CB_SILENT" || line === "__SILENT__" || line === "SILENT");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function sanitizeReplyText(replyTarget, plainReplyText) {
|
|
1123
|
+
const normalized = normalizeLineEndings(String(plainReplyText || ""));
|
|
1124
|
+
if (!normalized) {
|
|
1125
|
+
return { suppress: false, text: "" };
|
|
1126
|
+
}
|
|
1127
|
+
const protocolSanitized = sanitizeProtocolLeakText(normalized);
|
|
1128
|
+
const safeText = protocolSanitized.text || "";
|
|
1129
|
+
if (shouldSuppressSystemReply(replyTarget, safeText)) {
|
|
1130
|
+
return { suppress: true, text: "" };
|
|
1131
|
+
}
|
|
1132
|
+
const cleaned = stripSilentSentinelArtifacts(safeText);
|
|
1133
|
+
return {
|
|
1134
|
+
suppress: false,
|
|
1135
|
+
text: trimOuterBlankLines(cleaned),
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function normalizeSilentSentinelText(value) {
|
|
1140
|
+
return String(value || "")
|
|
1141
|
+
.normalize("NFKC")
|
|
1142
|
+
.toUpperCase()
|
|
1143
|
+
.replace(/[^A-Z_]/g, "");
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function stripSilentSentinelArtifacts(value) {
|
|
1147
|
+
return normalizeLineEndings(String(value || ""))
|
|
1148
|
+
.replace(/\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/gi, "")
|
|
1149
|
+
.split("\n")
|
|
1150
|
+
.map((line) => {
|
|
1151
|
+
const parts = line.split(/\s+/);
|
|
1152
|
+
const kept = parts.filter((part) => !isSilentSentinelToken(part));
|
|
1153
|
+
return kept.join(" ").trim();
|
|
1154
|
+
})
|
|
1155
|
+
.filter((line, index, lines) => line || (index > 0 && index < lines.length - 1))
|
|
1156
|
+
.join("\n")
|
|
1157
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function isSilentSentinelToken(value) {
|
|
1161
|
+
const normalized = normalizeSilentSentinelText(value);
|
|
1162
|
+
return normalized === "CB_SILENT" || normalized === "__SILENT__" || normalized === "SILENT";
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function containsStructuredSilentSignal(value) {
|
|
1166
|
+
return /\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/i.test(String(value || ""));
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
module.exports = { StreamDelivery };
|