From 898dbbecd439a4fdff40ec145ad68bc734fd48b7 Mon Sep 17 00:00:00 2001 From: btc69m979y-dotcom Date: Wed, 18 Mar 2026 17:22:20 +0800 Subject: [PATCH] fix: always prefer UTF-8 when strict decode succeeds in proxy encoding detection The scoring heuristic for GB18030 vs UTF-8 could false-positive on longer CJK strings because UTF-8 CJK bytes reinterpreted as GB18030 also produce CJK characters with comparable scores, defeating the margin. Since genuine GB18030 CJK byte sequences are almost never valid UTF-8 (strict mode), a successful UTF-8 strict decode means the body IS UTF-8. Remove the scoring heuristic and always return utf8Decoded when strict decode passes. GB18030 fallback still applies when UTF-8 decode fails. --- src/main/libs/coworkOpenAICompatProxy.ts | 56 +++--------------------- 1 file changed, 7 insertions(+), 49 deletions(-) diff --git a/src/main/libs/coworkOpenAICompatProxy.ts b/src/main/libs/coworkOpenAICompatProxy.ts index 655a11e..af2a6d7 100644 --- a/src/main/libs/coworkOpenAICompatProxy.ts +++ b/src/main/libs/coworkOpenAICompatProxy.ts @@ -909,43 +909,6 @@ function readRequestBody(req: http.IncomingMessage): Promise { return ''; } - const collectStringValues = (input: unknown, out: string[]): void => { - if (typeof input === 'string') { - out.push(input); - return; - } - if (Array.isArray(input)) { - for (const item of input) collectStringValues(item, out); - return; - } - if (input && typeof input === 'object') { - for (const value of Object.values(input as Record)) { - collectStringValues(value, out); - } - } - }; - - const scoreDecodedJsonText = (text: string): number => { - let parsed: unknown; - try { - parsed = JSON.parse(text); - } catch { - return -10000; - } - - const values: string[] = []; - collectStringValues(parsed, values); - const joined = values.join('\n'); - if (!joined) return 0; - - const cjkCount = (joined.match(/[\u3400-\u9FFF]/g) || []).length; - const replacementCount = (joined.match(/\uFFFD/g) || []).length; - const mojibakeCount = (joined.match(/[ÃÂÐÑØÙÞæçèéêëìíîïðñòóôõöøùúûüýþÿ]/g) || []).length; - const nonAsciiCount = (joined.match(/[^\x00-\x7F]/g) || []).length; - - return cjkCount * 4 + nonAsciiCount - replacementCount * 8 - mojibakeCount * 3; - }; - // BOM-aware decoding first. if (raw.length >= 3 && raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf) { return new TextDecoder('utf-8', { fatal: false }).decode(raw.subarray(3)); @@ -976,18 +939,13 @@ function readRequestBody(req: http.IncomingMessage): Promise { } if (utf8Decoded && gbDecoded) { - const utf8Score = scoreDecodedJsonText(utf8Decoded); - const gbScore = scoreDecodedJsonText(gbDecoded); - // Require GB18030 to score significantly higher than UTF-8 to avoid - // false positives on short CJK strings (e.g. "你好" scores UTF-8=10 - // vs GB18030=11 due to one extra non-ASCII char from misaligned - // multi-byte boundaries). UTF-8 is the overwhelmingly expected - // encoding, so give it a comfortable margin. - const GB18030_SCORE_MARGIN = 5; - if (gbScore > utf8Score + GB18030_SCORE_MARGIN) { - console.warn(`[CoworkProxy] Decoded request body using gb18030 (score ${gbScore} > utf8 ${utf8Score} + ${GB18030_SCORE_MARGIN})`); - return gbDecoded; - } + // UTF-8 strict decode succeeded → the bytes ARE valid UTF-8. + // Genuine GB18030 CJK byte sequences (first byte 0xB0-0xF7, + // second byte 0xA1-0xFE) are almost never valid UTF-8, so if + // strict UTF-8 passes the body is overwhelmingly UTF-8. + // The previous scoring heuristic could false-positive on longer + // CJK strings where GB18030 reinterpretation also yields CJK + // characters with comparable scores. return utf8Decoded; }