Skip to content

Merge trae branch into main - #4

Merged
mcocdaa merged 3 commits into
mainfrom
trae
Apr 19, 2026
Merged

mcocdaa merged 3 commits into
mainfrom
trae

Conversation

@mcocdaa

@mcocdaa mcocdaa commented Apr 19, 2026 •

Copy link
Copy Markdown
Collaborator

变更摘要

将 trae 分支的更改合并到 main,并修复 CodeRabbit 审查提出的全部问题。

变更类型

  • 新功能
  • Bug兼容性变更 (Breaking Change)
  • 文档更新 (Documentation)
  • CI/CD 变更 (CI/CD)
  • Bug 修复 (bugfix) — 修复线程安全、API 正确性等问题
  • 代码重构 (refactor) — 不改变外部行为的代码整理
  • 文档更新 (docs) — INTRODUCTION.txt、本地化文件

详细说明

线程安全修复(Major)

  • AIDebugLog.cs: 将 Find.TickManager.TicksGame 从 Record()(后台线程)移至 GameComponentTick()(主线程),避免后台线程访问 RimWorld API
  • AIRequestQueue.cs: 将 FireRequest 中的所有状态变更(_activeRequests、_requestIdToActive、_modQueues、_isProcessingLocalRequest)从后台 Task.Run 移至主线程,通过新增的 PendingFireResult 队列桥接
  • AIRequestQueue.cs: 新增 IsLocalEndpointSnapshot 字段到 TrackedRequest,在入队时快照 IsLocalEndpoint 值,避免后台线程读取可变的 settings

API 正确性修复(Minor)

  • OpenAIClient.cs: 新增 AIHttpException 内部类,在 HTTP 4xx/5xx 时保留状态码,使 SendAsync 的 catch 分支能正确填充 response.HttpStatusCode
  • OpenAIClient.cs: IsLocalEndpoint 从 string.Contains 改为 Uri.IsLoopback + 显式检查 localhost/host.docker.internal,修复 IPv6 [::1] 和大小写误判
  • AICoreAPI.cs: GetRegisteredCategories() 现在通过 exposedProviders 过滤,与 GetProviderData 系列方法行为一致
  • AICoreAPI.cs: 所有 SkipCheck 迭代和回调调用改为 .ToList() 快照,防止回调中注册/注销导致集合修改异常
  • AICoreAPI.cs: 新增 UnregisterIncidentExecutedCallback 和 UnregisterStorytellerIncidentSkipCheck 对称卸载方法

UI/本地化修复

  • Window_AIDebugLog.cs: 遥测标签(tok/retry/wait/attempt/queueWait)从硬编码英文改为翻译键
  • AICoreSettingsUI.cs: 队列标签页高度计算合并 GetAllQueueDepths、GetAllCooldowns、ModCooldownGetters 三源,修复仅冷却 mod 行缺失导致滚动异常
  • RimMind_Core.xml: IncludeLocation.Desc 从"位置坐标"改为"室内/室外/房间类型"(中英文同步修正)
  • INTRODUCTION.txt: API Key 安全说明从"不会上传到任何服务器"改为"会发送给你配置的 API 端点,不会上传到 RimMind 自有服务器"

其他

  • ContextComposer.cs: BuildFromSections 添加 PromptSanitizer.Sanitize 调用,与 StructuredPromptBuilder.Build 行为统一
  • AIRequestState.cs: 枚举值显式赋值(Queued=0, Processing=1, ...),保证序列化稳定性

测试说明

  • 编译通过(dotnet build 成功)
  • 在游戏中测试通过
  • 代码审查通过
  • 无兼容性破坏

版本号影响判断

  • 是否修改了存档兼容性相关的 XML Defs:否
  • 是否删除或修改了公共 API / Harmony Patch target:是(新增 UnregisterIncidentExecutedCallback、UnregisterStorytellerIncidentSkipCheck;TrackedRequest 新增 IsLocalEndpointSnapshot 字段)
  • 是否新增了玩家可配置的设置项:否
  • 是否只是纯文本/文档更新:否

其他信息

本次修改回应了 CodeRabbit PR#4 审查的全部 9 条可操作建议和 3 条 Nitpick 建议。

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加请求队列管理功能,支持暂停/恢复队列、取消请求
    • 支持请求优先级设置(高/中/低)和自动重试机制
    • 新增队列标签页,显示实时队列状态和请求详情
    • 增强上下文过滤,支持显示殖民者携带物品库存
    • 增加请求超时、最大并发数、最大重试次数等配置选项
  • 改进

    • 优化调试日志显示,新增优先级、重试次数、等待时间等信息
    • 完善物品显示,添加品质标记和损坏状态识别
    • 扩展请求选项提示功能
  • 文档

    • 新增模组综合说明文档
    • 完善多语言UI本地化文本

@github-actions

github-actions Bot commented Apr 19, 2026 •

Copy link
Copy Markdown

⚠️ PR 描述不完整

请补充以下内容:

  • 变更摘要
  • 变更类型
  • 详细说明
  • 未选择变更类型(请在方括号中打 x)

请参考 PR 模板 填写。

@coderabbitai

coderabbitai Bot commented Apr 19, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'version'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a548bb2e-a55f-4e47-97b4-281f3698bf8f

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff24bc and 8210a69.

⛔ Files ignored due to path filters (4)
  • AGENTS.md is excluded by !**/*.md
  • CLAUDE.md is excluded by !**/*.md
  • INTRODUCTION.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
📒 Files selected for processing (23)
  • About/About.xml
  • INTRODUCTION.txt
  • Languages/ChineseSimplified/Keyed/RimMind_Core.xml
  • Languages/English/Keyed/RimMind_Core.xml
  • Source/AICoreAPI.cs
  • Source/Client/AIRequest.cs
  • Source/Client/AIRequestState.cs
  • Source/Client/AIResponse.cs
  • Source/Client/IAIClient.cs
  • Source/Client/OpenAI/OpenAIClient.cs
  • Source/Core/AIDebugLog.cs
  • Source/Core/AIRequestQueue.cs
  • Source/Core/GameContextBuilder.cs
  • Source/Core/Prompt/ContextComposer.cs
  • Source/Core/Prompt/PromptSanitizer.cs
  • Source/Core/Prompt/StructuredPromptBuilder.cs
  • Source/Debug/AICoreDebugActions.cs
  • Source/Settings/AICoreSettings.cs
  • Source/Settings/ContextSettings.cs
  • Source/UI/AICoreSettingsUI.cs
  • Source/UI/RequestEntry.cs
  • Source/UI/RequestOverlay.cs
  • Source/UI/Window_AIDebugLog.cs

Walkthrough

添加并实现了异步 AI 请求队列与控制 API,扩展客户端/响应遥测与优先级/重试机制,增加上下文构建与提示清理、库存支持、本地化字符串、设置与 UI(含队列页)以及调试/日志增强与若干公共注册回调/跳过检查接口。

Changes

Cohort / File(s) Summary
模组元数据与文档
About/About.xml, INTRODUCTION.txt
添加 modVersion,新增 RimMind - Core 文档,描述核心架构、功能、API 与配置说明。
本地化字符串
Languages/English/Keyed/RimMind_Core.xml, Languages/ChineseSimplified/Keyed/RimMind_Core.xml
新增大量英文/简体中文本地化键(队列与请求控制、队列状态/操作标签、上下文过滤描述、提示片段等)。
公共 API 扩展
Source/AICoreAPI.cs
将静态 provider 签名改为可空返回,新增队列控制/查询、提供者查询、跳过检查注册/评估、事件回调与故事讲述器跳过检查等多项公共方法与属性。
请求/响应/客户端 类型
Source/Client/AIRequest.cs, Source/Client/AIRequestState.cs, Source/Client/AIResponse.cs, Source/Client/IAIClient.cs
新增 Priority/MaxRetryCount/State 等字段与枚举;AIResponse 增加多项遥测字段与 Cancelled 工厂;IAIClient 增加 IsLocalEndpoint 属性。
OpenAI 客户端实现
Source/Client/OpenAI/OpenAIClient.cs
新增 IsLocalEndpoint 检测、引入 AIHttpException、PostAsync 返回 (text,statusCode)、并在响应中填充遥测(处理时长、HTTP 状态、请求字节数、优先级)并记录调试日志。
队列实现重写
Source/Core/AIRequestQueue.cs
用 TrackedRequest 列表替代旧 FIFO,按优先级选择就绪项、处理冷却/过期、超时与重试、局部模型互斥、暂停/恢复与取消,新增多项公开控制/查询 API 与活动/队列检查。
调试与遥测 UI/工具
Source/Core/AIDebugLog.cs, Source/Debug/AICoreDebugActions.cs, Source/UI/Window_AIDebugLog.cs
AIDebugEntry 增加遥测字段(Priority/State/AttemptCount/QueueWaitMs/ProcessingMs/HttpStatusCode/RequestPayloadBytes);添加队列调试动作(ShowQueueState/PauseQueue/ResumeQueue);日志窗口展示额外遥测与优先级/HTTP 状态等。
上下文构建与提示处理
Source/Core/GameContextBuilder.cs, Source/Core/Prompt/ContextComposer.cs, Source/Core/Prompt/PromptSanitizer.cs, Source/Core/Prompt/StructuredPromptBuilder.cs
新增 PromptSanitizer 并在构建后调用;添加物品质量/损坏标记和按数量汇总的库存输出;将硬编码省略文本替换为可翻译字符串。
设置与上下文过滤
Source/Settings/AICoreSettings.cs, Source/Settings/ContextSettings.cs
新增 maxConcurrentRequests、maxRetryCount、requestTimeoutMs 并持久化;添加 IncludeInventory 与 exposedProviders 并在预设中调整 IncludeInventory。
设置 UI 与请求 UI
Source/UI/AICoreSettingsUI.cs, Source/UI/RequestEntry.cs, Source/UI/RequestOverlay.cs
实现多行换行选项卡栏并新增“队列”选项卡与队列管理界面;在 API 页添加并发/重试/超时控件;上下文复选框增加描述;RequestEntry 支持 optionTooltips,RequestOverlay 显示选项提示。

Sequence Diagram(s)

sequenceDiagram
    participant Mod as rgba(46,134,193,0.5)
    participant API as rgba(39,174,96,0.5)
    participant Queue as rgba(241,196,15,0.5)
    participant Client as rgba(155,89,182,0.5)
    participant LLM as rgba(231,76,60,0.5)

    Mod->>API: Enqueue(AIRequest)
    API->>Queue: Enqueue(request, callback)
    Note over Queue: 按模块头部选取就绪请求并按优先级排序
    alt 无法处理(已暂停/并发满/局部模型占用)
        Queue-->>Mod: 保留在队列中
    else 可处理
        Queue->>Client: FireRequest(TrackedRequest)
        Client->>LLM: SendAsync(AIRequest)
        LLM-->>Client: AIResponse (+status, tokens)
        Client-->>Queue: 返回 AIResponse(含 ProcessingMs/HttpStatus/Bytes/Priority/Attempt)
        alt 成功
            Queue->>Mod: 回调(Completed response)
        else 可重试且未超限
            Queue->>Queue: 增加 Attempt 重入队列
        else 失败/取消
            Queue->>Mod: 回调(Error/Cancelled)
        end
    end
    Note over API,Queue: API 可在任意时刻:Pause/Resume/Cancel/Query 状态
Loading

Estimated code review effort

🎯 4 (复杂) | ⏱️ ~60 分钟

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch trae

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Source/Core/AIDebugLog.cs (1)

25-31: ⚠️ Potential issue | 🟠 Major

把 Find.TickManager 访问移回主线程。

Record() 可能从 AI 请求完成的后台路径调用;这里在入队前读取 Find.TickManager.TicksGame 仍然是后台访问 RimWorld API 的风险。建议只在 GameComponentTick() drain 队列时填充 GameTick。

As per coding guidelines,**/*.cs: Never call RimWorld or Unity APIs from background threads; all game state operations must happen on the main thread.

建议修复
         public override void GameComponentTick()
         {
             while (_pendingEntries.TryDequeue(out var entry))
             {
+                entry.GameTick = Find.TickManager.TicksGame;
                 if (_entries.Count >= MaxEntries)
                     _entries.RemoveAt(0);
                 _entries.Add(entry);
             }
@@
             _instance?._pendingEntries.Enqueue(new AIDebugEntry
             {
-                GameTick          = Find.TickManager.TicksGame,
                 Source            = request.RequestId,
                 ModelName         = RimMindCoreMod.Settings.modelName,

Also applies to: 37-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AIDebugLog.cs` around lines 25 - 31, Record() currently reads
Find.TickManager.TicksGame on a background thread before enqueuing, which
violates the rule against calling RimWorld/Unity APIs off the main thread;
change Record() to only capture non-game-thread-safe data and enqueue a
placeholder entry (without accessing Find.TickManager), and then in
GameComponentTick() when draining _pendingEntries (the existing loop over
_pendingEntries.TryDequeue), set entry.GameTick = Find.TickManager.TicksGame on
the main thread before adding to _entries (ensure MaxEntries handling and
_entries.Add(entry) remain in GameComponentTick); update any code paths that
relied on Record() populating GameTick to expect GameTick to be filled during
GameComponentTick().
Source/Client/OpenAI/OpenAIClient.cs (1)

47-72: ⚠️ Potential issue | 🟡 Minor

失败响应会丢失 HTTP 状态码。

PostAsync() 在 4xx/5xx 时抛出普通 Exception,SendAsync 的失败分支无法拿到 responseCode,所以新增的 HttpStatusCode 遥测只在成功路径有效。

建议修复
+        private sealed class AIHttpException : Exception
+        {
+            public long StatusCode { get; }
+
+            public AIHttpException(long statusCode, string message) : base(message)
+            {
+                StatusCode = statusCode;
+            }
+        }
+
         public async Task<AIResponse> SendAsync(AIRequest request)
         {
             string endpoint = FormatEndpoint(_settings.apiEndpoint);
             string json = BuildRequestJson(request);
@@
-            catch (Exception ex)
+            catch (AIHttpException ex)
+            {
+                sw.Stop();
+                AIRequestQueue.LogFromBackground($"[RimMind] Request failed ({request.RequestId}): {ex.Message}", isWarning: true);
+                var response = AIResponse.Failure(request.RequestId, ex.Message);
+                response.ProcessingMs = sw.ElapsedMilliseconds;
+                response.HttpStatusCode = ex.StatusCode;
+                response.RequestPayloadBytes = Encoding.UTF8.GetByteCount(json);
+                response.Priority = request.Priority;
+                AIDebugLog.Record(request, response, (int)sw.ElapsedMilliseconds);
+                return response;
+            }
+            catch (Exception ex)
             {
                 sw.Stop();
                 AIRequestQueue.LogFromBackground($"[RimMind] Request failed ({request.RequestId}): {ex.Message}", isWarning: true);
                 var response = AIResponse.Failure(request.RequestId, ex.Message);
@@
-                throw new Exception($"HTTP {webRequest.responseCode}: {detail}");
+                throw new AIHttpException(webRequest.responseCode, $"HTTP {webRequest.responseCode}: {detail}");
             }

Also applies to: 152-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/OpenAI/OpenAIClient.cs` around lines 47 - 72, PostAsync
currently throws a generic Exception on 4xx/5xx so the catch in SendAsync can't
populate HttpStatusCode; update PostAsync to surface the HTTP status (either by
throwing a custom exception or HttpRequestException that includes StatusCode, or
by returning a result object that always contains the status code), then in
SendAsync's catch (the try/catch around PostAsync usage) catch that specific
exception, extract the HTTP status code and set response.HttpStatusCode before
creating AIResponse.Failure and calling AIDebugLog.Record; apply the same
extraction logic to the other failing branch noted (lines ~152-158) so telemetry
always records the HTTP status code or a sensible default when unavailable.
🧹 Nitpick comments (3)
Source/Core/Prompt/StructuredPromptBuilder.cs (1)

98-98: 消毒路径不统一

Build() 现在会通过 PromptSanitizer.Sanitize 归一化花括号,但 ContextComposer.BuildFromSections()(Source/Core/Prompt/ContextComposer.cs:40-52)在直接从 PromptSection 组装时并未调用同一消毒逻辑。当下游通过 ToSection(...) 再拼入 BuildFromSections 时,内容已被消毒一次,而外部直接构造的 PromptSection 则未消毒,最终发给 LLM 的文本行为取决于构造路径。建议将消毒统一到最终"即将发送到 LLM"的边界(例如 BuildFromSections 或客户端发送前),避免双重消毒或漏消毒。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Prompt/StructuredPromptBuilder.cs` at line 98, Build() currently
calls PromptSanitizer.Sanitize but ContextComposer.BuildFromSections(...) does
not, causing inconsistent sanitization depending on whether sections were
produced via ToSection(...) or constructed externally; change the sanitization
boundary so BuildFromSections (or the single place immediately before sending to
the LLM) is responsible for sanitizing all assembled text: remove duplicate
sanitization from Build()/ToSection if present and add a single call to
PromptSanitizer.Sanitize inside ContextComposer.BuildFromSections (or the final
send path), ensuring PromptSanitizer.Sanitize(...) is applied exactly once to
the final concatenated prompt.
Source/Client/AIRequestState.cs (1)

3-17: 枚举定义 LGTM

AIRequestState 覆盖了 Queued/Processing/Completed/Error/Cancelled 五态,与 AIRequestQueue 中 Enqueue/Processing/Retry/Cancel 路径一一对应;AIRequestPriority 显式赋值 High=0 / Normal=1 / Low=2,与队列按 Request.Priority 升序插入(低数值先出)的逻辑一致。

仅一条可选建议:若后续希望在存档中持久化状态/优先级,可考虑显式固定每个枚举值(如 Queued = 0, Processing = 1 ...),避免将来增删成员时改变序号。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/AIRequestState.cs` around lines 3 - 17, The enums
AIRequestState and AIRequestPriority currently rely on implicit integer values;
to make them stable for future persistence or serialization, explicitly assign
integer values to each member (e.g., Queued = 0, Processing = 1, Completed = 2,
Error = 3, Cancelled = 4 and keep High = 0, Normal = 1, Low = 2) by updating the
AIRequestState and AIRequestPriority declarations so adding/removing members
won't shift underlying numeric values.
Source/Core/Prompt/PromptSanitizer.cs (1)

5-9: 花括号反转义的边界行为值得注意

当前实现使用顺序 Replace("{{","{") 和 Replace("}}","}"),对奇数个连续大括号的处理结果可能不符合直觉,例如 "{{{" 会变成 "{{"(第一次替换后剩余 "{{",第二次 Replace 针对 }} 不会再影响它),而 "{{{{" 会正确变成 "{{"。如果上游 Prompt 文本中可能包含用户内容里的连续花括号(例如 JSON 示例、代码片段),建议在文档/注释里明确该函数的语义(是"反转义模板占位符"还是"去重双花括号"),以避免后续模块误用。

另外,StructuredPromptBuilder.Build() 会调用 Sanitize,但 ContextComposer.BuildFromSections()(位于 Source/Core/Prompt/ContextComposer.cs:40-52)直接拼接 PromptSection.Content 并未经过该消毒器,若 Section 内容里有 {{ ... }} 转义意图,会出现两条路径行为不一致。建议要么在 BuildFromSections 中也统一调用 PromptSanitizer.Sanitize,要么只在最终提交给 LLM 的边界处消毒一次。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Prompt/PromptSanitizer.cs` around lines 5 - 9, Sanitize currently
does sequential Replace("{{","{") and Replace("}}","}") which yields surprising
results for odd runs of braces (e.g., "{{{" -> "{{"); clarify its intended
semantics (whether it is strictly "unescape template placeholders" vs "collapse
double braces") in the PromptSanitizer.Sanitize XML doc comment and/or public
API docs, and make behavior consistent across builders by either calling
PromptSanitizer.Sanitize on each PromptSection.Content inside
ContextComposer.BuildFromSections or else removing per-section sanitization and
ensuring StructuredPromptBuilder.Build (the single final submission point) is
the only place that calls Sanitize; pick one approach and apply it consistently
so StructuredPromptBuilder.Build(), ContextComposer.BuildFromSections(), and
PromptSection.Content handling produce identical results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@INTRODUCTION.txt`:
- Around line 67-68: 将“API Key 仅存储在本地 RimWorld
设置文件中,不会上传到任何服务器。”这句改为明确且不误导的表述:保留“本地存储”信息,同时说明 API Key 会作为认证信息发送到用户配置的 AI API
服务商(即只会发往用户指定的 endpoint),且不会发送到 RimMind 自有服务器或其他未经授权的第三方;替换原问答中“Q: API Key
安全吗?”下的回答文本(定位标识为问句文本 "Q: API Key 安全吗?" 及其当前回答行)为上述更精确的说明。

In `@Languages/ChineseSimplified/Keyed/RimMind_Core.xml`:
- Around line 132-133: RimMind.Core.Context.IncludeLocation
的描述误导用户说会注入“位置坐标”,但实际只注入房间/室内外和温度;更新 RimMind.Core.Context.IncludeLocation.Desc
文本以去掉“位置坐标”并明确说明注入的是房间/室内外状态和温度(例如改为“将小人的房间/室内或室外位置状态及所在区域温度注入 AI 上下文”),确保符号
RimMind.Core.Context.IncludeLocation 和 RimMind.Core.Context.IncludeLocation.Desc
保持一致且语义准确。

In `@Source/AICoreAPI.cs`:
- Around line 339-346: GetRegisteredCategories currently returns all keys from
_staticProviders, _pawnProviders, and _dynamicProviders without applying the
exposedProviders filter, which leaks unexposed category names; update
GetRegisteredCategories to filter the combined list through exposedProviders
(the same predicate used by the Get*ProviderData methods) before returning so
only exposed categories are listed, referencing GetRegisteredCategories,
exposedProviders, and the existing Get*ProviderData implementations to mirror
their filtering behavior.
- Around line 419-527: The registries currently iterate the live collections and
lack unregister APIs for incident/storyteller, which can cause "collection
modified" exceptions when callbacks register/unregister themselves and leave
stale delegates; fix by iterating over snapshots instead (e.g. enumerate
_dialogueSkipChecks.Values.ToList(), _floatMenuSkipChecks.Values.ToList(),
_actionSkipChecks.Values.ToList(), _incidentExecutedCallbacks.ToList(),
_storytellerIncidentSkipChecks.ToList() in
ShouldSkipDialogue/ShouldSkipFloatMenu/ShouldSkipAction/NotifyIncidentExecuted/ShouldSkipStorytellerIncident)
so modifications during callbacks are safe, and add symmetric Unregister methods
for the incident API (e.g. UnregisterIncidentExecutedCallback(Action callback))
and storyteller skip checks (e.g.
UnregisterStorytellerIncidentSkipCheck(Func<bool> check)) to remove stale
delegates.

In `@Source/Client/IAIClient.cs`:
- Line 11: The current IsLocalEndpoint implementation (referenced from
IAIClient.IsLocalEndpoint and OpenAIClient) incorrectly detects local endpoints
by simple substring checks and is vulnerable to false positives/negatives and
race conditions when RimMindCoreSettings.apiEndpoint changes during request
processing; fix it by parsing the endpoint with System.Uri, extract host, try
IPAddress.TryParse and use IPAddress.IsLoopback for loopback addresses (handle
IPv4 and IPv6 like 127.0.0.1 and ::1), explicitly treat "localhost" and known
hostnames such as "host.docker.internal" as local, and avoid substring matches
on the whole URL path to prevent false positives; additionally ensure stability
by snapshotting the computed bool into the TrackedRequest (e.g.,
TrackedRequest.IsLocalSnapshot) at enqueue time or by making
OpenAIClient.IsLocalEndpoint return a value derived from an immutable cached Uri
so concurrent changes to RimMindCoreSettings.apiEndpoint do not change the
result mid-request.

In `@Source/Client/OpenAI/OpenAIClient.cs`:
- Around line 27-34: The IsLocalEndpoint property incorrectly uses
string.Contains on _settings.apiEndpoint, which misdetects loopback hosts and
misses variants like uppercase LOCALHOST and IPv6 [::1]; change the check to
parse the endpoint as a Uri and use Uri.IsLoopback (eg. add a helper like
IsLoopbackEndpoint(string endpoint) that calls Uri.TryCreate(..., out uri) and
returns uri.IsLoopback) and update IsLocalEndpoint (and the similar check at the
other occurrence around the class) to call that helper so detection is based on
the URI host rather than substring matching.

In `@Source/Core/AIRequestQueue.cs`:
- Around line 261-329: FireRequest currently mutates game state from a
background Task.Run (removing from _activeRequests/_requestIdToActive, modifying
_modQueues, setting _isProcessingLocalRequest and enqueuing results), causing
races with GameComponentTick(), UI and cancel/timeout paths; instead, have the
background task only build an immutable outcome object (e.g. response +
tracked.TrackingId + intent: Retry/Complete) and enqueue that into a thread-safe
pending-actions queue (or reuse _results with an explicit action kind), then
perform all state changes (removing from _activeRequests/_requestIdToActive,
re-queuing into _modQueues, setting _isProcessingLocalRequest, invoking
callbacks) on the main thread inside GameComponentTick() or a new
ProcessPendingActions() called from it; ensure FireRequest no longer touches
_activeRequests/_requestIdToActive/_modQueues/_isProcessingLocalRequest and that
GameComponentTick() checks for timeouts/cancellations before applying a queued
retry/completion.

In `@Source/UI/AICoreSettingsUI.cs`:
- Around line 371-430: The height calculation under contentH currently uses
modCount = queue.GetAllQueueDepths().Count which misses cooldown-only mods and
ModCooldownGetters entries; compute the full set first (using
queue.GetAllQueueDepths(), queue.GetAllCooldowns(), and
RimMindAPI.ModCooldownGetters) into allModIds and use allModIds.Count when
computing contentH (replace references to GetAllQueueDepths().Count with the
unioned allModIds.Count) so the viewRect height matches the actual per-mod rows
and scrolling works correctly.

In `@Source/UI/Window_AIDebugLog.cs`:
- Around line 116-126: The telemetry UILabels in Window_AIDebugLog.cs build
hardcoded English strings (the telemetry variable and the retry/wait/token
labels) which must be replaced with translation keys; update the construction of
telemetry (used in the Widgets.Label call that renders telemetry) to use keyed
translations such as "RimMind.Core.UI.DebugLog.retry".Translate(),
"RimMind.Core.UI.DebugLog.wait".Translate(),
"RimMind.Core.UI.DebugLog.tok".Translate() (and any other needed keys like
attempt/queueWait) concatenated with the runtime values (entry.AttemptCount,
entry.QueueWaitMs, entry.TokensUsed, entry.ModelName) so all visible UI text
uses the RimMind.Core.UI.DebugLog.* keys and call .Translate() before rendering.

---

Outside diff comments:
In `@Source/Client/OpenAI/OpenAIClient.cs`:
- Around line 47-72: PostAsync currently throws a generic Exception on 4xx/5xx
so the catch in SendAsync can't populate HttpStatusCode; update PostAsync to
surface the HTTP status (either by throwing a custom exception or
HttpRequestException that includes StatusCode, or by returning a result object
that always contains the status code), then in SendAsync's catch (the try/catch
around PostAsync usage) catch that specific exception, extract the HTTP status
code and set response.HttpStatusCode before creating AIResponse.Failure and
calling AIDebugLog.Record; apply the same extraction logic to the other failing
branch noted (lines ~152-158) so telemetry always records the HTTP status code
or a sensible default when unavailable.

In `@Source/Core/AIDebugLog.cs`:
- Around line 25-31: Record() currently reads Find.TickManager.TicksGame on a
background thread before enqueuing, which violates the rule against calling
RimWorld/Unity APIs off the main thread; change Record() to only capture
non-game-thread-safe data and enqueue a placeholder entry (without accessing
Find.TickManager), and then in GameComponentTick() when draining _pendingEntries
(the existing loop over _pendingEntries.TryDequeue), set entry.GameTick =
Find.TickManager.TicksGame on the main thread before adding to _entries (ensure
MaxEntries handling and _entries.Add(entry) remain in GameComponentTick); update
any code paths that relied on Record() populating GameTick to expect GameTick to
be filled during GameComponentTick().

---

Nitpick comments:
In `@Source/Client/AIRequestState.cs`:
- Around line 3-17: The enums AIRequestState and AIRequestPriority currently
rely on implicit integer values; to make them stable for future persistence or
serialization, explicitly assign integer values to each member (e.g., Queued =
0, Processing = 1, Completed = 2, Error = 3, Cancelled = 4 and keep High = 0,
Normal = 1, Low = 2) by updating the AIRequestState and AIRequestPriority
declarations so adding/removing members won't shift underlying numeric values.

In `@Source/Core/Prompt/PromptSanitizer.cs`:
- Around line 5-9: Sanitize currently does sequential Replace("{{","{") and
Replace("}}","}") which yields surprising results for odd runs of braces (e.g.,
"{{{" -> "{{"); clarify its intended semantics (whether it is strictly "unescape
template placeholders" vs "collapse double braces") in the
PromptSanitizer.Sanitize XML doc comment and/or public API docs, and make
behavior consistent across builders by either calling PromptSanitizer.Sanitize
on each PromptSection.Content inside ContextComposer.BuildFromSections or else
removing per-section sanitization and ensuring StructuredPromptBuilder.Build
(the single final submission point) is the only place that calls Sanitize; pick
one approach and apply it consistently so StructuredPromptBuilder.Build(),
ContextComposer.BuildFromSections(), and PromptSection.Content handling produce
identical results.

In `@Source/Core/Prompt/StructuredPromptBuilder.cs`:
- Line 98: Build() currently calls PromptSanitizer.Sanitize but
ContextComposer.BuildFromSections(...) does not, causing inconsistent
sanitization depending on whether sections were produced via ToSection(...) or
constructed externally; change the sanitization boundary so BuildFromSections
(or the single place immediately before sending to the LLM) is responsible for
sanitizing all assembled text: remove duplicate sanitization from
Build()/ToSection if present and add a single call to PromptSanitizer.Sanitize
inside ContextComposer.BuildFromSections (or the final send path), ensuring
PromptSanitizer.Sanitize(...) is applied exactly once to the final concatenated
prompt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 91eacd5f-1248-4d86-bf74-a8ab03d72605

📥 Commits

Reviewing files that changed from the base of the PR and between 83c9a94 and 1ff24bc.

⛔ Files ignored due to path filters (4)
  • AGENTS.md is excluded by !**/*.md
  • CLAUDE.md is excluded by !**/*.md
  • INTRODUCTION.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
📒 Files selected for processing (23)
  • About/About.xml
  • INTRODUCTION.txt
  • Languages/ChineseSimplified/Keyed/RimMind_Core.xml
  • Languages/English/Keyed/RimMind_Core.xml
  • Source/AICoreAPI.cs
  • Source/Client/AIRequest.cs
  • Source/Client/AIRequestState.cs
  • Source/Client/AIResponse.cs
  • Source/Client/IAIClient.cs
  • Source/Client/OpenAI/OpenAIClient.cs
  • Source/Core/AIDebugLog.cs
  • Source/Core/AIRequestQueue.cs
  • Source/Core/GameContextBuilder.cs
  • Source/Core/Prompt/ContextComposer.cs
  • Source/Core/Prompt/PromptSanitizer.cs
  • Source/Core/Prompt/StructuredPromptBuilder.cs
  • Source/Debug/AICoreDebugActions.cs
  • Source/Settings/AICoreSettings.cs
  • Source/Settings/ContextSettings.cs
  • Source/UI/AICoreSettingsUI.cs
  • Source/UI/RequestEntry.cs
  • Source/UI/RequestOverlay.cs
  • Source/UI/Window_AIDebugLog.cs

Comment thread INTRODUCTION.txt Outdated
Comment thread Languages/ChineseSimplified/Keyed/RimMind_Core.xml Outdated
Comment thread Source/AICoreAPI.cs
Comment thread Source/AICoreAPI.cs
Comment thread Source/Client/IAIClient.cs
Comment thread Source/Client/OpenAI/OpenAIClient.cs Outdated
Comment thread Source/Core/AIRequestQueue.cs Outdated
Comment thread Source/UI/AICoreSettingsUI.cs Outdated
Comment thread Source/UI/Window_AIDebugLog.cs
@github-actions

Copy link
Copy Markdown

🔄 需要修改 - @coderabbitai[bot]

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Source/Core/AIDebugLog.cs (1)

25-31: ⚠️ Potential issue | 🟠 Major

把 Find.TickManager 访问移回主线程。

Record() 可能从 AI 请求完成的后台路径调用;这里在入队前读取 Find.TickManager.TicksGame 仍然是后台访问 RimWorld API 的风险。建议只在 GameComponentTick() drain 队列时填充 GameTick。

As per coding guidelines,**/*.cs: Never call RimWorld or Unity APIs from background threads; all game state operations must happen on the main thread.

建议修复
         public override void GameComponentTick()
         {
             while (_pendingEntries.TryDequeue(out var entry))
             {
+                entry.GameTick = Find.TickManager.TicksGame;
                 if (_entries.Count >= MaxEntries)
                     _entries.RemoveAt(0);
                 _entries.Add(entry);
             }
@@
             _instance?._pendingEntries.Enqueue(new AIDebugEntry
             {
-                GameTick          = Find.TickManager.TicksGame,
                 Source            = request.RequestId,
                 ModelName         = RimMindCoreMod.Settings.modelName,

Also applies to: 37-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AIDebugLog.cs` around lines 25 - 31, Record() currently reads
Find.TickManager.TicksGame on a background thread before enqueuing, which
violates the rule against calling RimWorld/Unity APIs off the main thread;
change Record() to only capture non-game-thread-safe data and enqueue a
placeholder entry (without accessing Find.TickManager), and then in
GameComponentTick() when draining _pendingEntries (the existing loop over
_pendingEntries.TryDequeue), set entry.GameTick = Find.TickManager.TicksGame on
the main thread before adding to _entries (ensure MaxEntries handling and
_entries.Add(entry) remain in GameComponentTick); update any code paths that
relied on Record() populating GameTick to expect GameTick to be filled during
GameComponentTick().
Source/Client/OpenAI/OpenAIClient.cs (1)

47-72: ⚠️ Potential issue | 🟡 Minor

失败响应会丢失 HTTP 状态码。

PostAsync() 在 4xx/5xx 时抛出普通 Exception,SendAsync 的失败分支无法拿到 responseCode,所以新增的 HttpStatusCode 遥测只在成功路径有效。

建议修复
+        private sealed class AIHttpException : Exception
+        {
+            public long StatusCode { get; }
+
+            public AIHttpException(long statusCode, string message) : base(message)
+            {
+                StatusCode = statusCode;
+            }
+        }
+
         public async Task<AIResponse> SendAsync(AIRequest request)
         {
             string endpoint = FormatEndpoint(_settings.apiEndpoint);
             string json = BuildRequestJson(request);
@@
-            catch (Exception ex)
+            catch (AIHttpException ex)
+            {
+                sw.Stop();
+                AIRequestQueue.LogFromBackground($"[RimMind] Request failed ({request.RequestId}): {ex.Message}", isWarning: true);
+                var response = AIResponse.Failure(request.RequestId, ex.Message);
+                response.ProcessingMs = sw.ElapsedMilliseconds;
+                response.HttpStatusCode = ex.StatusCode;
+                response.RequestPayloadBytes = Encoding.UTF8.GetByteCount(json);
+                response.Priority = request.Priority;
+                AIDebugLog.Record(request, response, (int)sw.ElapsedMilliseconds);
+                return response;
+            }
+            catch (Exception ex)
             {
                 sw.Stop();
                 AIRequestQueue.LogFromBackground($"[RimMind] Request failed ({request.RequestId}): {ex.Message}", isWarning: true);
                 var response = AIResponse.Failure(request.RequestId, ex.Message);
@@
-                throw new Exception($"HTTP {webRequest.responseCode}: {detail}");
+                throw new AIHttpException(webRequest.responseCode, $"HTTP {webRequest.responseCode}: {detail}");
             }

Also applies to: 152-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/OpenAI/OpenAIClient.cs` around lines 47 - 72, PostAsync
currently throws a generic Exception on 4xx/5xx so the catch in SendAsync can't
populate HttpStatusCode; update PostAsync to surface the HTTP status (either by
throwing a custom exception or HttpRequestException that includes StatusCode, or
by returning a result object that always contains the status code), then in
SendAsync's catch (the try/catch around PostAsync usage) catch that specific
exception, extract the HTTP status code and set response.HttpStatusCode before
creating AIResponse.Failure and calling AIDebugLog.Record; apply the same
extraction logic to the other failing branch noted (lines ~152-158) so telemetry
always records the HTTP status code or a sensible default when unavailable.
🧹 Nitpick comments (3)
Source/Core/Prompt/StructuredPromptBuilder.cs (1)

98-98: 消毒路径不统一

Build() 现在会通过 PromptSanitizer.Sanitize 归一化花括号,但 ContextComposer.BuildFromSections()(Source/Core/Prompt/ContextComposer.cs:40-52)在直接从 PromptSection 组装时并未调用同一消毒逻辑。当下游通过 ToSection(...) 再拼入 BuildFromSections 时,内容已被消毒一次,而外部直接构造的 PromptSection 则未消毒,最终发给 LLM 的文本行为取决于构造路径。建议将消毒统一到最终"即将发送到 LLM"的边界(例如 BuildFromSections 或客户端发送前),避免双重消毒或漏消毒。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Prompt/StructuredPromptBuilder.cs` at line 98, Build() currently
calls PromptSanitizer.Sanitize but ContextComposer.BuildFromSections(...) does
not, causing inconsistent sanitization depending on whether sections were
produced via ToSection(...) or constructed externally; change the sanitization
boundary so BuildFromSections (or the single place immediately before sending to
the LLM) is responsible for sanitizing all assembled text: remove duplicate
sanitization from Build()/ToSection if present and add a single call to
PromptSanitizer.Sanitize inside ContextComposer.BuildFromSections (or the final
send path), ensuring PromptSanitizer.Sanitize(...) is applied exactly once to
the final concatenated prompt.
Source/Client/AIRequestState.cs (1)

3-17: 枚举定义 LGTM

AIRequestState 覆盖了 Queued/Processing/Completed/Error/Cancelled 五态,与 AIRequestQueue 中 Enqueue/Processing/Retry/Cancel 路径一一对应;AIRequestPriority 显式赋值 High=0 / Normal=1 / Low=2,与队列按 Request.Priority 升序插入(低数值先出)的逻辑一致。

仅一条可选建议:若后续希望在存档中持久化状态/优先级,可考虑显式固定每个枚举值(如 Queued = 0, Processing = 1 ...),避免将来增删成员时改变序号。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/AIRequestState.cs` around lines 3 - 17, The enums
AIRequestState and AIRequestPriority currently rely on implicit integer values;
to make them stable for future persistence or serialization, explicitly assign
integer values to each member (e.g., Queued = 0, Processing = 1, Completed = 2,
Error = 3, Cancelled = 4 and keep High = 0, Normal = 1, Low = 2) by updating the
AIRequestState and AIRequestPriority declarations so adding/removing members
won't shift underlying numeric values.
Source/Core/Prompt/PromptSanitizer.cs (1)

5-9: 花括号反转义的边界行为值得注意

当前实现使用顺序 Replace("{{","{") 和 Replace("}}","}"),对奇数个连续大括号的处理结果可能不符合直觉,例如 "{{{" 会变成 "{{"(第一次替换后剩余 "{{",第二次 Replace 针对 }} 不会再影响它),而 "{{{{" 会正确变成 "{{"。如果上游 Prompt 文本中可能包含用户内容里的连续花括号(例如 JSON 示例、代码片段),建议在文档/注释里明确该函数的语义(是"反转义模板占位符"还是"去重双花括号"),以避免后续模块误用。

另外,StructuredPromptBuilder.Build() 会调用 Sanitize,但 ContextComposer.BuildFromSections()(位于 Source/Core/Prompt/ContextComposer.cs:40-52)直接拼接 PromptSection.Content 并未经过该消毒器,若 Section 内容里有 {{ ... }} 转义意图,会出现两条路径行为不一致。建议要么在 BuildFromSections 中也统一调用 PromptSanitizer.Sanitize,要么只在最终提交给 LLM 的边界处消毒一次。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Prompt/PromptSanitizer.cs` around lines 5 - 9, Sanitize currently
does sequential Replace("{{","{") and Replace("}}","}") which yields surprising
results for odd runs of braces (e.g., "{{{" -> "{{"); clarify its intended
semantics (whether it is strictly "unescape template placeholders" vs "collapse
double braces") in the PromptSanitizer.Sanitize XML doc comment and/or public
API docs, and make behavior consistent across builders by either calling
PromptSanitizer.Sanitize on each PromptSection.Content inside
ContextComposer.BuildFromSections or else removing per-section sanitization and
ensuring StructuredPromptBuilder.Build (the single final submission point) is
the only place that calls Sanitize; pick one approach and apply it consistently
so StructuredPromptBuilder.Build(), ContextComposer.BuildFromSections(), and
PromptSection.Content handling produce identical results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@INTRODUCTION.txt`:
- Around line 67-68: 将“API Key 仅存储在本地 RimWorld
设置文件中,不会上传到任何服务器。”这句改为明确且不误导的表述:保留“本地存储”信息,同时说明 API Key 会作为认证信息发送到用户配置的 AI API
服务商(即只会发往用户指定的 endpoint),且不会发送到 RimMind 自有服务器或其他未经授权的第三方;替换原问答中“Q: API Key
安全吗?”下的回答文本(定位标识为问句文本 "Q: API Key 安全吗?" 及其当前回答行)为上述更精确的说明。

In `@Languages/ChineseSimplified/Keyed/RimMind_Core.xml`:
- Around line 132-133: RimMind.Core.Context.IncludeLocation
的描述误导用户说会注入“位置坐标”,但实际只注入房间/室内外和温度;更新 RimMind.Core.Context.IncludeLocation.Desc
文本以去掉“位置坐标”并明确说明注入的是房间/室内外状态和温度(例如改为“将小人的房间/室内或室外位置状态及所在区域温度注入 AI 上下文”),确保符号
RimMind.Core.Context.IncludeLocation 和 RimMind.Core.Context.IncludeLocation.Desc
保持一致且语义准确。

In `@Source/AICoreAPI.cs`:
- Around line 339-346: GetRegisteredCategories currently returns all keys from
_staticProviders, _pawnProviders, and _dynamicProviders without applying the
exposedProviders filter, which leaks unexposed category names; update
GetRegisteredCategories to filter the combined list through exposedProviders
(the same predicate used by the Get*ProviderData methods) before returning so
only exposed categories are listed, referencing GetRegisteredCategories,
exposedProviders, and the existing Get*ProviderData implementations to mirror
their filtering behavior.
- Around line 419-527: The registries currently iterate the live collections and
lack unregister APIs for incident/storyteller, which can cause "collection
modified" exceptions when callbacks register/unregister themselves and leave
stale delegates; fix by iterating over snapshots instead (e.g. enumerate
_dialogueSkipChecks.Values.ToList(), _floatMenuSkipChecks.Values.ToList(),
_actionSkipChecks.Values.ToList(), _incidentExecutedCallbacks.ToList(),
_storytellerIncidentSkipChecks.ToList() in
ShouldSkipDialogue/ShouldSkipFloatMenu/ShouldSkipAction/NotifyIncidentExecuted/ShouldSkipStorytellerIncident)
so modifications during callbacks are safe, and add symmetric Unregister methods
for the incident API (e.g. UnregisterIncidentExecutedCallback(Action callback))
and storyteller skip checks (e.g.
UnregisterStorytellerIncidentSkipCheck(Func<bool> check)) to remove stale
delegates.

In `@Source/Client/IAIClient.cs`:
- Line 11: The current IsLocalEndpoint implementation (referenced from
IAIClient.IsLocalEndpoint and OpenAIClient) incorrectly detects local endpoints
by simple substring checks and is vulnerable to false positives/negatives and
race conditions when RimMindCoreSettings.apiEndpoint changes during request
processing; fix it by parsing the endpoint with System.Uri, extract host, try
IPAddress.TryParse and use IPAddress.IsLoopback for loopback addresses (handle
IPv4 and IPv6 like 127.0.0.1 and ::1), explicitly treat "localhost" and known
hostnames such as "host.docker.internal" as local, and avoid substring matches
on the whole URL path to prevent false positives; additionally ensure stability
by snapshotting the computed bool into the TrackedRequest (e.g.,
TrackedRequest.IsLocalSnapshot) at enqueue time or by making
OpenAIClient.IsLocalEndpoint return a value derived from an immutable cached Uri
so concurrent changes to RimMindCoreSettings.apiEndpoint do not change the
result mid-request.

In `@Source/Client/OpenAI/OpenAIClient.cs`:
- Around line 27-34: The IsLocalEndpoint property incorrectly uses
string.Contains on _settings.apiEndpoint, which misdetects loopback hosts and
misses variants like uppercase LOCALHOST and IPv6 [::1]; change the check to
parse the endpoint as a Uri and use Uri.IsLoopback (eg. add a helper like
IsLoopbackEndpoint(string endpoint) that calls Uri.TryCreate(..., out uri) and
returns uri.IsLoopback) and update IsLocalEndpoint (and the similar check at the
other occurrence around the class) to call that helper so detection is based on
the URI host rather than substring matching.

In `@Source/Core/AIRequestQueue.cs`:
- Around line 261-329: FireRequest currently mutates game state from a
background Task.Run (removing from _activeRequests/_requestIdToActive, modifying
_modQueues, setting _isProcessingLocalRequest and enqueuing results), causing
races with GameComponentTick(), UI and cancel/timeout paths; instead, have the
background task only build an immutable outcome object (e.g. response +
tracked.TrackingId + intent: Retry/Complete) and enqueue that into a thread-safe
pending-actions queue (or reuse _results with an explicit action kind), then
perform all state changes (removing from _activeRequests/_requestIdToActive,
re-queuing into _modQueues, setting _isProcessingLocalRequest, invoking
callbacks) on the main thread inside GameComponentTick() or a new
ProcessPendingActions() called from it; ensure FireRequest no longer touches
_activeRequests/_requestIdToActive/_modQueues/_isProcessingLocalRequest and that
GameComponentTick() checks for timeouts/cancellations before applying a queued
retry/completion.

In `@Source/UI/AICoreSettingsUI.cs`:
- Around line 371-430: The height calculation under contentH currently uses
modCount = queue.GetAllQueueDepths().Count which misses cooldown-only mods and
ModCooldownGetters entries; compute the full set first (using
queue.GetAllQueueDepths(), queue.GetAllCooldowns(), and
RimMindAPI.ModCooldownGetters) into allModIds and use allModIds.Count when
computing contentH (replace references to GetAllQueueDepths().Count with the
unioned allModIds.Count) so the viewRect height matches the actual per-mod rows
and scrolling works correctly.

In `@Source/UI/Window_AIDebugLog.cs`:
- Around line 116-126: The telemetry UILabels in Window_AIDebugLog.cs build
hardcoded English strings (the telemetry variable and the retry/wait/token
labels) which must be replaced with translation keys; update the construction of
telemetry (used in the Widgets.Label call that renders telemetry) to use keyed
translations such as "RimMind.Core.UI.DebugLog.retry".Translate(),
"RimMind.Core.UI.DebugLog.wait".Translate(),
"RimMind.Core.UI.DebugLog.tok".Translate() (and any other needed keys like
attempt/queueWait) concatenated with the runtime values (entry.AttemptCount,
entry.QueueWaitMs, entry.TokensUsed, entry.ModelName) so all visible UI text
uses the RimMind.Core.UI.DebugLog.* keys and call .Translate() before rendering.

---

Outside diff comments:
In `@Source/Client/OpenAI/OpenAIClient.cs`:
- Around line 47-72: PostAsync currently throws a generic Exception on 4xx/5xx
so the catch in SendAsync can't populate HttpStatusCode; update PostAsync to
surface the HTTP status (either by throwing a custom exception or
HttpRequestException that includes StatusCode, or by returning a result object
that always contains the status code), then in SendAsync's catch (the try/catch
around PostAsync usage) catch that specific exception, extract the HTTP status
code and set response.HttpStatusCode before creating AIResponse.Failure and
calling AIDebugLog.Record; apply the same extraction logic to the other failing
branch noted (lines ~152-158) so telemetry always records the HTTP status code
or a sensible default when unavailable.

In `@Source/Core/AIDebugLog.cs`:
- Around line 25-31: Record() currently reads Find.TickManager.TicksGame on a
background thread before enqueuing, which violates the rule against calling
RimWorld/Unity APIs off the main thread; change Record() to only capture
non-game-thread-safe data and enqueue a placeholder entry (without accessing
Find.TickManager), and then in GameComponentTick() when draining _pendingEntries
(the existing loop over _pendingEntries.TryDequeue), set entry.GameTick =
Find.TickManager.TicksGame on the main thread before adding to _entries (ensure
MaxEntries handling and _entries.Add(entry) remain in GameComponentTick); update
any code paths that relied on Record() populating GameTick to expect GameTick to
be filled during GameComponentTick().

---

Nitpick comments:
In `@Source/Client/AIRequestState.cs`:
- Around line 3-17: The enums AIRequestState and AIRequestPriority currently
rely on implicit integer values; to make them stable for future persistence or
serialization, explicitly assign integer values to each member (e.g., Queued =
0, Processing = 1, Completed = 2, Error = 3, Cancelled = 4 and keep High = 0,
Normal = 1, Low = 2) by updating the AIRequestState and AIRequestPriority
declarations so adding/removing members won't shift underlying numeric values.

In `@Source/Core/Prompt/PromptSanitizer.cs`:
- Around line 5-9: Sanitize currently does sequential Replace("{{","{") and
Replace("}}","}") which yields surprising results for odd runs of braces (e.g.,
"{{{" -> "{{"); clarify its intended semantics (whether it is strictly "unescape
template placeholders" vs "collapse double braces") in the
PromptSanitizer.Sanitize XML doc comment and/or public API docs, and make
behavior consistent across builders by either calling PromptSanitizer.Sanitize
on each PromptSection.Content inside ContextComposer.BuildFromSections or else
removing per-section sanitization and ensuring StructuredPromptBuilder.Build
(the single final submission point) is the only place that calls Sanitize; pick
one approach and apply it consistently so StructuredPromptBuilder.Build(),
ContextComposer.BuildFromSections(), and PromptSection.Content handling produce
identical results.

In `@Source/Core/Prompt/StructuredPromptBuilder.cs`:
- Line 98: Build() currently calls PromptSanitizer.Sanitize but
ContextComposer.BuildFromSections(...) does not, causing inconsistent
sanitization depending on whether sections were produced via ToSection(...) or
constructed externally; change the sanitization boundary so BuildFromSections
(or the single place immediately before sending to the LLM) is responsible for
sanitizing all assembled text: remove duplicate sanitization from
Build()/ToSection if present and add a single call to PromptSanitizer.Sanitize
inside ContextComposer.BuildFromSections (or the final send path), ensuring
PromptSanitizer.Sanitize(...) is applied exactly once to the final concatenated
prompt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 91eacd5f-1248-4d86-bf74-a8ab03d72605

📥 Commits

Reviewing files that changed from the base of the PR and between 83c9a94 and 1ff24bc.

⛔ Files ignored due to path filters (4)
  • AGENTS.md is excluded by !**/*.md
  • CLAUDE.md is excluded by !**/*.md
  • INTRODUCTION.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
📒 Files selected for processing (23)
  • About/About.xml
  • INTRODUCTION.txt
  • Languages/ChineseSimplified/Keyed/RimMind_Core.xml
  • Languages/English/Keyed/RimMind_Core.xml
  • Source/AICoreAPI.cs
  • Source/Client/AIRequest.cs
  • Source/Client/AIRequestState.cs
  • Source/Client/AIResponse.cs
  • Source/Client/IAIClient.cs
  • Source/Client/OpenAI/OpenAIClient.cs
  • Source/Core/AIDebugLog.cs
  • Source/Core/AIRequestQueue.cs
  • Source/Core/GameContextBuilder.cs
  • Source/Core/Prompt/ContextComposer.cs
  • Source/Core/Prompt/PromptSanitizer.cs
  • Source/Core/Prompt/StructuredPromptBuilder.cs
  • Source/Debug/AICoreDebugActions.cs
  • Source/Settings/AICoreSettings.cs
  • Source/Settings/ContextSettings.cs
  • Source/UI/AICoreSettingsUI.cs
  • Source/UI/RequestEntry.cs
  • Source/UI/RequestOverlay.cs
  • Source/UI/Window_AIDebugLog.cs

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

⚠️ Note: I'm replying to the latest comment without access to the full earlier conversation.

@mcocdaa 以下是本次审查中需要重点关注的问题汇总,按严重程度排列:


🔴 需要优先处理(线程安全 / 数据竞争)

  1. AIDebugLog.cs — 后台线程访问 Find.TickManager(Major)
    Record() 在 AI 请求完成的后台路径中调用时读取了 Find.TickManager.TicksGame,违反了"不在后台线程访问 RimWorld API"的规则。修复方案:Record() 不读取 TicksGame,在 GameComponentTick() drain 队列时再赋值。

  2. AIRequestQueue.cs — FireRequest 在后台 Task 中修改游戏状态(Inline Major)
    FireRequest 直接从 Task.Run 中修改 _activeRequests、_requestIdToActive、_modQueues、_isProcessingLocalRequest,与主线程路径(tick、超时、取消)存在数据竞争。建议后台任务只生成不可变结果对象,主线程在 GameComponentTick() 中统一应用状态变更。


🟡 次要问题

  1. OpenAIClient.cs — 失败响应丢失 HTTP 状态码(Minor)
    PostAsync 在 4xx/5xx 时抛出普通 Exception,导致 SendAsync 失败分支无法填充 HttpStatusCode 遥测。建议引入自定义 AIHttpException 携带状态码。

  2. AICoreAPI.cs — GetRegisteredCategories 泄露未暴露的分类名(Inline)
    该方法未应用 exposedProviders 过滤,与 Get*ProviderData 方法行为不一致。

  3. IAIClient.cs / OpenAIClient.cs — IsLocalEndpoint 使用字符串匹配(Inline)
    应改用 Uri.IsLoopback 解析端点,避免大小写、IPv6 [::1] 等误判。


🔵 Nitpick(可选优化)

  1. AIRequestState.cs — AIRequestState 枚举缺少显式整数值,序列化时可能因成员顺序变化而产生兼容性问题。
  2. PromptSanitizer.cs — 奇数连续花括号边界行为不直观,建议补充文档说明语义。
  3. StructuredPromptBuilder.cs — 消毒路径不统一(Build() 消毒但 BuildFromSections() 不消毒),建议统一到最终发送边界。
  4. AICoreSettingsUI.cs — 高度计算未涵盖 cooldown-only mods,可能导致滚动区域截断。
  5. Window_AIDebugLog.cs — 遥测标签使用硬编码英文字符串,应改为翻译键。

优先级最高的是 第1、2条线程安全问题,建议尽快修复以避免潜在的崩溃或状态不一致。其余问题可以在后续迭代中处理。如需针对某个具体问题生成修复代码,请告知!

mcocdaa added 3 commits April 19, 2026 21:04
… i18n

- AIDebugLog: move Find.TickManager.TicksGame to GameComponentTick (main thread)
- OpenAIClient: add AIHttpException to preserve HTTP status code on failure
- OpenAIClient: replace string.Contains with Uri.IsLoopback for IsLocalEndpoint
- AIRequestQueue: move all state mutations from FireRequest background task to main thread via PendingFireResult queue
- AIRequestQueue: add IsLocalEndpointSnapshot to TrackedRequest for thread safety
- AICoreAPI: filter GetRegisteredCategories by exposedProviders
- AICoreAPI: snapshot iteration for skip checks and callbacks
- AICoreAPI: add UnregisterIncidentExecutedCallback and UnregisterStorytellerIncidentSkipCheck
- INTRODUCTION.txt: fix misleading API Key security statement
- RimMind_Core.xml: fix IncludeLocation.Desc (position -> room/indoors/outdoors)
- AICoreSettingsUI: fix queue tab height calculation to include cooldown-only mods
- Window_AIDebugLog: replace hardcoded English strings with translation keys
- ContextComposer: add PromptSanitizer.Sanitize to BuildFromSections
- AIRequestState: explicit enum values for serialization stability
@mcocdaa
mcocdaa merged commit 2d5cb48 into main Apr 19, 2026
1 check was pending
@mcocdaa
mcocdaa deleted the trae branch April 19, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant