Revert: UTF-8-Encoding-Fixes für Claude CLI auf Windows

Setzt die folgenden Commits zurück, da sie das Problem nicht behoben haben:
- e75c437 fix(encoding): UTF-8 prozess-global + 16-KB-StreamReader für CLI-Output
- bfd550e fix: UTF-8-Encoding gehärtet (SSE-Reader + CLI-Workspace)
- 32aad7f fix: UTF-8-Encoding für Claude CLI auf Windows erzwingen

Trotz cmd.exe-Wrapper mit chcp 65001, explizitem UTF-8-StreamReader (16 KB Buffer,
ohne BOM-Detection), Console.OutputEncoding global auf UTF-8 und LANG/LC_ALL/
PYTHONIOENCODING-Env-Vars treten weiterhin Replacement-Chars (�) für Umlaute
und Em-Dashes auf Windows auf. Der Bug muss separat per Diagnose-Logging
nachgestellt werden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.7 committed 2026-04-26 16:27:16 +02:00
1 parent e75c4370fd
commit 01346a952e
4 files changed
+9 -141

No files matched your search

-15
View File
@@ -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))
+1 -11
View File
@@ -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<object>();
+7 -105
View File
@@ -35,10 +35,11 @@ public class ClaudeCliService
public bool IsAvailable => _isAvailable;
public string CliPath => _cliPath;
private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false, string workingDirectory = null)
private ProcessStartInfo CreateCliStartInfo(List<string> 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;
}
/// <summary>
/// Quotet ein Argument im Stil von Microsoft CRT (CommandLineToArgvW),
/// passend für die Verwendung innerhalb von cmd.exe /C "...".
/// </summary>
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<CliChatResult> RunCliStreamingAsync(
List<string> args,
string stdinPrompt,
Func<CliStreamEvent, Task> 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)
{
+1 -10
View File
@@ -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();