This commit is contained in:
2026-08-24 04:23:43 +02:00
parent f33a6829e8
commit 579cd8e4b3
5 changed files with 611 additions and 85 deletions
+582 -78
View File
@@ -10,6 +10,8 @@ using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -96,6 +98,7 @@ namespace AnotherReplayReader
private (Paragraph? Think, Paragraph? Content)? _currentContent;
private Paragraph? _lastContentParagraph;
private bool _suppressDisplay;
private bool _nextThinkingIsContinuation;
// 进度计时
private TimeSpan _previousTotalTime;
@@ -106,6 +109,18 @@ namespace AnotherReplayReader
private DispatcherTimer? _uiTimer;
// 缓冲区
private readonly ConcurrentQueue<(AIAnalyze.AIChunk Chunk, DateTimeOffset Time)> _chunkQueue = new();
private readonly Queue<Action> _pendingUiActions = new();
private bool _isFlushingPendingUiActions;
// 阶段化 UI
private readonly Dictionary<string, Section> _stageSections = new();
private string? _currentStage;
private Section? _currentVersionSection;
private Paragraph? _currentVersionBody;
private string? _currentVersionTitle;
private UpdateCollapsibleSection? _currentVersionUpdate;
private Action<Block>? _currentVersionAddBlock;
private bool _currentVersionBodyAdded;
// Token 累计
private int _totalPromptTokens;
@@ -199,11 +214,22 @@ namespace AnotherReplayReader
_currentSegmentIndex = 0;
_document.Blocks.Clear();
_stageSections.Clear();
_currentStage = null;
_currentVersionSection = null;
_currentVersionBody = null;
_currentVersionTitle = null;
_currentVersionUpdate = null;
_currentVersionAddBlock = null;
_currentVersionBodyAdded = false;
_pendingUiActions.Clear();
_isFlushingPendingUiActions = false;
_updateCurrentThinkingSection = null;
_thinkBlockWasExpanded = false;
_currentContent = null;
_lastContentParagraph = null;
_suppressDisplay = false;
_nextThinkingIsContinuation = false;
_previousTotalTime = TimeSpan.Zero;
_previousTotalChars = 0;
@@ -291,6 +317,7 @@ namespace AnotherReplayReader
FinishCurrentContent();
var requestContext = GetRequestContext();
_systemPrompt = AIAnalyze.GetSystemPrompt(replay, players, GetPromptSettings?.Invoke());
BeginStage("准备");
// ---- 机械分段(M2 ----
var model = requestContext.Model;
@@ -351,6 +378,12 @@ namespace AnotherReplayReader
// ---- 总览轮 ----
_phaseText.Text = "正在生成总览...";
BeginStage("总览");
AppendLog(
"总览用户消息",
AIAnalyze.BuildOverviewUserPrompt(_slices),
true);
StartVersion("版本 1:总览正文", collapsedByDefault: false);
var overviewMessages = BuildSystemMessages(_systemPrompt, _digest, null)
.Add(new AIAnalyze.ChatMessage("user", AIAnalyze.BuildOverviewUserPrompt(_slices)));
CheckAndLogContextUsage(overviewMessages, requestContext);
@@ -367,10 +400,6 @@ namespace AnotherReplayReader
ReportReasoningGuardResult(overviewResult);
_overview = OverviewParser.Parse(overviewResult.Response);
_overviewNarrative = _overview.Narrative;
AppendLog(
overviewAttempt == 0 ? "整局总览" : "整局总览(重试)",
overviewResult.Response,
false);
overviewAttempt++;
}
while (_overview.Segments.IsEmpty && overviewAttempt < 2);
@@ -438,6 +467,7 @@ namespace AnotherReplayReader
_phaseText.Text = $"正在分析第 {segmentIndex}/{totalSegments} 段";
BeginStage($"第{segmentIndex}段");
FinishCurrentContent();
// 记录回滚点
_blockCountBeforeSegment = _document.Blocks.Count;
@@ -475,6 +505,7 @@ namespace AnotherReplayReader
instruction + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
true);
CheckAndLogContextUsage(messages, requestContext);
StartVersion("版本 1:正文", collapsedByDefault: false);
// 回查循环(M3):同一段会话内,模型可多次请求远处原始区间
const int maxBackqueriesPerSegment = 3;
@@ -524,19 +555,22 @@ namespace AnotherReplayReader
break;
}
backqueryCount += pendingTexts.Count;
var backqueryPrompt = AIAnalyze.BuildBackqueryUserPrompt(
string.Join("\n\n", pendingTexts));
segmentMessages = segmentMessages
.Add(new AIAnalyze.ChatMessage("assistant", segmentResponse))
.Add(new AIAnalyze.ChatMessage(
"user", AIAnalyze.BuildBackqueryUserPrompt(string.Join("\n\n", pendingTexts))));
"user", backqueryPrompt));
AppendLog(
$"第{segmentIndex}段回查(第 {backqueryCount} 次)",
$"已提供 {pendingTexts.Count} 个区间,继续分析。",
$"已提供 {pendingTexts.Count} 个区间,继续分析。\n\n用户消息:\n" + backqueryPrompt,
true);
}
FinishCurrentContent();
var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(
segmentResponse, _factIndex, playerNameToIndex, structuredKnowledge);
AppendMachineJsonAndValidation(segmentResponse, validationResult);
if (validationResult.HasIssues)
{
AppendLog(
@@ -555,15 +589,16 @@ namespace AnotherReplayReader
or AIValidationSeverity.Fatal
or AIValidationSeverity.Warning
or AIValidationSeverity.WeakEvidence);
AppendLog(
$"第{segmentIndex}段修订",
$"验证器发现 {issueCount} 个需要修正/降级的问题,正在请求 AI 修正...",
false);
var relevantFacts = RelevantFactsFormatter.Format(validationResult.Claims, _factIndex!);
var revisionPrompt = AIAnalyze.BuildRevisionUserPrompt(
segmentResponse,
AIAnalysisValidation.FormatIssues(validationResult.Issues),
string.IsNullOrWhiteSpace(relevantFacts) ? "(无额外事实)" : relevantFacts);
AppendLog(
$"第{segmentIndex}段修订",
$"验证器发现 {issueCount} 个需要修正/降级的问题,正在请求 AI 修正...\n\n用户消息:\n"
+ revisionPrompt,
true);
var revisionMessages = segmentMessages
.Add(new AIAnalyze.ChatMessage("assistant", segmentResponse))
.Add(new AIAnalyze.ChatMessage("user", revisionPrompt));
@@ -595,7 +630,10 @@ namespace AnotherReplayReader
finalResponse = revisionResult.Response;
finalValidation = AIAnalysisValidation.ValidateMachineReadableClaims(
revisionResult.Response, _factIndex, playerNameToIndex, structuredKnowledge);
ReplaceSegmentContent(finalResponse);
CollapseCurrentVersion();
StartVersion("版本 2:修订后正文", collapsedByDefault: false);
AppendDirectText(finalResponse);
AppendMachineJsonAndValidation(finalResponse, finalValidation);
AppendLog(
"修订完成",
"已采用修正后的分析。" + (finalValidation.HasIssues
@@ -639,6 +677,7 @@ namespace AnotherReplayReader
_linkedCts.Token.ThrowIfCancellationRequested();
_phaseText.Text = "正在生成总结...";
BeginStage("总结");
FinishCurrentContent();
_blockCountBeforeSegment = _document.Blocks.Count;
@@ -671,6 +710,7 @@ namespace AnotherReplayReader
finalPrompt + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
true);
CheckAndLogContextUsage(messages, requestContext);
StartVersion("版本 1:总结正文", collapsedByDefault: false);
var result = await _analyzer!.CompleteAsync(
messages,
requestContext,
@@ -688,6 +728,7 @@ namespace AnotherReplayReader
var backqueries = BackqueryParser.Parse(result.Response);
if (backqueries.IsEmpty)
{
AppendLog("总结完成", "已生成最终总结。", false);
return;
}
@@ -715,16 +756,19 @@ namespace AnotherReplayReader
}
_phaseText.Text = "正在根据总结回查补充信息...";
var backqueryPrompt = AIAnalyze.BuildBackqueryUserPrompt(
string.Join("\n\n", pendingTexts));
var backqueryMessages = messages
.Add(new AIAnalyze.ChatMessage("assistant", result.Response))
.Add(new AIAnalyze.ChatMessage(
"user",
AIAnalyze.BuildBackqueryUserPrompt(string.Join("\n\n", pendingTexts))));
.Add(new AIAnalyze.ChatMessage("user", backqueryPrompt));
AppendLog(
"总结回查",
$"已提供 {pendingTexts.Count} 个区间,正在生成最终总结。",
$"已提供 {pendingTexts.Count} 个区间,正在生成最终总结。\n\n用户消息:\n"
+ backqueryPrompt,
true);
CheckAndLogContextUsage(backqueryMessages, requestContext);
CollapseCurrentVersion();
StartVersion("版本 2:最终总结", collapsedByDefault: false);
var finalSummary = await _analyzer!.CompleteAsync(
backqueryMessages,
requestContext,
@@ -815,46 +859,31 @@ namespace AnotherReplayReader
private void ReportReasoningGuardResult(AIAnalyze.Result result)
{
if (!result.ReasoningInterrupted
&& !result.ContinuationApplied
&& string.IsNullOrWhiteSpace(result.ReasoningContinuationError))
if (string.IsNullOrWhiteSpace(result.ReasoningContinuationError))
{
return;
}
if (!string.IsNullOrWhiteSpace(result.ReasoningContinuationError))
{
AppendLog("推理保护警告", result.ReasoningContinuationError, false);
}
else if (result.ContinuationApplied)
{
AppendLog("推理保护", "已请求 AI 尽快收尾,并完成最终回答。", false);
}
else if (result.ReasoningInterrupted)
{
AppendLog("推理保护", "检测到推理内容超限,正在处理续写结果。", false);
}
if (!string.IsNullOrWhiteSpace(result.ReasoningOriginalRequestJson))
{
AppendLog(
"推理保护:首次请求消息",
result.ReasoningOriginalRequestJson,
true);
}
if (!string.IsNullOrWhiteSpace(result.ReasoningContinuationRequestJson))
{
AppendLog(
"推理保护:续写请求完整消息",
result.ReasoningContinuationRequestJson,
true);
}
AppendLog("推理保护警告", result.ReasoningContinuationError, false, _currentStage);
}
private void StartThinkingBlock()
{
_thinkingStartTime = DateTime.Now;
var (_, content, update) = CreateCollapsibleSection("💭 AI 思考中...", collapsedByDefault: false);
var stage = _currentStage ?? "分析";
var label = _nextThinkingIsContinuation ? "AI 续写中..." : "AI 思考中...";
_nextThinkingIsContinuation = false;
var parent = _currentVersionAddBlock is null
? GetStageBlocks(_currentStage)
: null;
var (section, content, update, _) = CreateCollapsibleSection(
$"💭 [{stage}] {label}",
collapsedByDefault: false,
parent);
if (_currentVersionAddBlock is { } addBlock)
{
addBlock(section);
}
_currentContent = (Think: content, _currentContent?.Content);
_updateCurrentThinkingSection = update;
_thinkBlockWasExpanded = true;
@@ -873,35 +902,25 @@ namespace AnotherReplayReader
var timeText = elapsed < 60
? $"{elapsed:F0} 秒"
: $"{elapsed / 60:F1} 分钟";
var stage = _currentStage ?? "分析";
// 自动折叠:移除段落,更新按钮文字
_updateCurrentThinkingSection($"💭 AI 已思考完毕(用时 {timeText}", true);
_updateCurrentThinkingSection(
$"💭 [{stage}] AI 已思考完毕(用时 {timeText}",
true);
_updateCurrentThinkingSection = null;
}
private void StartContentParagraph()
{
_currentContent = (_currentContent?.Think, Content: new Paragraph());
EnsureVersionBodyAdded();
var body = _currentVersionBody ?? new Paragraph();
_currentContent = (_currentContent?.Think, Content: body);
_lastContentParagraph = _currentContent.Value.Content;
_document.Blocks.Add(_currentContent.Value.Content);
if (_currentVersionBody is null)
{
GetStageBlocks(_currentStage).Add(body);
}
private void ReplaceSegmentContent(string text)
{
FinishCurrentContent();
// 移除本段已显示的全部块(草稿/回查中间内容),只保留最终修正版
while (_document.Blocks.Count > _blockCountBeforeSegment)
{
_document.Blocks.Remove(_document.Blocks.LastBlock);
}
_currentContent = null;
_lastContentParagraph = null;
StartContentParagraph();
_lastContentParagraph!.Inlines.Add(new Run(text)
{
Foreground = ContentStyle.Foreground,
FontSize = ContentStyle.FontSize
});
}
private void AppendToParagraph(Paragraph? paragraph, string text, ParagraphStyle style)
@@ -922,9 +941,414 @@ namespace AnotherReplayReader
});
}
// ---- 折叠块辅助 ----
private (Section Section, Paragraph Content, UpdateCollapsibleSection UpdateSection)
CreateCollapsibleSection(string title, bool collapsedByDefault)
// ---- 阶段化与折叠块辅助 ----
private Section BeginStage(string stage)
{
if (_stageSections.TryGetValue(stage, out var existing))
{
_currentStage = stage;
return existing;
}
var section = new Section();
var header = new Paragraph(new Run($"📌 {stage}")
{
Foreground = CollapsibleSectionHeaderStyle.Foreground,
FontSize = DefaultFontSize,
FontWeight = FontWeights.Bold
});
section.Blocks.Add(header);
_document.Blocks.Add(section);
_stageSections[stage] = section;
_currentStage = stage;
return section;
}
private BlockCollection GetStageBlocks(string? stage = null)
{
stage ??= _currentStage;
return stage != null && _stageSections.TryGetValue(stage, out var section)
? section.Blocks
: _document.Blocks;
}
private void StartVersion(string title, bool collapsedByDefault)
{
var (section, content, update, addBlock) =
CreateCollapsibleSection(
title,
collapsedByDefault,
GetStageBlocks(_currentStage));
_currentVersionSection = section;
_currentVersionBody = content;
_currentVersionTitle = title;
_currentVersionUpdate = update;
_currentVersionAddBlock = addBlock;
_currentVersionBodyAdded = false;
_currentContent = null;
_lastContentParagraph = null;
// 正文段落先不加入文档;思考块/保护日志会按时间线先追加,
// 真正开始输出正文时才把正文追加到它们之后。
section.Blocks.Remove(content);
}
private void CollapseCurrentVersion()
{
FinishCurrentContent();
if (_currentVersionUpdate is { } update)
{
update(_currentVersionTitle ?? "版本", true);
}
_currentVersionSection = null;
_currentVersionBody = null;
_currentVersionTitle = null;
_currentVersionUpdate = null;
_currentVersionAddBlock = null;
_currentVersionBodyAdded = false;
}
private void EnsureVersionBodyAdded()
{
if (_currentVersionBody is { } body
&& !_currentVersionBodyAdded
&& _currentVersionAddBlock is { } addBlock)
{
addBlock(body);
_currentVersionBodyAdded = true;
}
}
private void AppendDirectText(string text)
{
EnsureVersionBodyAdded();
AppendToParagraph(_currentVersionBody, text, ContentStyle);
}
private void ReplaceVersionBodyText(string text)
{
if (_currentVersionBody is not { } body)
{
return;
}
body.Inlines.Clear();
AppendTextToParagraph(body, text, ContentStyle);
}
private static void AppendTextToParagraph(
Paragraph paragraph,
string text,
ParagraphStyle style)
{
foreach (var rawLine in text.Split('\n'))
{
var line = rawLine.TrimEnd('\r');
paragraph.Inlines.Add(new Run(line)
{
Foreground = style.Foreground,
FontSize = style.FontSize
});
paragraph.Inlines.Add(new LineBreak());
}
}
private void AppendMachineJsonAndValidation(
string response,
AIValidationResult validation)
{
if (_currentVersionAddBlock is not { } addBlock)
{
return;
}
var json = ExtractMachineReadableJson(response);
if (!string.IsNullOrWhiteSpace(json))
{
ReplaceVersionBodyText(StripMachineReadableSection(response));
var (jsonSection, jsonContent, _, _) =
CreateCollapsibleSection("机器可读声明 JSON", true);
jsonContent.Inlines.Add(new Run(json)
{
Foreground = LogStyle.Foreground,
FontSize = DetailsFontSize
});
addBlock(jsonSection);
}
var validationTitle = validation.HasIssues
? $"验证结果:{validation.Issues.Length} 个问题"
: "验证结果:通过";
var validationDetails = validation.HasIssues
? AIAnalysisValidation.FormatIssues(validation.Issues)
: "机器可读声明验证通过。";
var (validationSection, validationContent, _, _) =
CreateCollapsibleSection(validationTitle, true);
validationContent.Inlines.Add(new Run(validationDetails)
{
Foreground = LogStyle.Foreground,
FontSize = DetailsFontSize
});
addBlock(validationSection);
}
private static string StripMachineReadableSection(string text)
{
var markerIndex = text.IndexOf(
"[机器可读声明]",
StringComparison.Ordinal);
if (markerIndex < 0)
{
return text;
}
var after = text.Substring(markerIndex);
var fenceStart = after.IndexOf("```", StringComparison.Ordinal);
if (fenceStart < 0)
{
return text.Substring(0, markerIndex).TrimEnd();
}
var fenceEnd = after.IndexOf(
"```",
fenceStart + 3,
StringComparison.Ordinal);
if (fenceEnd < 0)
{
return text.Substring(0, markerIndex).TrimEnd();
}
var removeEnd = markerIndex + fenceEnd + 3;
var before = text.Substring(0, markerIndex).TrimEnd();
var rest = text.Substring(removeEnd).TrimStart();
return rest.Length > 0 ? before + "\n\n" + rest : before;
}
private static string? ExtractMachineReadableJson(string text)
{
var markerIndex = text.IndexOf(
"[机器可读声明]",
StringComparison.Ordinal);
if (markerIndex < 0)
{
return null;
}
var after = text.Substring(markerIndex);
var fenceStart = after.IndexOf("```", StringComparison.Ordinal);
if (fenceStart < 0)
{
return null;
}
var fenceEnd = after.IndexOf(
"```",
fenceStart + 3,
StringComparison.Ordinal);
if (fenceEnd < 0)
{
return null;
}
var block = after.Substring(
fenceStart + 3,
fenceEnd - fenceStart - 3);
var jsonStart = block.IndexOf('{');
var jsonEnd = block.LastIndexOf('}');
if (jsonStart < 0 || jsonEnd <= jsonStart)
{
return null;
}
return block.Substring(jsonStart, jsonEnd - jsonStart + 1).Trim();
}
private void AppendReasoningDiagnostics(string payload)
{
try
{
using var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
var originalText = root.TryGetProperty("originalRequestJson", out var original)
&& original.ValueKind == JsonValueKind.String
? original.GetString()
: null;
var continuationText =
root.TryGetProperty("continuationRequestJson", out var continuation)
&& continuation.ValueKind == JsonValueKind.String
? continuation.GetString()
: null;
var summary = BuildReasoningDiagnosticSummary(
originalText,
continuationText);
if (!string.IsNullOrWhiteSpace(summary))
{
AppendLog("推理保护诊断", summary, true, _currentStage);
}
if (!string.IsNullOrWhiteSpace(continuationText))
{
AppendLog(
"完整续写请求 JSON",
PrettyPrintJson(continuationText!),
true,
_currentStage);
}
}
catch
{
AppendLog("推理保护诊断", payload, true, _currentStage);
}
}
private static string BuildReasoningDiagnosticSummary(
string? originalJson,
string? continuationJson)
{
var sb = new StringBuilder();
sb.AppendLine($"消息数:{GetMessageCount(originalJson)} → {GetMessageCount(continuationJson)}");
if (string.IsNullOrWhiteSpace(continuationJson))
{
return sb.ToString().TrimEnd();
}
try
{
using var doc = JsonDocument.Parse(continuationJson!);
if (!doc.RootElement.TryGetProperty("messages", out var messages)
|| messages.ValueKind != JsonValueKind.Array)
{
return sb.ToString().TrimEnd();
}
for (var i = messages.GetArrayLength() - 1; i >= 0; --i)
{
var message = messages[i];
if (!message.TryGetProperty("tool_calls", out var toolCalls)
|| toolCalls.ValueKind != JsonValueKind.Array
|| toolCalls.GetArrayLength() == 0)
{
continue;
}
var toolName = "unknown";
if (toolCalls[0].TryGetProperty("function", out var function)
&& function.TryGetProperty("name", out var name))
{
toolName = name.GetString() ?? toolName;
}
var reasoning = message.TryGetProperty("reasoning_content", out var reasoningProp)
? reasoningProp.GetString()
: null;
sb.AppendLine("新增 assistant 消息:");
sb.AppendLine($" - reasoning_content 长度:{reasoning?.Length ?? 0}");
if (!string.IsNullOrEmpty(reasoning))
{
var markerIndex = reasoning.IndexOf(
AiReasoningGuard.TruncationMarker,
StringComparison.Ordinal);
var tail = markerIndex >= 0
? reasoning.Substring(markerIndex)
: "(未找到截断标记)";
sb.AppendLine($" - 末尾追加:{Excerpt(tail, 160)}");
}
sb.AppendLine($" - tool_calls{toolName}");
break;
}
for (var i = messages.GetArrayLength() - 1; i >= 0; --i)
{
var message = messages[i];
if (!message.TryGetProperty("role", out var role)
|| role.GetString() != "tool")
{
continue;
}
var toolCallId = message.TryGetProperty("tool_call_id", out var id)
? id.GetString()
: "?";
var content = message.TryGetProperty("content", out var contentProp)
? contentProp.GetString()
: null;
sb.AppendLine("新增 tool 消息:");
sb.AppendLine($" - tool_call_id{toolCallId}");
sb.AppendLine($" - 指令开头:{Excerpt(content ?? "()", 160)}");
break;
}
}
catch
{
// 摘要解析失败时保留原始 JSON 回退。
}
return sb.ToString().TrimEnd();
}
private static int GetMessageCount(string? requestJson)
{
if (string.IsNullOrWhiteSpace(requestJson))
{
return 0;
}
try
{
using var doc = JsonDocument.Parse(requestJson!);
return doc.RootElement.TryGetProperty("messages", out var messages)
&& messages.ValueKind == JsonValueKind.Array
? messages.GetArrayLength()
: 0;
}
catch
{
return 0;
}
}
private static string PrettyPrintJson(string json)
{
try
{
using var doc = JsonDocument.Parse(json);
return JsonSerializer.Serialize(
doc.RootElement,
new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
catch
{
return json;
}
}
private static string Excerpt(string text, int maxLength)
{
var normalized = text.Replace("\r", "").Replace("\n", " ");
return normalized.Length <= maxLength
? normalized
: normalized.Substring(0, maxLength) + "…";
}
private void FlushPendingUiActions()
{
_isFlushingPendingUiActions = true;
try
{
while (_pendingUiActions.Count > 0)
{
_pendingUiActions.Dequeue()();
}
}
finally
{
_isFlushingPendingUiActions = false;
}
}
private (Section Section, Paragraph Content, UpdateCollapsibleSection UpdateSection, Action<Block> AddBlock)
CreateCollapsibleSection(
string title,
bool collapsedByDefault,
BlockCollection? parent = null)
{
var currentTitle = title;
var currentlyCollapsed = collapsedByDefault;
@@ -932,6 +1356,7 @@ namespace AnotherReplayReader
var section = new Section();
var header = new Paragraph();
var content = new Paragraph();
var contentBlocks = new List<Block> { content };
var hyperLink = new Hyperlink(new Run(""))
{
Foreground = CollapsibleSectionHeaderStyle.Foreground,
@@ -951,16 +1376,41 @@ namespace AnotherReplayReader
run.Text = currentlyCollapsed ? $"{currentTitle} ⯈" : $"{currentTitle} ⯆";
if (currentlyCollapsed)
{
section.Blocks.Remove(content);
foreach (var block in contentBlocks)
{
section.Blocks.Remove(block);
}
}
else
{
if (!section.Blocks.Contains(content))
foreach (var block in contentBlocks)
{
section.Blocks.Add(content);
if (!section.Blocks.Contains(block))
{
section.Blocks.Add(block);
}
}
}
}
void AddBlock(Block block)
{
if (block == content)
{
// 正文段落应在思考/日志之后进入时间线;
// 但它作为版本的初始 content 已在列表中,需要移到末尾。
contentBlocks.Remove(block);
contentBlocks.Add(block);
}
else if (!contentBlocks.Contains(block))
{
contentBlocks.Add(block);
}
if (!currentlyCollapsed && !section.Blocks.Contains(block))
{
section.Blocks.Add(block);
}
}
hyperLink.Click += (s, e) =>
{
@@ -977,32 +1427,60 @@ namespace AnotherReplayReader
UpdateCollapsibleSection(currentTitle, collapsedByDefault);
_document.Blocks.Add(section);
(parent ?? _document.Blocks).Add(section);
return (section, content, UpdateCollapsibleSection);
return (section, content, UpdateCollapsibleSection, AddBlock);
}
private void AppendLog(string title, string? details, bool collapsed)
private void AppendLog(
string title,
string? details,
bool collapsed,
string? stage = null)
{
if (!_isFlushingPendingUiActions)
{
AutoScroll();
FinishCurrentContent();
}
title = $"📋 {title}";
var addBlock = _currentVersionAddBlock;
var parent = addBlock is null ? GetStageBlocks(stage) : null;
if (string.IsNullOrEmpty(details))
{
var paragraph = new Paragraph(new Run(title)
{
Foreground = LogStyle.Foreground,
});
_document.Blocks.Add(paragraph);
if (addBlock is { } add)
{
add(paragraph);
}
else
{
parent!.Add(paragraph);
}
return;
}
var logParagraph = CreateCollapsibleSection(title, collapsed).Content;
logParagraph.Inlines.Add(new Run(details)
var (logSection, logParagraph, _, _) = CreateCollapsibleSection(
title,
collapsed,
parent);
if (addBlock is { } addLog)
{
addLog(logSection);
}
foreach (var rawLine in details.Split('\n'))
{
var line = rawLine.TrimEnd('\r');
logParagraph.Inlines.Add(new Run(line)
{
Foreground = LogStyle.Foreground,
FontSize = LogStyle.FontSize
});
logParagraph.Inlines.Add(new LineBreak());
}
}
// ---- UI 更新与计时 ----
@@ -1024,6 +1502,7 @@ namespace AnotherReplayReader
{
// discard everything in _chunkQueue
while (_chunkQueue.TryDequeue(out _)) { }
_pendingUiActions.Clear();
if (_uiTimer is not null)
{
_uiTimer.Stop();
@@ -1038,6 +1517,7 @@ namespace AnotherReplayReader
private void OnUiTimerTick(object? sender, EventArgs e)
{
FlushPendingChunks();
FlushPendingUiActions();
var now = DateTime.Now;
var elapsed = now - _analysisStartTime;
@@ -1071,9 +1551,25 @@ namespace AnotherReplayReader
if (_suppressDisplay)
{
while (_chunkQueue.TryDequeue(out var data))
{
if (data.Chunk.Type == AIAnalyze.AIChunkType.ReasoningGuard)
{
var text = data.Chunk.Text;
_nextThinkingIsContinuation = true;
_pendingUiActions.Enqueue(() =>
AppendLog("推理保护", text, false, _currentStage));
}
else if (data.Chunk.Type == AIAnalyze.AIChunkType.ReasoningGuardRequest)
{
var text = data.Chunk.Text;
_pendingUiActions.Enqueue(() =>
AppendReasoningDiagnostics(text));
}
else
{
_currentOutputChars += data.Chunk.Text.Length;
}
}
return;
}
@@ -1111,8 +1607,16 @@ namespace AnotherReplayReader
else if (chunk.Type == AIAnalyze.AIChunkType.ReasoningGuard)
{
EndThinkingBlock();
_nextThinkingIsContinuation = true;
_pendingUiActions.Enqueue(() =>
AppendLog("推理保护", chunk.Text, false, _currentStage));
guardTriggered = true;
}
else if (chunk.Type == AIAnalyze.AIChunkType.ReasoningGuardRequest)
{
_pendingUiActions.Enqueue(() =>
AppendReasoningDiagnostics(chunk.Text));
}
}
if (thinkSb.Length > 0)
+2
View File
@@ -241,6 +241,8 @@ system
- 保留推理在其末尾依次追加 `[INTERNAL_REASONING_TRUNCATED]` 与中文收尾句;收尾句不插入中间 checkpoint,也不提及“token limit/被截断”等技术细节。
- 续写工具结果包含“进入收尾阶段、立即停止展开、直接输出最终结果”和“最后一句已经宣告收尾,请立即执行”等强化措辞。
- UI 在触发推理保护后提供两个可折叠日志:首次请求消息与续写请求完整消息;用户可展开查看完整 `messages``tools``tool_choice` 和收尾指令。
- UI 进一步改为阶段化时间线:推理保护事件与诊断在续写思考块之前输出;修订保留旧版本并折叠,最新版本展开;机器可读声明 JSON 与验证结果独立展示。
- UI 正文在成功提取机器可读声明后会移除 JSON 原文;推理保护诊断显示可读摘要(消息数变化、新增 assistant/tool 消息),完整续写 JSON 作为折叠附件;总览、修订和回查的 user prompt 会以折叠日志展示。
- 默认测试新增 `ReasoningGuard` 套件后总计 173 项通过;真实 API A/B 仍需要用户手工运行 `ARR_AI_E2E=1` 验证。
- 可选真实环境测试为 `OpenCodeGoGuardE2e`:设置 `ARR_AI_GUARD_E2E=1` 启用,报告路径可用 `ARR_AI_GUARD_REPORT_PATH` 指定。
+4
View File
@@ -54,6 +54,10 @@
- 续写只在保留推理末尾依次追加 `[INTERNAL_REASONING_TRUNCATED]` 与中文收尾句,不插入中间 checkpoint;首请求不携带 `tools`,续写请求才注入工具定义和 `tool_choice=none`
- `ChatMessage` 支持 `reasoning_content`/`tool_calls`/`tool_call_id``Result` 保留完整推理、中断/续写状态与预算/错误信息。
- UI 触发推理保护后会显示首次请求与续写请求的完整消息日志;日志默认折叠,用户可展开检查 `messages`/`tools`/`tool_choice`
- UI 改为按阶段分组:总览/各段/总结各自独立容器;修订时保留旧版本并折叠,最新版本默认展开;机器可读声明 JSON 与验证结果作为独立折叠块,不再混在正文中。
- 推理保护诊断改为在触发时通过流式事件输出,位于续写思考块之前;总览重复日志已移除。
- 成功提取机器可读声明后,UI 正文会移除 `[机器可读声明]` JSON,只在折叠块中显示一次;推理保护诊断改为“消息数变化 + 新增 assistant/tool 消息摘要”,完整续写 JSON 仍作为折叠附件。
- 总览、分段修订、分段回查和总结回查都会显示实际发送的 user prompt(默认折叠)。
- `AiV2.Tests` 新增 `ReasoningGuard` 测试套件;默认测试总计 173 项通过。
## 1. 背景与目标
+2 -2
View File
@@ -102,11 +102,11 @@ namespace AnotherReplayReader.ReplayFile
[0x20A] = "出售建筑", // 522
// 523
[0x20C] = "从进驻的建筑撤出(?)", // 524
[0x20C] = "从进驻的建筑或载具撤出(?)", // 524
[0x20D] = "集火攻击", // 525
[0x20E] = "强制攻击单位(Ctrl", // 526
[0x20F] = "强制攻击地板(Ctrl", // 527
[0x210] = "进驻建筑", // 528
[0x210] = "进驻建筑或载具", // 528
// 529
[0x212] = "命令矿车交矿", // 530
// 531
+16
View File
@@ -1211,6 +1211,7 @@ PlayerA: 开始出兵
{
Reasoning,
ReasoningGuard,
ReasoningGuardRequest,
Content,
Error,
Json
@@ -1330,6 +1331,21 @@ PlayerA: 开始出兵
continuationBudgetWarning = continuationCheck.Message;
}
onChunk?.Invoke(new AIChunk
{
Type = AIChunkType.ReasoningGuardRequest,
Text = JsonSerializer.Serialize(
new
{
originalRequestJson,
continuationRequestJson
},
new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
});
Result secondResult = default;
var continuationError = string.Empty;
try