codeksei 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +661 -0
  2. package/README.en.md +215 -0
  3. package/README.md +259 -0
  4. package/bin/codeksei.js +10 -0
  5. package/bin/cyberboss.js +11 -0
  6. package/package.json +86 -0
  7. package/scripts/install-background-tasks.ps1 +135 -0
  8. package/scripts/open_shared_wechat_thread.sh +94 -0
  9. package/scripts/open_wechat_thread.sh +117 -0
  10. package/scripts/shared-common.js +791 -0
  11. package/scripts/shared-open.js +46 -0
  12. package/scripts/shared-start.js +41 -0
  13. package/scripts/shared-status.js +74 -0
  14. package/scripts/shared-supervisor.js +141 -0
  15. package/scripts/shared-task-runner.ps1 +87 -0
  16. package/scripts/shared-watchdog.js +290 -0
  17. package/scripts/show_shared_status.sh +53 -0
  18. package/scripts/start_shared_app_server.sh +65 -0
  19. package/scripts/start_shared_wechat.sh +108 -0
  20. package/scripts/timeline-screenshot.sh +15 -0
  21. package/scripts/uninstall-background-tasks.ps1 +23 -0
  22. package/src/adapters/channel/weixin/account-store.js +135 -0
  23. package/src/adapters/channel/weixin/api-v2.js +258 -0
  24. package/src/adapters/channel/weixin/api.js +180 -0
  25. package/src/adapters/channel/weixin/context-token-store.js +84 -0
  26. package/src/adapters/channel/weixin/index.js +605 -0
  27. package/src/adapters/channel/weixin/legacy.js +567 -0
  28. package/src/adapters/channel/weixin/login-common.js +63 -0
  29. package/src/adapters/channel/weixin/login-legacy.js +124 -0
  30. package/src/adapters/channel/weixin/login-v2.js +186 -0
  31. package/src/adapters/channel/weixin/media-mime.js +22 -0
  32. package/src/adapters/channel/weixin/media-receive.js +370 -0
  33. package/src/adapters/channel/weixin/media-send.js +331 -0
  34. package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
  35. package/src/adapters/channel/weixin/message-utils.js +199 -0
  36. package/src/adapters/channel/weixin/protocol.js +77 -0
  37. package/src/adapters/channel/weixin/redact.js +41 -0
  38. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
  39. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
  40. package/src/adapters/runtime/codex/events.js +252 -0
  41. package/src/adapters/runtime/codex/index.js +502 -0
  42. package/src/adapters/runtime/codex/message-utils.js +141 -0
  43. package/src/adapters/runtime/codex/model-catalog.js +106 -0
  44. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
  45. package/src/adapters/runtime/codex/rpc-client.js +443 -0
  46. package/src/adapters/runtime/codex/session-store.js +376 -0
  47. package/src/app/channel-send-file-cli.js +57 -0
  48. package/src/app/diary-write-cli.js +620 -0
  49. package/src/app/note-auto-cli.js +201 -0
  50. package/src/app/note-sync-cli.js +130 -0
  51. package/src/app/project-radar-cli.js +165 -0
  52. package/src/app/reminder-write-cli.js +210 -0
  53. package/src/app/review-cli.js +134 -0
  54. package/src/app/system-checkin-poller.js +100 -0
  55. package/src/app/system-send-cli.js +129 -0
  56. package/src/app/timeline-event-cli.js +273 -0
  57. package/src/app/timeline-screenshot-cli.js +109 -0
  58. package/src/core/app.js +1810 -0
  59. package/src/core/branding.js +167 -0
  60. package/src/core/command-registry.js +609 -0
  61. package/src/core/config.js +84 -0
  62. package/src/core/default-targets.js +163 -0
  63. package/src/core/durable-note-schema.js +325 -0
  64. package/src/core/instructions-template.js +31 -0
  65. package/src/core/note-sync.js +433 -0
  66. package/src/core/project-radar.js +402 -0
  67. package/src/core/review-semantic.js +524 -0
  68. package/src/core/review.js +1081 -0
  69. package/src/core/shared-bridge-heartbeat.js +140 -0
  70. package/src/core/stream-delivery.js +990 -0
  71. package/src/core/system-message-dispatcher.js +68 -0
  72. package/src/core/system-message-queue-store.js +128 -0
  73. package/src/core/thread-state-store.js +135 -0
  74. package/src/core/timeline-screenshot-queue-store.js +134 -0
  75. package/src/core/workspace-alias.js +163 -0
  76. package/src/core/workspace-bootstrap.js +338 -0
  77. package/src/index.js +270 -0
  78. package/src/integrations/timeline/index.js +191 -0
  79. package/templates/weixin-instructions.md +53 -0
  80. package/templates/weixin-operations.md +69 -0
@@ -0,0 +1,990 @@
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
+ class StreamDelivery {
10
+ constructor({
11
+ channelAdapter,
12
+ sessionStore,
13
+ weixinReplyMode = "settled",
14
+ deliveryTraceEnabled = false,
15
+ onDeliveryFailure = null,
16
+ }) {
17
+ this.channelAdapter = channelAdapter;
18
+ this.sessionStore = sessionStore;
19
+ this.weixinReplyMode = normalizeWeixinReplyMode(weixinReplyMode);
20
+ this.deliveryTraceEnabled = Boolean(deliveryTraceEnabled);
21
+ this.onDeliveryFailure = typeof onDeliveryFailure === "function" ? onDeliveryFailure : null;
22
+ this.replyTargetByBindingKey = new Map();
23
+ this.pendingReplyTargetsByThreadId = new Map();
24
+ this.stateByRunKey = new Map();
25
+ this.ignoredRunKeys = new Set();
26
+ this.recentSettledWeixinDeliveries = new Map();
27
+ }
28
+
29
+ setReplyTarget(bindingKey, target) {
30
+ if (!bindingKey || !target?.userId || !target?.contextToken) {
31
+ return;
32
+ }
33
+ this.replyTargetByBindingKey.set(bindingKey, {
34
+ userId: String(target.userId).trim(),
35
+ contextToken: String(target.contextToken).trim(),
36
+ provider: normalizeText(target.provider),
37
+ });
38
+ }
39
+
40
+ queueReplyTargetForThread(threadId, target) {
41
+ const normalizedThreadId = normalizeText(threadId);
42
+ if (!normalizedThreadId || !target?.userId || !target?.contextToken) {
43
+ return;
44
+ }
45
+ const queue = this.pendingReplyTargetsByThreadId.get(normalizedThreadId) || [];
46
+ queue.push({
47
+ userId: String(target.userId).trim(),
48
+ contextToken: String(target.contextToken).trim(),
49
+ provider: normalizeText(target.provider),
50
+ });
51
+ this.pendingReplyTargetsByThreadId.set(normalizedThreadId, queue);
52
+ }
53
+
54
+ async handleRuntimeEvent(event) {
55
+ const threadId = normalizeText(event?.payload?.threadId);
56
+ const turnId = normalizeText(event?.payload?.turnId);
57
+ if (!threadId) {
58
+ return;
59
+ }
60
+ const ignoredRunKey = turnId ? buildRunKey(threadId, turnId) : "";
61
+ if (ignoredRunKey && this.ignoredRunKeys.has(ignoredRunKey)) {
62
+ if (event.type === "runtime.turn.completed" || event.type === "runtime.turn.failed") {
63
+ this.ignoredRunKeys.delete(ignoredRunKey);
64
+ }
65
+ return;
66
+ }
67
+
68
+ switch (event.type) {
69
+ case "runtime.turn.started": {
70
+ const state = this.ensureRunState(threadId, turnId);
71
+ state.turnId = turnId || state.turnId;
72
+ this.attachReplyTarget(state);
73
+ return;
74
+ }
75
+ case "runtime.reply.delta": {
76
+ const state = this.ensureRunState(threadId, turnId);
77
+ this.upsertItem(state, {
78
+ itemId: normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`,
79
+ text: normalizeLineEndings(event.payload.text),
80
+ completed: false,
81
+ phase: normalizeAssistantPhase(event.payload.phase),
82
+ });
83
+ return;
84
+ }
85
+ case "runtime.reply.completed": {
86
+ const state = this.ensureRunState(threadId, turnId);
87
+ const itemId = normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`;
88
+ const phase = normalizeAssistantPhase(event.payload.phase);
89
+ this.upsertItem(state, {
90
+ itemId,
91
+ text: normalizeLineEndings(event.payload.text),
92
+ completed: true,
93
+ phase,
94
+ });
95
+ await this.flush(state, {
96
+ force: false,
97
+ trigger: {
98
+ source: event.type,
99
+ itemId,
100
+ phase,
101
+ },
102
+ });
103
+ return;
104
+ }
105
+ case "runtime.turn.completed": {
106
+ const state = this.ensureRunState(threadId, turnId);
107
+ state.turnId = turnId || state.turnId;
108
+ await this.flush(state, {
109
+ force: true,
110
+ trigger: { source: event.type },
111
+ });
112
+ this.disposeRunState(state.runKey);
113
+ return;
114
+ }
115
+ case "runtime.turn.failed":
116
+ this.disposeRunState(buildRunKey(threadId, turnId));
117
+ return;
118
+ default:
119
+ return;
120
+ }
121
+ }
122
+
123
+ async finishTurn({ threadId, finalText }) {
124
+ const normalizedThreadId = normalizeText(threadId);
125
+ const normalizedFinalText = normalizeLineEndings(finalText);
126
+ if (!normalizedThreadId || !normalizedFinalText) {
127
+ return;
128
+ }
129
+
130
+ const state = this.ensureRunState(normalizedThreadId, "");
131
+ this.attachReplyTarget(state);
132
+ if (!state.itemOrder.length) {
133
+ this.upsertItem(state, {
134
+ itemId: "final",
135
+ text: normalizedFinalText,
136
+ completed: true,
137
+ });
138
+ } else {
139
+ const itemId = state.itemOrder[state.itemOrder.length - 1] || "final";
140
+ this.setItemText(state, itemId, normalizedFinalText, true);
141
+ for (const candidateId of state.itemOrder) {
142
+ const item = state.items.get(candidateId);
143
+ if (item) {
144
+ item.currentText = item.completedText || item.currentText;
145
+ item.completed = true;
146
+ }
147
+ }
148
+ }
149
+
150
+ await this.flush(state, {
151
+ force: true,
152
+ trigger: {
153
+ source: "finishTurn",
154
+ itemId: state.itemOrder[state.itemOrder.length - 1] || "final",
155
+ },
156
+ });
157
+ this.disposeRunState(state.runKey);
158
+ }
159
+
160
+ async finalizeAbandonedTurn({ threadId, turnId = "", trailingText = "" }) {
161
+ const normalizedThreadId = normalizeText(threadId);
162
+ const normalizedTurnId = normalizeText(turnId);
163
+ const normalizedTrailingText = normalizeLineEndings(trailingText).trim();
164
+ if (!normalizedThreadId) {
165
+ return;
166
+ }
167
+
168
+ const state = this.findRunState(normalizedThreadId, normalizedTurnId);
169
+ if (!state) {
170
+ // If this exact turn is already gone, do not resurrect a new pending run
171
+ // just to send the watchdog suffix again. That creates duplicate "tail"
172
+ // messages after delivery failure or other local terminal states.
173
+ if (normalizedTurnId) {
174
+ return;
175
+ }
176
+ if (normalizedTrailingText) {
177
+ await this.finishTurn({
178
+ threadId: normalizedThreadId,
179
+ finalText: normalizedTrailingText,
180
+ });
181
+ }
182
+ return;
183
+ }
184
+
185
+ this.attachReplyTarget(state);
186
+ if (normalizedTrailingText) {
187
+ this.upsertItem(state, {
188
+ itemId: "__watchdog__",
189
+ text: normalizedTrailingText,
190
+ completed: true,
191
+ });
192
+ }
193
+ await this.flush(state, {
194
+ force: true,
195
+ trigger: {
196
+ source: "finalizeAbandonedTurn",
197
+ itemId: normalizedTrailingText ? "__watchdog__" : "",
198
+ },
199
+ });
200
+ this.ignoredRunKeys.add(state.runKey);
201
+ this.disposeRunState(state.runKey);
202
+ }
203
+
204
+ ensureRunState(threadId, turnId = "") {
205
+ const runKey = buildRunKey(threadId, turnId);
206
+ const existing = this.stateByRunKey.get(runKey);
207
+ if (existing) {
208
+ return existing;
209
+ }
210
+
211
+ const created = {
212
+ runKey,
213
+ threadId,
214
+ bindingKey: "",
215
+ replyTarget: null,
216
+ turnId: normalizeText(turnId),
217
+ itemOrder: [],
218
+ items: new Map(),
219
+ weixinReplyMode: this.weixinReplyMode,
220
+ sentText: "",
221
+ sendChain: Promise.resolve(),
222
+ flushPromise: null,
223
+ };
224
+ this.stateByRunKey.set(runKey, created);
225
+ this.attachReplyTarget(created);
226
+ return created;
227
+ }
228
+
229
+ findRunState(threadId, turnId = "") {
230
+ const normalizedThreadId = normalizeText(threadId);
231
+ const normalizedTurnId = normalizeText(turnId);
232
+ if (!normalizedThreadId) {
233
+ return null;
234
+ }
235
+ if (normalizedTurnId) {
236
+ const exact = this.stateByRunKey.get(buildRunKey(normalizedThreadId, normalizedTurnId));
237
+ if (exact) {
238
+ return exact;
239
+ }
240
+ }
241
+ const pending = this.stateByRunKey.get(buildRunKey(normalizedThreadId, ""));
242
+ if (pending && (!normalizedTurnId || !pending.turnId || pending.turnId === normalizedTurnId)) {
243
+ return pending;
244
+ }
245
+ for (const candidate of this.stateByRunKey.values()) {
246
+ if (candidate.threadId !== normalizedThreadId) {
247
+ continue;
248
+ }
249
+ if (!normalizedTurnId || candidate.turnId === normalizedTurnId) {
250
+ return candidate;
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+
256
+ attachReplyTarget(state) {
257
+ if (!state.replyTarget) {
258
+ const queue = this.pendingReplyTargetsByThreadId.get(state.threadId) || [];
259
+ if (queue.length) {
260
+ state.replyTarget = queue.shift();
261
+ if (queue.length) {
262
+ this.pendingReplyTargetsByThreadId.set(state.threadId, queue);
263
+ } else {
264
+ this.pendingReplyTargetsByThreadId.delete(state.threadId);
265
+ }
266
+ }
267
+ }
268
+ const linked = this.sessionStore.findBindingForThreadId(state.threadId);
269
+ if (!linked?.bindingKey) {
270
+ return;
271
+ }
272
+ state.bindingKey = linked.bindingKey;
273
+ if (!state.replyTarget) {
274
+ const target = this.replyTargetByBindingKey.get(linked.bindingKey);
275
+ state.replyTarget = target;
276
+ }
277
+ }
278
+
279
+ upsertItem(state, { itemId, text, completed, phase = "" }) {
280
+ if (!text) {
281
+ return;
282
+ }
283
+ if (!state.items.has(itemId)) {
284
+ state.itemOrder.push(itemId);
285
+ state.items.set(itemId, {
286
+ currentText: "",
287
+ completedText: "",
288
+ completed: false,
289
+ phase: "",
290
+ });
291
+ }
292
+
293
+ const current = state.items.get(itemId);
294
+ const normalizedPhase = normalizeAssistantPhase(phase);
295
+ if (normalizedPhase) {
296
+ current.phase = normalizedPhase;
297
+ }
298
+ if (completed) {
299
+ const merged = mergeCompletedItemText(current.currentText, text);
300
+ current.currentText = merged;
301
+ current.completedText = merged;
302
+ current.completed = true;
303
+ return;
304
+ }
305
+
306
+ current.currentText = appendStreamingText(current.currentText, text);
307
+ }
308
+
309
+ setItemText(state, itemId, text, completed) {
310
+ if (!text) {
311
+ return;
312
+ }
313
+ if (!state.items.has(itemId)) {
314
+ state.itemOrder.push(itemId);
315
+ state.items.set(itemId, {
316
+ currentText: "",
317
+ completedText: "",
318
+ completed: false,
319
+ phase: "",
320
+ });
321
+ }
322
+
323
+ const current = state.items.get(itemId);
324
+ current.currentText = text;
325
+ if (completed) {
326
+ current.completedText = text;
327
+ }
328
+ current.completed = Boolean(completed);
329
+ }
330
+
331
+ async flush(state, { force, trigger = null }) {
332
+ const previous = state.flushPromise || Promise.resolve();
333
+ const current = previous
334
+ .catch(() => {})
335
+ .then(() => this.flushNow(state, { force, trigger }));
336
+ const tracked = current.finally(() => {
337
+ const latestState = this.stateByRunKey.get(state.runKey);
338
+ if (latestState && latestState.flushPromise === tracked) {
339
+ latestState.flushPromise = null;
340
+ }
341
+ });
342
+ state.flushPromise = tracked;
343
+ await tracked;
344
+ }
345
+
346
+ async flushNow(state, { force, trigger = null }) {
347
+ if (!state.replyTarget) {
348
+ return;
349
+ }
350
+ // WeChat reply mode is explicit. `settled` waits for the terminal snapshot;
351
+ // `stream` ships user-visible assistant message blocks as they complete.
352
+ // Keep the modes distinct here so future "optimize the stream" tweaks do
353
+ // not accidentally reintroduce token-level spam or repeated block sends.
354
+ if (!force && prefersSettledDelivery(state)) {
355
+ return;
356
+ }
357
+
358
+ const plainText = markdownToPlainText(buildReplyText(state, {
359
+ completedOnly: !force,
360
+ preferLatestMessage: prefersSettledDelivery(state),
361
+ force,
362
+ }));
363
+ const sanitized = sanitizeReplyText(state.replyTarget, plainText);
364
+ if (sanitized.suppress) {
365
+ state.sentText = sanitized.text;
366
+ console.log(
367
+ `[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)) {
379
+ 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;
399
+ 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(
431
+ `[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);
438
+ 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) => {
464
+ console.error(`[codeksei] delivery failure callback crashed thread=${state.threadId}: ${callbackError.message}`);
465
+ });
466
+ }
467
+
468
+ disposeRunState(runKey) {
469
+ const normalizedRunKey = normalizeText(runKey);
470
+ if (!normalizedRunKey) {
471
+ return;
472
+ }
473
+ this.stateByRunKey.delete(normalizedRunKey);
474
+ }
475
+
476
+ wasRecentlyDelivered(key) {
477
+ this.pruneRecentDeliveries();
478
+ const deliveredAt = this.recentSettledWeixinDeliveries.get(key);
479
+ return Number.isFinite(deliveredAt) && (Date.now() - deliveredAt) <= RECENT_WEIXIN_DELIVERY_TTL_MS;
480
+ }
481
+
482
+ rememberRecentDelivery(key) {
483
+ this.pruneRecentDeliveries();
484
+ this.recentSettledWeixinDeliveries.set(key, Date.now());
485
+ }
486
+
487
+ pruneRecentDeliveries() {
488
+ const now = Date.now();
489
+ for (const [key, deliveredAt] of this.recentSettledWeixinDeliveries.entries()) {
490
+ if (!Number.isFinite(deliveredAt) || (now - deliveredAt) > RECENT_WEIXIN_DELIVERY_TTL_MS) {
491
+ this.recentSettledWeixinDeliveries.delete(key);
492
+ }
493
+ }
494
+ }
495
+
496
+ logDeliveryTrace(stage, payload, error = null) {
497
+ if (!this.deliveryTraceEnabled || !payload) {
498
+ return;
499
+ }
500
+ const parts = [
501
+ `[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
+ return buildSettledReplyText(state, { completedOnly });
535
+ }
536
+
537
+ if (preferLatestMessage) {
538
+ return buildSettledReplyText(state, { completedOnly });
539
+ }
540
+
541
+ if (prefersStreamingDelivery(state)) {
542
+ return buildStreamingReplyText(state, { completedOnly, force });
543
+ }
544
+
545
+ return buildAllVisibleReplyText(state, { completedOnly });
546
+ }
547
+
548
+ function buildStreamingReplyText(state, { completedOnly, force }) {
549
+ const visibleItems = collectVisibleItems(state, { completedOnly });
550
+ const parts = [];
551
+ const seenParts = new Set();
552
+ const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
553
+
554
+ for (let index = 0; index < visibleItems.length; index += 1) {
555
+ const item = visibleItems[index];
556
+ if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
557
+ continue;
558
+ }
559
+ rememberVisiblePart(parts, seenParts, item.text);
560
+ }
561
+
562
+ if (!force) {
563
+ return parts.join("\n\n");
564
+ }
565
+
566
+ const terminal = findStreamingTerminalReplyText(visibleItems);
567
+ if (terminal) {
568
+ rememberVisiblePart(parts, seenParts, terminal.text);
569
+ }
570
+ return parts.join("\n\n");
571
+ }
572
+
573
+ function buildSettledReplyText(state, { completedOnly }) {
574
+ const tail = readStateItemText(state, "__watchdog__", { completedOnly });
575
+ if (!tail) {
576
+ // Codex can emit several assistant messages inside one turn. In settled
577
+ // WeChat delivery we only want the latest user-facing reply, otherwise all
578
+ // intermediate progress updates get stitched onto the final answer.
579
+ return findLatestVisibleReplyText(state, { completedOnly });
580
+ }
581
+
582
+ // The watchdog message says "目前拿到的内容". When a turn never reaches its
583
+ // final assistant item, silently dropping earlier completed items here would
584
+ // contradict that promise and hide already-generated user-visible text.
585
+ const visible = buildAllVisibleReplyText(state, {
586
+ completedOnly,
587
+ skipItemIds: new Set(["__watchdog__"]),
588
+ });
589
+ return [visible, tail].filter(Boolean).join("\n\n");
590
+ }
591
+
592
+ function buildAllVisibleReplyText(
593
+ state,
594
+ { completedOnly, skipItemIds = null, collapseDuplicateVisibleItems = false }
595
+ ) {
596
+ const parts = [];
597
+ const seenVisibleParts = collapseDuplicateVisibleItems ? new Set() : null;
598
+ for (const item of collectVisibleItems(state, { completedOnly, skipItemIds })) {
599
+ if (!seenVisibleParts) {
600
+ parts.push(item.text);
601
+ continue;
602
+ }
603
+ rememberVisiblePart(parts, seenVisibleParts, item.text);
604
+ }
605
+ return parts.join("\n\n");
606
+ }
607
+
608
+ function findLatestVisibleReplyText(state, { completedOnly }) {
609
+ const visibleItems = collectVisibleItems(state, { completedOnly });
610
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
611
+ if (visibleItems[index].itemId !== "__watchdog__") {
612
+ return visibleItems[index].text;
613
+ }
614
+ }
615
+ return "";
616
+ }
617
+
618
+ function findStreamingTerminalReplyText(visibleItems) {
619
+ const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
620
+ if (lastVisibleReplyIndex < 0) {
621
+ return null;
622
+ }
623
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
624
+ const item = visibleItems[index];
625
+ if (item.itemId === "__watchdog__") {
626
+ continue;
627
+ }
628
+ if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
629
+ return item;
630
+ }
631
+ }
632
+ return visibleItems[lastVisibleReplyIndex] || null;
633
+ }
634
+
635
+ function findLastVisibleReplyIndex(visibleItems) {
636
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
637
+ if (visibleItems[index]?.itemId !== "__watchdog__") {
638
+ return index;
639
+ }
640
+ }
641
+ return -1;
642
+ }
643
+
644
+ function readStateItemText(state, itemId, { completedOnly }) {
645
+ const item = state.items.get(itemId);
646
+ if (!item) {
647
+ return "";
648
+ }
649
+ const sourceText = completedOnly
650
+ ? (item.completed ? item.completedText : "")
651
+ : (item.completed ? item.completedText : item.currentText);
652
+ return trimOuterBlankLines(sourceText);
653
+ }
654
+
655
+ function collectVisibleItems(state, { completedOnly, skipItemIds = null }) {
656
+ const items = [];
657
+ for (const itemId of state.itemOrder) {
658
+ if (skipItemIds?.has(itemId)) {
659
+ continue;
660
+ }
661
+ const text = readStateItemText(state, itemId, { completedOnly });
662
+ if (!text) {
663
+ continue;
664
+ }
665
+ const item = state.items.get(itemId);
666
+ items.push({
667
+ itemId,
668
+ text,
669
+ phase: normalizeAssistantPhase(item?.phase),
670
+ });
671
+ }
672
+ return items;
673
+ }
674
+
675
+ function markdownToPlainText(text) {
676
+ let result = normalizeLineEndings(text);
677
+ result = result.replace(/```([^\n]*)\n?([\s\S]*?)```/g, (_, language, code) => {
678
+ const label = String(language || "").trim();
679
+ const body = indentBlock(String(code || ""));
680
+ return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
681
+ });
682
+ result = result.replace(/```([^\n]*)\n?([\s\S]*)$/g, (_, language, code) => {
683
+ const label = String(language || "").trim();
684
+ const body = indentBlock(String(code || ""));
685
+ return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
686
+ });
687
+ result = result.replace(/!\[[^\]]*]\([^)]*\)/g, "");
688
+ result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
689
+ result = result.replace(/`([^`]+)`/g, "$1");
690
+ result = result.replace(/^#{1,6}\s*(.+)$/gm, "$1");
691
+ result = result.replace(/\*\*([^*]+)\*\*/g, "$1");
692
+ result = result.replace(/\*([^*]+)\*/g, "$1");
693
+ result = result.replace(/^>\s?/gm, "> ");
694
+ result = result.replace(/^\|[\s:|-]+\|$/gm, "");
695
+ result = result.replace(/^\|(.+)\|$/gm, (_, inner) =>
696
+ String(inner || "").split("|").map((cell) => cell.trim()).join(" ")
697
+ );
698
+ result = result.replace(/\n{3,}/g, "\n\n");
699
+ return trimOuterBlankLines(result);
700
+ }
701
+
702
+ function appendStreamingText(current, next) {
703
+ const base = String(current || "");
704
+ const incoming = String(next || "");
705
+ if (!incoming) {
706
+ return base;
707
+ }
708
+ if (!base) {
709
+ return incoming;
710
+ }
711
+ if (base.endsWith(incoming)) {
712
+ return base;
713
+ }
714
+ if (incoming.startsWith(base)) {
715
+ return incoming;
716
+ }
717
+
718
+ const maxOverlap = Math.min(base.length, incoming.length);
719
+ for (let size = maxOverlap; size > 0; size -= 1) {
720
+ if (base.slice(-size) === incoming.slice(0, size)) {
721
+ return `${base}${incoming.slice(size)}`;
722
+ }
723
+ }
724
+
725
+ return `${base}${incoming}`;
726
+ }
727
+
728
+ function prefersSettledDelivery(state) {
729
+ return normalizeText(state?.replyTarget?.provider) === "weixin"
730
+ && normalizeWeixinReplyMode(state?.weixinReplyMode) === "settled";
731
+ }
732
+
733
+ function prefersStreamingDelivery(state) {
734
+ return normalizeText(state?.replyTarget?.provider) === "weixin"
735
+ && normalizeWeixinReplyMode(state?.weixinReplyMode) === "stream";
736
+ }
737
+
738
+ function buildSettledWeixinDeliveryKey(state, safeText) {
739
+ if (!prefersSettledDelivery(state)) {
740
+ return "";
741
+ }
742
+ const threadId = normalizeText(state?.threadId);
743
+ const userId = normalizeText(state?.replyTarget?.userId);
744
+ const contextToken = normalizeText(state?.replyTarget?.contextToken);
745
+ const text = normalizeLineEndings(safeText).trim();
746
+ if (!threadId || !userId || !contextToken || !text) {
747
+ return "";
748
+ }
749
+ // Keep the scope narrow: only suppress the same settled payload on the same
750
+ // WeChat thread shortly after a successful send, which is where we see
751
+ // accidental duplicate terminal deliveries.
752
+ return `${threadId}|${userId}|${contextToken}|${text}`;
753
+ }
754
+
755
+ function buildDeliveryMode(state) {
756
+ if (prefersSettledDelivery(state)) {
757
+ return "settled";
758
+ }
759
+ if (prefersStreamingDelivery(state)) {
760
+ return "stream";
761
+ }
762
+ return normalizeText(state?.replyTarget?.provider) || "unknown";
763
+ }
764
+
765
+ function buildDeliveryTracePayload(state, {
766
+ force = false,
767
+ trigger = null,
768
+ traceId = "",
769
+ safeText = "",
770
+ delta = "",
771
+ } = {}) {
772
+ return {
773
+ traceId: normalizeText(traceId),
774
+ threadId: normalizeText(state?.threadId),
775
+ turnId: normalizeText(state?.turnId),
776
+ mode: buildDeliveryMode(state),
777
+ force: Boolean(force),
778
+ trigger: formatDeliveryTrigger(trigger),
779
+ sentCharsBefore: String(state?.sentText || "").length,
780
+ safeChars: String(safeText || "").length,
781
+ deltaChars: String(delta || "").length,
782
+ safeHash: hashReplyText(safeText),
783
+ deltaHash: hashReplyText(delta),
784
+ };
785
+ }
786
+
787
+ function formatDeliveryTrigger(trigger) {
788
+ if (!trigger || typeof trigger !== "object") {
789
+ return "";
790
+ }
791
+ return [
792
+ normalizeText(trigger.source),
793
+ normalizeText(trigger.itemId),
794
+ normalizeText(trigger.phase),
795
+ ].filter(Boolean).join("/");
796
+ }
797
+
798
+ function hashReplyText(text) {
799
+ return crypto.createHash("sha1").update(String(text || ""), "utf8").digest("hex").slice(0, 12);
800
+ }
801
+
802
+ function mergeCompletedItemText(current, completed) {
803
+ const streamed = String(current || "");
804
+ const finalized = String(completed || "");
805
+ if (!finalized) {
806
+ return streamed;
807
+ }
808
+ if (!streamed) {
809
+ return finalized;
810
+ }
811
+ return appendStreamingText(streamed, finalized);
812
+ }
813
+
814
+ function indentBlock(text) {
815
+ const normalized = trimOuterBlankLines(normalizeLineEndings(text));
816
+ if (!normalized) {
817
+ return "";
818
+ }
819
+ return normalized.split("\n").map((line) => ` ${line}`).join("\n");
820
+ }
821
+
822
+ function normalizeText(value) {
823
+ return typeof value === "string" ? value.trim() : "";
824
+ }
825
+
826
+ function normalizeWeixinReplyMode(value) {
827
+ return normalizeText(value).toLowerCase() === "settled" ? "settled" : "stream";
828
+ }
829
+
830
+ function normalizeLineEndings(value) {
831
+ return String(value || "").replace(/\r\n/g, "\n");
832
+ }
833
+
834
+ function normalizeDeliveryDelta(delta, { streaming = false } = {}) {
835
+ const normalized = String(delta || "");
836
+ if (!streaming) {
837
+ return normalized;
838
+ }
839
+ // Stream mode ships completed assistant items one message at a time. The
840
+ // snapshot diff can therefore start with the joiner's blank lines; trim only
841
+ // that transport artifact so the next item lands as a clean standalone send.
842
+ return normalized.replace(/^\n+/u, "");
843
+ }
844
+
845
+ function buildVisibleItemDedupKey(text) {
846
+ return trimOuterBlankLines(markdownToPlainText(normalizeLineEndings(text)));
847
+ }
848
+
849
+ function hasWatchdogTail(state, { completedOnly }) {
850
+ return Boolean(readStateItemText(state, "__watchdog__", { completedOnly }));
851
+ }
852
+
853
+ function rememberVisiblePart(parts, seenParts, text) {
854
+ const dedupeKey = buildVisibleItemDedupKey(text);
855
+ if (dedupeKey && seenParts.has(dedupeKey)) {
856
+ return;
857
+ }
858
+ if (dedupeKey) {
859
+ seenParts.add(dedupeKey);
860
+ }
861
+ parts.push(text);
862
+ }
863
+
864
+ function shouldStreamImmediately(item, { isTerminalVisibleItem = false } = {}) {
865
+ if (!item?.text || item.itemId === "__watchdog__") {
866
+ return false;
867
+ }
868
+ const phase = normalizeAssistantPhase(item.phase);
869
+ if (phase === "final") {
870
+ return false;
871
+ }
872
+ if (!isBriefStreamingProgressText(item.text)) {
873
+ return false;
874
+ }
875
+ if (phase === "commentary") {
876
+ return true;
877
+ }
878
+ // When phase is missing, the current terminal short block could still be the
879
+ // user's final answer. Hold only that trailing block until another visible
880
+ // item arrives or the turn completes, so we do not leak a short final reply
881
+ // early just because upstream omitted phase metadata once.
882
+ return !isTerminalVisibleItem;
883
+ }
884
+
885
+ function isBriefStreamingProgressText(text) {
886
+ const raw = trimOuterBlankLines(normalizeLineEndings(text));
887
+ if (!raw) {
888
+ return false;
889
+ }
890
+ if (
891
+ raw.includes("```")
892
+ || raw.includes("\n\n")
893
+ || /\[[^\]]+\]\([^)]+\)/u.test(raw)
894
+ || /^\s*(?:[-*]|\d+\.)\s/mu.test(raw)
895
+ || raw.includes("【继续任务】")
896
+ || raw.includes("【当前状态】")
897
+ || raw.includes("【执行前检查】")
898
+ ) {
899
+ return false;
900
+ }
901
+ const plain = buildVisibleItemDedupKey(raw);
902
+ if (!plain || plain.length > STREAM_PROGRESS_MAX_CHARS) {
903
+ return false;
904
+ }
905
+ const lineCount = plain.split("\n").filter(Boolean).length;
906
+ return lineCount > 0 && lineCount <= STREAM_PROGRESS_MAX_LINES;
907
+ }
908
+
909
+ function trimOuterBlankLines(text) {
910
+ return String(text || "")
911
+ .replace(/^\s*\n+/g, "")
912
+ .replace(/\n+\s*$/g, "");
913
+ }
914
+
915
+ function shouldSuppressSystemReply(replyTarget, plainReplyText) {
916
+ if (replyTarget?.provider !== "system") {
917
+ return false;
918
+ }
919
+ const normalized = normalizeLineEndings(String(plainReplyText || ""));
920
+ const compact = normalized.trim();
921
+ if (!compact) {
922
+ return false;
923
+ }
924
+ const sentinelNormalized = normalizeSilentSentinelText(compact);
925
+ if (compact === "CB_SILENT" || compact === "__SILENT__" || compact === "SILENT") {
926
+ return true;
927
+ }
928
+ if (containsStructuredSilentSignal(normalized)) {
929
+ return true;
930
+ }
931
+ if (compact.toUpperCase().includes("CB_SILENT") || compact.toUpperCase().includes("__SILENT__")) {
932
+ return true;
933
+ }
934
+ if (sentinelNormalized.includes("CB_SILENT") || sentinelNormalized.includes("__SILENT__") || sentinelNormalized.includes("SILENT")) {
935
+ return true;
936
+ }
937
+ return normalized
938
+ .split("\n")
939
+ .map((line) => normalizeSilentSentinelText(line.trim()))
940
+ .some((line) => line === "CB_SILENT" || line === "__SILENT__" || line === "SILENT");
941
+ }
942
+
943
+ function sanitizeReplyText(replyTarget, plainReplyText) {
944
+ const normalized = normalizeLineEndings(String(plainReplyText || ""));
945
+ if (!normalized) {
946
+ return { suppress: false, text: "" };
947
+ }
948
+ const protocolSanitized = sanitizeProtocolLeakText(normalized);
949
+ const safeText = protocolSanitized.text || "";
950
+ if (shouldSuppressSystemReply(replyTarget, safeText)) {
951
+ return { suppress: true, text: "" };
952
+ }
953
+ const cleaned = stripSilentSentinelArtifacts(safeText);
954
+ return {
955
+ suppress: false,
956
+ text: trimOuterBlankLines(cleaned),
957
+ };
958
+ }
959
+
960
+ function normalizeSilentSentinelText(value) {
961
+ return String(value || "")
962
+ .normalize("NFKC")
963
+ .toUpperCase()
964
+ .replace(/[^A-Z_]/g, "");
965
+ }
966
+
967
+ function stripSilentSentinelArtifacts(value) {
968
+ return normalizeLineEndings(String(value || ""))
969
+ .replace(/\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/gi, "")
970
+ .split("\n")
971
+ .map((line) => {
972
+ const parts = line.split(/\s+/);
973
+ const kept = parts.filter((part) => !isSilentSentinelToken(part));
974
+ return kept.join(" ").trim();
975
+ })
976
+ .filter((line, index, lines) => line || (index > 0 && index < lines.length - 1))
977
+ .join("\n")
978
+ .replace(/\n{3,}/g, "\n\n");
979
+ }
980
+
981
+ function isSilentSentinelToken(value) {
982
+ const normalized = normalizeSilentSentinelText(value);
983
+ return normalized === "CB_SILENT" || normalized === "__SILENT__" || normalized === "SILENT";
984
+ }
985
+
986
+ function containsStructuredSilentSignal(value) {
987
+ return /\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/i.test(String(value || ""));
988
+ }
989
+
990
+ module.exports = { StreamDelivery };