Revert: UTF-8-Encoding-Fixes für Claude CLI auf Windows
Setzt die folgenden Commits zurück, da sie das Problem nicht behoben haben: -e75c437fix(encoding): UTF-8 prozess-global + 16-KB-StreamReader für CLI-Output -bfd550efix: UTF-8-Encoding gehärtet (SSE-Reader + CLI-Workspace) -32aad7ffix: 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:
1 parent
e75c4370fd
commit
01346a952e
4 files changed
+9
-141
No files matched your search
-15
@@ -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))
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user