diff --git a/config/examples/weixin.service.env.example b/config/examples/weixin.service.env.example index 8e6aa32..85126f7 100644 --- a/config/examples/weixin.service.env.example +++ b/config/examples/weixin.service.env.example @@ -86,5 +86,9 @@ CODEX_NATIVE_API_ENABLE=1 # CODEXBRIDGE_INTERNAL_NATIVE_API_AUTH_TOKEN=replace-with-a-long-random-secret # CODEXBRIDGE_INTERNAL_NATIVE_API_TASK_CLASSES=intent_classification,normalization,small_verification,side_reasoning +# Weixin delivery behavior. Disabled by default so Weixin receives one final +# answer instead of streamed preview snippets plus a final commit. +# CODEXBRIDGE_WEIXIN_PROGRESS_DELIVERY=0 + # Debugging CODEXBRIDGE_DEBUG_WEIXIN=0 diff --git a/src/cli.ts b/src/cli.ts index 98e87fd..c5b1670 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -237,6 +237,7 @@ async function runWeixinServe(args: string[]) { automationJobs: runtime.services.automationJobs, agentJobs: runtime.services.agentJobs, assistantRecords: runtime.services.assistantRecords, + progressDeliveryEnabled: parseBooleanEnv(process.env.CODEXBRIDGE_WEIXIN_PROGRESS_DELIVERY, false), onError: (async (error: unknown) => { process.stderr.write(`[weixin] ${formatError(error)}\n`); }) as any, diff --git a/src/runtime/weixin_bridge_runtime.ts b/src/runtime/weixin_bridge_runtime.ts index c341521..f2b548b 100644 --- a/src/runtime/weixin_bridge_runtime.ts +++ b/src/runtime/weixin_bridge_runtime.ts @@ -145,6 +145,7 @@ interface WeixinBridgeRuntimeOptions { previewSoftTargetBytes?: number; previewHardLimitBytes?: number; previewIntervalMs?: number; + progressDeliveryEnabled?: boolean; typingKeepaliveMs?: number; inboundAttachmentMergeWindowMs?: number; automationPollMs?: number; @@ -175,6 +176,8 @@ export class WeixinBridgeRuntime { previewIntervalMs: number; + progressDeliveryEnabled: boolean; + typingKeepaliveMs: number; inboundAttachmentMergeWindowMs: number; @@ -219,6 +222,7 @@ export class WeixinBridgeRuntime { previewSoftTargetBytes = 2048, previewHardLimitBytes = 2048, previewIntervalMs = 3000, + progressDeliveryEnabled = true, typingKeepaliveMs = WeixinBridgeRuntime.DEFAULT_TYPING_KEEPALIVE_MS, inboundAttachmentMergeWindowMs = 3000, automationPollMs = 30_000, @@ -234,6 +238,7 @@ export class WeixinBridgeRuntime { this.previewSoftTargetBytes = previewSoftTargetBytes; this.previewHardLimitBytes = previewHardLimitBytes; this.previewIntervalMs = previewIntervalMs; + this.progressDeliveryEnabled = progressDeliveryEnabled; this.typingKeepaliveMs = typingKeepaliveMs; this.inboundAttachmentMergeWindowMs = inboundAttachmentMergeWindowMs; this.automationPollMs = automationPollMs; @@ -534,7 +539,7 @@ export class WeixinBridgeRuntime { try { const response = await this.bridgeCoordinator.handleInboundEvent(event, { onProgress: async (progress) => { - if (options.suppressProgressDelivery) { + if (options.suppressProgressDelivery || !this.progressDeliveryEnabled) { return; } await this.handleProgressUpdate(event, streamState, progress); diff --git a/test/runtime/weixin_bridge_runtime.test.ts b/test/runtime/weixin_bridge_runtime.test.ts index ebc8381..6e54243 100644 --- a/test/runtime/weixin_bridge_runtime.test.ts +++ b/test/runtime/weixin_bridge_runtime.test.ts @@ -37,6 +37,7 @@ interface RuntimeHarnessOptions { commitSyncCursor?: (syncCursor: string) => Promise | void; previewSoftTargetBytes?: number; previewIntervalMs?: number; + progressDeliveryEnabled?: boolean; typingKeepaliveMs?: number; inboundAttachmentMergeWindowMs?: number; automationPollMs?: number; @@ -55,6 +56,7 @@ function makeRuntime({ commitSyncCursor, previewSoftTargetBytes = 1, previewIntervalMs = 0, + progressDeliveryEnabled = true, typingKeepaliveMs = 8000, inboundAttachmentMergeWindowMs = 3000, automationPollMs = 30_000, @@ -109,6 +111,7 @@ function makeRuntime({ assistantRecords, previewSoftTargetBytes, previewIntervalMs, + progressDeliveryEnabled, typingKeepaliveMs, inboundAttachmentMergeWindowMs, automationPollMs, @@ -1538,6 +1541,38 @@ test('WeixinBridgeRuntime merges commentary and final-answer progress into the p ]); }); +test('WeixinBridgeRuntime can disable Weixin progress preview delivery and send only the final response', async () => { + const sent: Array<{ externalScopeId: string; content: string }> = []; + const runtime = makeRuntime({ + sendText: async ({ externalScopeId, content }) => { + sent.push({ externalScopeId, content }); + }, + progressDeliveryEnabled: false, + previewSoftTargetBytes: 1024, + coordinator: { + async handleInboundEvent(_event: any, options: any = {}) { + await options.onProgress?.({ + text: '第一段预览。', + delta: '第一段预览。', + outputKind: 'commentary', + }); + await options.onProgress?.({ + text: '最终答案。', + delta: '最终答案。', + outputKind: 'final_answer', + }); + return completeResponse('最终答案。'); + }, + }, + }); + + await runtime.runOnce(); + + assert.deepEqual(sent, [ + { externalScopeId: 'wxid_1', content: '最终答案。' }, + ]); +}); + test('WeixinBridgeRuntime dedupes overlapping cumulative final-answer updates in the preview stream', async () => { const sent: Array<{ externalScopeId: string; content: string }> = []; const runtime = makeRuntime({