diff --git a/Program.cs b/Program.cs index 51c9ec9..556a54e 100644 --- a/Program.cs +++ b/Program.cs @@ -3,7 +3,6 @@ using System.IO; using System.Net; using System.Net.Http; using System.Runtime.InteropServices; -using System.Text; using Gott.Endpoints; using Gott.Middleware; using Gott.Services; @@ -14,20 +13,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; -// Auf Windows: Console-Codepage des Server-Prozesses auf UTF-8 erzwingen. -// Wirkt prozess-global (Win32 SetConsoleOutputCP/SetConsoleCP) und wird von -// direkt gestarteten Child-Prozessen geerbt – damit Tools/Sub-Prozesse, die -// die Console-Codepage abfragen, UTF-8 als Output-Encoding nutzen. -if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) -{ - try - { - Console.OutputEncoding = Encoding.UTF8; - Console.InputEncoding = Encoding.UTF8; - } - catch { /* In Service-Kontexten ohne Console kann das werfen – ignorieren. */ } -} - var builder = WebApplication.CreateBuilder(args); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) diff --git a/Services/AnthropicService.Streaming.cs b/Services/AnthropicService.Streaming.cs index 706774a..0ae42ec 100644 --- a/Services/AnthropicService.Streaming.cs +++ b/Services/AnthropicService.Streaming.cs @@ -402,17 +402,7 @@ 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 { } }); - // 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); + using var reader = new StreamReader(stream); var stopReason = ""; var assistantContent = new List(); diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index f374d4a..c553c19 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -35,10 +35,11 @@ public class ClaudeCliService public bool IsAvailable => _isAvailable; public string CliPath => _cliPath; - private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string workingDirectory = null) + private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false) { var psi = new ProcessStartInfo { + FileName = _cliPath, RedirectStandardInput = redirectInput, RedirectStandardOutput = true, RedirectStandardError = true, @@ -48,101 +49,12 @@ 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("/D /S /C \"chcp 65001 >nul && "); - sb.Append(QuoteArgForCmd(_cliPath)); - foreach (var a in args) - { - sb.Append(' '); - sb.Append(QuoteArgForCmd(a)); - } - sb.Append('"'); - - psi.FileName = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe"; - psi.Arguments = sb.ToString(); - } - else - { - psi.FileName = _cliPath; - foreach (var arg in args) - psi.ArgumentList.Add(arg); - } - - // UTF-8-Locale für den Child setzen (Node.js / diverse Tools respektieren das). - // Schadet auf Windows nicht, hilft auf manchen Linux-Setups (z.B. systemd-Diensten ohne LANG). - psi.Environment["LANG"] = "en_US.UTF-8"; - psi.Environment["LC_ALL"] = "en_US.UTF-8"; - psi.Environment["PYTHONIOENCODING"] = "utf-8"; + foreach (var arg in args) + psi.ArgumentList.Add(arg); return psi; } - /// - /// Quotet ein Argument im Stil von Microsoft CRT (CommandLineToArgvW), - /// passend für die Verwendung innerhalb von cmd.exe /C "...". - /// - private static string QuoteArgForCmd(string arg) - { - if (string.IsNullOrEmpty(arg)) - return "\"\""; - - bool needsQuoting = false; - foreach (var c in arg) - { - if (c == ' ' || c == '\t' || c == '"' || c == '\n' || c == '\v') - { - needsQuoting = true; - break; - } - } - - if (!needsQuoting) - return arg; - - var sb = new StringBuilder(); - sb.Append('"'); - int backslashes = 0; - foreach (var c in arg) - { - if (c == '\\') - { - backslashes++; - } - else if (c == '"') - { - // 2n+1 Backslashes vor einem Anführungszeichen - sb.Append('\\', backslashes * 2 + 1); - sb.Append('"'); - backslashes = 0; - } - else - { - sb.Append('\\', backslashes); - sb.Append(c); - backslashes = 0; - } - } - // 2n Backslashes am Ende (vor dem schließenden ") - sb.Append('\\', backslashes * 2); - sb.Append('"'); - return sb.ToString(); - } - private static string FindCliPath() { // 1. Expliziter Pfad über Umgebungsvariable (höchste Priorität) @@ -285,14 +197,13 @@ public class ClaudeCliService args.Add(mcpConfigPath); } - return await RunCliStreamingAsync(args, prompt, onEvent, workspace, cancellationToken); + return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken); } private async Task RunCliStreamingAsync( List args, string stdinPrompt, Func onEvent, - string workingDirectory, CancellationToken cancellationToken) { var result = new CliChatResult(); @@ -310,7 +221,7 @@ public class ClaudeCliService try { - var startInfo = CreateCliStartInfo(args, redirectInput: true, workingDirectory: workingDirectory); + var startInfo = CreateCliStartInfo(args, redirectInput: true); using var process = new Process { StartInfo = startInfo }; @@ -337,16 +248,7 @@ public class ClaudeCliService await process.StandardInput.FlushAsync(); process.StandardInput.Close(); - // Eigener StreamReader mit großem Buffer + explizit UTF-8 ohne BOM-Detection. - // Default-Reader (process.StandardOutput) nutzt nur 1 KB Buffer, was bei - // gefragmentierten Pipe-Reads zu zerschnittenen Multi-Byte-UTF-8-Sequenzen - // und damit zu �-Replacement-Chars für Umlaute/Em-Dashes führen kann. - using var reader = new StreamReader( - process.StandardOutput.BaseStream, - new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), - detectEncodingFromByteOrderMarks: false, - bufferSize: 16384, - leaveOpen: false); + using var reader = process.StandardOutput; string line; while ((line = await reader.ReadLineAsync(cancellationToken)) != null) { diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index ddbcb9e..36048b9 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -337,16 +337,7 @@ public class OpenAiService // SSE-Stream lesen using var stream = await httpResp.Content.ReadAsStreamAsync(workCt); - // 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); + using var reader = new System.IO.StreamReader(stream); var fullText = new StringBuilder(); var thinkingText = new StringBuilder();