fix: CLI-Stream überlebt Page-Reload (Token-Trennung workCt/sseCt)
workCt steuert den CLI-Prozess (nur Stop-Button), sseCt die SSE-Writes (Client-Disconnect). Page-Reload cancelt nur sseCt → CLI läuft weiter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7d55e8e818
commit
ee0b817f25
2 files changed
+26
-17
No files matched your search
@@ -245,7 +245,7 @@ public static class ChatEndpoint
|
|||||||
{
|
{
|
||||||
if (anthropic.IsCliAvailable)
|
if (anthropic.IsCliAvailable)
|
||||||
{
|
{
|
||||||
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, cancellationToken: sseCt);
|
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, workCt: workCt, sseCt: sseCt);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1886,12 +1886,13 @@ public class AnthropicService
|
|||||||
bool planMode = false,
|
bool planMode = false,
|
||||||
bool webSearch = false,
|
bool webSearch = false,
|
||||||
string sessionId = null,
|
string sessionId = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken workCt = default,
|
||||||
|
CancellationToken sseCt = default)
|
||||||
{
|
{
|
||||||
if (_cliService == null || !_cliService.IsAvailable)
|
if (_cliService == null || !_cliService.IsAvailable)
|
||||||
{
|
{
|
||||||
await SseHelper.TryWriteSse(response, "error", new { error = "Claude CLI nicht verfügbar" }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "error", new { error = "Claude CLI nicht verfügbar" }, sseCt);
|
||||||
await SseHelper.TryWriteSse(response, "done", new { chainId }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "done", new { chainId }, sseCt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1964,14 +1965,14 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
switch (evt.Type)
|
switch (evt.Type)
|
||||||
{
|
{
|
||||||
case CliEventType.TextStart:
|
case CliEventType.TextStart:
|
||||||
await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt);
|
||||||
break;
|
break;
|
||||||
case CliEventType.TextDelta:
|
case CliEventType.TextDelta:
|
||||||
fullText.Append(evt.Text);
|
fullText.Append(evt.Text);
|
||||||
await SseHelper.TryWriteSse(response, "text_delta", new { text = evt.Text }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "text_delta", new { text = evt.Text }, sseCt);
|
||||||
break;
|
break;
|
||||||
case CliEventType.TextEnd:
|
case CliEventType.TextEnd:
|
||||||
await SseHelper.TryWriteSse(response, "text_end", new { }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "text_end", new { }, sseCt);
|
||||||
break;
|
break;
|
||||||
case CliEventType.ToolStart:
|
case CliEventType.ToolStart:
|
||||||
{
|
{
|
||||||
@@ -1989,7 +1990,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
fullText.Clear();
|
fullText.Clear();
|
||||||
}
|
}
|
||||||
var mappedName = MapCliToolName(evt.ToolName);
|
var mappedName = MapCliToolName(evt.ToolName);
|
||||||
await SseHelper.TryWriteSse(response, "tool_start", new { name = mappedName, id = evt.ToolId }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "tool_start", new { name = mappedName, id = evt.ToolId }, sseCt);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CliEventType.ToolEnd:
|
case CliEventType.ToolEnd:
|
||||||
@@ -2007,7 +2008,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
ToolCallId = evt.ToolId,
|
ToolCallId = evt.ToolId,
|
||||||
Content = mappedInput
|
Content = mappedInput
|
||||||
});
|
});
|
||||||
await SseHelper.TryWriteSse(response, "tool_end", new { name = mappedName, id = evt.ToolId, input = mappedInput }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "tool_end", new { name = mappedName, id = evt.ToolId, input = mappedInput }, sseCt);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CliEventType.ToolResult:
|
case CliEventType.ToolResult:
|
||||||
@@ -2030,7 +2031,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
id = evt.ToolId,
|
id = evt.ToolId,
|
||||||
result = resultContent,
|
result = resultContent,
|
||||||
isError = evt.ToolResultIsError
|
isError = evt.ToolResultIsError
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CliEventType.UserChoice:
|
case CliEventType.UserChoice:
|
||||||
@@ -2066,7 +2067,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
toolCallId = choiceToolCallId,
|
toolCallId = choiceToolCallId,
|
||||||
question = evt.ChoiceQuestion,
|
question = evt.ChoiceQuestion,
|
||||||
choices = evt.ChoiceOptions
|
choices = evt.ChoiceOptions
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
|
|
||||||
isUserChoice = true;
|
isUserChoice = true;
|
||||||
break;
|
break;
|
||||||
@@ -2078,7 +2079,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
hasWorkspace: !string.IsNullOrEmpty(workspace),
|
hasWorkspace: !string.IsNullOrEmpty(workspace),
|
||||||
planMode: planMode,
|
planMode: planMode,
|
||||||
webSearch: webSearch,
|
webSearch: webSearch,
|
||||||
cancellationToken: cancellationToken
|
cancellationToken: workCt
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bei CLI-Fehler → Fehlermeldung an Client senden und in DB speichern
|
// Bei CLI-Fehler → Fehlermeldung an Client senden und in DB speichern
|
||||||
@@ -2094,7 +2095,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
Type = "error",
|
Type = "error",
|
||||||
Content = $"CLI-Fehler: {errorMsg}"
|
Content = $"CLI-Fehler: {errorMsg}"
|
||||||
});
|
});
|
||||||
await SseHelper.TryWriteSse(response, "error", new { error = $"CLI-Fehler: {errorMsg}" }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "error", new { error = $"CLI-Fehler: {errorMsg}" }, sseCt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Antwort in DB speichern (nur wenn nicht schon im UserChoice-Handler gespeichert)
|
// Antwort in DB speichern (nur wenn nicht schon im UserChoice-Handler gespeichert)
|
||||||
@@ -2132,10 +2133,11 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
durationMs = sw.ElapsedMilliseconds,
|
durationMs = sw.ElapsedMilliseconds,
|
||||||
stopReason,
|
stopReason,
|
||||||
viaCli = true
|
viaCli = true
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException) when (workCt.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
// Stop-Button gedrückt → CLI-Prozess wurde gestoppt
|
||||||
await FinalizeChain(chain, usage, 1, "stopped", sw);
|
await FinalizeChain(chain, usage, 1, "stopped", sw);
|
||||||
await SseHelper.TryWriteSse(response, "done", new
|
await SseHelper.TryWriteSse(response, "done", new
|
||||||
{
|
{
|
||||||
@@ -2146,6 +2148,13 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
viaCli = true
|
viaCli = true
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Client-Disconnect (sseCt gecancelled, aber NICHT workCt)
|
||||||
|
// CLI-Prozess läuft weiter im Hintergrund – hier nur Chain finalisieren
|
||||||
|
// (sollte nicht mehr passieren, da CLI jetzt workCt nutzt)
|
||||||
|
await FinalizeChain(chain, usage, 1, "client_disconnected", sw);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var errorMsg = $"CLI-Fehler: {ex.Message}";
|
var errorMsg = $"CLI-Fehler: {ex.Message}";
|
||||||
@@ -2157,7 +2166,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
Type = "error",
|
Type = "error",
|
||||||
Content = errorMsg
|
Content = errorMsg
|
||||||
});
|
});
|
||||||
await SseHelper.TryWriteSse(response, "error", new { error = errorMsg }, cancellationToken);
|
await SseHelper.TryWriteSse(response, "error", new { error = errorMsg }, sseCt);
|
||||||
await FinalizeChain(chain, usage, 1, "error", sw);
|
await FinalizeChain(chain, usage, 1, "error", sw);
|
||||||
await SseHelper.TryWriteSse(response, "done", new
|
await SseHelper.TryWriteSse(response, "done", new
|
||||||
{
|
{
|
||||||
@@ -2166,7 +2175,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
|||||||
durationMs = sw.ElapsedMilliseconds,
|
durationMs = sw.ElapsedMilliseconds,
|
||||||
stopReason = "error",
|
stopReason = "error",
|
||||||
viaCli = true
|
viaCli = true
|
||||||
}, cancellationToken);
|
}, sseCt);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user