fix: UTF-8-Encoding gehärtet (SSE-Reader + CLI-Workspace)
- 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) <noreply@anthropic.com>
This commit is contained in:
1 parent
32aad7feef
commit
bfd550e090
3 files changed
+36
-6
No files matched your search
@@ -402,7 +402,17 @@ public partial class AnthropicService
|
|||||||
// an den underlying NetworkStream – der Read blockiert sonst bis Anthropic
|
// an den underlying NetworkStream – der Read blockiert sonst bis Anthropic
|
||||||
// das nächste Chunk sendet (kann bei extended thinking Minuten dauern).
|
// das nächste Chunk sendet (kann bei extended thinking Minuten dauern).
|
||||||
using var ctReg = workCt.Register(() => { try { stream.Dispose(); } catch { } });
|
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 stopReason = "";
|
||||||
var assistantContent = new List<object>();
|
var assistantContent = new List<object>();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class ClaudeCliService
|
|||||||
public bool IsAvailable => _isAvailable;
|
public bool IsAvailable => _isAvailable;
|
||||||
public string CliPath => _cliPath;
|
public string CliPath => _cliPath;
|
||||||
|
|
||||||
private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false)
|
private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false, string workingDirectory = null)
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
@@ -48,13 +48,23 @@ public class ClaudeCliService
|
|||||||
StandardErrorEncoding = Encoding.UTF8
|
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())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
// Auf Windows: über cmd.exe wrappen und Codepage 65001 (UTF-8) erzwingen.
|
// Auf Windows: über cmd.exe wrappen und Codepage 65001 (UTF-8) erzwingen.
|
||||||
// Sonst kann der Child-Prozess (claude.exe) Output mit CP1252/CP850 schreiben,
|
// 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.
|
// 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();
|
var sb = new StringBuilder();
|
||||||
sb.Append("/S /C \"chcp 65001 >nul && ");
|
sb.Append("/D /S /C \"chcp 65001 >nul && ");
|
||||||
sb.Append(QuoteArgForCmd(_cliPath));
|
sb.Append(QuoteArgForCmd(_cliPath));
|
||||||
foreach (var a in args)
|
foreach (var a in args)
|
||||||
{
|
{
|
||||||
@@ -275,13 +285,14 @@ public class ClaudeCliService
|
|||||||
args.Add(mcpConfigPath);
|
args.Add(mcpConfigPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken);
|
return await RunCliStreamingAsync(args, prompt, onEvent, workspace, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CliChatResult> RunCliStreamingAsync(
|
private async Task<CliChatResult> RunCliStreamingAsync(
|
||||||
List<string> args,
|
List<string> args,
|
||||||
string stdinPrompt,
|
string stdinPrompt,
|
||||||
Func<CliStreamEvent, Task> onEvent,
|
Func<CliStreamEvent, Task> onEvent,
|
||||||
|
string workingDirectory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = new CliChatResult();
|
var result = new CliChatResult();
|
||||||
@@ -299,7 +310,7 @@ public class ClaudeCliService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var startInfo = CreateCliStartInfo(args, redirectInput: true);
|
var startInfo = CreateCliStartInfo(args, redirectInput: true, workingDirectory: workingDirectory);
|
||||||
|
|
||||||
using var process = new Process { StartInfo = startInfo };
|
using var process = new Process { StartInfo = startInfo };
|
||||||
|
|
||||||
|
|||||||
@@ -337,7 +337,16 @@ public class OpenAiService
|
|||||||
|
|
||||||
// SSE-Stream lesen
|
// SSE-Stream lesen
|
||||||
using var stream = await httpResp.Content.ReadAsStreamAsync(workCt);
|
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 fullText = new StringBuilder();
|
||||||
var thinkingText = new StringBuilder();
|
var thinkingText = new StringBuilder();
|
||||||
|
|||||||
Reference in new issue
Block a user