From bfd550e090fd542378ec7f5bdee483d0d415240f Mon Sep 17 00:00:00 2001 From: Meleki Date: Sun, 26 Apr 2026 12:08:56 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20UTF-8-Encoding=20geh=C3=A4rtet=20(SSE-Re?= =?UTF-8?q?ader=20+=20CLI-Workspace)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnthropicService.Streaming.cs + OpenAiService.cs: SSE-StreamReader nutzt jetzt expliziten UTF-8-Encoder mit 16-KB-Buffer (statt default 1 KB) und ohne BOM-Detection. Verhindert intermittierende Replacement-Chars (�), die entstehen können, wenn Multi-Byte-UTF-8-Sequenzen ungünstig über die Read-Boundary eines NetworkStreams gesplittet werden. - ClaudeCliService.cs: cmd.exe-Wrapper jetzt mit /D (AutoRun-Skripte aus Registry deaktivieren) und WorkingDirectory wird auf den Session-Workspace gesetzt. Vorher erbte der Child-Prozess das CWD von gott (bei Windows- Diensten C:\WINDOWS\system32). Co-Authored-By: Claude Opus 4.7 (1M context) --- Services/AnthropicService.Streaming.cs | 12 +++++++++++- Services/ClaudeCliService.cs | 19 +++++++++++++++---- Services/OpenAiService.cs | 11 ++++++++++- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Services/AnthropicService.Streaming.cs b/Services/AnthropicService.Streaming.cs index 0ae42ec..706774a 100644 --- a/Services/AnthropicService.Streaming.cs +++ b/Services/AnthropicService.Streaming.cs @@ -402,7 +402,17 @@ public partial class AnthropicService // an den underlying NetworkStream – der Read blockiert sonst bis Anthropic // das nächste Chunk sendet (kann bei extended thinking Minuten dauern). using var ctReg = workCt.Register(() => { try { stream.Dispose(); } catch { } }); - using var reader = new StreamReader(stream); + // Explizit UTF-8 ohne BOM-Detection, größerer Buffer (16 KB statt default 1024). + // Verhindert intermittierende Replacement-Chars (�) für Multi-Byte-Sequenzen + // (ü, ö, ä, ß, em-dash …), die entstehen können, wenn ein Multi-Byte-UTF-8-Char + // ungünstig über die Read-Boundary eines NetworkStreams gesplittet wird und der + // Decoder bei kleinem Buffer den State verliert. + using var reader = new StreamReader( + stream, + encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false), + detectEncodingFromByteOrderMarks: false, + bufferSize: 16384, + leaveOpen: false); var stopReason = ""; var assistantContent = new List(); diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 99e80f2..8df2771 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -35,7 +35,7 @@ public class ClaudeCliService public bool IsAvailable => _isAvailable; public string CliPath => _cliPath; - private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false) + private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string workingDirectory = null) { var psi = new ProcessStartInfo { @@ -48,13 +48,23 @@ public class ClaudeCliService StandardErrorEncoding = Encoding.UTF8 }; + // CWD explizit auf den Workspace setzen, wenn einer angegeben wurde. + // Sonst erbt der Child-Prozess das CWD von gott (bei Windows-Diensten meist C:\WINDOWS\system32), + // und Claude meldet "your current working directory is C:\WINDOWS\system32". + if (!string.IsNullOrEmpty(workingDirectory) && Directory.Exists(workingDirectory)) + psi.WorkingDirectory = workingDirectory; + if (OperatingSystem.IsWindows()) { // Auf Windows: über cmd.exe wrappen und Codepage 65001 (UTF-8) erzwingen. // Sonst kann der Child-Prozess (claude.exe) Output mit CP1252/CP850 schreiben, // was beim UTF-8-Decode zu Replacement-Chars (�) für Umlaute / Em-Dash führt. + // /D = AutoRun-Skripte aus der Registry deaktivieren (sonst kann der User-AutoRun + // das CWD vor dem claude.exe-Start ändern) + // /S = Spezialregel für äußere Quotes – strippt führende/abschließende Quote + // /C = Kommando ausführen und beenden var sb = new StringBuilder(); - sb.Append("/S /C \"chcp 65001 >nul && "); + sb.Append("/D /S /C \"chcp 65001 >nul && "); sb.Append(QuoteArgForCmd(_cliPath)); foreach (var a in args) { @@ -275,13 +285,14 @@ public class ClaudeCliService args.Add(mcpConfigPath); } - return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken); + return await RunCliStreamingAsync(args, prompt, onEvent, workspace, cancellationToken); } private async Task RunCliStreamingAsync( List args, string stdinPrompt, Func onEvent, + string workingDirectory, CancellationToken cancellationToken) { var result = new CliChatResult(); @@ -299,7 +310,7 @@ public class ClaudeCliService try { - var startInfo = CreateCliStartInfo(args, redirectInput: true); + var startInfo = CreateCliStartInfo(args, redirectInput: true, workingDirectory: workingDirectory); using var process = new Process { StartInfo = startInfo }; diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index 36048b9..ddbcb9e 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -337,7 +337,16 @@ public class OpenAiService // SSE-Stream lesen using var stream = await httpResp.Content.ReadAsStreamAsync(workCt); - using var reader = new System.IO.StreamReader(stream); + // Explizit UTF-8 ohne BOM-Detection, größerer Buffer (16 KB statt default 1024). + // Verhindert intermittierende Replacement-Chars (�) für Multi-Byte-Sequenzen, + // die entstehen können, wenn UTF-8-Bytes über die Read-Boundary eines + // NetworkStreams gesplittet werden und der Decoder bei kleinem Buffer State verliert. + using var reader = new System.IO.StreamReader( + stream, + encoding: new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false), + detectEncodingFromByteOrderMarks: false, + bufferSize: 16384, + leaveOpen: false); var fullText = new StringBuilder(); var thinkingText = new StringBuilder();