diff --git a/.coderabbit.yaml b/.coderabbit.yaml
index 0b55ff1a..29184fb6 100644
--- a/.coderabbit.yaml
+++ b/.coderabbit.yaml
@@ -63,4 +63,4 @@ knowledge_base:
issues:
scope: local
pull_requests:
- scope: local
\ No newline at end of file
+ scope: local
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..94b0942e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+# Dependabot configuration for GitHub Actions
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ commit-message:
+ prefix: "ci(deps)"
+ open-pull-requests-limit: 5
diff --git a/.github/scripts/analyze_release.py b/.github/scripts/analyze_release.py
index 91a5f326..32b79fb6 100755
--- a/.github/scripts/analyze_release.py
+++ b/.github/scripts/analyze_release.py
@@ -107,7 +107,7 @@ def generate_fallback_notes(prs: list, commits: str) -> tuple:
lines = [f"- {pr.get('title', '无标题')}" for pr in prs]
if not lines:
lines = ["- 常规代码更新与维护"]
-
+
notes_zh = "### 变更内容\n" + "\n".join(lines)
notes_en = "### Changes\n" + "\n".join(lines)
return notes_zh, notes_en
@@ -171,4 +171,4 @@ def main():
f.write(combined_notes)
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml
index 8b137891..e69de29b 100644
--- a/.github/workflows/pr-review.yml
+++ b/.github/workflows/pr-review.yml
@@ -1 +0,0 @@
-
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2364abdc..56f33feb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,3 +1,5 @@
+default_stages: [pre-commit]
+
repos:
# - repo: https://github.com/pre-commit/pre-commit-hooks
- repo: https://gitlab.cern.ch/pre-commit-hook-mirrors/pre-commit/pre-commit-hooks
@@ -10,6 +12,12 @@ repos:
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
+ # Windows represents Git symlinks as target text; a newline changes the target.
+ exclude: '(^|/)CLAUDE\.md$'
- id: mixed-line-ending
- id: requirements-txt-fixer
- id: trailing-whitespace
+ - repo: https://github.com/mcocdaa/ai-commit.git
+ rev: v1.0.2
+ hooks:
+ - id: ai-commit
diff --git a/AGENTS.md b/AGENTS.md
index 06da3a89..b341f7e3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,862 +1,77 @@
# AGENTS.md — RimMind-Core
-本文件供 AI 编码助手阅读,描述 RimMind-Core 的架构、代码约定和扩展模式。
+RimMind 的运行时与公共 API 前置。Core 负责 LLM 请求、上下文、Agent、ToolCall、生命周期、设置和调试基础设施;其他模组只通过公开边界接入。
-## 项目定位
+## Start here
-RimMind-Core 是 RimMind AI 模组套件的核心基础设施层。所有子模组(Actions、Personality、Advisor、Memory 等)均依赖本模组。职责:
+先按变更类型选择入口,不要从全仓搜索开始:
-1. **LLM 客户端**:OpenAI Chat Completions 兼容 + Player2 服务,通过 `UnityWebRequest` 发送
-2. **异步请求队列**:后台线程发请求,主线程回调,`ConcurrentQueue` 桥接,支持重试/暂停/取消
-3. **游戏上下文构建**:将游戏状态打包为文本,供 AI Prompt 使用
-4. **Prompt 组装系统**:`StructuredPromptBuilder` + `PromptSection` + `PromptBudget` + `ContextComposer`,支持优先级排序、压缩与 Token 预算裁剪
-5. **Provider 注册机制**:子模组通过注册 API 注入上下文段,支持卸载与覆盖,实现解耦
-6. **请求审批悬浮窗**:`RequestOverlay` + `RequestEntry`,子模组可注册待审批请求供玩家选择
-7. **SkipCheck 机制**:子模组可注册对话/浮菜单/动作/叙事者的跳过检查,实现互斥控制
-8. **设置 UI**:多分页设置界面(API/队列/提示词/上下文),子模组可注册额外分页
-9. **调试工具**:AI Debug Log 窗口、Dev DebugAction
+- AI 请求:`Source/Application/Features/Requests/README.md`
+- 上下文:`Source/Presentation/Context/README.md`
+- Agent:`Source/Presentation/Agent/PawnAgent.cs`
+- ToolCall:`Source/Application/Features/Pipeline/Unified/ToolCallDispatchMiddleware.cs`
+- 组合与生命周期:`Source/Presentation/Runtime/RimMindCompositionRoot.cs`
+- 游戏内调试:`Source/Infrastructure/UI/DebugActions/README.md`
+- Agent 流程实验室:`Source/Infrastructure/UI/AgentFlow/README.md`
-## 源码结构
+## Structure
-```
+```text
Source/
-├── AICoreMod.cs Mod 入口,注册 Harmony,持有 Settings 单例
-├── AICoreAPI.cs 静态公共 API(RimMindAPI),供子模组调用
-├── Client/
-│ ├── IAIClient.cs AI 客户端接口
-│ ├── AIRequest.cs 请求数据结构(含 ChatMessage 多轮支持)
-│ ├── AIResponse.cs 响应数据结构(含状态/优先级/重试/遥测)
-│ ├── AIRequestState.cs 请求状态枚举 + 优先级枚举
-│ ├── OpenAI/
-│ │ ├── OpenAIClient.cs OpenAI 兼容客户端实现
-│ │ └── OpenAIDto.cs 请求/响应 DTO(internal)
-│ └── Player2/
-│ ├── Player2Client.cs Player2 客户端实现(本地应用 + 远程 API)
-│ └── Player2Models.cs Player2 请求/响应 DTO(internal)
-├── Core/
-│ ├── AIRequestQueue.cs GameComponent,异步请求队列 + 冷却 + 重试 + 暂停/取消
-│ ├── GameContextBuilder.cs 静态工具类,构建地图/Pawn 上下文文本(含 Section 和精简版本)
-│ ├── JsonTagExtractor.cs 从 AI 响应提取 JSON 内容
-│ ├── AIDebugLog.cs GameComponent,存储最近 200 条请求记录(含 AIDebugEntry)
-│ └── Prompt/
-│ ├── StructuredPromptBuilder.cs 流式 Prompt 构建器(链式 API + FromKeyPrefix + ToSection)
-│ ├── PromptSection.cs Prompt 段落(Tag/Content/Priority/EstimatedTokens/Compress)
-│ ├── PromptBudget.cs Token 预算管理(先压缩后裁剪)
-│ ├── ContextComposer.cs 段落排序 + 历史压缩
-│ └── PromptSanitizer.cs Prompt 清理(去除双花括号)
-├── Settings/
-│ ├── AICoreSettings.cs 模组设置(Provider 选择 + API 配置 + 请求控制 + 调试)
-│ ├── AIProvider.cs AI 提供者枚举(OpenAI / Player2)
-│ └── ContextSettings.cs 上下文过滤器(28 个 Include* 字段 + ContextPreset 预设 + disabledProviders + exposedProviders)
-├── UI/
-│ ├── AICoreSettingsUI.cs 多分页设置界面(API/队列/提示词/上下文)
-│ ├── Window_AIDebugLog.cs AI Debug Log 浮动窗口
-│ ├── RequestOverlay.cs 请求审批悬浮窗(可拖拽/可缩放/可持久化位置)
-│ ├── RequestEntry.cs 悬浮窗请求条目数据结构
-│ ├── Window_RequestLog.cs 请求日志窗口
-│ └── SettingsUIHelper.cs 设置 UI 辅助工具类
-├── Patch/
-│ ├── AITogglePatch.cs 右下角 AI 图标按钮注入
-│ └── Patch_UIRoot_OnGUI.cs 每帧调用 RequestOverlay.OnGUI
-└── Debug/
- └── AICoreDebugActions.cs Dev 菜单调试动作
-```
-
-## 关键类与 API
-
-### RimMindAPI(AICoreAPI.cs)
-
-所有子模组通过此静态类与 Core 交互:
-
-```csharp
-// ── 请求 ──
-RimMindAPI.RequestAsync(AIRequest request, Action onComplete)
-RimMindAPI.RequestImmediate(AIRequest request, Action onComplete) // 绕过队列/冷却
-RimMindAPI.CancelRequest(string requestId) // 取消指定请求
-RimMindAPI.PauseQueue() // 暂停队列
-RimMindAPI.ResumeQueue() // 恢复队列
-RimMindAPI.IsQueuePaused // 队列是否暂停
-RimMindAPI.ActiveRequestCount // 当前活跃请求数
-RimMindAPI.TotalQueuedCount // 排队中请求总数
-RimMindAPI.GetActiveRequests() // 获取活跃请求列表
-RimMindAPI.GetAllQueuedRequests() // 获取所有排队请求
-
-// ── 上下文构建 ──
-RimMindAPI.BuildMapContext(Map map, bool brief = false)
-RimMindAPI.BuildPawnContext(Pawn pawn)
-RimMindAPI.BuildStaticContext()
-RimMindAPI.BuildHistoryContext(int maxEntries = 10)
-RimMindAPI.BuildFullPawnPrompt(Pawn pawn, string? currentQuery, string[]? excludeProviders)
-RimMindAPI.BuildFullPawnPrompt(Pawn pawn, PromptBudget budget, string? currentQuery, string[]? excludeProviders)
-RimMindAPI.BuildFullPawnSections(Pawn pawn, string? currentQuery, string[]? excludeProviders)
-
-// ── Provider 注册(子模组在 Mod 构造时调用)──
-RimMindAPI.RegisterStaticProvider(string category, Func provider, int priority, string modId = "", bool overrideExisting = true)
-RimMindAPI.RegisterDynamicProvider(string category, Func provider, int priority, string modId = "", bool overrideExisting = true)
-RimMindAPI.RegisterPawnContextProvider(string category, Func provider, int priority, string modId = "", bool overrideExisting = true)
-
-// ── Provider 查询(供外部 Mod 读取 RimMind 数据)──
-RimMindAPI.GetProviderData(string category, Pawn pawn) // Pawn 级 Provider 数据
-RimMindAPI.GetStaticProviderData(string category) // 静态 Provider 数据
-RimMindAPI.GetDynamicProviderData(string category, string query) // 动态 Provider 数据
-RimMindAPI.GetRegisteredCategories() // 所有已注册的 category 列表
-
-// ── Provider 卸载 ──
-RimMindAPI.UnregisterStaticProvider(string category)
-RimMindAPI.UnregisterDynamicProvider(string category)
-RimMindAPI.UnregisterPawnContextProvider(string category)
-RimMindAPI.UnregisterModProviders(string modId) // 卸载某模组注册的所有 Provider
-
-// ── UI 扩展 ──
-RimMindAPI.RegisterSettingsTab(string tabId, Func labelFn, Action drawFn)
-RimMindAPI.SettingsTabs // IReadOnlyList<(tabId, labelFn, drawFn)>
-RimMindAPI.RegisterToggleBehavior(string id, Func isActive, Action toggle)
-RimMindAPI.HasToggleBehaviors // getter
-
-// ── 冷却控制 ──
-RimMindAPI.RegisterModCooldown(string modId, Func getCooldownTicks)
-RimMindAPI.GetModCooldownGetter(string modId) // Func?
-RimMindAPI.ModCooldownGetters // IReadOnlyDictionary>
-
-// ── 对话触发 ──
-RimMindAPI.RegisterDialogueTrigger(Action triggerFn)
-RimMindAPI.TriggerDialogue(Pawn pawn, string context, Pawn? recipient)
-RimMindAPI.CanTriggerDialogue // getter
-
-// ── SkipCheck(互斥控制)──
-RimMindAPI.RegisterDialogueSkipCheck(string sourceId, Func skipCheck)
-RimMindAPI.UnregisterDialogueSkipCheck(string sourceId)
-RimMindAPI.ShouldSkipDialogue(Pawn pawn, string triggerType) // 任一检查返回 true 则跳过
-
-RimMindAPI.RegisterFloatMenuSkipCheck(string sourceId, Func skipCheck)
-RimMindAPI.UnregisterFloatMenuSkipCheck(string sourceId)
-RimMindAPI.ShouldSkipFloatMenu() // 任一检查返回 true 则跳过
-
-RimMindAPI.RegisterActionSkipCheck(string sourceId, Func skipCheck)
-RimMindAPI.UnregisterActionSkipCheck(string sourceId)
-RimMindAPI.ShouldSkipAction(string intentId) // 任一检查返回 true 则跳过
-
-// ── Incident 回调 ──
-RimMindAPI.RegisterIncidentExecutedCallback(Action callback)
-RimMindAPI.UnregisterIncidentExecutedCallback(Action callback)
-RimMindAPI.NotifyIncidentExecuted() // 通知所有回调
-
-RimMindAPI.RegisterStorytellerIncidentSkipCheck(Func check)
-RimMindAPI.UnregisterStorytellerIncidentSkipCheck(Func check)
-RimMindAPI.ShouldSkipStorytellerIncident() // 任一检查返回 true 则跳过
-
-// ── 请求审批悬浮窗 ──
-RimMindAPI.RegisterPendingRequest(RequestEntry entry)
-RimMindAPI.GetPendingRequests() // IReadOnlyList
-RimMindAPI.RemovePendingRequest(RequestEntry entry)
-
-// ── 状态查询 ──
-RimMindAPI.IsConfigured()
-RimMindAPI.IsAnyToggleActive()
-RimMindAPI.ToggleAll()
-
-// ── 客户端缓存 ──
-RimMindAPI.InvalidateClientCache() // 切换 Provider 时调用
-```
-
-### AIProvider(AIProvider.cs)
-
-```csharp
-enum AIProvider {
- OpenAI = 0, // OpenAI 兼容 API(DeepSeek / Ollama 等)
- Player2 = 1, // Player2 服务(本地应用 + 远程 API)
-}
-```
-
-### AIRequest / AIResponse / AIRequestState
-
-```csharp
-class AIRequest {
- string SystemPrompt;
- string UserPrompt; // 单轮模式
- List? Messages; // 多轮模式(非 null 时忽略 UserPrompt)
- int MaxTokens; // 默认 800
- float Temperature; // 默认 0.7
- string RequestId; // 格式:"ModName_Purpose_Tick"
- string ModId; // 模组标识,用于冷却分组
- int ExpireAtTicks; // 过期时间,0=不过期
- bool UseJsonMode; // 默认 true,设 false 绕过 response_format
- AIRequestPriority Priority; // 默认 Normal
- int MaxRetryCount; // 默认 -1(使用全局设置)
-}
-
-class ChatMessage {
- string Role; // "system" / "user" / "assistant"
- string Content;
-}
-
-class AIResponse {
- bool Success;
- string Content;
- string Error;
- int TokensUsed;
- string RequestId;
- AIRequestState State; // 请求状态
- AIRequestPriority Priority; // 请求优先级
- int QueuePosition; // 队列位置
- int AttemptCount; // 尝试次数
- long QueueWaitMs; // 队列等待时间
- long ProcessingMs; // 处理时间
- long HttpStatusCode; // HTTP 状态码
- int RequestPayloadBytes; // 请求体字节数
- string CancelReason; // 取消原因
-
- static AIResponse Failure(string requestId, string error);
- static AIResponse Ok(string requestId, string content, int tokens);
- static AIResponse Cancelled(string requestId, string reason);
-}
-
-enum AIRequestState { Queued, Processing, Completed, Error, Cancelled }
-enum AIRequestPriority { High = 0, Normal = 1, Low = 2 }
-```
-
-### IAIClient
-
-```csharp
-interface IAIClient {
- Task SendAsync(AIRequest request);
- bool IsConfigured();
- bool IsLocalEndpoint { get; } // 本地端点(Ollama/Player2 本地),串行处理
-}
-```
-
-### OpenAIClient
-
-OpenAI Chat Completions 兼容客户端,支持:
-- 自动拼接 `/v1/chat/completions` 端点路径
-- JSON 强制模式(`response_format: json_object`)
-- 本地端点检测(localhost / 127.0.0.1 / host.docker.internal)
-- 连接超时(本地 300s / 远程 60s)+ 读取超时(60s)
-- 游戏卸载时自动取消请求
-
-### Player2Client
-
-Player2 服务客户端,支持两种连接模式:
-- **本地模式**:自动检测本地 Player2 应用(`localhost:4315`),自动登录获取 API Key
-- **远程模式**:使用手动填写的 API Key 连接 `api.player2.game`
-- 定期健康检查(60s 间隔)
-- 缓存客户端实例,切换 Provider 时需调用 `InvalidateClientCache()`
-
-### Prompt 组装系统
-
-#### StructuredPromptBuilder
-
-流式构建 System Prompt,链式 API:
-
-```csharp
-var prompt = new StructuredPromptBuilder()
- .Role("你是一个 RimWorld 殖民者")
- .Goal("根据状态做出决策")
- .Process("1. 分析状态 2. 选择动作")
- .Constraint("只能选择候选列表中的动作")
- .Output("JSON 格式")
- .Example("{\"action\": \"assign_work\", ...}")
- .Fallback("选择 force_rest")
- .Build();
+├── Domain/ 纯模型、值对象和领域规则
+├── Application/ 用例、端口、管线和调度
+├── Infrastructure/ 客户端、存储、Verse 与 UI 实现
+└── Presentation/ 公共 API、组合根、运行时和游戏入口
```
-翻译键版本:`RoleFromKey()`, `GoalFromKey()` 等,配合 Keyed 翻译系统使用。
+依赖方向是 Presentation/Infrastructure → Application → Domain。Domain 和 Application 不得引用 Verse、Unity 或 Harmony。
-快捷方式:
+## Main request flow
-```csharp
-// 从翻译键前缀一键构建(自动拼接 .Role/.Goal/.Process/... 后缀)
-StructuredPromptBuilder.FromKeyPrefix("RimMind.MyMod")
-
-// 带翻译键表头的自定义段
-.WithCustom(customText, "RimMind.MyMod.CustomHeader")
-
-// 启用段落标签输出([角色] [目标] ...)
-.WithSectionLabels(true)
-
-// 直接转为 PromptSection(用于注入上下文系统)
-.ToSection(tag: "system_prompt", priority: PromptSection.PriorityCore)
-```
-
-#### PromptSection
-
-```csharp
-class PromptSection {
- string Tag; // 段落标识
- string Content; // 段落内容
- int Priority; // 优先级(低=重要,不可裁剪)
- int EstimatedTokens; // 估算 Token 数
- Func? Compress; // 压缩回调(非 null 时可在超预算时压缩而非删除)
-
- // 优先级常量
- const int PriorityCore = 0; // 核心指令,不可裁剪
- const int PriorityCurrentInput = 1; // 当前输入
- const int PriorityKeyState = 3; // 关键状态
- const int PriorityMemory = 5; // 记忆上下文
- const int PriorityAuxiliary = 8; // 辅助上下文
- const int PriorityCustom = 10; // 自定义内容
-
- bool IsTrimable => Priority > PriorityCore;
- bool IsCompressible => Compress != null && IsTrimable;
- static int EstimateTokens(string text); // 混合 CJK/Latin 估算
-}
-```
-
-#### PromptBudget
-
-```csharp
-class PromptBudget {
- int TotalBudget = 4000; // 总 Token 预算
- int ReserveForOutput = 800; // 为输出预留
- int AvailableForInput; // 可用于输入的 Token 数
-
- PromptBudget()
- PromptBudget(int totalBudget, int reserveForOutput = 800)
-
- List Compose(List sections); // 按预算裁剪
- string ComposeToString(List sections); // 裁剪后拼接
-}
+```text
+RimMindAPI.Request
+ → IRequestSubmissionService
+ → RequestSubmissionService
+ → IRequestQueue / RequestQueue
+ → IPipeline
+ → IAIClient
```
-裁剪逻辑(两阶段):
-1. **压缩阶段**:对 `IsCompressible` 的段,按优先级从高到低调用 `Compress` 回调,直到总 Token 在预算内
-2. **裁剪阶段**:压缩后仍超预算,对 `IsTrimable` 的段按优先级从高到低删除,直到总 Token 在预算内
-
-#### ContextComposer
+请求队列的后台结果只能通过 `RequestCompletionInbox` 回到主线程。稳定中间件仍位于 `Application/Features/Pipeline/Unified`,客户端位于 `Infrastructure/Services/Clients`。
-```csharp
-static class ContextComposer {
- List Reorder(List sections); // 按优先级排序
- string BuildFromSections(List sections); // 排序后拼接
- string CompressHistory(string historyText, int maxLines = 6, string summaryLine = ""); // 历史压缩
-}
-```
-
-#### PromptSanitizer
+## Public boundary
-```csharp
-static class PromptSanitizer {
- string Sanitize(string prompt); // 去除 {{ }} 双花括号,防止模板注入
-}
-```
+子模组使用 `RimMind.Presentation.Api.RimMindAPI`、Domain 模型和明确公开的 Application 合同。不要访问 `RimMind.Core.Internal`、`RimMindCoreMod.Settings`、组合根或具体队列实现。
-### GameContextBuilder
+跨子模组的同步字符串数据通过 `RimMindAPI.Providers` 注册和读取;Provider 必须声明稳定 owner ID,并保持 Verse 读取在主线程。
-```csharp
-static class GameContextBuilder {
- // 纯文本版本
- string BuildMapContext(Map map, bool brief = false);
- string BuildPawnContext(Pawn pawn);
- string BuildCompactPawnContext(Pawn pawn); // 精简版
- string BuildHistoryContext(int maxEntries = 10);
+上下文快照统一使用 `IContextBuilder.BuildSnapshotFromEnvelopeAsync`;不要另建同步快照管线或阻塞等待异步 Provider。默认 Agent 保留 Reactive / Proactive 模式、周期与感知触发;反思、日规划、梦境、社交组织、性格演化不装配占位实现,仅通过明确可选的策略扩展接入。
- // PromptSection 版本(带压缩回调,可参与 PromptBudget 压缩)
- PromptSection BuildMapContextSection(Map map, bool brief = false);
- PromptSection BuildPawnContextSection(Pawn pawn);
- PromptSection BuildCompactPawnContextSection(Pawn pawn);
-}
-```
-
-### BuildFullPawnPrompt 组装顺序
-
-```
-1. 静态段 → RegisterStaticProvider 注册的段(Rules、Skills 等)
-2. Pawn段 → RegisterPawnContextProvider 注册的段(人格、记忆等)
-3. 游戏状态 → BuildPawnContextSection(pawn)(带压缩回调)
-4. 地图状态 → BuildMapContextSection(pawn.Map)(带压缩回调)
-5. 动态段 → RegisterDynamicProvider 注册的段(Memory 语义检索等)
-6. 自定义提示词 → customPawnPrompt + customMapPrompt
-```
-
-所有段落以 `PromptSection` 形式传递,按 `Priority` 排序后拼接。带 `PromptBudget` 的版本会先压缩再裁剪。
-
-### JsonTagExtractor
-
-统一 AI 响应解析工具。所有子模组应使用 `{JSON} ` 格式:
-
-```csharp
-T? result = JsonTagExtractor.Extract(aiResponse, "TagName");
-List results = JsonTagExtractor.ExtractAll(aiResponse, "TagName");
-string? raw = JsonTagExtractor.ExtractRaw(aiResponse, "TagName");
-List allRaw = JsonTagExtractor.ExtractAllRaw(aiResponse, "TagName");
-```
-
-### AIRequestQueue
-
-```csharp
-class AIRequestQueue : GameComponent {
- static AIRequestQueue Instance { get; }
- static void LogFromBackground(string msg, bool isWarning = false);
-
- void Enqueue(AIRequest request, Action callback, IAIClient client);
- void EnqueueImmediate(AIRequest request, Action callback, IAIClient client);
- bool CancelRequest(string requestId);
-
- void PauseQueue();
- void ResumeQueue();
- bool IsPaused { get; }
- int ActiveRequestCount { get; }
- bool IsLocalModelBusy { get; } // 本地模型是否正在处理
-
- int GetCooldownTicksLeft(string modId);
- int GetQueueDepth(string modId);
- void ClearCooldown(string modId);
- void ClearAllCooldowns();
- void ClearAllQueues();
- IReadOnlyDictionary GetAllCooldowns();
- IReadOnlyDictionary GetAllQueueDepths();
- IReadOnlyList GetActiveRequests();
- IReadOnlyList GetQueuedRequests(string modId);
- IReadOnlyList GetAllQueuedRequests();
- int TotalQueuedCount { get; }
-}
-```
-
-重试逻辑:
-- 瞬态错误自动重试(timeout / connection / network / 502 / 503 / 429 / rate limit)
-- 重试次数由 `AIRequest.MaxRetryCount` 控制(-1 使用全局设置 `maxRetryCount`)
-- 本地端点(Ollama / Player2 本地)串行处理,同时只允许一个请求
-
-### AIDebugLog / AIDebugEntry
-
-```csharp
-class AIDebugLog : GameComponent {
- static AIDebugLog? Instance { get; }
- IReadOnlyList Entries { get; }
- void Clear();
- static void Record(AIRequest request, AIResponse response, int elapsedMs);
-}
-
-class AIDebugEntry {
- int GameTick;
- string Source;
- string ModelName;
- string FullSystemPrompt;
- string FullUserPrompt;
- string FullResponse;
- int ElapsedMs;
- int TokensUsed;
- bool IsError;
- string ErrorMsg;
- AIRequestPriority Priority;
- AIRequestState State;
- int AttemptCount;
- long QueueWaitMs;
- long ProcessingMs;
- long HttpStatusCode;
- int RequestPayloadBytes;
- string FormattedTime; // 格式化时间
-}
-```
-
-### RequestEntry / RequestOverlay
-
-请求审批悬浮窗系统:
-
-```csharp
-class RequestEntry {
- string source; // 来源模组标识
- Pawn? pawn; // 相关小人
- string title; // 请求标题
- string? description; // 请求描述
- string[] options; // 选项列表
- string[]? optionTooltips; // 选项提示文本
- Action? callback; // 选择回调(参数为选中的选项文本)
- bool systemBlocked; // 是否被系统拦截
- int tick; // 创建时间
- int expireTicks; // 过期 tick 数
-}
-
-static class RequestOverlay {
- void Register(RequestEntry entry);
- IReadOnlyList Pending { get; }
- void Remove(RequestEntry entry);
- Rect GetWindowRect(); // 获取悬浮窗位置(持久化用)
- void SetWindowRect(Rect rect);
- void OnGUI();
-}
-```
+## Local invariants
-过期后自动触发最后一个选项(视为"忽略")。
+- AI 请求异步执行;Verse/Unity 副作用仅在主线程发生。
+- 生命周期代际退役后,旧回调不得继续产生副作用。
+- 请求入口只转发;客户端选择、追踪和取消属于 Application。
+- 调度、活动请求和断路状态共享转移规则,不任意拆成接口层级。
+- API 密钥和玩家数据不得写入日志。
+- Core 全部测试项目累计少于 1000 个发现用例;参数化测试每个数据行计数。
+- 测试覆盖真实行为、失败边界和模块协作;替身仅隔离外部依赖,不锁定私有实现形状,不为压低数量合并无关场景。
-### SettingsUIHelper
+## Smallest useful verification
-```csharp
-static class SettingsUIHelper {
- DrawSectionHeader(Listing_Standard listing, string label);
- DrawCustomPromptSection(Listing_Standard listing, string label, ref string prompt, float height = 80f);
- SplitContentArea(inRect); // 分割内容区域
- SplitBottomBar(inRect); // 分割底部栏
- DrawBottomBar(barRect, onReset); // 重置按钮
-}
+```powershell
+dotnet test Tests/RimMindCore.Tests.csproj -c Release
+dotnet test ArchTests/RimMindCore.ArchTests.csproj -c Release
+dotnet test IntegrationTests/RimMindCore.Integration.Tests.csproj -c Release
+dotnet build Source/RimMindCore.csproj -c Release
```
-### RimMindCoreSettings
-
-```csharp
-class RimMindCoreSettings : ModSettings {
- AIProvider provider; // 默认 OpenAI
- string apiKey;
- string apiEndpoint; // 默认 "https://api.deepseek.com/v1"
- string modelName; // 默认 "deepseek-chat"
- bool forceJsonMode; // 默认 true
- bool useStreaming; // 流式响应(预留,默认 false)
- int maxTokens; // 默认 800
- int maxConcurrentRequests; // 默认 3
- int maxRetryCount; // 默认 2
- int requestTimeoutMs; // 默认 120000 (120秒)
- bool debugLogging;
- ContextSettings Context; // 上下文过滤器
- string customPawnPrompt;
- string customMapPrompt;
- bool requestOverlayEnabled;
- float requestOverlayX, requestOverlayY, requestOverlayW, requestOverlayH;
-
- bool IsConfigured(); // Player2 模式无需 API Key
- bool IsOpenAIConfigured(); // OpenAI 模式需要 API Key + 端点
- override void ExposeData();
-}
-```
-
-### ContextSettings / ContextPreset
-
-```csharp
-class ContextSettings : IExposable {
- // 28 个 Include* bool 字段控制上下文注入
- // 小人信息 (20项): IncludeRace, IncludeAge, IncludeGender, IncludeBackstory,
- // IncludeIdeology, IncludeTraits, IncludeSkills, IncludeCapacities,
- // IncludeHealth, IncludeMood, IncludeMoodThoughts, IncludeCurrentJob,
- // IncludeWorkPriorities, IncludeEquipment, IncludeInventory, IncludeLocation,
- // IncludeRelations, IncludeGenes, IncludeSurroundings, IncludeCombatStatus
- // 地图/环境信息 (8项): IncludeGameTime, IncludeColonistCount, IncludeColonistNames,
- // IncludeWealth, IncludeFood, IncludeSeason, IncludeWeather, IncludeThreats
- int MinSkillLevel; // 技能显示阈值(默认 4)
- HashSet disabledProviders; // 禁用的 Provider category 列表
- HashSet exposedProviders; // 允许对外暴露的 Provider category 列表
-
- void ExposeData();
- void ApplyPreset(ContextPreset preset);
-}
-
-enum ContextPreset { Minimal, Standard, Full, Custom }
-```
-
-## 线程安全规则
-
-- **主线程**:读写游戏状态、消费 `ConcurrentQueue` 结果、所有 RimWorld/Unity API
-- **后台线程**:HTTP 请求、JSON 解析、生产 `ConcurrentQueue` 结果
-- **严禁**在后台线程调用任何 RimWorld/Unity API
-- 后台线程结果通过 `_pendingFireResults` 队列传回主线程处理(含重试逻辑)
-- 后台线程日志必须通过 `AIRequestQueue.LogFromBackground()` 写入,主线程 Tick 时输出
-
-## 数据流
-
-```
-游戏主线程 (Tick)
- │
- ├── 子模组触发条件满足
- │ ▼
- │ 构建 AIRequest(SystemPrompt + UserPrompt)
- │ ▼
- │ RimMindAPI.RequestAsync(request, callback)
- │ ▼
- │ AIRequestQueue.Enqueue()
- │ ├── 检查冷却 → 跳过或接受
- │ ├── 检查过期 → 丢弃过期请求
- │ ├── 按优先级排序
- │ └── Task.Run → IAIClient.SendAsync()
- │ ▼ (后台线程)
- │ HTTP 请求 → 解析响应
- │ ▼
- │ _pendingFireResults.Enqueue()
- │
- ├── GameComponentTick()
- │ ▼
- │ 消费 _pendingFireResults
- │ ├── 瞬态错误 → 重试(重新入队)
- │ └── 成功/最终失败 → _results.Enqueue()
- │
- │ 消费 _results 队列
- │ ▼
- │ callback(response) ← 主线程安全
- │ ▼
- │ 子模组处理响应(解析 JSON、执行动作等)
- └── ...
-```
-
-## RimMind 套件架构
-
-```
- ┌─────────────────┐
- │ Harmony │
- └────────┬────────┘
- │
- ┌────────▼────────┐
- │ RimMind-Core │
- └──┬──┬──┬──┬──┬─┘
- │ │ │ │ │
- ┌────────────┘ │ │ │ └──────────────┐
- │ │ │ │ │
- ┌──────▼──────┐ ┌─────▼──┐ │ ┌───────────────▼──────┐
- │RimMind- │ │RimMind-│ │ │ RimMind-Personality │
- │Actions │ │Memory │ │ └──────────────────────┘
- └──────┬──────┘ └────────┘ │
- │ │
- ┌──────▼──────┐ ┌───────▼──────┐
- │RimMind- │ │RimMind- │
- │Advisor │ │Dialogue │
- └─────────────┘ └──────────────┘
- │
- ┌────────▼────────┐
- │RimMind- │
- │Storyteller │
- └─────────────────┘
-```
-
-### 上下文注入方式
-
-| 子模组 | 注入方式 | 注册的 Provider |
-|--------|---------|----------------|
-| RimMind-Personality | `RegisterPawnContextProvider` | personality_profile + personality_state + personality_shaping |
-| RimMind-Memory | `RegisterPawnContextProvider` + `RegisterStaticProvider` | memory_pawn + memory_narrator |
-| RimMind-Dialogue | `RegisterPawnContextProvider` | dialogue_state + dialogue_relation |
-| RimMind-Advisor | `RegisterPawnContextProvider` | advisor_history |
-| RimMind-Actions | 以记忆方式注入(通过 Memory) | - |
-| RimMind-Storyteller | 不注入上下文 | - |
-
-### 数据依赖关系
-
-```
-(人格, 记忆) → 想法 → 行动 或 对话
- ↑ ↑
- └── 想法和记忆反哺人格
-```
-
-- 人格通过 `RegisterPawnContextProvider` 注入上下文
-- 想法自动打包进入上下文(Thought 系统)
-- 记忆通过 `RegisterPawnContextProvider` + `RegisterStaticProvider` 注入上下文
-- 行动以记忆方式注入上下文
-- 对话状态通过 `RegisterPawnContextProvider` 注入上下文
-- Advisor 历史通过 `RegisterPawnContextProvider` 注入上下文
-
-## 代码约定
-
-### 命名空间
-
-| 命名空间 | 目录 | 职责 |
-|---------|------|------|
-| `RimMind.Core` | Source/ 根目录 | Mod 入口、API |
-| `RimMind.Core.Client` | Client/ | AI 客户端接口与数据结构 |
-| `RimMind.Core.Client.OpenAI` | Client/OpenAI/ | OpenAI 兼容实现 |
-| `RimMind.Core.Client.Player2` | Client/Player2/ | Player2 实现 |
-| `RimMind.Core.Internal` | Core/ | 内部组件(队列、上下文构建、日志、JSON 提取) |
-| `RimMind.Core.Prompt` | Core/Prompt/ | Prompt 组装(段落、预算、排序、结构化构建、清理) |
-| `RimMind.Core.Settings` | Settings/ | 设置 |
-| `RimMind.Core.UI` | UI/ | 界面 |
-| `RimMind.Core.Patch` | Patch/ | Harmony 补丁 |
-| `RimMind.Core.Debug` | Debug/ | 调试动作 |
-
-### 序列化
-
-- `ModSettings` → `ExposeData()`,需调 `base.ExposeData()`
-- `GameComponent` → `ExposeData()`
-- `ThingComp` → `PostExposeData()`(不是 ExposeData)
-- `WorldComponent` → `ExposeData()`
-
-### GameComponent 自动发现
-
-GameComponent / WorldComponent 不需要 XML 注册。RimWorld 自动扫描并实例化,前提是构造函数签名正确:
-
-```csharp
-public AIRequestQueue(Game game) { _instance = this; }
-```
-
-RimWorld 1.6 的 GameComponent 基类无参构造,但 `Game.InitNewGame` 仍用 `Activator.CreateInstance(type, game)`,所以必须保留 `(Game game)` 签名。
-
-### UI 本地化
-
-所有 UI 文本通过 `Languages/ChineseSimplified/Keyed/RimMind_Core.xml` 的 Keyed 翻译,禁止硬编码中文。代码中使用 `"Key".Translate()`。
-
-### Harmony
-
-- Harmony ID:`mcocdaa.RimMindCore`
-- 优先使用 PostFix
-- Patch 类放在 `Patch/` 目录
-
-### 构建
-
-| 配置项 | 值 |
-|--------|-----|
-| 目标框架 | `net48` |
-| C# 语言版本 | 9.0 |
-| Nullable | enable |
-| RimWorld 版本 | 1.6 |
-| 输出路径 | `../1.6/Assemblies/` |
-| 部署 | 设置 `RIMWORLD_DIR` 环境变量后自动部署 |
-| NuGet 依赖 | `Krafs.Rimworld.Ref 1.6.*-*`, `Lib.Harmony.Ref 2.*`, `Newtonsoft.Json 13.0.*` |
-
-### 测试
-
-- 单元测试项目:`Tests/`,使用 xUnit,目标 `net10.0`
-- 测试纯逻辑层,不依赖 RimWorld
-- 测试文件直接 `` 引用源码(不引用主项目 DLL)
-- 已有测试:`JsonTagExtractorTests`
-
-## 扩展指南(子模组开发)
-
-### 1. 编译期引用
-
-在 `.csproj` 中引用 RimMindCore.dll(Private=false):
-
-```xml
-
- ../../RimMind-Core/$(GameVersion)/Assemblies/RimMindCore.dll
- false
-
-```
-
-### 2. 注册 Provider
-
-在 Mod 构造函数中注册,传入 `modId` 以支持卸载:
-
-```csharp
-public class MyMod : Mod
-{
- public MyMod(ModContentPack content) : base(content)
- {
- RimMindAPI.RegisterPawnContextProvider("my_category", pawn =>
- {
- return $"[{pawn.Name.ToStringShort} 自定义信息]\n...";
- }, PromptSection.PriorityMemory, modId: "MyMod");
-
- RimMindAPI.RegisterSettingsTab("my_tab", () => "我的设置", rect =>
- {
- // 绘制设置 UI
- });
- }
-}
-```
-
-### 3. 卸载 Provider
-
-模组卸载时清理注册:
-
-```csharp
-RimMindAPI.UnregisterModProviders("MyMod");
-// 或按 category 卸载:
-RimMindAPI.UnregisterPawnContextProvider("my_category");
-```
-
-### 4. 发起 AI 请求
-
-```csharp
-var request = new AIRequest
-{
- SystemPrompt = "你是一个...",
- UserPrompt = RimMindAPI.BuildFullPawnPrompt(pawn),
- MaxTokens = 400,
- Temperature = 0.7f,
- RequestId = $"MyMod_{pawn.ThingID}",
- ModId = "MyMod",
-};
-
-RimMindAPI.RequestAsync(request, response =>
-{
- if (!response.Success) { Log.Warning($"失败: {response.Error}"); return; }
- var result = JsonTagExtractor.Extract(response.Content, "MyTag");
- // 处理结果...
-});
-```
-
-### 5. 使用 StructuredPromptBuilder 构建 System Prompt
-
-```csharp
-// 方式一:逐项构建
-var systemPrompt = new StructuredPromptBuilder()
- .RoleFromKey("RimMind.MyMod.Role")
- .GoalFromKey("RimMind.MyMod.Goal")
- .ProcessFromKey("RimMind.MyMod.Process")
- .ConstraintFromKey("RimMind.MyMod.Constraint")
- .OutputFromKey("RimMind.MyMod.Output")
- .ExampleFromKey("RimMind.MyMod.Example")
- .FallbackFromKey("RimMind.MyMod.Fallback")
- .Build();
-
-// 方式二:从翻译键前缀一键构建
-var systemPrompt = StructuredPromptBuilder.FromKeyPrefix("RimMind.MyMod")
- .Build();
-
-// 方式三:转为 PromptSection 注入上下文系统
-var section = StructuredPromptBuilder.FromKeyPrefix("RimMind.MyMod")
- .ToSection("my_system_prompt", PromptSection.PriorityCore);
-```
-
-### 6. 注册请求审批
-
-```csharp
-RimMindAPI.RegisterPendingRequest(new RequestEntry
-{
- source = "my_mod",
- pawn = pawn,
- title = "标题",
- description = "描述",
- options = new[] { "选项A", "选项B", "忽略" },
- optionTooltips = new[] { "执行A", "执行B", "跳过" },
- expireTicks = 30000,
- callback = choice =>
- {
- if (choice == "选项A") { /* 处理 */ }
- }
-});
-```
-
-### 7. 使用 SkipCheck 实现互斥
-
-```csharp
-// 注册:当对话系统正在处理时,跳过浮菜单 AI 选项
-RimMindAPI.RegisterFloatMenuSkipCheck("MyMod", () => isProcessing);
-
-// 卸载时清理
-RimMindAPI.UnregisterFloatMenuSkipCheck("MyMod");
-```
-
-### 8. 响应格式约定
-
-所有子模组统一使用 `{JSON} ` 格式,AI 可在标签前后输出思考过程:
-
-```
-让我分析一下当前局势...
-
-{"action": "social_relax", "target": "Alice", "reason": "渴望社交"}
-
-```
-
-### 9. 冷却机制
-
-- Core 层冷却:默认 3600 ticks,按 ModId 独立
-- 子模组通过 `RegisterModCooldown` 注册自定义冷却 Getter
-- 本地端点(Ollama / Player2 本地)串行处理,同一时刻只有一个请求
-- DebugAction 可清除冷却:`AIRequestQueue.Instance?.ClearCooldown(modId)`
-
-### 10. Incident 回调
-
-子模组可注册事件执行回调,用于跨模组协调:
-
-```csharp
-RimMindAPI.RegisterIncidentExecutedCallback(() =>
-{
- // 事件执行后的处理逻辑
-});
-
-// 叙事者执行事件后通知
-RimMindAPI.NotifyIncidentExecuted();
-```
+仅修改请求切片时,先运行 `Source/Application/Features/Requests/README.md` 中的聚焦命令。
-## AI 响应格式标准
+## Do not
-| 子模组 | 标签 | JSON Schema |
-|--------|------|------------|
-| RimMind-Storyteller | `` | `{"defName": string, "reason": string, "params": {...}?, "chain": {...}?}` |
-| RimMind-Advisor | `` | `{"advices": [{action, pawn?, target?, param?, reason, request_type?}]}` |
-| RimMind-Personality | `` | `{"thoughts": [{type, label, description, intensity, duration_hours?}], "narrative": string}` |
-| RimMind-Dialogue | `` | `{"reply": string, "thought": {"tag": string, "description": string}, "relation_delta"?: float}` |
+- 不在 Tick 中遍历全地图小人或直接发起网络请求。
+- 不用 `Task.Run` 包装 Verse/Unity 调用。
+- 不新增服务定位器、DI 框架、镜像队列或仅有一个实现的装饰性接口。
+- 不硬编码 UI 中文;使用 Keyed XML。
+- 不运行或宣称已通过当前缺失资源所阻塞的游戏内 E2E。
diff --git a/About/About.xml b/About/About.xml
index 17d283f6..0d7c9e7a 100644
--- a/About/About.xml
+++ b/About/About.xml
@@ -1,21 +1,21 @@
- RimMind - Core
- mcocdaa
- mcocdaa.RimMindCore
- 0.0.1
-
- 1.6
-
- Infrastructure for AI-powered RimWorld mods. Provides LLM client, async request queue, game context builders, and debug tooling. Required by all RimMind suite components.
-
-
- brrainz.harmony
- Harmony
- steam://url/CommunityFilePage/2009463077
-
-
-
- brrainz.harmony
-
+ RimMind - Core
+ mcocdaa
+ mcocdaa.RimMindCore
+ 2.0.0
+
+ 1.6
+
+ Infrastructure for AI-powered RimWorld mods. Provides LLM client, async request queue, game context builders, and debug tooling. Required by all RimMind suite components. v2.0 uses a three-DLL loading architecture (0_RimMindDomain, 1_RimMindApplication, 2_RimMindCore) for modular assembly loading. [v2.0 BREAKING CHANGES] Save games from v1.x are NOT compatible with v2.0.
+
+
+ brrainz.harmony
+ Harmony
+ steam://url/CommunityFilePage/2009463077
+
+
+
+ brrainz.harmony
+
diff --git a/About/PublishedFileId.txt b/About/PublishedFileId.txt
index 1137cc4d..16e1049f 100644
--- a/About/PublishedFileId.txt
+++ b/About/PublishedFileId.txt
@@ -1 +1 @@
-3707741395
\ No newline at end of file
+3707741395
diff --git a/ArchTests/ArchTestExtensions.cs b/ArchTests/ArchTestExtensions.cs
new file mode 100644
index 00000000..73297a83
--- /dev/null
+++ b/ArchTests/ArchTestExtensions.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using System.Xml.Linq;
+using NetArchTest.Rules;
+
+namespace RimMind.Core.ArchTests
+{
+ public static class ArchTestExtensions
+ {
+ public static string FormatFailingTypes(this TestResult result)
+ {
+ if (result.IsSuccessful)
+ return string.Empty;
+
+ var names = result.FailingTypes?
+ .Select(t => t.FullName ?? t.Name)
+ .OrderBy(n => n)
+ ?? Enumerable.Empty();
+
+ return string.Join("\n ", names);
+ }
+
+ public static string FormatFailingTypes(this IEnumerable types)
+ {
+ var names = types
+ .Select(t => t.FullName ?? t.Name)
+ .OrderBy(n => n);
+
+ return string.Join("\n ", names);
+ }
+
+ public static string FindSourceDirectory()
+ {
+ var dir = Path.GetDirectoryName(typeof(ArchTestExtensions).Assembly.Location);
+ while (dir != null)
+ {
+ var candidate = Path.Combine(dir, "RimMind-Core", "Source");
+ if (Directory.Exists(candidate)) return candidate;
+
+ candidate = Path.Combine(dir, "Source");
+ if (Directory.Exists(candidate)) return candidate;
+
+ dir = Directory.GetParent(dir)?.FullName;
+ }
+ return "";
+ }
+
+ public static string LocateAssembly(string fileName)
+ {
+ var sourceDir = FindSourceDirectory();
+ var gameVersion = "1.6";
+
+ var candidates = new List();
+
+ if (!string.IsNullOrEmpty(sourceDir))
+ {
+ candidates.Add(Path.Combine(sourceDir, "..", gameVersion, "Assemblies", fileName));
+ candidates.Add(Path.Combine(sourceDir, "..", "Assemblies", fileName));
+ }
+
+ candidates.Add(Path.Combine(AppContext.BaseDirectory, fileName));
+
+ foreach (var candidate in candidates)
+ {
+ var full = Path.GetFullPath(candidate);
+ if (File.Exists(full))
+ return full;
+ }
+
+ throw new FileNotFoundException(
+ $"R-D: 找不到 {fileName}。搜索路径:\n {string.Join("\n ", candidates.Select(Path.GetFullPath))}\n" +
+ "请先构建三个源码项目: dotnet build RimMind-Core/Source/Domain/RimMindCore.Domain.csproj 等");
+ }
+
+ public static bool TryLocateAssembly(string fileName, out string? path)
+ {
+ path = null;
+ try
+ {
+ path = LocateAssembly(fileName);
+ return true;
+ }
+ catch (FileNotFoundException)
+ {
+ return false;
+ }
+ }
+
+ public static HashSet GetAssemblyReferences(string assemblyPath)
+ {
+ var refs = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ using var stream = File.OpenRead(assemblyPath);
+ using var peReader = new PEReader(stream);
+ var metadataReader = peReader.GetMetadataReader();
+
+ foreach (var handle in metadataReader.AssemblyReferences)
+ {
+ var asmRef = metadataReader.GetAssemblyReference(handle);
+ var name = metadataReader.GetString(asmRef.Name);
+ refs.Add(name);
+ }
+
+ return refs;
+ }
+
+ public static CsprojAnalysis AnalyzeCsproj(string csprojPath)
+ {
+ if (!File.Exists(csprojPath))
+ throw new FileNotFoundException($"csproj not found: {csprojPath}");
+
+ var doc = XDocument.Load(csprojPath);
+ var ns = doc.Root?.Name.Namespace ?? XNamespace.None;
+
+ var packageRefs = doc.Descendants(ns + "PackageReference")
+ .Select(el => new PackageRef
+ {
+ Include = el.Attribute("Include")?.Value ?? "",
+ Version = el.Attribute("Version")?.Value ?? el.Element(ns + "Version")?.Value ?? ""
+ })
+ .ToList();
+
+ var projectRefs = doc.Descendants(ns + "ProjectReference")
+ .Select(el => el.Attribute("Include")?.Value ?? "")
+ .ToList();
+
+ var assemblyName = doc.Descendants(ns + "AssemblyName")
+ .Select(el => el.Value)
+ .FirstOrDefault() ?? Path.GetFileNameWithoutExtension(csprojPath);
+
+ var targetFramework = doc.Descendants(ns + "TargetFramework")
+ .Select(el => el.Value)
+ .FirstOrDefault() ?? "";
+
+ return new CsprojAnalysis
+ {
+ Path = csprojPath,
+ AssemblyName = assemblyName,
+ TargetFramework = targetFramework,
+ PackageReferences = packageRefs,
+ ProjectReferences = projectRefs
+ };
+ }
+ }
+
+ public class CsprojAnalysis
+ {
+ public string Path { get; set; } = "";
+ public string AssemblyName { get; set; } = "";
+ public string TargetFramework { get; set; } = "";
+ public List PackageReferences { get; set; } = new();
+ public List ProjectReferences { get; set; } = new();
+
+ public bool HasPackageRef(string name) =>
+ PackageReferences.Any(pr => string.Equals(pr.Include, name, StringComparison.OrdinalIgnoreCase));
+
+ public bool HasProjectRef(string name) =>
+ ProjectReferences.Any(pr => pr.Contains(name, StringComparison.OrdinalIgnoreCase));
+ }
+
+ public class PackageRef
+ {
+ public string Include { get; set; } = "";
+ public string Version { get; set; } = "";
+ }
+}
diff --git a/ArchTests/Contracts/CrossModBoundaryContracts.cs b/ArchTests/Contracts/CrossModBoundaryContracts.cs
new file mode 100644
index 00000000..4beaa271
--- /dev/null
+++ b/ArchTests/Contracts/CrossModBoundaryContracts.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using RimMind.Testing;
+using Xunit;
+
+namespace RimMind.Core.ArchTests.Contracts
+{
+ public sealed class CrossModBoundaryContracts
+ {
+ [Fact]
+ public void Dependent_mods_use_capabilities_instead_of_runtime_hub_access()
+ {
+ ContractCaseRunner.Run(
+ ("runtime singleton is not consumed", () => AssertDependentSourceAbsent(@"RimMindRuntime\s*\.\s*Instance")),
+ ("legacy locator is not consumed", () => AssertDependentSourceAbsent(@"\bRimMindServiceLocator\b")),
+ ("generic service lookup is not consumed", () => AssertDependentSourceAbsent(@"\bGetService\s*<")),
+ ("generic try lookup is not consumed", () => AssertDependentSourceAbsent(@"\bTryGetService\s*<")),
+ ("runtime service hub is not consumed", () => AssertDependentSourceAbsent(@"\bRuntimeServiceHub\b")),
+ ("runtime snapshots are not consumed", () => AssertDependentSourceAbsent(@"\bRuntimeServiceSnapshot\b")));
+ }
+
+ [Fact]
+ public void Feature_mod_dependencies_remain_directional()
+ {
+ ContractCaseRunner.Run(
+ ("actions does not depend on feature mods", () =>
+ AssertNoUsing("RimMind-Actions", "Advisor", "Memory", "Personality", "Dialogue", "Storyteller", "Bridge")),
+ ("advisor does not depend on state-producing feature mods", () =>
+ AssertNoUsing("RimMind-Advisor", "Memory", "Personality", "Dialogue", "Storyteller", "Bridge")),
+ ("memory does not depend on peer feature mods", () =>
+ AssertNoUsing("RimMind-Memory", "Actions", "Advisor", "Personality", "Dialogue", "Storyteller", "Bridge")),
+ ("personality does not depend on peer feature mods", () =>
+ AssertNoUsing("RimMind-Personality", "Actions", "Advisor", "Memory", "Dialogue", "Storyteller", "Bridge")),
+ ("dialogue does not depend on peer feature mods", () =>
+ AssertNoUsing("RimMind-Dialogue", "Actions", "Advisor", "Memory", "Personality", "Storyteller", "Bridge")),
+ ("storyteller does not depend on peer feature mods", () =>
+ AssertNoUsing("RimMind-Storyteller", "Actions", "Advisor", "Memory", "Personality", "Dialogue", "Bridge")));
+ }
+
+ [Fact]
+ public void Bridge_mods_stay_on_declared_public_seams()
+ {
+ ContractCaseRunner.Run(
+ ("RimTalk consumes only public data seams from feature mods", () =>
+ AssertNoPattern(
+ "RimMind-Bridge-RimTalk",
+ @"^\s*using\s+RimMind\.(?:Advisor|Memory|Personality)\.(?!Data(?:\.|;))")),
+ ("RimTalk does not consume dialogue or storyteller implementations", () =>
+ AssertNoUsing("RimMind-Bridge-RimTalk", "Dialogue", "Storyteller")),
+ ("RimChat does not consume peer feature implementations", () =>
+ AssertNoUsing("RimMind-Bridge-RimChat", "Actions", "Advisor", "Memory", "Personality", "Dialogue", "Storyteller")),
+ ("bridges do not consume Core infrastructure", () =>
+ {
+ AssertNoPattern("RimMind-Bridge-RimTalk", @"^\s*using\s+RimMind\.Infrastructure(?:\.|;)");
+ AssertNoPattern("RimMind-Bridge-RimChat", @"^\s*using\s+RimMind\.Infrastructure(?:\.|;)");
+ }),
+ ("bridges do not consume Core runtime composition", () =>
+ {
+ AssertNoPattern("RimMind-Bridge-RimTalk", @"^\s*using\s+RimMind\.Presentation\.Runtime(?:\.|;)");
+ AssertNoPattern("RimMind-Bridge-RimChat", @"^\s*using\s+RimMind\.Presentation\.Runtime(?:\.|;)");
+ }));
+ }
+
+ private static void AssertDependentSourceAbsent(string forbiddenPattern)
+ {
+ var pattern = new Regex(forbiddenPattern, RegexOptions.CultureInvariant);
+ string root = RepositoryRoot();
+ IReadOnlyList violations = DiscoverDependentMods()
+ .SelectMany(mod => SourceFiles(Path.Combine(root, mod, "Source")))
+ .Where(file => pattern.IsMatch(StripCommentsAndStrings(File.ReadAllText(file))))
+ .Select(file => Path.GetRelativePath(root, file))
+ .OrderBy(path => path, StringComparer.Ordinal)
+ .ToArray();
+ Assert.True(
+ violations.Count == 0,
+ $"Dependent Mods must not consume '{forbiddenPattern}':{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
+ }
+
+ private static void AssertNoUsing(string mod, params string[] forbiddenModules)
+ {
+ string alternatives = string.Join("|", forbiddenModules.Select(Regex.Escape));
+ AssertNoPattern(mod, $@"^\s*using\s+RimMind\.(?:{alternatives})(?:\.|;)");
+ }
+
+ private static void AssertNoPattern(string mod, string forbiddenPattern)
+ {
+ string root = RepositoryRoot();
+ var pattern = new Regex(
+ forbiddenPattern,
+ RegexOptions.Multiline | RegexOptions.CultureInvariant);
+ IReadOnlyList violations = SourceFiles(Path.Combine(root, mod, "Source"))
+ .Where(file => pattern.IsMatch(StripCommentsAndStrings(File.ReadAllText(file))))
+ .Select(file => Path.GetRelativePath(root, file))
+ .OrderBy(path => path, StringComparer.Ordinal)
+ .ToArray();
+ Assert.True(
+ violations.Count == 0,
+ $"{mod} crosses a forbidden Mod boundary:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
+ }
+
+ private static IReadOnlyList DiscoverDependentMods()
+ {
+ string root = RepositoryRoot();
+ return Directory.EnumerateDirectories(root, "RimMind-*", SearchOption.TopDirectoryOnly)
+ .Where(directory => !Path.GetFileName(directory).Equals("RimMind-Core", StringComparison.OrdinalIgnoreCase))
+ .Where(directory => Directory.Exists(Path.Combine(directory, "Source")))
+ .Select(Path.GetFileName)
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .Cast()
+ .OrderBy(name => name, StringComparer.Ordinal)
+ .ToArray();
+ }
+
+ private static string StripCommentsAndStrings(string source)
+ {
+ return Regex.Replace(
+ source,
+ @"@""(?:""""|[^""])*""|""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])'|//[^\r\n]*|/\*.*?\*/",
+ " ",
+ RegexOptions.Singleline | RegexOptions.CultureInvariant);
+ }
+
+ private static IEnumerable SourceFiles(string directory)
+ {
+ Assert.True(Directory.Exists(directory), $"Source directory must exist: {directory}");
+ return Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories)
+ .Where(path => !path.Split(Path.DirectorySeparatorChar).Any(part =>
+ part.Equals("bin", StringComparison.OrdinalIgnoreCase) ||
+ part.Equals("obj", StringComparison.OrdinalIgnoreCase) ||
+ part.Equals("backup", StringComparison.OrdinalIgnoreCase)));
+ }
+
+ private static string RepositoryRoot()
+ {
+ string source = ArchTestExtensions.FindSourceDirectory();
+ Assert.True(Directory.Exists(source), "RimMind-Core/Source must be discoverable.");
+ DirectoryInfo? core = Directory.GetParent(source);
+ Assert.NotNull(core);
+ DirectoryInfo? root = core!.Parent;
+ Assert.NotNull(root);
+ return root!.FullName;
+ }
+ }
+}
diff --git a/ArchTests/Contracts/LayeringContracts.cs b/ArchTests/Contracts/LayeringContracts.cs
new file mode 100644
index 00000000..629c6403
--- /dev/null
+++ b/ArchTests/Contracts/LayeringContracts.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using RimMind.Testing;
+using Xunit;
+
+namespace RimMind.Core.ArchTests.Contracts
+{
+ public sealed class LayeringContracts
+ {
+ [Fact]
+ public void Domain_remains_independent_of_outer_layers_and_game_frameworks()
+ {
+ ContractCaseRunner.Run(
+ ("domain does not depend on application", () => AssertNoUsing("Domain", "RimMind.Application")),
+ ("domain does not depend on presentation", () => AssertNoUsing("Domain", "RimMind.Presentation")),
+ ("domain does not depend on infrastructure", () => AssertNoUsing("Domain", "RimMind.Infrastructure")),
+ ("domain does not depend on Verse", () => AssertNoUsing("Domain", "Verse")),
+ ("domain does not depend on Harmony", () => AssertNoUsing("Domain", "HarmonyLib")),
+ ("domain does not depend on Unity", () => AssertNoUsing("Domain", "UnityEngine")));
+ }
+
+ [Fact]
+ public void Application_depends_on_ports_not_outer_adapters()
+ {
+ ContractCaseRunner.Run(
+ ("application does not depend on presentation", () => AssertNoUsing("Application", "RimMind.Presentation")),
+ ("application does not depend on infrastructure", () => AssertNoUsing("Application", "RimMind.Infrastructure")),
+ ("application does not depend on Verse", () => AssertNoUsing("Application", "Verse")),
+ ("application does not depend on Harmony", () => AssertNoUsing("Application", "HarmonyLib")),
+ ("application does not depend on Unity", () => AssertNoUsing("Application", "UnityEngine")),
+ ("application does not depend on RimWorld adapters", () => AssertNoUsing("Application", "RimWorld")));
+ }
+
+ [Fact]
+ public void Project_references_follow_the_clean_architecture_direction()
+ {
+ ContractCaseRunner.Run(
+ ("domain has no project dependency", () =>
+ Assert.Empty(Project("Domain", "RimMindCore.Domain.csproj").ProjectReferences)),
+ ("application depends inward on domain", () =>
+ Assert.True(Project("Application", "RimMindCore.Application.csproj").HasProjectRef("Domain"))),
+ ("application does not depend on outer core", () =>
+ Assert.False(Project("Application", "RimMindCore.Application.csproj").HasProjectRef("RimMindCore.csproj"))),
+ ("outer core depends on application", () =>
+ Assert.True(Project(".", "RimMindCore.csproj").HasProjectRef("Application"))),
+ ("game framework packages stay out of domain", () =>
+ {
+ CsprojAnalysis domain = Project("Domain", "RimMindCore.Domain.csproj");
+ Assert.False(domain.HasPackageRef("Krafs.Rimworld.Ref"));
+ Assert.False(domain.HasPackageRef("Lib.Harmony.Ref"));
+ }),
+ ("game framework packages stay out of application", () =>
+ {
+ CsprojAnalysis application = Project("Application", "RimMindCore.Application.csproj");
+ Assert.False(application.HasPackageRef("Krafs.Rimworld.Ref"));
+ Assert.False(application.HasPackageRef("Lib.Harmony.Ref"));
+ }));
+ }
+
+ private static CsprojAnalysis Project(string directory, string projectName)
+ {
+ string source = RequireSourceDirectory();
+ return ArchTestExtensions.AnalyzeCsproj(Path.GetFullPath(Path.Combine(source, directory, projectName)));
+ }
+
+ private static void AssertNoUsing(string relativeLayer, string forbiddenNamespace)
+ {
+ string source = RequireSourceDirectory();
+ string layer = Path.Combine(source, relativeLayer);
+ var pattern = new Regex(
+ $@"^\s*using\s+(?:global::)?{Regex.Escape(forbiddenNamespace)}(?:\.|;)",
+ RegexOptions.Multiline | RegexOptions.CultureInvariant);
+ IReadOnlyList violations = SourceFiles(layer)
+ .Where(file => pattern.IsMatch(StripCommentsAndStrings(File.ReadAllText(file))))
+ .Select(file => Path.GetRelativePath(source, file))
+ .OrderBy(path => path, StringComparer.Ordinal)
+ .ToArray();
+
+ Assert.True(
+ violations.Count == 0,
+ $"{relativeLayer} must not reference {forbiddenNamespace}:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
+ }
+
+ private static string StripCommentsAndStrings(string source)
+ {
+ return Regex.Replace(
+ source,
+ @"@""(?:""""|[^""])*""|""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])'|//[^\r\n]*|/\*.*?\*/",
+ " ",
+ RegexOptions.Singleline | RegexOptions.CultureInvariant);
+ }
+
+ private static IEnumerable SourceFiles(string directory)
+ {
+ return Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories)
+ .Where(path => !path.Split(Path.DirectorySeparatorChar).Any(part =>
+ part.Equals("bin", StringComparison.OrdinalIgnoreCase) ||
+ part.Equals("obj", StringComparison.OrdinalIgnoreCase) ||
+ part.Equals("backup", StringComparison.OrdinalIgnoreCase)));
+ }
+
+ private static string RequireSourceDirectory()
+ {
+ string source = ArchTestExtensions.FindSourceDirectory();
+ Assert.True(Directory.Exists(source), "RimMind-Core/Source must be discoverable.");
+ return source;
+ }
+ }
+}
diff --git a/ArchTests/Contracts/RuntimeBoundaryContracts.cs b/ArchTests/Contracts/RuntimeBoundaryContracts.cs
new file mode 100644
index 00000000..20333e43
--- /dev/null
+++ b/ArchTests/Contracts/RuntimeBoundaryContracts.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using RimMind.Testing;
+using Xunit;
+
+namespace RimMind.Core.ArchTests.Contracts
+{
+ public sealed class RuntimeBoundaryContracts
+ {
+ [Fact]
+ public void Production_code_has_no_global_service_lookup_backdoor()
+ {
+ ContractCaseRunner.Run(
+ ("runtime singleton pulls are absent", () => AssertSourceAbsent(@"RimMindRuntime\s*\.\s*Instance")),
+ ("legacy locator symbol is absent", () => AssertSourceAbsent(@"\bRimMindServiceLocator\b")),
+ ("generic service lookup is absent", () => AssertSourceAbsent(@"\bGetService\s*<")),
+ ("generic try lookup is absent", () => AssertSourceAbsent(@"\bTryGetService\s*<")),
+ ("ambient service provider pulls are absent", () => AssertSourceAbsent(@"\bServiceProvider\s*\.\s*GetService\b")));
+ }
+
+ [Fact]
+ public void Application_cannot_pull_runtime_or_game_state()
+ {
+ ContractCaseRunner.Run(
+ ("application does not name runtime host", () => AssertLayerAbsent("Application", @"\bRimMindRuntimeHost\b")),
+ ("application does not name composition root", () => AssertLayerAbsent("Application", @"\bRimMindCompositionRoot\b")),
+ ("application does not pull Verse Find", () => AssertLayerAbsent("Application", @"\b(?:Verse\.)?Find\s*\.")),
+ ("application does not open window stack", () => AssertLayerAbsent("Application", @"\bWindowStack\b")),
+ ("application does not pull current game", () => AssertLayerAbsent("Application", @"\bCurrent\s*\.\s*Game\b")),
+ ("application does not construct runtime service refs", () => AssertLayerAbsent("Application", @"\bRuntimeServiceRef\s*<")));
+ }
+
+ [Fact]
+ public void Runtime_container_types_remain_in_outer_adapters()
+ {
+ ContractCaseRunner.Run(
+ ("runtime host belongs to presentation runtime", () =>
+ AssertDeclarationUnder("RimMindRuntimeHost", Path.Combine("Presentation", "Runtime"))),
+ ("composition root belongs to presentation runtime", () =>
+ AssertDeclarationUnder("RimMindCompositionRoot", Path.Combine("Presentation", "Runtime"))),
+ ("service hub belongs to runtime services", () =>
+ AssertDeclarationUnder("RuntimeServiceHub", Path.Combine("Presentation", "Runtime", "Services"))),
+ ("service snapshot belongs to runtime services", () =>
+ AssertDeclarationUnder("RuntimeServiceSnapshot", Path.Combine("Presentation", "Runtime", "Services"))),
+ ("service scope belongs to runtime services", () =>
+ AssertDeclarationUnder("RuntimeServiceScope", Path.Combine("Presentation", "Runtime", "Services"))),
+ ("completion fence remains an application port", () =>
+ AssertDeclarationUnder("ICompletionFence", Path.Combine("Application", "Common", "Interfaces"))));
+ }
+
+ private static void AssertSourceAbsent(string forbiddenPattern)
+ {
+ AssertAbsent(SourceFiles(RequireSourceDirectory()), RequireSourceDirectory(), forbiddenPattern);
+ }
+
+ private static void AssertLayerAbsent(string layer, string forbiddenPattern)
+ {
+ string source = RequireSourceDirectory();
+ AssertAbsent(SourceFiles(Path.Combine(source, layer)), source, forbiddenPattern);
+ }
+
+ private static void AssertAbsent(
+ IEnumerable files,
+ string source,
+ string forbiddenPattern)
+ {
+ var pattern = new Regex(forbiddenPattern, RegexOptions.CultureInvariant);
+ IReadOnlyList violations = files
+ .Where(file => pattern.IsMatch(StripCommentsAndStrings(File.ReadAllText(file))))
+ .Select(file => Path.GetRelativePath(source, file))
+ .OrderBy(path => path, StringComparer.Ordinal)
+ .ToArray();
+ Assert.True(
+ violations.Count == 0,
+ $"Forbidden runtime dependency '{forbiddenPattern}':{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
+ }
+
+ private static void AssertDeclarationUnder(string typeName, string expectedRelativeDirectory)
+ {
+ string source = RequireSourceDirectory();
+ var declaration = new Regex(
+ $@"\b(?:class|interface|struct|record)\s+{Regex.Escape(typeName)}\b",
+ RegexOptions.CultureInvariant);
+ string[] matches = SourceFiles(source)
+ .Where(file => declaration.IsMatch(StripCommentsAndStrings(File.ReadAllText(file))))
+ .ToArray();
+ string expected = Path.GetFullPath(Path.Combine(source, expectedRelativeDirectory))
+ .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
+
+ Assert.NotEmpty(matches);
+ Assert.All(matches, file => Assert.StartsWith(expected, Path.GetFullPath(file), StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static string StripCommentsAndStrings(string source)
+ {
+ return Regex.Replace(
+ source,
+ @"@""(?:""""|[^""])*""|""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])'|//[^\r\n]*|/\*.*?\*/",
+ " ",
+ RegexOptions.Singleline | RegexOptions.CultureInvariant);
+ }
+
+ private static IEnumerable SourceFiles(string directory)
+ {
+ return Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories)
+ .Where(path => !path.Split(Path.DirectorySeparatorChar).Any(part =>
+ part.Equals("bin", StringComparison.OrdinalIgnoreCase) ||
+ part.Equals("obj", StringComparison.OrdinalIgnoreCase) ||
+ part.Equals("backup", StringComparison.OrdinalIgnoreCase)));
+ }
+
+ private static string RequireSourceDirectory()
+ {
+ string source = ArchTestExtensions.FindSourceDirectory();
+ Assert.True(Directory.Exists(source), "RimMind-Core/Source must be discoverable.");
+ return source;
+ }
+ }
+}
diff --git a/ArchTests/RimMindCore.ArchTests.csproj b/ArchTests/RimMindCore.ArchTests.csproj
new file mode 100644
index 00000000..88ef41b2
--- /dev/null
+++ b/ArchTests/RimMindCore.ArchTests.csproj
@@ -0,0 +1,42 @@
+
+
+
+ Library
+ net10.0
+ RimMind.Core.ArchTests
+ RimMindCore.ArchTests
+ 10.0
+ enable
+ $(NoWarn);NU1702;CS0436
+ false
+ true
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ArchTests/xunit.runner.json b/ArchTests/xunit.runner.json
new file mode 100644
index 00000000..48e1cffb
--- /dev/null
+++ b/ArchTests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "parallelizeTestCollections": true,
+ "maxParallelThreads": -1
+}
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 24d2f3ac..00000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,685 +0,0 @@
-# AGENTS.md — RimMind-Core
-
-本文件供 AI 编码助手阅读,描述 RimMind-Core 的架构、代码约定和扩展模式。
-
-## 项目定位
-
-RimMind-Core 是 RimMind AI 模组套件的核心基础设施层。所有子模组(Actions、Personality、Advisor、Memory 等)均依赖本模组。职责:
-
-1. **LLM 客户端**:OpenAI Chat Completions 兼容,通过 `UnityWebRequest` 发送
-2. **异步请求队列**:后台线程发请求,主线程回调,`ConcurrentQueue` 桥接
-3. **游戏上下文构建**:将游戏状态打包为中文文本,供 AI Prompt 使用
-4. **Prompt 组装系统**:`StructuredPromptBuilder` + `PromptSection` + `PromptBudget` + `ContextComposer`,支持优先级排序、压缩与 Token 预算裁剪
-5. **Provider 注册机制**:子模组通过注册 API 注入上下文段,支持卸载与覆盖,实现解耦
-6. **请求审批悬浮窗**:`RequestOverlay` + `RequestEntry`,子模组可注册待审批请求供玩家选择
-7. **设置 UI**:多分页设置界面,子模组可注册额外分页
-8. **调试工具**:AI Debug Log 窗口、Dev DebugAction
-
-## 源码结构
-
-```
-Source/
-├── AICoreMod.cs Mod 入口,注册 Harmony,持有 Settings 单例
-├── AICoreAPI.cs 静态公共 API(RimMindAPI),供子模组调用
-├── Client/
-│ ├── IAIClient.cs AI 客户端接口
-│ ├── AIRequest.cs 请求数据结构(含 ChatMessage 多轮支持)
-│ ├── AIResponse.cs 响应数据结构
-│ └── OpenAI/
-│ ├── OpenAIClient.cs OpenAI 兼容客户端实现
-│ └── OpenAIDto.cs 请求/响应 DTO(internal)
-├── Core/
-│ ├── AIRequestQueue.cs GameComponent,异步请求队列 + 冷却管理
-│ ├── GameContextBuilder.cs 静态工具类,构建地图/Pawn 上下文文本(含 Section 和精简版本)
-│ ├── JsonTagExtractor.cs 从 AI 响应提取 JSON 内容
-│ ├── AIDebugLog.cs GameComponent,存储最近 200 条请求记录(含 AIDebugEntry)
-│ └── Prompt/
-│ ├── StructuredPromptBuilder.cs 流式 Prompt 构建器(链式 API + FromKeyPrefix + ToSection)
-│ ├── PromptSection.cs Prompt 段落(Tag/Content/Priority/EstimatedTokens/Compress)
-│ ├── PromptBudget.cs Token 预算管理(先压缩后裁剪)
-│ └── ContextComposer.cs 段落排序 + 历史压缩
-├── Settings/
-│ ├── AICoreSettings.cs 模组设置(API 配置 + 性能 + 调试 + 流式)
-│ └── ContextSettings.cs 上下文过滤器(21 个 Include* 字段 + ContextPreset 预设)
-├── UI/
-│ ├── AICoreSettingsUI.cs 多分页设置界面
-│ ├── Window_AIDebugLog.cs AI Debug Log 浮动窗口
-│ ├── RequestOverlay.cs 请求审批悬浮窗(可拖拽/可缩放/可持久化位置)
-│ ├── RequestEntry.cs 悬浮窗请求条目数据结构
-│ ├── Window_RequestLog.cs 请求日志窗口
-│ └── SettingsUIHelper.cs 设置 UI 辅助工具类
-├── Patch/
-│ ├── AITogglePatch.cs 右下角 AI 图标按钮注入
-│ └── Patch_UIRoot_OnGUI.cs 每帧调用 RequestOverlay.OnGUI
-└── Debug/
- └── AICoreDebugActions.cs Dev 菜单调试动作
-```
-
-## 关键类与 API
-
-### RimMindAPI(AICoreAPI.cs)
-
-所有子模组通过此静态类与 Core 交互:
-
-```csharp
-// ── 请求 ──
-RimMindAPI.RequestAsync(AIRequest request, Action onComplete)
-RimMindAPI.RequestImmediate(AIRequest request, Action onComplete) // 绕过队列/冷却
-
-// ── 上下文构建 ──
-RimMindAPI.BuildMapContext(Map map, bool brief = false)
-RimMindAPI.BuildPawnContext(Pawn pawn)
-RimMindAPI.BuildStaticContext()
-RimMindAPI.BuildHistoryContext(int maxEntries = 10)
-RimMindAPI.BuildFullPawnPrompt(Pawn pawn, string? currentQuery, string[]? excludeProviders)
-RimMindAPI.BuildFullPawnPrompt(Pawn pawn, PromptBudget budget, string? currentQuery, string[]? excludeProviders)
-RimMindAPI.BuildFullPawnSections(Pawn pawn, string? currentQuery, string[]? excludeProviders)
-
-// ── Provider 注册(子模组在 Mod 构造时调用)──
-RimMindAPI.RegisterStaticProvider(string category, Func provider, int priority, string modId = "", bool overrideExisting = true)
-RimMindAPI.RegisterDynamicProvider(string category, Func provider, int priority, string modId = "", bool overrideExisting = true)
-RimMindAPI.RegisterPawnContextProvider(string category, Func provider, int priority, string modId = "", bool overrideExisting = true)
-
-// ── Provider 卸载 ──
-RimMindAPI.UnregisterStaticProvider(string category)
-RimMindAPI.UnregisterDynamicProvider(string category)
-RimMindAPI.UnregisterPawnContextProvider(string category)
-RimMindAPI.UnregisterModProviders(string modId) // 卸载某模组注册的所有 Provider
-
-// ── UI 扩展 ──
-RimMindAPI.RegisterSettingsTab(string tabId, Func labelFn, Action drawFn)
-RimMindAPI.SettingsTabs // IReadOnlyList<(tabId, labelFn, drawFn)>
-RimMindAPI.RegisterToggleBehavior(string id, Func isActive, Action toggle)
-RimMindAPI.HasToggleBehaviors // getter
-
-// ── 冷却控制 ──
-RimMindAPI.RegisterModCooldown(string modId, Func getCooldownTicks)
-RimMindAPI.GetModCooldownGetter(string modId) // Func?
-RimMindAPI.ModCooldownGetters // IReadOnlyDictionary>
-
-// ── 对话触发 ──
-RimMindAPI.RegisterDialogueTrigger(Action triggerFn)
-RimMindAPI.TriggerDialogue(Pawn pawn, string context, Pawn? recipient)
-RimMindAPI.CanTriggerDialogue // getter
-
-// ── 请求审批悬浮窗 ──
-RimMindAPI.RegisterPendingRequest(RequestEntry entry)
-RimMindAPI.GetPendingRequests() // IReadOnlyList
-RimMindAPI.RemovePendingRequest(RequestEntry entry)
-
-// ── 状态查询 ──
-RimMindAPI.IsConfigured()
-RimMindAPI.IsAnyToggleActive()
-RimMindAPI.ToggleAll()
-```
-
-### AIRequest / AIResponse
-
-```csharp
-class AIRequest {
- string SystemPrompt;
- string UserPrompt; // 单轮模式
- List? Messages; // 多轮模式(非 null 时忽略 UserPrompt)
- int MaxTokens; // 默认 800
- float Temperature; // 默认 0.7
- string RequestId; // 格式:"ModName_Purpose_Tick"
- string ModId; // 模组标识,用于冷却分组
- int ExpireAtTicks; // 过期时间,0=不过期
- bool UseJsonMode; // 默认 true,设 false 绕过 response_format
-}
-
-class ChatMessage {
- string Role; // "system" / "user" / "assistant"
- string Content;
-}
-
-class AIResponse {
- bool Success;
- string Content;
- string Error;
- int TokensUsed;
- string RequestId;
- static AIResponse Failure(string requestId, string error);
- static AIResponse Ok(string requestId, string content, int tokens);
-}
-```
-
-### Prompt 组装系统
-
-#### StructuredPromptBuilder
-
-流式构建 System Prompt,链式 API:
-
-```csharp
-var prompt = new StructuredPromptBuilder()
- .Role("你是一个 RimWorld 殖民者")
- .Goal("根据状态做出决策")
- .Process("1. 分析状态 2. 选择动作")
- .Constraint("只能选择候选列表中的动作")
- .Output("JSON 格式")
- .Example("{\"action\": \"assign_work\", ...}")
- .Fallback("选择 force_rest")
- .Build();
-```
-
-翻译键版本:`RoleFromKey()`, `GoalFromKey()` 等,配合 Keyed 翻译系统使用。
-
-快捷方式:
-
-```csharp
-// 从翻译键前缀一键构建(自动拼接 .Role/.Goal/.Process/... 后缀)
-StructuredPromptBuilder.FromKeyPrefix("RimMind.MyMod")
-
-// 带翻译键表头的自定义段
-.WithCustom(customText, "RimMind.MyMod.CustomHeader")
-
-// 启用段落标签输出([角色] [目标] ...)
-.WithSectionLabels(true)
-
-// 直接转为 PromptSection(用于注入上下文系统)
-.ToSection(tag: "system_prompt", priority: PromptSection.PriorityCore)
-```
-
-#### PromptSection
-
-```csharp
-class PromptSection {
- string Tag; // 段落标识
- string Content; // 段落内容
- int Priority; // 优先级(低=重要,不可裁剪)
- int EstimatedTokens; // 估算 Token 数
- Func? Compress; // 压缩回调(非 null 时可在超预算时压缩而非删除)
-
- // 优先级常量
- const int PriorityCore = 0; // 核心指令,不可裁剪
- const int PriorityCurrentInput = 1; // 当前输入
- const int PriorityKeyState = 3; // 关键状态
- const int PriorityMemory = 5; // 记忆上下文
- const int PriorityAuxiliary = 8; // 辅助上下文
- const int PriorityCustom = 10; // 自定义内容
-
- bool IsTrimable => Priority > PriorityCore;
- bool IsCompressible => Compress != null && IsTrimable;
- static int EstimateTokens(string text); // 混合 CJK/Latin 估算
-}
-```
-
-#### PromptBudget
-
-```csharp
-class PromptBudget {
- int TotalBudget = 4000; // 总 Token 预算
- int ReserveForOutput = 800; // 为输出预留
- int AvailableForInput; // 可用于输入的 Token 数
-
- PromptBudget()
- PromptBudget(int totalBudget, int reserveForOutput = 800)
-
- List Compose(List sections); // 按预算裁剪
- string ComposeToString(List sections); // 裁剪后拼接
-}
-```
-
-裁剪逻辑(两阶段):
-1. **压缩阶段**:对 `IsCompressible` 的段,按优先级从高到低调用 `Compress` 回调,直到总 Token 在预算内
-2. **裁剪阶段**:压缩后仍超预算,对 `IsTrimable` 的段按优先级从高到低删除,直到总 Token 在预算内
-
-#### ContextComposer
-
-```csharp
-static class ContextComposer {
- List Reorder(List sections); // 按优先级排序
- string BuildFromSections(List sections); // 排序后拼接
- string CompressHistory(string historyText, int maxLines = 6, string summaryLine = ""); // 历史压缩
-}
-```
-
-### GameContextBuilder
-
-```csharp
-static class GameContextBuilder {
- // 纯文本版本
- string BuildMapContext(Map map, bool brief = false);
- string BuildPawnContext(Pawn pawn);
- string BuildCompactPawnContext(Pawn pawn); // 精简版
- string BuildHistoryContext(int maxEntries = 10);
-
- // PromptSection 版本(带压缩回调,可参与 PromptBudget 压缩)
- PromptSection BuildMapContextSection(Map map, bool brief = false);
- PromptSection BuildPawnContextSection(Pawn pawn);
- PromptSection BuildCompactPawnContextSection(Pawn pawn);
-}
-```
-
-### BuildFullPawnPrompt 组装顺序
-
-```
-1. 静态段 → RegisterStaticProvider 注册的段(Rules、Skills 等)
-2. Pawn段 → RegisterPawnContextProvider 注册的段(人格、记忆等)
-3. 游戏状态 → BuildPawnContextSection(pawn)(带压缩回调)
-4. 地图状态 → BuildMapContextSection(pawn.Map)(带压缩回调)
-5. 动态段 → RegisterDynamicProvider 注册的段(Memory 语义检索等)
-```
-
-所有段落以 `PromptSection` 形式传递,按 `Priority` 排序后拼接。带 `PromptBudget` 的版本会先压缩再裁剪。
-
-### JsonTagExtractor
-
-统一 AI 响应解析工具。所有子模组应使用 `{JSON} ` 格式:
-
-```csharp
-T? result = JsonTagExtractor.Extract(aiResponse, "TagName");
-List results = JsonTagExtractor.ExtractAll(aiResponse, "TagName");
-string? raw = JsonTagExtractor.ExtractRaw(aiResponse, "TagName");
-List allRaw = JsonTagExtractor.ExtractAllRaw(aiResponse, "TagName");
-```
-
-### AIRequestQueue
-
-```csharp
-class AIRequestQueue : GameComponent {
- static AIRequestQueue Instance { get; }
- static void LogFromBackground(string msg, bool isWarning = false);
-
- void Enqueue(AIRequest request, Action callback, IAIClient client);
- void EnqueueImmediate(AIRequest request, Action callback, IAIClient client);
-
- int GetCooldownTicksLeft(string modId);
- int GetQueueDepth(string modId);
- void ClearCooldown(string modId);
- void ClearAllCooldowns();
- void ClearAllQueues();
- IReadOnlyDictionary GetAllCooldowns();
- IReadOnlyDictionary GetAllQueueDepths();
-}
-```
-
-### AIDebugLog / AIDebugEntry
-
-```csharp
-class AIDebugLog : GameComponent {
- static AIDebugLog? Instance { get; }
- IReadOnlyList Entries { get; }
- void Clear();
- static void Record(AIRequest request, AIResponse response, int elapsedMs);
-}
-
-class AIDebugEntry {
- int GameTick;
- string Source;
- string ModelName;
- string FullSystemPrompt;
- string FullUserPrompt;
- string FullResponse;
- int ElapsedMs;
- int TokensUsed;
- bool IsError;
- string ErrorMsg;
- string FormattedTime; // 格式化时间
-}
-```
-
-### RequestEntry / RequestOverlay
-
-请求审批悬浮窗系统:
-
-```csharp
-class RequestEntry {
- string source; // 来源模组标识
- Pawn? pawn; // 相关小人
- string title; // 请求标题
- string? description; // 请求描述
- string[] options; // 选项列表
- Action? callback; // 选择回调(参数为选中的选项文本)
- bool systemBlocked; // 是否被系统拦截
- int tick; // 创建时间
- int expireTicks; // 过期 tick 数
-}
-
-static class RequestOverlay {
- void Register(RequestEntry entry);
- IReadOnlyList Pending { get; }
- void Remove(RequestEntry entry);
- Rect GetWindowRect(); // 获取悬浮窗位置(持久化用)
- void SetWindowRect(Rect rect);
- void OnGUI();
-}
-```
-
-过期后自动触发最后一个选项(视为"忽略")。
-
-### SettingsUIHelper
-
-```csharp
-static class SettingsUIHelper {
- DrawSectionHeader(Listing_Standard listing, string label);
- DrawCustomPromptSection(Listing_Standard listing, string label, ref string prompt, float height = 80f);
- SplitContentArea(inRect); // 分割内容区域
- SplitBottomBar(inRect); // 分割底部栏
- DrawBottomBar(barRect, onReset); // 重置按钮
-}
-```
-
-### RimMindCoreSettings
-
-```csharp
-class RimMindCoreSettings : ModSettings {
- string apiKey;
- string apiEndpoint;
- string modelName;
- bool forceJsonMode;
- bool useStreaming; // 流式响应(预留)
- int maxTokens;
- bool debugLogging;
- ContextSettings Context; // 上下文过滤器
- string customPawnPrompt;
- string customMapPrompt;
- bool requestOverlayEnabled;
- float requestOverlayX, requestOverlayY, requestOverlayW, requestOverlayH;
-
- bool IsConfigured();
- override void ExposeData();
-}
-```
-
-### ContextSettings / ContextPreset
-
-```csharp
-class ContextSettings : IExposable {
- // 21 个 Include* bool 字段控制上下文注入
- // 例:IncludePawnSkills, IncludePawnNeeds, IncludeMapWeather ...
- int MinSkillLevel; // 技能显示阈值
- List disabledProviders; // 禁用的 Provider category 列表
-
- void ExposeData();
- void ApplyPreset(ContextPreset preset);
-}
-
-enum ContextPreset { Minimal, Standard, Full, Custom }
-```
-
-## 线程安全规则
-
-- **主线程**:读写游戏状态、消费 `ConcurrentQueue` 结果、所有 RimWorld/Unity API
-- **后台线程**:HTTP 请求、JSON 解析、生产 `ConcurrentQueue` 结果
-- **严禁**在后台线程调用任何 RimWorld/Unity API
-- 后台线程日志必须通过 `AIRequestQueue.LogFromBackground()` 写入,主线程 Tick 时输出
-
-## 数据流
-
-```
-游戏主线程 (Tick)
- │
- ├── 子模组触发条件满足
- │ ▼
- │ 构建 AIRequest(SystemPrompt + UserPrompt)
- │ ▼
- │ RimMindAPI.RequestAsync(request, callback)
- │ ▼
- │ AIRequestQueue.Enqueue()
- │ ├── 检查冷却 → 跳过或接受
- │ └── Task.Run → OpenAIClient.SendAsync()
- │ ▼ (后台线程)
- │ HTTP 请求 → 解析响应
- │ ▼
- │ _results.Enqueue((response, callback))
- │
- ├── GameComponentTick()
- │ ▼
- │ 消费 _results 队列
- │ ▼
- │ callback(response) ← 主线程安全
- │ ▼
- │ 子模组处理响应(解析 JSON、执行动作等)
- └── ...
-```
-
-## RimMind 套件架构
-
-```
- ┌─────────────────┐
- │ Harmony │
- └────────┬────────┘
- │
- ┌────────▼────────┐
- │ RimMind-Core │
- └──┬──┬──┬──┬──┬─┘
- │ │ │ │ │
- ┌────────────┘ │ │ │ └──────────────┐
- │ │ │ │ │
- ┌──────▼──────┐ ┌─────▼──┐ │ ┌───────────────▼──────┐
- │RimMind- │ │RimMind-│ │ │ RimMind-Personality │
- │Actions │ │Memory │ │ └──────────────────────┘
- └──────┬──────┘ └────────┘ │
- │ │
- ┌──────▼──────┐ ┌───────▼──────┐
- │RimMind- │ │RimMind- │
- │Advisor │ │Dialogue │
- └─────────────┘ └──────────────┘
- │
- ┌────────▼────────┐
- │RimMind- │
- │Storyteller │
- └─────────────────┘
-```
-
-### 上下文注入方式
-
-| 子模组 | 注入方式 | 注册的 Provider |
-|--------|---------|----------------|
-| RimMind-Personality | `RegisterPawnContextProvider` | personality_profile + personality_state + personality_shaping |
-| RimMind-Memory | `RegisterPawnContextProvider` + `RegisterStaticProvider` | memory_pawn + memory_narrator |
-| RimMind-Dialogue | `RegisterPawnContextProvider` | dialogue_state + dialogue_relation |
-| RimMind-Advisor | `RegisterPawnContextProvider` | advisor_history |
-| RimMind-Actions | 以记忆方式注入(通过 Memory) | - |
-| RimMind-Storyteller | 不注入上下文 | - |
-
-### 数据依赖关系
-
-```
-(人格, 记忆) → 想法 → 行动 或 对话
- ↑ ↑
- └── 想法和记忆反哺人格
-```
-
-- 人格通过 `RegisterPawnContextProvider` 注入上下文
-- 想法自动打包进入上下文(Thought 系统)
-- 记忆通过 `RegisterPawnContextProvider` + `RegisterStaticProvider` 注入上下文
-- 行动以记忆方式注入上下文
-- 对话状态通过 `RegisterPawnContextProvider` 注入上下文
-- Advisor 历史通过 `RegisterPawnContextProvider` 注入上下文
-
-## 代码约定
-
-### 命名空间
-
-| 命名空间 | 目录 | 职责 |
-|---------|------|------|
-| `RimMind.Core` | Source/ 根目录 | Mod 入口、API |
-| `RimMind.Core.Client` | Client/ | AI 客户端接口与数据结构 |
-| `RimMind.Core.Client.OpenAI` | Client/OpenAI/ | OpenAI 实现 |
-| `RimMind.Core.Internal` | Core/ | 内部组件(队列、上下文构建、日志、JSON 提取) |
-| `RimMind.Core.Prompt` | Core/Prompt/ | Prompt 组装(段落、预算、排序、结构化构建) |
-| `RimMind.Core.Settings` | Settings/ | 设置 |
-| `RimMind.Core.UI` | UI/ | 界面 |
-| `RimMind.Core.Patch` | Patch/ | Harmony 补丁 |
-| `RimMind.Core.Debug` | Debug/ | 调试动作 |
-
-### 序列化
-
-- `ModSettings` → `ExposeData()`,需调 `base.ExposeData()`
-- `GameComponent` → `ExposeData()`
-- `ThingComp` → `PostExposeData()`(不是 ExposeData)
-- `WorldComponent` → `ExposeData()`
-
-### GameComponent 自动发现
-
-GameComponent / WorldComponent 不需要 XML 注册。RimWorld 自动扫描并实例化,前提是构造函数签名正确:
-
-```csharp
-public AIRequestQueue(Game game) { _instance = this; }
-```
-
-RimWorld 1.6 的 GameComponent 基类无参构造,但 `Game.InitNewGame` 仍用 `Activator.CreateInstance(type, game)`,所以必须保留 `(Game game)` 签名。
-
-### UI 本地化
-
-所有 UI 文本通过 `Languages/ChineseSimplified/Keyed/RimMind_Core.xml` 的 Keyed 翻译,禁止硬编码中文。代码中使用 `"Key".Translate()`。
-
-### Harmony
-
-- Harmony ID:`mcocdaa.RimMindCore`
-- 优先使用 PostFix
-- Patch 类放在 `Patch/` 目录
-
-### 构建
-
-| 配置项 | 值 |
-|--------|-----|
-| 目标框架 | `net48` |
-| C# 语言版本 | 9.0 |
-| Nullable | enable |
-| RimWorld 版本 | 1.6 |
-| 输出路径 | `../1.6/Assemblies/` |
-| 部署 | 设置 `RIMWORLD_DIR` 环境变量后自动部署 |
-| NuGet 依赖 | `Krafs.Rimworld.Ref 1.6.*-*`, `Lib.Harmony.Ref 2.*`, `Newtonsoft.Json 13.0.*` |
-
-### 测试
-
-- 单元测试项目:`Tests/`,使用 xUnit,目标 `net10.0`
-- 测试纯逻辑层,不依赖 RimWorld
-- 测试文件直接 `` 引用源码(不引用主项目 DLL)
-- 已有测试:`JsonTagExtractorTests`
-
-## 扩展指南(子模组开发)
-
-### 1. 编译期引用
-
-在 `.csproj` 中引用 RimMindCore.dll(Private=false):
-
-```xml
-
- ../../RimMind-Core/$(GameVersion)/Assemblies/RimMindCore.dll
- false
-
-```
-
-### 2. 注册 Provider
-
-在 Mod 构造函数中注册,传入 `modId` 以支持卸载:
-
-```csharp
-public class MyMod : Mod
-{
- public MyMod(ModContentPack content) : base(content)
- {
- RimMindAPI.RegisterPawnContextProvider("my_category", pawn =>
- {
- return $"[{pawn.Name.ToStringShort} 自定义信息]\n...";
- }, PromptSection.PriorityMemory, modId: "MyMod");
-
- RimMindAPI.RegisterSettingsTab("my_tab", () => "我的设置", rect =>
- {
- // 绘制设置 UI
- });
- }
-}
-```
-
-### 3. 卸载 Provider
-
-模组卸载时清理注册:
-
-```csharp
-RimMindAPI.UnregisterModProviders("MyMod");
-// 或按 category 卸载:
-RimMindAPI.UnregisterPawnContextProvider("my_category");
-```
-
-### 4. 发起 AI 请求
-
-```csharp
-var request = new AIRequest
-{
- SystemPrompt = "你是一个...",
- UserPrompt = RimMindAPI.BuildFullPawnPrompt(pawn),
- MaxTokens = 400,
- Temperature = 0.7f,
- RequestId = $"MyMod_{pawn.ThingID}",
- ModId = "MyMod",
-};
-
-RimMindAPI.RequestAsync(request, response =>
-{
- if (!response.Success) { Log.Warning($"失败: {response.Error}"); return; }
- var result = JsonTagExtractor.Extract(response.Content, "MyTag");
- // 处理结果...
-});
-```
-
-### 5. 使用 StructuredPromptBuilder 构建 System Prompt
-
-```csharp
-// 方式一:逐项构建
-var systemPrompt = new StructuredPromptBuilder()
- .RoleFromKey("RimMind.MyMod.Role")
- .GoalFromKey("RimMind.MyMod.Goal")
- .ProcessFromKey("RimMind.MyMod.Process")
- .ConstraintFromKey("RimMind.MyMod.Constraint")
- .OutputFromKey("RimMind.MyMod.Output")
- .ExampleFromKey("RimMind.MyMod.Example")
- .FallbackFromKey("RimMind.MyMod.Fallback")
- .Build();
-
-// 方式二:从翻译键前缀一键构建
-var systemPrompt = StructuredPromptBuilder.FromKeyPrefix("RimMind.MyMod")
- .Build();
-
-// 方式三:转为 PromptSection 注入上下文系统
-var section = StructuredPromptBuilder.FromKeyPrefix("RimMind.MyMod")
- .ToSection("my_system_prompt", PromptSection.PriorityCore);
-```
-
-### 6. 注册请求审批
-
-```csharp
-RimMindAPI.RegisterPendingRequest(new RequestEntry
-{
- source = "my_mod",
- pawn = pawn,
- title = "标题",
- description = "描述",
- options = new[] { "选项A", "选项B", "忽略" },
- expireTicks = 30000,
- callback = choice =>
- {
- if (choice == "选项A") { /* 处理 */ }
- }
-});
-```
-
-### 7. 响应格式约定
-
-所有子模组统一使用 `{JSON} ` 格式,AI 可在标签前后输出思考过程:
-
-```
-让我分析一下当前局势...
-
-{"action": "social_relax", "target": "Alice", "reason": "渴望社交"}
-
-```
-
-### 8. 冷却机制
-
-- Core 层冷却:`globalCooldownTicks`(默认 3600),按 ModId 独立
-- 子模组通过 `RegisterModCooldown` 注册自定义冷却 Getter
-- DebugAction 可清除冷却:`AIRequestQueue.Instance?.ClearCooldown(modId)`
-
-## AI 响应格式标准
-
-| 子模组 | 标签 | JSON Schema |
-|--------|------|------------|
-| RimMind-Storyteller | `` | `{"defName": string, "reason": string, "params": {...}?, "chain": {...}?}` |
-| RimMind-Advisor | `` | `{"advices": [{action, pawn?, target?, param?, reason, request_type?}]}` |
-| RimMind-Personality | `` | `{"thoughts": [{type, label, description, intensity, duration_hours?}], "narrative": string}` |
-| RimMind-Dialogue | `` | `{"reply": string, "thought": {"tag": string, "description": string}, "relation_delta"?: float}` |
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 00000000..47dc3e3d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/Defs/DutyDefs_RimMind.xml b/Defs/DutyDefs_RimMind.xml
new file mode 100644
index 00000000..26c6d5a7
--- /dev/null
+++ b/Defs/DutyDefs_RimMind.xml
@@ -0,0 +1,20 @@
+
+
+
+ RimMind_AgentDecision
+
+ 5
+
+ HighPriority
+ Normal
+
+
+
+ RimMind_AgentRest
+
+ 5
+
+ HighPriority
+ Quiet
+
+
diff --git a/Defs/HediffDefs_RimMind.xml b/Defs/HediffDefs_RimMind.xml
new file mode 100644
index 00000000..1ad4a3e4
--- /dev/null
+++ b/Defs/HediffDefs_RimMind.xml
@@ -0,0 +1,54 @@
+
+
+
+ RimMind_AIFocus
+ AI enhanced focus
+ This pawn is operating under AI guidance, resulting in enhanced focus and decision-making.
+ HediffWithComps
+ (0.4, 0.8, 1.0)
+ 0.1
+ 1.0
+ false
+
+
+ 30000
+
+
+
+
+
+
+ Consciousness
+ 0.05
+
+
+
+ 0.05
+
+
+
+
+
+
+ RimMind_AIDecisionCooldown
+ AI decision cooldown
+ This pawn recently made an AI-guided decision and is in a brief cooldown period before the next one.
+ HediffWithComps
+ (0.6, 0.6, 0.8)
+ 1.0
+ 1.0
+ false
+
+
+ 15000
+
+
+
+
+
+ 0.02
+
+
+
+
+
diff --git a/Defs/JobDefs_RimMind.xml b/Defs/JobDefs_RimMind.xml
new file mode 100644
index 00000000..7e28eb2f
--- /dev/null
+++ b/Defs/JobDefs_RimMind.xml
@@ -0,0 +1,38 @@
+
+
+
+ RimMind_GenericAction
+ RimMind.Infrastructure.Patches.JobDriver_RimMindAction
+ Executing AI decision
+ true
+
+
+
+ RimMind_Rest
+ RimMind.Infrastructure.Patches.JobDriver_RimMindAction
+ Resting (AI decision)
+ true
+
+
+
+ RimMind_Work
+ RimMind.Infrastructure.Patches.JobDriver_RimMindAction
+ Working (AI decision)
+ true
+
+
+
+ RimMind_Socialize
+ RimMind.Infrastructure.Patches.JobDriver_RimMindAction
+ Socializing (AI decision)
+ true
+
+
+
+ RimMind_EmergencyTend
+ RimMind.Infrastructure.Patches.JobDriver_RimMindAction
+ Tending to emergency (AI decision)
+ true
+ false
+
+
diff --git a/Defs/MainButtons/AICore_DebugLog.xml b/Defs/MainButtons/AICore_DebugLog.xml
index 71b24499..cf3af71b 100644
--- a/Defs/MainButtons/AICore_DebugLog.xml
+++ b/Defs/MainButtons/AICore_DebugLog.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/Defs/MainButtons/RimMind_Main.xml b/Defs/MainButtons/RimMind_Main.xml
new file mode 100644
index 00000000..3a7d06d6
--- /dev/null
+++ b/Defs/MainButtons/RimMind_Main.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/Defs/RimMindActionDefs.xml b/Defs/RimMindActionDefs.xml
new file mode 100644
index 00000000..6ce2b95f
--- /dev/null
+++ b/Defs/RimMindActionDefs.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/Defs/RimMind_DynamicThought.xml b/Defs/RimMind_DynamicThought.xml
new file mode 100644
index 00000000..45d9ab80
--- /dev/null
+++ b/Defs/RimMind_DynamicThought.xml
@@ -0,0 +1,14 @@
+
+
+
+ RimMind_DynamicThought
+ 1
+
+
+ AI-generated thought
+ An AI-generated thought affecting this pawn's mood.
+ 0
+
+
+
+
diff --git a/Defs/TaleDefs_RimMind.xml b/Defs/TaleDefs_RimMind.xml
new file mode 100644
index 00000000..a8533840
--- /dev/null
+++ b/Defs/TaleDefs_RimMind.xml
@@ -0,0 +1,40 @@
+
+
+
+ RimMind_AgentDecision
+ AI agent decision
+ Tale_SinglePawn
+ Volatile
+ 1
+
+
+ tale_noun->[PAWN_nameDef] making an AI-guided decision
+ image->[PAWN_nameDef] contemplating [circumstance_group] with a thoughtful expression
+ image->[PAWN_nameDef] standing still while considering options
+ circumstance_group->the situation at hand
+ circumstance_group->the colony's needs
+ desc_sentence->[PAWN_nameDef] seems focused and determined.
+ desc_sentence->[PAWN_possessive] eyes scan the surroundings carefully.
+
+
+
+
+
+ RimMind_GoalAchieved
+ AI goal achieved
+ Tale_SinglePawn
+ PermanentHistorical
+ 3
+
+
+ tale_noun->[PAWN_nameDef] achieving an AI-guided goal
+ image->[PAWN_nameDef] standing triumphantly after completing [circumstance_group]
+ image->[PAWN_nameDef] looking satisfied with [PAWN_possessive] accomplishment
+ circumstance_group->an important task
+ circumstance_group->a challenging objective
+ desc_sentence->[PAWN_nameDef] smiles with quiet satisfaction.
+ desc_sentence->A sense of purpose fills the air.
+
+
+
+
diff --git a/Defs/ThinkTree_RimMindAgent.xml b/Defs/ThinkTree_RimMindAgent.xml
new file mode 100644
index 00000000..24e2cf75
--- /dev/null
+++ b/Defs/ThinkTree_RimMindAgent.xml
@@ -0,0 +1,11 @@
+
+
+
+ RimMindAgent
+ Humanlike_Main
+ 5
+
+ 5
+
+
+
diff --git a/INTRODUCTION.txt b/INTRODUCTION.txt
index 15f8250c..de3b3bc5 100644
--- a/INTRODUCTION.txt
+++ b/INTRODUCTION.txt
@@ -1,79 +1,134 @@
-[h1]RimMind - Core[/h1]
-RimMind 套件的核心基础设施,提供 LLM 客户端、异步请求队列和游戏上下文构建器,是所有 RimMind 子模组的前置依赖。
-
-[h2] RimMind 是什么 [/h2]
-RimMind 是一套 AI 驱动的 RimWorld 模组套件,通过接入大语言模型 (LLM),让殖民者拥有人格、记忆、对话和自主决策能力。
-
-[h2] 子模组列表与依赖关系 [/h2]
-[table][tr][td][b] 模组 [/b][/td][td][b] 职责 [/b][/td][td][b] 依赖 [/b][/td][td][b] GitHub [/b][/td][/tr][tr][td][b] RimMind-Core [/b][/td][td] API 客户端、请求调度、上下文打包 [/td][td] Harmony [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core] 链接 [/url][/td][/tr][tr][td] RimMind-Actions [/td][td] AI 控制小人的动作执行库 [/td][td] Core [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Actions] 链接 [/url][/td][/tr][tr][td] RimMind-Advisor [/td][td] AI 扮演小人做出工作决策 [/td][td] Core, Actions [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Advisor] 链接 [/url][/td][/tr][tr][td] RimMind-Dialogue [/td][td] AI 驱动的对话系统 [/td][td] Core [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Dialogue] 链接 [/url][/td][/tr][tr][td] RimMind-Memory [/td][td] 记忆采集与上下文注入 [/td][td] Core [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Memory] 链接 [/url][/td][/tr][tr][td] RimMind-Personality [/td][td] AI 生成人格与想法 [/td][td] Core [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Personality] 链接 [/url][/td][/tr][tr][td] RimMind-Storyteller [/td][td] AI 叙事者,智能选择事件 [/td][td] Core [/td][td][url=https://github.com/RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Storyteller] 链接 [/url][/td][/tr][/table]
-[code]Core ── Actions ── Advisor
- ├── Dialogue
- ├── Memory
- ├── Personality
- └── Storyteller[/code]
-
-[h2] 核心能力 [/h2]
-[b] 智能上下文构建 [/b] - 自动采集游戏状态:殖民者心情、健康、技能、装备、地图财富、威胁等级、季节天气等,为 AI 决策提供完整情报。
-[b] 异步请求队列 [/b] - 智能管理 LLM 请求,避免阻塞游戏主线程,支持请求优先级、冷却控制、自动重试和暂停/取消。
-[b] 模块化 API 设计 [/b] - 其他 RimMind 组件通过统一接口调用,无需关心底层实现细节。
-[b] 多 Provider 支持 [/b] - 兼容 OpenAI / DeepSeek / Ollama 等 OpenAI Chat Completions 格式的 API,同时支持 Player2 服务(本地应用 + 远程 API)。
-[b] 上下文过滤器 [/b] - 28+ 个可配置选项,精确控制注入 Prompt 的信息量,节省 Token 开销。
-[b] SkipCheck 互斥机制 [/b] - 子模组可注册对话/浮菜单/动作/叙事者的跳过检查,实现跨模组互斥控制。
-[b] 调试工具集 [/b] - 内置 AI 请求日志窗口,实时查看每次 AI 调用的输入输出,方便排查问题。
-
-[h2] 技术亮点 [/h2]
+═══════════════════════════════════════════════
+ RimMind — 你的小人有了真正的灵魂
+═══════════════════════════════════════════════
+[h1]RimMind - Core (核心引擎)[/h1]
+
+想象这个场景:
+[i]赋予殖民者真正的认知与灵魂——RimMind 系列模组的基础运行时与核心引擎。[/i]
+[i]Bring true cognition and souls to your colonists — The foundation runtime and core AI engine for the RimMind suite.[/i]
+
+ 袭击刚结束。Alice 蹲在废墟旁,她的爱人倒在血泊中。
+ 你点开她的思维面板——她在想:
+ "我受够了。我要离开这个鬼地方。"
+[hr]
+[h2]📖 中文介绍 / Chinese Overview[/h2]
+
+ 另一边,Bob 刚被救回来。他看到 Alice 独自发抖,
+ 决定走过去安慰她。不是你下的命令——是他自己想的。
+[b]RimMind-Core[/b] 是整个 RimMind 智能体套件的心脏与基础设施。它将大型语言模型(LLM)无缝嵌入 RimWorld 1.6 的游戏循环中,通过高效的上下文编排、异步队列与原生 ToolCall 机制,让殖民地的小人获得真正的思考、记忆与行动能力。
+
+ 这不是脚本。不是随机事件表。这是 AI 在那一刻,
+ 为这两个有性格、有记忆、有情感的小人做出的真实决定。
+[h3]🌟 核心特性[/h3]
[list]
-[*] 兼容 OpenAI / DeepSeek / Ollama / Player2 等多种 AI 服务
-[*] 可配置的上下文过滤器,提供最小 / 标准 / 完整三种预设
-[*] JSON 模式支持,确保 AI 返回结构化数据
-[*] 请求悬浮窗,实时显示 AI 请求状态
-[*] 瞬态错误自动重试(timeout / 429 / 502 / 503 等)
-[*] 本地模型串行处理,避免资源竞争
-[*] Prompt Token 预算管理,自动压缩和裁剪
+[*] [b]4-Zone KV-Cache 深度缓存架构[/b]:创新将系统提示词、角色背景、长期记忆与易变环境(天气/心情/时间)分层解耦,实现高达 [b]80%~90% 的缓存命中率[/b],大幅降低 API Token 消耗并提升响应速度!
+[*] [b]全能模型服务适配[/b]:开箱即用支持 [b]OpenAI (GPT-4o/mini)[/b]、[b]DeepSeek (Chat/Reasoner)[/b]、[b]本地 Ollama / LM Studio / vLLM[/b] 以及 [b]OpenCode Go[/b] 订阅直连。
+[*] [b]原生 ToolCall 工具调用体系[/b]:支持复杂嵌套函数调用(Function Calling),AI 不仅能输出台词,更能精准执行游戏内动作。
+[*] [b]直观的 F8 智能体调试与控制中心[/b]:按 [b]F8[/b] 随时唤出总控中心,提供实时连通性测试、队列状态、Token 消耗统计、报文检查器与殖民者智能体监控。
+[*] [b]统一频率调节滑块[/b]:在设置中提供动态活动频率滑块(0.33x ~ 3.33x),支持从“每3天1次”到“每天10次”的平滑调谐,彻底告别死板定时打卡。
+[*] [b]极致性能保护[/b]:网络请求与高开销纯计算全部运行在后台线程,游戏主线程 0 掉帧、0 阻断。
[/list]
-[h2] 支持的 API 端点 [/h2]
-[table][tr][td][b] 服务 [/b][/td][td][b] 端点 [/b][/td][td][b] 说明 [/b][/td][/tr][tr][td] OpenAI [/td][td] https://api.openai.com/v1 [/td][td] GPT-4o-mini 等模型 [/td][/tr][tr][td] DeepSeek [/td][td] https://api.deepseek.com/v1 [/td][td] deepseek-chat 等模型 [/td][/tr][tr][td] Ollama (本地) [/td][td] http://localhost:11434/v1 [/td][td] 本地部署的模型 [/td][/tr][tr][td] Player2 [/td][td] 自动检测 / 手动配置 [/td][td] Player2 本地应用或远程 API [/td][/tr][tr][td] 其他 [/td][td] 填入 Base URL [/td][td] 任何 OpenAI 兼容接口 [/td][/tr][/table]
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+[hr]
+[h2]🌐 English Overview[/h2]
-[h2] 核心功能 [/h2]
-[b] LLM 客户端 [/b]
-兼容 OpenAI / DeepSeek / 本地 Ollama 等所有 OpenAI Chat Completions 格式的 API,同时支持 Player2 服务(本地应用自动检测 + 远程 API)。支持 JSON 强制模式(response_format: json_object),本地模型可关闭。
+ 这 Mod 到底好玩在哪?
+ ────────────────────
+[b]RimMind-Core[/b] is the foundational AI runtime engine for the entire RimMind mod suite in RimWorld 1.6. It bridges large language models (LLMs) with RimWorld's simulation loop, providing intelligent context management, async queue scheduling, and structured tool execution.
-[b] 异步请求队列 [/b]
-所有 AI 请求在后台线程执行,不阻塞游戏主线程。每个请求独立冷却,过期请求自动丢弃。支持瞬态错误自动重试(timeout / 429 / 502 / 503 等),本地模型串行处理避免资源竞争。
-
-[b] 上下文构建 [/b]
-自动采集游戏状态并打包为文本供各模块使用:
+ ▸ 小人真的会"想"
+ 每个 Pawn 都有独立的认知循环:感知→思考→行动→记忆。
+ 它们会注意到同伴的心情、殖民地的食物短缺、窗外的威胁。
+ 然后做出自己的判断,甚至形成长期目标。
+[h3]🌟 Key Highlights[/h3]
[list]
-[*] [b]地图上下文[/b]:时间、殖民者、食物、威胁、季节、天气
-[*] [b]小人上下文[/b]:年龄、背景、心情、健康、技能、装备、工作分配、关系
+[*] [b]4-Zone Prompt Caching (KV-Cache)[/b]: Isolates static definitions and pawn profiles from volatile environmental states (time/weather/mood), achieving [b]80%~90% prefix cache hit rate[/b] for massive token cost reduction and lightning-fast responses!
+[*] [b]Universal Model Compatibility[/b]: Out-of-the-box support for [b]OpenAI[/b], [b]DeepSeek[/b], local [b]Ollama / LM Studio[/b], and high-speed [b]OpenCode Go[/b].
+[*] [b]Unified Native ToolCall Architecture[/b]: Enables structured function calling, allowing LLMs to directly read game states and invoke game mechanics without brittle text parsing.
+[*] [b]F8 RimMind Debug Hub & Inspector[/b]: Press [b]F8[/b] in-game to view live AI connectivity, test API ping, inspect token payloads, and monitor colonist agents.
+[*] [b]Dynamic Frequency Slider[/b]: Seamlessly scale AI interaction frequency from eco-mode (1 event per 3 days) to active-mode (10+ events per day).
+[*] [b]Lag-Free Threading[/b]: All network and compute workloads run on background threads; your RimWorld main thread stays completely butter-smooth.
[/list]
-[b] 上下文过滤器 [/b]
-通过"上下文过滤"设置页精确控制哪些游戏信息注入 Prompt,节省 Token。提供最小/标准/完整三种预设,也可自定义勾选 28+ 个选项。
+ ▸ 上下文引擎——AI 真正"懂"你的殖民地
+ 不是把一堆数据丢给 LLM,而是用五层架构精准筛选:
+ 只给 AI 此刻最相关的信息。BudgetScheduler 控制每次请求
+ 都在 token 预算内。所以小人不会"失忆"或"胡说八道"。
-[b] 调试工具 [/b]
-[list]
-[*] [b]AI Debug Log[/b]:浮动窗口,查看每次 AI 调用的完整 Prompt + Response
-[*] [b]请求悬浮窗[/b]:右下角实时显示 AI 请求状态
-[*] [b]Dev 菜单[/b]:测试连接、查看上下文、清除冷却、暂停/恢复队列等
-[/list]
+ ▸ 飞轮自动调优——越用越聪明
+ 不需要手动调参数。Flywheel 持续监控 AI 响应质量,
+ 自动调整温度、预算、权重。用得越久,小人越"对味"。
+
+ ▸ 感知桥接——小人不是木头人
+ 5 个 Patch 把 RimWorld 的原生事件翻译成 AI 能理解的信号。
+ 受伤、心情崩溃、社交冲突、袭击警报——小人会主动感知,
+ 不是等你点按钮才反应。
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ 推荐怎么玩?
+ ────────────
+
+ 第一步:装 RimMind-Core
+ 这是引擎。装好,填入你的 API Key,就能跑了。
+ 支持任何 OpenAI 兼容的 API(OpenAI / 本地 Ollama / 其他)。
+
+ 第二步:加 RimMind-Dialogue
+ 和小人自由对话。这是最直观的体验——直接跟你的殖民者聊天,
+ 问它们在想什么,听它们抱怨食物难吃。
+
+ 第三步:加 RimMind-Personality
+ 给每个小人分配大五人格特质。内向的科学家、外向的社交达人、
+ 神经质的艺术家——性格不同,反应完全不同。
-[h2] 设置项 [/h2]
-[table][tr][td][b] 设置 [/b][/td][td][b] 默认值 [/b][/td][td][b] 说明 [/b][/td][/tr][tr][td] AI Provider [/td][td] OpenAI [/td][td] 选择 OpenAI 兼容 API 或 Player2 [/td][/tr][tr][td] API Key [/td][td] - [/td][td] 你的 API 密钥(Player2 模式可选) [/td][/tr][tr][td] API 端点 [/td][td] https://api.deepseek.com/v1 [/td][td] OpenAI 兼容端点 [/td][/tr][tr][td] 模型名称 [/td][td] deepseek-chat [/td][td] 任意模型 ID [/td][/tr][tr][td] 强制 JSON 模式 [/td][td] 开启 [/td][td] 不支持的本地模型请关闭 [/td][/tr][tr][td] 最大 Token [/td][td] 800 [/td][td] 响应长度上限 [/td][/tr][tr][td] 最大并发请求数 [/td][td] 3 [/td][td] 同时等待响应的请求上限 [/td][/tr][tr][td] 最大重试次数 [/td][td] 2 [/td][td] 请求失败后重试次数 [/td][/tr][tr][td] 请求超时 [/td][td] 120 秒 [/td][td] 单次请求超时时间 [/td][/tr][tr][td] 详细日志 [/td][td] 关闭 [/td][td] 输出到 Player.log [/td][/tr][tr][td] 请求悬浮窗 [/td][td] 开启 [/td][td] 右下角显示请求状态 [/td][/tr][/table]
+ 第四步:加 RimMind-Memory
+ 小人开始记住事情了。三个月前那次袭击,它还记得谁救了它。
+ 暗记忆系统让 AI 在不知不觉中被过去的经历影响。
-[h2] 常见问题 [/h2]
-[b] Q: 支持哪些大模型? [/b]
-A: 任何兼容 OpenAI Chat Completions API 的模型均可使用,包括 OpenAI GPT 系列、DeepSeek、本地 Ollama 等。同时支持 Player2 服务(本地应用自动检测 + 远程 API)。
+ 进阶:RimMind-Actions + RimMind-Storyteller
+ Actions 让小人自主行动——工作、社交、逃跑,全凭自己判断。
+ Storyteller 让 AI 当叙事者,动态生成故事线和事件。
+ 这两个会让游戏难度和不可预测性大幅上升,建议熟悉基础后再加。
-[b] Q: 会不会影响游戏帧率? [/b]
-A: 不会。所有 AI 请求在后台线程执行,主线程只处理回调结果。
+ 还有:RimMind-Advisor
+ AI 顾问模式。不改变小人行为,而是帮你分析殖民地状况、
+ 给出建议。适合想用 AI 辅助决策但不想让小人"失控"的玩家。
-[b] Q: API Key 安全吗? [/b]
-A: API Key 仅存储在本地 RimWorld 设置文件中;发起 AI 请求时会发送给你配置的 API 端点,不会上传到 RimMind 自有服务器。
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-[b] Q: 不填 API Key 会怎样? [/b]
-A: Core 本身不会报错,但所有依赖 AI 的子模组功能将无法工作。
+ 常见问题
+ ────────
-[b] Q: 推荐用什么模型? [/b]
-A: 推荐使用 gpt-4o-mini 或 deepseek-chat,性价比高且响应速度快。本地 Ollama 用户可使用 qwen2.5:7b 等模型。
+ Q: 贵不贵?
+ A: 默认每 500 秒思考一次,token 用量可控。
+ 用本地 Ollama 的话完全免费。Flywheel 会自动优化避免浪费。
+
+ Q: 和其他 Mod 冲突吗?
+ A: 不冲突。RimMind 通过 Harmony Patch 桥接,不修改游戏原生逻辑。
+
+ Q: 可以只装 Core 吗?
+ A: 可以跑,但小人只会"想"不会"做"。
+ 建议至少搭配 Dialogue,体验最完整。
+
+ Q: Player2 模式是什么?
+ A: 你自己扮演 NPC 的 AI。适合想亲自"演"角色的玩家。
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ 给 Mod 开发者
+
+ - 公共 API: RimMindAPI 静态类
+ - 注册上下文: ContextKeyRegistry.Register(key, layer, priority, provider, modId)
+ - 上下文架构: L0(静态)→L1(基线+Diff)→L2(环境)→L3(状态)→L4(历史)→L5(感知)
+ - 请求队列: AIRequestQueue,支持优先级、冷却、取消
+ - 详细规范: 见 AGENTS.md
+
+═══════════════════════════════════════════════
+[hr]
+[h2]⚙️ 安装与依赖 / Requirements & Setup[/h2]
+[list]
+[*] [b]必需前置 (Required)[/b]: [url=https://steamcommunity.com/sharedfiles/filedetails/?id=2009463077]Harmony[/url]
+[*] [b]加载顺序 (Load Order)[/b]: Harmony -> RimWorld Core -> [b]RimMind-Core[/b] -> (其他 RimMind 子模组)
+[*] [b]配置指南[/b]: 进入游戏主菜单或选项 -> Mod 设置 -> [b]RimMind-Core[/b],选择你的 AI 服务商并填入 API Key 与端点即可!
+[/list]
diff --git a/IntegrationTests/Contracts/MechanismContracts.cs b/IntegrationTests/Contracts/MechanismContracts.cs
new file mode 100644
index 00000000..5e6ba706
--- /dev/null
+++ b/IntegrationTests/Contracts/MechanismContracts.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using RimMind.Application.Common.Interfaces.Extension;
+using RimMind.Application.Common.Interfaces.Mechanisms;
+using RimMind.Application.Common.Models.Mechanisms;
+using RimMind.Domain.Common;
+using RimMind.Domain.Enums;
+using RimMind.Domain.ValueObjects;
+using RimMind.Infrastructure.Mechanisms;
+using RimMind.Testing;
+using Xunit;
+
+namespace RimMind.IntegrationTests.Contracts
+{
+ public sealed class MechanismContracts
+ {
+ [Fact]
+ public void Mechanism_registry_has_replaceable_identity_semantics()
+ {
+ ContractCaseRunner.Run(
+ ("new registry is empty", () => Assert.Empty(new GameMechanismRegistry().All)),
+ ("null registration is ignored", () =>
+ {
+ var registry = new GameMechanismRegistry();
+ registry.Register(null!);
+ Assert.Empty(registry.All);
+ }),
+ ("registered mechanism is found by identity", () =>
+ {
+ var registry = new GameMechanismRegistry();
+ var mechanism = new StubMechanism("pawn.work", "Core", "first");
+ registry.Register(mechanism);
+ Assert.Same(mechanism, registry.FindById("pawn.work"));
+ }),
+ ("same identity replaces the previous mechanism", () =>
+ {
+ var registry = new GameMechanismRegistry();
+ var first = new StubMechanism("pawn.work", "Core", "first");
+ var second = new StubMechanism("pawn.work", "Core", "second");
+ registry.Register(first);
+ registry.Register(second);
+ Assert.Same(second, registry.FindById("pawn.work"));
+ Assert.Single(registry.All);
+ }),
+ ("unregister reports whether identity existed", () =>
+ {
+ var registry = new GameMechanismRegistry();
+ registry.Register(new StubMechanism("pawn.work", "Core", "value"));
+ Assert.True(registry.Unregister("pawn.work"));
+ Assert.False(registry.Unregister("pawn.work"));
+ Assert.Null(registry.FindById("pawn.work"));
+ }));
+ }
+
+ [Fact]
+ public void Mechanism_registry_retires_only_the_selected_owner()
+ {
+ ContractCaseRunner.Run(
+ ("null owner is rejected", () =>
+ Assert.Throws(() => new GameMechanismRegistry().UnregisterByOwner(null!))),
+ ("empty registry removes nothing", () =>
+ Assert.Equal(0, new GameMechanismRegistry().UnregisterByOwner("Core"))),
+ ("every mechanism for the owner is retired", () =>
+ {
+ var registry = RegistryWithOwners();
+ Assert.Equal(2, registry.UnregisterByOwner("Core"));
+ Assert.Null(registry.FindById("core.work"));
+ Assert.Null(registry.FindById("core.job"));
+ }),
+ ("mechanisms for another owner remain available", () =>
+ {
+ var registry = RegistryWithOwners();
+ registry.UnregisterByOwner("Core");
+ Assert.NotNull(registry.FindById("Other.feature"));
+ Assert.Single(registry.All);
+ }),
+ ("unknown owner leaves the graph unchanged", () =>
+ {
+ var registry = RegistryWithOwners();
+ string[] before = registry.All
+ .Select(mechanism => mechanism.MechanismId)
+ .OrderBy(id => id, StringComparer.Ordinal)
+ .ToArray();
+ Assert.Equal(0, registry.UnregisterByOwner("Missing"));
+ string[] after = registry.All
+ .Select(mechanism => mechanism.MechanismId)
+ .OrderBy(id => id, StringComparer.Ordinal)
+ .ToArray();
+ Assert.Equal(before, after);
+ }));
+ }
+
+ [Fact]
+ public async Task Mechanism_operation_boundary_preserves_results_and_risk()
+ {
+ await ContractCaseRunner.RunAsync(
+ ("query returns the mechanism value", async () =>
+ {
+ var mechanism = new StubMechanism("pawn.work", "Core", "priorities");
+ Result result = await mechanism.ExecuteQueryAsync(
+ new MechanismReadArgs { MechanismId = "pawn.work" },
+ CancellationToken.None);
+ Assert.True(result.IsOk);
+ Assert.Equal("priorities", result.Value);
+ }),
+ ("supported write returns success", async () =>
+ {
+ var mechanism = new StubMechanism("pawn.work", "Core", "priorities");
+ Result result = await mechanism.ExecuteSetAsync(
+ new MechanismWriteArgs { MechanismId = "pawn.work", Action = "set_priority" },
+ CancellationToken.None);
+ Assert.True(result.IsOk);
+ Assert.True(result.Value);
+ }),
+ ("unsupported operation returns a typed error", async () =>
+ {
+ var mechanism = new StubMechanism("pawn.work", "Core", "priorities");
+ Result result = await mechanism.ExecuteAddAsync(
+ new MechanismWriteArgs { MechanismId = "pawn.work", Action = "add" },
+ CancellationToken.None);
+ Assert.True(result.IsErr);
+ Assert.Equal(RimMindErrorCode.MechanismOperationNotSupported, result.Error.Code);
+ }),
+ ("list returns a stable read-only option set", async () =>
+ {
+ var mechanism = new StubMechanism("pawn.work", "Core", "priorities");
+ Result, RimMindError> result =
+ await mechanism.ExecuteListAsync(7, CancellationToken.None);
+ Assert.True(result.IsOk);
+ Assert.Collection(result.Value, item => Assert.Equal("option", item.DefName));
+ }),
+ ("operation risk is exposed through the public boundary", () =>
+ {
+ var mechanism = new StubMechanism("pawn.work", "Core", "priorities", MechanismRisk.Dangerous);
+ Assert.Equal(MechanismRisk.Dangerous, mechanism.GetRiskForOperation(MechanismOperationType.Set));
+ return Task.CompletedTask;
+ }));
+ }
+
+ private static GameMechanismRegistry RegistryWithOwners()
+ {
+ var registry = new GameMechanismRegistry();
+ registry.Register(new StubMechanism("core.work", "Core", "work"));
+ registry.Register(new StubMechanism("core.job", "Core", "job"));
+ registry.Register(new StubMechanism("Other.feature", "Other", "other"));
+ return registry;
+ }
+
+ private sealed class StubMechanism : IGameMechanism
+ {
+ private readonly string _ownerModId;
+ private readonly string _queryValue;
+
+ public StubMechanism(
+ string mechanismId,
+ string ownerModId,
+ string queryValue,
+ MechanismRisk risk = MechanismRisk.Safe)
+ {
+ MechanismId = mechanismId;
+ _ownerModId = ownerModId;
+ _queryValue = queryValue;
+ Risk = risk;
+ SupportedOperations = new[]
+ {
+ MechanismOperationType.Query,
+ MechanismOperationType.Set,
+ MechanismOperationType.List
+ };
+ }
+
+ string IExtension.Id => MechanismId;
+ string IExtension.OwnerModId => _ownerModId;
+ public string MechanismId { get; }
+ public MechanismScope Scope => MechanismScope.Pawn;
+ public MechanismRisk Risk { get; }
+ public IReadOnlyList SupportedOperations { get; }
+ public MechanismDocs Docs { get; } = new() { Summary = "contract mechanism" };
+
+ public Task> ExecuteQueryAsync(
+ MechanismReadArgs args,
+ CancellationToken ct) =>
+ Task.FromResult(Result.Ok(_queryValue));
+
+ public Task> ExecuteSetAsync(
+ MechanismWriteArgs args,
+ CancellationToken ct) =>
+ Task.FromResult(Result.Ok(true));
+
+ public Task> ExecuteAddAsync(
+ MechanismWriteArgs args,
+ CancellationToken ct) =>
+ Unsupported(MechanismOperationType.Add);
+
+ public Task> ExecuteRemoveAsync(
+ MechanismWriteArgs args,
+ CancellationToken ct) =>
+ Unsupported(MechanismOperationType.Remove);
+
+ public Task> ExecuteToggleAsync(
+ MechanismWriteArgs args,
+ CancellationToken ct) =>
+ Unsupported(MechanismOperationType.Toggle);
+
+ public Task> ExecuteTriggerAsync(
+ MechanismWriteArgs args,
+ CancellationToken ct) =>
+ Unsupported(MechanismOperationType.Trigger);
+
+ public Task, RimMindError>> ExecuteListAsync(
+ int? pawnId,
+ CancellationToken ct) =>
+ Task.FromResult(Result, RimMindError>.Ok(
+ new[] { new MechanismEnumResult { DefName = "option", Label = "Option" } }));
+
+ public Task> ExecuteWatchAsync(
+ MechanismWriteArgs args,
+ CancellationToken ct) =>
+ Unsupported(MechanismOperationType.Watch);
+
+ public IReadOnlyList? GetWriteActions() => null;
+
+ public MechanismRisk GetRiskForOperation(MechanismOperationType operation) => Risk;
+
+ private Task> Unsupported(MechanismOperationType operation)
+ {
+ return Task.FromResult(Result.Err(
+ RimMindErrors.MechanismOperationNotSupported(MechanismId, operation.ToString())));
+ }
+ }
+ }
+}
diff --git a/IntegrationTests/Contracts/RuntimeAdapterContracts.cs b/IntegrationTests/Contracts/RuntimeAdapterContracts.cs
new file mode 100644
index 00000000..788e27a5
--- /dev/null
+++ b/IntegrationTests/Contracts/RuntimeAdapterContracts.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using RimMind.Application.Common.Interfaces.Client;
+using RimMind.Application.Common.Models.Pipeline;
+using RimMind.Application.Common.Models.Npc;
+using RimMind.Application.Features.Pipeline.Unified;
+using RimMind.Domain.Common;
+using RimMind.Domain.Llm;
+using RimMind.Domain.ValueObjects;
+using RimMind.Infrastructure.Services.Clients;
+using RimMind.Testing;
+using Xunit;
+
+namespace RimMind.IntegrationTests.Contracts
+{
+ public sealed class RuntimeAdapterContracts
+ {
+ [Fact]
+ public async Task Client_adapter_routes_non_streaming_requests_without_losing_context()
+ {
+ await ContractCaseRunner.RunAsync(
+ ("missing client short-circuits with a stable error", async () =>
+ {
+ var context = Context("missing-client");
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.True(context.IsShortCircuited);
+ Assert.True(context.Result!.Value.IsErr);
+ Assert.Equal(RimMindErrorCode.ClientNotConfigured, context.Result.Value.Error.Code);
+ }),
+ ("regular request selects non-streaming transport", async () =>
+ {
+ var client = new RecordingClient();
+ var context = Context("regular", client);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.Equal(1, client.NonStreamingCalls);
+ Assert.Equal(0, client.StreamingCalls);
+ }),
+ ("unsupported streaming falls back to regular transport", async () =>
+ {
+ var client = new RecordingClient { SupportsStreamingValue = false };
+ var context = Context("fallback", client, streaming: true);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.Equal(1, client.NonStreamingCalls);
+ Assert.Equal(0, client.StreamingCalls);
+ }),
+ ("queue cancellation is copied to the transport envelope", async () =>
+ {
+ using var cancellation = new CancellationTokenSource();
+ var client = new RecordingClient();
+ var context = Context("token", client, cancellation.Token);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.Equal(cancellation.Token, client.LastEnvelope!.Ct);
+ }),
+ ("successful result and downstream continuation are preserved", async () =>
+ {
+ var client = new RecordingClient();
+ var context = Context("success", client);
+ bool downstream = false;
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ =>
+ {
+ downstream = true;
+ return Task.CompletedTask;
+ });
+ Assert.True(downstream);
+ Assert.True(context.Result!.Value.IsOk);
+ Assert.Equal("regular-response", context.Result.Value.Value.Content);
+ }));
+ }
+
+ [Fact]
+ public async Task Client_adapter_routes_streaming_requests_and_normalizes_results()
+ {
+ await ContractCaseRunner.RunAsync(
+ ("streaming-capable client receives streaming call", async () =>
+ {
+ var client = new RecordingClient { SupportsStreamingValue = true };
+ var context = Context("stream", client, streaming: true);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.Equal(0, client.NonStreamingCalls);
+ Assert.Equal(1, client.StreamingCalls);
+ }),
+ ("streaming call receives the request cancellation token", async () =>
+ {
+ using var cancellation = new CancellationTokenSource();
+ var client = new RecordingClient { SupportsStreamingValue = true };
+ var context = Context("stream-token", client, cancellation.Token, streaming: true);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.Equal(cancellation.Token, client.StreamingToken);
+ }),
+ ("streaming chunks are accepted before the final response", async () =>
+ {
+ var client = new RecordingClient
+ {
+ SupportsStreamingValue = true,
+ EmitChunks = true
+ };
+ var context = Context("chunks", client, streaming: true);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.Equal(2, client.EmittedChunks);
+ Assert.Equal("stream-response", context.Result!.Value.Value.Content);
+ }),
+ ("final response retains transport metrics", async () =>
+ {
+ var client = new RecordingClient { SupportsStreamingValue = true };
+ var context = Context("metrics", client, streaming: true);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ LlmResponse response = context.Result!.Value.Value;
+ Assert.Equal(9, response.TokensUsed);
+ Assert.Equal(5, response.PromptTokens);
+ Assert.Equal(4, response.CompletionTokens);
+ Assert.Equal(200, response.HttpStatusCode);
+ }),
+ ("transport error remains an error result", async () =>
+ {
+ var client = new RecordingClient
+ {
+ SupportsStreamingValue = true,
+ StreamingResult = Result.Err(
+ RimMindErrors.ClientTransient("stream failed"))
+ };
+ var context = Context("stream-error", client, streaming: true);
+ await new ClientInvokeMiddleware().InvokeAsync(context, _ => Task.CompletedTask);
+ Assert.True(context.Result!.Value.IsErr);
+ Assert.Equal(RimMindErrorCode.ClientTransientFailure, context.Result.Value.Error.Code);
+ }));
+ }
+
+ [Fact]
+ public void Http_adapter_exception_preserves_transport_status()
+ {
+ ContractCaseRunner.Run(
+ ("rate limit status is retained", () => AssertHttpException(429, "rate limited")),
+ ("authentication status is retained", () => AssertHttpException(401, "unauthorized")),
+ ("forbidden status is retained", () => AssertHttpException(403, "forbidden")),
+ ("server status is retained", () => AssertHttpException(503, "unavailable")),
+ ("unknown transport status is retained", () => AssertHttpException(0, "transport failure")));
+ }
+
+ private static LlmRequestContext Context(
+ string requestId,
+ IAIClient? client = null,
+ CancellationToken cancellationToken = default,
+ bool streaming = false)
+ {
+ var envelope = new LlmRequestEnvelope
+ {
+ RequestId = requestId,
+ TraceId = $"trace-{requestId}",
+ ScenarioId = "contracts",
+ ModId = "RimMindCore",
+ IsStreaming = streaming,
+ Messages = new List
+ {
+ new() { Role = "user", Content = "request" }
+ }
+ };
+ return new LlmRequestContext(envelope, ct: cancellationToken)
+ {
+ Client = client
+ };
+ }
+
+ private static void AssertHttpException(int statusCode, string message)
+ {
+ var exception = new HttpTransport.HttpException(message, statusCode);
+ Assert.Equal(statusCode, exception.StatusCode);
+ Assert.Equal(message, exception.Message);
+ }
+
+ private sealed class RecordingClient : IAIClient
+ {
+ public bool IsLocalEndpoint => false;
+ public bool SupportsStreaming => SupportsStreamingValue;
+ public bool SupportsNpcServerState => false;
+ public bool SupportsStreamingValue { get; set; }
+ public bool EmitChunks { get; set; }
+ public int EmittedChunks { get; private set; }
+ public int NonStreamingCalls { get; private set; }
+ public int StreamingCalls { get; private set; }
+ public CancellationToken StreamingToken { get; private set; }
+ public LlmRequestEnvelope? LastEnvelope { get; private set; }
+ public Result? StreamingResult { get; set; }
+
+ public bool IsConfigured() => true;
+
+ public Task> SendAsync(LlmRequestEnvelope envelope)
+ {
+ NonStreamingCalls++;
+ LastEnvelope = envelope;
+ return Task.FromResult(Result.Ok(new LlmResponse
+ {
+ RequestId = envelope.RequestId,
+ Content = "regular-response"
+ }));
+ }
+
+ public Task> SendStreamAsync(
+ LlmRequestEnvelope envelope,
+ Action onChunk,
+ CancellationToken ct = default)
+ {
+ StreamingCalls++;
+ StreamingToken = ct;
+ LastEnvelope = envelope;
+ if (EmitChunks)
+ {
+ onChunk(new LlmChunk { DeltaContent = "stream-", IsLast = false });
+ onChunk(new LlmChunk { DeltaContent = "response", IsLast = true });
+ EmittedChunks = 2;
+ }
+
+ return Task.FromResult(StreamingResult ??
+ Result.Ok(new LlmResponse
+ {
+ RequestId = envelope.RequestId,
+ Content = "stream-response",
+ TokensUsed = 9,
+ PromptTokens = 5,
+ CompletionTokens = 4,
+ HttpStatusCode = 200,
+ State = AIRequestState.Completed
+ }));
+ }
+
+ public Task> SpawnNpcAsync(NpcProfile profile) =>
+ Task.FromResult(Result.Ok(true));
+
+ public Task> KillNpcAsync(string npcId) =>
+ Task.FromResult(Result.Ok(true));
+
+ public Task, RimMindError>> QueryNpcMemoriesAsync(
+ string npcId,
+ string query,
+ int limit) =>
+ Task.FromResult(Result, RimMindError>.Ok(new List()));
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/IntegrationTests/RimMindCore.Integration.Tests.csproj b/IntegrationTests/RimMindCore.Integration.Tests.csproj
new file mode 100644
index 00000000..12ee3341
--- /dev/null
+++ b/IntegrationTests/RimMindCore.Integration.Tests.csproj
@@ -0,0 +1,42 @@
+
+
+
+ Library
+ net10.0
+ RimMind.IntegrationTests
+ RimMindCore.Integration.Tests
+ 10.0
+ enable
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IntegrationTests/xunit.runner.json b/IntegrationTests/xunit.runner.json
new file mode 100644
index 00000000..ab3c8d1e
--- /dev/null
+++ b/IntegrationTests/xunit.runner.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
+ "shadowCopy": false,
+ "methodDisplay": "method",
+ "diagnosticMessages": true
+}
diff --git a/Languages/ChineseSimplified/Keyed/RimMind_Core.xml b/Languages/ChineseSimplified/Keyed/RimMind_Core.xml
index fa8d96c4..2aa82a7b 100644
--- a/Languages/ChineseSimplified/Keyed/RimMind_Core.xml
+++ b/Languages/ChineseSimplified/Keyed/RimMind_Core.xml
@@ -1,290 +1,786 @@
-
- AI 请求调试日志
- 搜索来源或响应...
- 清空
- 复制响应
- (点击左侧条目查看详情)
- {0} 条记录
- tok
- 重试
- 等待
- 尝试
- 排队等待
- (空)
-
-
- RimMind-Core\n\n点击切换已注册功能的开/关\nShift+点击打开模组设置\nCtrl+点击打开调试日志窗口
-
-
- API 配置
- 上下文过滤
- 自定义提示词
-
-
- AI 服务商
- 选择 AI 服务提供商。Player2 可自动检测本地桌面应用,无需手动配置。
- OpenAI 兼容
- Player2
- API 密钥
- 你的 AI 服务商提供的 API Key,用于身份验证。
- API 端点
- AI 服务的 API 地址。支持 OpenAI 兼容接口(如 DeepSeek、本地 Ollama 等)。
- 模型名称
- 要使用的模型 ID,如 gpt-4o-mini、deepseek-chat 等。
- 强制 JSON 模式(response_format)
- 向 API 追加 response_format=json_object,强制模型返回 JSON。不支持该参数的本地模型请关闭。
- 最大 Token 数
- AI 单次响应的最大 Token 数。增大可获得更长回复,但增加费用。
- 重置为默认
- 详细日志(Player.log)
- 在 Player.log 中输出详细的 AI 请求和响应信息,用于排查问题。
- 测试连接
- ✓ 已连接
- ⚠ 未配置 API Key
- ✗ 连接失败
- 测试中...
- 显示
- 隐藏
-
-
- Player2 是一个 AI 游戏助手平台。选择此模式后,系统会自动检测本地 Player2 桌面应用并完成认证,无需手动输入 API Key。如未安装本地应用,也可手动输入 API Key。
- 可选
- Player2 优先使用本地桌面应用自动认证。仅在未安装本地应用时需要手动输入 API Key。
- 检测本地 Player2 应用
- Player2 不可用:未检测到本地应用且未配置 API Key
- RimMind: 已检测到 Player2 桌面应用!使用自动认证(无需 API Key)。
- RimMind: 未检测到 Player2 桌面应用。请启动应用或手动输入 API Key。
-
-
- 模型行为
- 请求设置
- 调试
- 最大并发请求数
- 同时向 AI 发送请求的上限。增大可提高响应速度,但会增加 API 费用。
- 最大重试次数
- 请求失败后的最大重试次数。设为 0 则不重试。
- 请求超时(秒)
- 等待 AI 响应的最大时间,超时后请求将被取消。网络较慢或模型较大时可适当增大。
- 队列:已暂停
- 队列:运行中
- 队列状态已移至「队列」标签页查看
- 队列
- 队列尚未初始化(需进入游戏存档后可用)
- 总体状态
- 活跃请求
- 排队请求
- 本地模型
- 忙碌
- 空闲
- 恢复队列
- 暂停队列
- 清空队列
- 清空冷却
- 各 Mod 队列
- 暂无注册的 Mod 队列
- 冷却
- 就绪
- 排队
- 活跃请求详情
- 当前无活跃请求
- 排队请求详情
- 当前无排队请求
- 优先级
- 尝试
- 已用时
- 等待
- 这是一个测试信息,请用一句话回复。
-
-
- 控制哪些游戏状态信息注入 AI Prompt。关闭不需要的字段可节省 Token 消耗。
- 快速预设
- 小人信息
- 环境信息
- 重置为默认(标准)
-
-
- 最小
- 仅核心信息,最少 Token
- 标准
- 平衡细节与消耗(推荐)
- 完整
- 所有字段,Token 消耗最高
- 自定义
- 手动勾选各项
-
-
- 种族
- 将小人的种族信息注入 AI 上下文。
- 年龄
- 将小人的年龄注入 AI 上下文。
- 性别
- 将小人的性别注入 AI 上下文。
- 背景故事
- 将小人的童年和成年背景故事注入 AI 上下文。
- 意识形态(需 Ideology DLC)
- 将小人的意识形态和信念注入 AI 上下文。
- 特性
- 将小人的性格特性注入 AI 上下文。
- 技能
- 将小人的技能等级注入 AI 上下文。低于最低技能等级的技能会被过滤。
- 最低技能等级
- 低于此等级的技能不会注入 AI 上下文,可节省 Token。
- 健康状况
- 将小人的健康问题(伤口、疾病、义体等)注入 AI 上下文。
- 行动能力
- 将小人的身体行动能力(操纵、移动、视觉等)注入 AI 上下文。
- 心情
- 将小人的当前心情百分比注入 AI 上下文。
- 心情因子(前8条)
- 将影响心情的前8条因子注入 AI 上下文。
- 当前任务
- 将小人正在执行的任务注入 AI 上下文。
- 工作分配
- 将小人的工作优先级分配注入 AI 上下文。
- 武器与服装
- 将小人装备的武器和服装注入 AI 上下文。
- 携带物品
- 将小人携带的物品(背包)注入 AI 上下文。
- 位置与温度
- 将小人所在区域(室内/室外/房间类型)和温度注入 AI 上下文。
- 社交关系
- 将小人与其他人的社交关系注入 AI 上下文。
- 基因(需 Biotech DLC)
- 将小人的基因信息注入 AI 上下文。
- 周围环境
- 将小人周围的建筑、物品和动物注入 AI 上下文。
- 战斗状态
- 将小人是否在战斗中、是否被征召等状态注入 AI 上下文。
-
-
- 游戏时间
- 将当前游戏日期和时间注入 AI 上下文。
- 殖民者数量
- 将殖民者和囚犯数量注入 AI 上下文。
- 殖民者名单
- 将殖民者和囚犯的名字注入 AI 上下文。
- 财富等级
- 将殖民地财富和威胁等级注入 AI 上下文。
- 食物储量
- 将殖民地的食物营养总量注入 AI 上下文。
- 季节
- 将当前季节注入 AI 上下文。
- 天气
- 将当前天气状况注入 AI 上下文。
- 威胁等级
- 将当前地图的威胁等级注入 AI 上下文。
-
-
- [当前地图状态]
- 时间:{0} {1}:00
- 殖民者:{0}人({1})
- 殖民者:{0}人
- 犯人:{0}人({1})
- 极高
- 高
- 中
- 低
- 财富:{0}(威胁等级{1})
- 威胁等级:{0}
- 食物储量:约 {0} 营养
- 季节:{0}
- 天气:{0}
-
-
- [{0} 的状态]
- {0}岁
- 童年: {0}
- 成年: {0}
- 背景:{0}
- 意识形态:{0}{1}
- 心情:{0}(精神崩溃:{1})
- 心情:{0}(倒地中)
- 心情:{0} {1}%
- 心情因子:{0}
- 全身
- 健康问题:{0}
- 行动能力:{0}
- 技能:{0}
- 无
- 当前任务:{0}
- 工作分配:{0}
- 特性:{0}
- 武器:{0}
- 服装:{0}
- 携带:{0}
- 损坏
- 室内
- 室外
- 位置:{0} 温度:{1}°C
- 基因:{0}
- 关系:{0}
- 战斗中(目标:{0})
- 征召中
- 未知
- 周围:{0}
- 建筑:{0}
- 物品:{0}
- 动物:{0}
-
-
- 崩溃
- 倒地
- 心情:{0}
- 健康:{0}
- 任务:{0}
- 位置:{0} 温度:{1}°C
- 武器:{0}
-
-
- 日{0} {1}:{2}
-
-
- [RimMind] 连接成功:{0}
- [RimMind] 连接失败:{0}
- [RimMind] 请求已发送,等待响应...
-
-
- 在此编辑全局自定义提示词,将追加到 AI Prompt 末尾。留空则不追加。
- 全局人物提示词
- 追加在人物上下文末尾的自定义提示词(留空则不追加):
- 全局地图提示词
- 追加在地图上下文末尾的自定义提示词(留空则不追加):
-
-
- ({0}条更早的记录已省略)
-
-
- [玩家自定义人物提示词]
- [玩家自定义地图提示词]
-
-
- 只返回有效的 JSON 格式,不要输出任何其他内容。不要用 markdown 代码块包裹。
- 如果无法判断,返回默认值或空结果。
- 你处于 RimWorld 游戏世界中。不要打破第四面墙,不要提及游戏机制,不要提及"玩家"或"游戏"。
- 不要打破第四面墙。不要提及游戏机制、代码、数据结构或"玩家"概念。以角色身份自然行动。
- [玩家自定义规则]
-
-
- 角色
- 目标
- 流程
- 约束
- 示例
- 输出
- 兜底
- 自定义
-
-
- RimMind 请求
- [系统拦截] {0}
- 暂无待审批请求
- 详细
- 拖拽调整大小
-
-
- 审批与请求
- 全部清除
- {0} 条待审批
+
+ RimMind-Core\n\n点击开关请求浮窗\nShift+点击打开模组设置\nCtrl+点击打开调试中心
+
+ API 配置
+ 上下文过滤
+ 自定义提示词
+
+ AI 服务商
+ 选择 AI 服务提供商。Player2 可自动检测本地桌面应用,无需手动配置。
+ OpenAI 兼容
+ Player2
+ 点击切换 AI 服务商或选用主流预设模板。
+ 自定义端点
+ ⚡ OpenCode Go (订阅直连 / sub2api)
+ 🌟 DeepSeek (深度求索)
+ 🚀 SiliconFlow (硅基流动)
+ 🌙 Moonshot AI (月之暗面 Kimi)
+ 🦙 Ollama (本地私有化部署)
+ 🤖 OpenAI 官方接口
+ 🌐 自定义 OpenAI 兼容端点
+ 🛡️ 扩展模型服务 (ModelService 负载均衡/订阅代理)
+ 🛡️ 扩展模型服务 (未安装 - 支持 OpenCode Go / Codex 代理)
+ 扩展模型服务已激活 (RimMind-Extension-ModelService)
+ AI 请求将由 ModelService 调度器接管,支持 OpenCode Go 订阅、Codex 本地网关以及多端点自动容灾重试与负载均衡。
+ ⚙️ 前往「模型服务」配置端点池 (OpenCode Go / Codex / 多节点)
+ 切换到模型服务设置标签页,管理多个 API 端点、配置权重与优先级容灾。
+ ⚠ 扩展模型服务未配置可用端点,请在「模型服务」标签页添加节点
+ API 密钥
+ 你的 AI 服务商提供的 API Key,用于身份验证。
+ 已保存({0} 字符)。点击显示后可编辑。
+ 点击此处输入 API 密钥...
+ API 端点
+ AI 服务的 API 地址。支持 OpenAI 兼容接口(如 DeepSeek、本地 Ollama 等)。
+ 模型名称
+ 要使用的模型 ID,如 deepseek-v4-flash、gpt-4o-mini 等。
+ 强制 JSON 模式(response_format)
+ 向 API 追加 response_format=json_object,强制模型返回 JSON。不支持该参数的本地模型请关闭。
+ 最大 Token 数
+ AI 单次响应的最大 Token 数。增大可获得更长回复,但增加费用。
+ 温度
+ 控制随机性。低值更确定,高值更有创意。范围:0.0-2.0
+ 详细日志(Player.log)
+ 在 Player.log 中输出详细的 AI 请求和响应信息,用于排查问题。
+ 测试连接
+ ⚠ 未配置 API Key
+ 测试中...
+ 显示
+ 隐藏
+
+ Player2 是一个 AI 游戏助手平台。选择此模式后,系统会自动检测本地 Player2 桌面应用并完成认证,无需手动输入 API Key。如未安装本地应用,也可手动输入 API Key。
+ 可选
+ Player2 优先使用本地桌面应用自动认证。仅在未安装本地应用时需要手动输入 API Key。
+ 检测本地 Player2 应用
+ Player2 不可用:未检测到本地应用且未配置 API Key
+ 远程 API 地址
+ Player2 远程服务地址。默认为 https://api.player2.game,一般无需修改。
+ 余额:未知
+ 刷新余额
+
+ Agent 控制
+ 打开此小人的 RimMind 调试中心 Agent 页面。
+ Dev: 查看 Agent
+ 在日志中输出 Agent 内部状态
+ 激活
+ 休眠
+ 暂停
+ 终止
+ RimMind: 已检测到 Player2 桌面应用!使用自动认证(无需 API Key)。
+ RimMind: 未检测到 Player2 桌面应用。请启动应用或手动输入 API Key。
+ 最大并发请求数
+ 同时向 AI 发送请求的上限。增大可提高响应速度,但会增加 API 费用。
+ 日常活跃与触发频率
+ 统一调控殖民者心智思考、日常对话与叙事建议的触发频率。\n\n• 极低 (0.2x):每 3 天约触发 1 次事件(适合极低 API 消耗需求)。\n• 中等/默认 (1.0x):以 3 名殖民者为基准,全殖民地每天约触发 3 次(每人每天约 1 次,兼顾日常生动与开销)。\n• 极高 (3.0x):全殖民地每天最多约 10 次(就餐、劳作、娱乐、晨昏频繁互动与讨论)。\n\n提示:若开启了小人自主 Agent 决策循环,实际请求量会随小人自主行为额外提升。
+ 极低 (约3天1次)
+ 较少 (约2天1次)
+ 默认日常 (3人约每天3次)
+ 活跃 (3人约每天6次)
+ 极度活跃 (3人约每天10次)
+ 自动激活殖民者智能体
+ 开启后,加入殖民地的新生或新加入殖民者将自动启动智能体自主感知与行动循环(无需手动在智能体面板逐个点击激活)。
+ 最大重试次数
+ 请求失败后的最大重试次数。设为 0 则不重试。
+ 请求超时(秒)
+ 等待 AI 响应的最大时间,超时后请求将被取消。网络较慢或模型较大时可适当增大。
+ 请求过期时长(tick)
+ 超过此 tick 数的请求将自动从队列中移除,防止过期请求堆积。范围:6000-120000。
+ 行为历史记录上限
+ 每个 Agent 保留的最大行为历史记录数。较高的值提供更多上下文但增加 Token 消耗。范围:10-500。
+ 队列处理间隔(tick)
+ 队列处理周期之间的 tick 间隔。较低的值处理请求更快但消耗更多 CPU。范围:10-300。
+ 默认 Mod 冷却(tick)
+ 同一 Mod 连续请求之间的默认冷却 tick 数,防止 API 频率限制。各 Mod 可覆盖此值。范围:600-36000。
+ 队列:已暂停
+ 队列:运行中
+ 队列状态已移至「队列」标签页查看
+ 队列
+ 队列尚未初始化(需进入游戏存档后可用)
+ 总体状态
+ 活跃请求
+ 排队请求
+ 本地模型
+ 忙碌
+ 空闲
+ 恢复队列
+ 暂停队列
+ 清空队列
+ 清空冷却
+ 各 Mod 队列
+ 暂无注册的 Mod 队列
+ 冷却
+ 就绪
+ 排队
+ 活跃请求详情
+ 当前无活跃请求
+ 排队请求详情
+ 当前无排队请求
+ 优先级
+ 尝试
+ 已用时
+ 等待
+ 这是一个测试信息,请用一句话回复。
+
+ 控制哪些游戏状态信息注入 AI Prompt。关闭不需要的字段可节省 Token 消耗。
+ 快速预设
+ 小人信息
+ 环境信息
+ 重置为默认(标准)
+ 将所有上下文过滤选项重置为标准推荐预设。
+
+ 最小
+ 仅核心信息,最少 Token
+ 标准
+ 平衡细节与消耗(推荐)
+ 完整
+ 所有字段,Token 消耗最高
+ 自定义
+ 手动勾选各项
+
+ 种族
+ 将小人的种族信息注入 AI 上下文。
+ 年龄
+ 将小人的年龄注入 AI 上下文。
+ 性别
+ 将小人的性别注入 AI 上下文。
+ 背景故事
+ 将小人的童年和成年背景故事注入 AI 上下文。
+ 意识形态(需 Ideology DLC)
+ 将小人的意识形态和信念注入 AI 上下文。
+ 特性
+ 将小人的性格特性注入 AI 上下文。
+ 技能
+ 将小人的技能等级注入 AI 上下文。低于最低技能等级的技能会被过滤。
+ 最低技能等级
+ 注入上下文的最低技能等级阈值。低于此等级的技能将不会被发送。
+ 健康状况
+ 将小人的健康问题(伤口、疾病、义体等)注入 AI 上下文。
+ 行动能力
+ 将小人的身体行动能力(操纵、移动、视觉等)注入 AI 上下文。
+ 心情
+ 将小人的当前心情百分比注入 AI 上下文。
+ 心情因子(前8条)
+ 将影响心情的前8条因子注入 AI 上下文。
+ 当前任务
+ 将小人正在执行的任务注入 AI 上下文。
+ 工作分配
+ 将小人的工作优先级分配注入 AI 上下文。
+ 武器与服装
+ 将小人装备的武器和服装注入 AI 上下文。
+ 携带物品
+ 将小人携带的物品(背包)注入 AI 上下文。
+ 位置与温度
+ 将小人所在区域(室内/室外/房间类型)和温度注入 AI 上下文。
+ 社交关系
+ 将小人与其他人的社交关系注入 AI 上下文。
+ 基因(需 Biotech DLC)
+ 将小人的基因信息注入 AI 上下文。
+ 周围环境
+ 将小人周围的建筑、物品和动物注入 AI 上下文。
+ 战斗状态
+ 将小人是否在战斗中、是否被征召等状态注入 AI 上下文。
+
+ 游戏时间
+ 将当前游戏日期和时间注入 AI 上下文。
+ 殖民者数量
+ 将殖民者和囚犯数量注入 AI 上下文。
+ 殖民者名单
+ 将殖民者和囚犯的名字注入 AI 上下文。
+ 财富等级
+ 将殖民地财富和威胁等级注入 AI 上下文。
+ 食物储量
+ 将殖民地的食物营养总量注入 AI 上下文。
+ 季节
+ 将当前季节注入 AI 上下文。
+ 天气
+ 将当前天气状况注入 AI 上下文。
+ 威胁等级
+ 将当前地图的威胁等级注入 AI 上下文。
+ 上下文预算比例
+ 控制 AI 请求中上下文占用总窗口的最大比例(推荐 0.8,即保留 20% 空间供模型思考与输出;超过 1.0 时仅允许在超大窗口模型下放宽限制)。
+ 上下文变更保留时长
+ 上下文变更差异保留的时长(tick)。较低的值使上下文保持更及时,较高的值保留更长的变更历史。
+ 上下文校准间隔
+ Flywheel 上下文校准间隔(tick)。较短的间隔更新更频繁但消耗更多 CPU。范围:5000-60000。
+
+ [当前地图状态]
+ 时间:{0} {1}:00
+ 殖民者:{0}人({1})
+ 殖民者:{0}人
+ 犯人:{0}人({1})
+ 极高
+ 高
+ 中
+ 低
+ 财富:{0}(威胁等级{1})
+ 威胁等级:{0}
+ 食物储量:约 {0} 营养
+ 季节:{0}
+ 天气:{0}
+
+ [{0} 的状态]
+ {0}岁
+ 童年: {0}
+ 成年: {0}
+ 背景:{0}
+ 意识形态:{0}{1}
+ 心情:{0}(精神崩溃:{1})
+ 心情:{0}(倒地中)
+ 心情:{0} {1}%
+ 心情因子:{0}
+ 全身
+ 健康问题:{0}
+ 行动能力:{0}
+ 技能:{0}
+ 无
+ 当前任务:{0}
+ 工作分配:{0}
+ 特性:{0}
+ 武器:{0}
+ 服装:{0}
+ 携带:{0}
+ 位置:{0} 温度:{1}°C
+ 基因:{0}
+ 关系:{0}
+ 战斗中(目标:{0})
+ 征召中
+ 未知
+ 周围:{0}
+ 建筑:{0}
+ 物品:{0}
+ 动物:{0}
+
+ 崩溃
+ 倒地
+ 心情:{0}
+ 健康:{0}
+ 任务:{0}
+ 位置:{0} 温度:{1}°C
+ 武器:{0}
+
+ [RimMind] 连接成功:{0}
+ [RimMind] 连接失败:{0}
+ [RimMind] 请求已发送,等待响应...
+
+ 在此编辑全局自定义提示词,将追加到 AI Prompt 末尾。留空则不追加。
+ 全局人物提示词
+ 追加在人物上下文末尾的自定义提示词(留空则不追加):
+ 全局地图提示词
+ 追加在地图上下文末尾的自定义提示词(留空则不追加):
+ 将自定义人物提示词清空并恢复默认设置。
+ 将自定义地图提示词清空并恢复默认设置。
+ 全部显示
+ 同时显示人物和地图的所有自定义提示词。
+
+ RimMind 请求
+ [系统拦截] {0}
+ 暂无待审批请求
+ 详细
+ 拖拽调整大小
+
+ 审批与请求
+ 全部清除
+ {0} 条待审批
+ 请求队列已暂停,请在设置中恢复
+ 未配置 API Key,请在设置中配置
+ 当前地图无 Agent,请先为小人创建 Agent
+ 暂无请求产生,Agent 会在思考周期中自动生成请求
+
+ Agent 对话
+ 发送
+ 玩家
+ Agent
+ (思考中...)
+ 请求 {0}:{1}
+ 空闲
+ 流式响应中
+ 已完成
+ 失败
+ 已丢弃(运行时已切换)
+
+ 你处于 RimWorld 游戏世界中。\n\n输出约束:\n- 必须返回有效的 JSON 格式\n- 不要用 markdown 代码块包裹\n- 如果无法判断,返回默认值或空结果\n\n行为约束:\n- 不要打破第四面墙\n- 不要提及游戏机制、代码、数据结构或"玩家"概念\n- 以角色身份自然行动
+ 你处于 RimWorld 游戏世界中。
+ {0}对你说:{1}
+ 根据以上上下文,提供你的{0}回应。
+
+ 姓名:{0}
+ 简称:{0}
+ 描述:{0}
+
+ 可用命令:
+ - {0}:{1}
+
+ 核心规则:
+ - 殖民者必须通过管理食物、住所和心情来生存
+ - 战斗很危险;避免不必要的战斗
+ - 人际关系影响心情和生产力
+ - 天气和季节影响生存
+ - 医疗需求必须及时处理
+
+ 健康
+ 精神崩溃:{0}
+ 闲逛
+ 已征召
+ 战斗中:{0}
+ 未在战斗中
+ 人口:{0} 财富:{1} 威胁:{2}
+ 目标:{0}
+
+ {0} 切换为 {1} 模式
+
+ 自动应用参数建议
+ 根据系统运行历史和模型反馈,自动调整模型调用参数的自适应优化策略。
+ 关闭
+ 仅记录
+ 记录并应用
+ 置信度阈值:{0}
+ 推荐信度达到此阈值时才自动应用
+ 对话场景
+ 决策场景
+ 人格评估场景
+ 叙事场景
+ 暗记忆生成场景
+ RimMind v2.0 包含破坏性变更。旧存档可能不完全兼容。AI 请求队列和部分内部结构已重组。建议开始新游戏以获得最佳体验。
+
+ 远程同步
+ 自动同步
+ 加载时自动拉取
+ 加载存档时自动从远程拉取数据。
+ 变更时自动推送
+ 本地数据变更时自动推送到远程存储。
+ 同步粒度
+ 同步记忆
+ 将智能体记忆数据纳入远程同步。
+ 同步设置
+ 将模组设置纳入远程同步。
+ 同步智能体身份
+ 将智能体身份和人格数据纳入远程同步。
+ 手动操作
+ 远程后端未配置。手动操作需要已配置的远程存储后端。
+ 从远程拉取
+ 推送到远程
+ 正在拉取...
+ 正在推送...
+ 拉取成功。
+ 拉取失败:{0}
+ 推送成功。
+ 推送失败:{0}
+
+ ToolCall 调试
+ 无已注册工具
+ 工具由子模组注册。安装 Actions 等子模组后,工具会自动出现在此列表中。
+ ID: {0}
+ 描述: {0}
+ 参数 Schema:
+ JSON 参数:
+ 执行
+ 执行中...
+ 结果:
+ 成功 ({0}ms)
+ 失败 ({0}ms)
+ 分类: {0}
+ 从列表中选择工具
+
+ 机制状态
+ 无已注册机制
+ 机制是游戏状态操作面,由子模组注册。安装 Actions 等子模组后,机制会自动出现。
+ 描述: {0}
+ 所属: {0}
+ 作用域: {0}
+ 风险: {0}
+ 操作: {0}
+ 工具: {0}
+ 过滤作用域
+ 过滤风险
+ (无工具映射)
+
+ 智能体模式调试
+ 当前地图无智能体
+ 为小人创建 Agent 后,其模式信息会显示在此处
+ 无已注册模式
+ 模式
+ 允许工具
+ 应思考
+ 切换模式
+ 选择一个小人
+ 模式变更历史
+ 无模式变更记录
+ {0}: {1} → {2}
+ 已注册模式
+ {0} - {1}
+ 小人列表
+ 模式详情
+ 切换至:
+
+ 智能体
+ 未分配智能体
+ 此小人尚未拥有 Agent。点击下方按钮创建一个。
+ 创建/获取 Agent
+ 无法创建 Agent:工厂不可用
+ 状态
+ 目标
+ 策略权重
+ 行为历史
+
+ RimMind 调试中心
+ 概览
+ Agent
+ AI 请求
+ ToolCall
+ 机制
+ 上下文键
+ 设置
+ 已选择小人
+ 未选择小人
+ 运行时 Agent 状态
+ 待处理 AI 请求
+ Agent 运行循环
+ 最近循环 Tick
+ 循环故障数
+ 小人
+ 作用域
+ 尚未运行
+ 队列:尚未初始化
+ 队列状态
+ 快捷操作
+ 设置
+ 设置
+ 调试中心只提供设置入口,完整配置仍在 RimMind 设置窗口中维护。
+ 打开设置
+ 实时 LLM 连通性与模型状态
+ 显示当前配置的模型服务商、端点与连通性,支持一键发起即时测试。
+ ⚡ 发起实时测试请求
+ 在后台向已配置的 AI 模型发送快速测试请求,测量往返延迟、HTTP 状态和响应令牌数。
+ 连通状态
+ 未测试
+ 延迟
+ Tokens
+ 响应摘要
+ 请求队列与冷却控制台
+ 监控请求队列并发与冷却状态,支持暂停派发、清空排队以及重置模组频控。
+ 已清空所有排队中的请求。
+ 已重置所有模组的请求冷却。
+ 暂停或继续请求队列的任务消费与后台派发。
+ 丢弃所有正在排队等待发送的 AI 请求。
+ 重置所有模组的熔断冷却时间与频控限制。
+ 运行时配置快捷微调
+ 直接微调核心运行时参数并即时保存生效,无需重启游戏。
+ 允许同时向大模型发送的并发请求上限 (1 ~ 5)。
+ 等待 AI 响应的最长超时时间 (15秒 ~ 120秒)。
+ 启用详细的开发者调试日志输出到控制台。
+ 离线模拟模式
+ 启用离线模拟响应,无需连接外部 AI 接口即可测试闭环。
+ 殖民者智能体实时状态
+ 查看当前选中殖民者的 Agent 状态与思考周期,可单步触发思考或检查上下文报文。
+ 👥 选择殖民者
+ 从当前地图的存活殖民者中快速切换目标。
+ 工作流阶段
+ 自主级别
+ 🔍 检查当前小人报文
+ 打开当前殖民者的完整上下文报文检查器,查看 Prompt、记忆与环境数据。
+ ⚡ 立即触发单次思考
+ 重置思考冷却并立即执行一次 Agent Tick,驱动该小人发起决策。
+ 已触发小人思考循环: {0}
+ 小人尚未初始化 Agent 组件。
+ ⚡ 激活全员智能体
+ 立即将当前殖民地内所有处于休眠状态的可用殖民者激活为自主智能体。
+ 已成功将 {0} 位殖民者激活为自主智能体!
+
+ ID
+ 状态
+ 时间
+ 作用域
+ 角色
+ 通道
+ 模型
+ 摘要
+ 请求
+ 暂无调试记录。
+ 等待中
+ 流式返回中
+ 已完成
+ 已失败
+ 已取消
+ 无操作
+ {0} 个操作
+ 优先级 {0}
+ 有效优先级 {0}
+ 更新次数 {0}
+ 上下文
+ 分数 {0}
+ 小人
+ 地图
+ 建筑
+ 物体
+ 世界
+ 殖民地
+ 安全
+ 中等风险
+ 危险
+ 查询
+ 设置
+ 添加
+ 移除
+ 切换
+ 触发
+ 列出
+ 监视
+ 静态
+ 基线
+ 环境
+ 状态
+ 历史
+ 传感器
+ 静态
+ 小人
+ 地图
+ 叙事者
+ 剧本
+
+ Agent 状态调试
+ 未选择小人
+ 点击下方「创建/获取 Agent」按钮为此小人创建 Agent
+ 小人: {0} (ID: {1})
+ NPC ID
+ Agent: 未创建
+ 模式
+ 思考中
+ 上次思考
+ 创建/获取 Agent
+ 构建上下文
+ 发送测试请求
+ 请求日志
+ ToolCall 调试
+ 机制状态
+ 队列状态
+ 上下文快照
+ 无数据
+ 正在构建上下文…
+ 队列状态:已暂停
+ 队列状态:运行中(活跃: {0},排队: {1})
+ 作用域 ID
+ 地图 ID
+ 成功率
+ 近期行为
+ 无行为历史
+ 身份信息
+ 作用域类型
+ 状态
+ 小人信息
+ 智能体状态
+ 销毁代理
+
+ 上下文键调试
+ 无已注册上下文键
+ 上下文键由 Core 和子模组注册。进入游戏存档后,键会自动出现在此列表中。
+ 过滤层
+ 过滤所属
+ 当前过滤条件下无匹配键
+ 从列表中选择上下文键
+ 键: {0}
+ 层: {0}
+ 优先级: {0} (自适应: {1})
+ 所属 Mod: {0}
+ 提供者: {0}
+ 场景: {0}
+ 重复键: {0} (被 {1} 覆盖)
+ 检测重复键
+ 无重复键
+ 已注册
+ 未注册
+ 缓存范围: {0}
+ 覆盖:此前由 {0} 注册
+
+ Agent 流程实验室
+ 未选择小人
+ 当前小人: {0}
+ 刷新
+ 范围
+ 小人
+ 地图
+ 殖民地
+ 全局
+ 此范围已显示用于规划,但运行时控制尚未接线;当前可用的是小人 Agent。
+ {0} 范围 Agent。上下文构建和决策流程可用;完整运行时控制受限。
+ 范围 Agent: {0}:{1} [{2}]
+ Agent 生命周期
+ Agent: 已存在 ({0})
+ Agent: 未创建
+ 创建/获取 Agent
+ 上下文构建
+ 构建上下文快照
+ 估算 Token: {0}
+ LLM 请求
+ 发送测试请求
+ 请求状态: {0}
+ 决策解析
+ 机制映射
+ 试运行机制映射
+ 执行机制(需确认)
+ 确认执行?此操作将实际执行游戏机制,可能影响游戏状态。
+ 队列状态
+ 错误日志
+ 清除错误
+ 无数据
+ 无错误
+ 消息数: {0}
+ 离线模式(存根)
+ 在线模式(真实 API)
+ 不发送 API 请求,使用存根响应进行测试。
+ 发送真实 API 请求,消耗 Token。
+ 使用离线存根响应
+ 执行(需先试运行)
+ 试运行: 已注册 {0} 个机制
+ 决策动作: {0}
+ 目标机制: {0}(风险: {1})
+ 未找到机制: {0}
+ 没有可用于机制映射的已解析决策。
+ 执行已阻止:试运行没有映射出的写入参数。
+ 执行已阻止:机制注册表不可用。
+ 验证结果: {0}
+ 通过
+ 失败: {0}
+ 打开日志
+ 请求日志
+ 工具调用调试
+ 机制状态
+ 上下文键
+ 智能体进度
+
+ 智能体进度
+ 无活跃智能体
+ 为小人创建智能体后,其进度会显示在此处
+ 队列:已暂停
+ 队列:运行中(活跃: {0},排队: {1})
+ 空闲
+ 感知中
+ 思考中
+ 执行中
+ 记录中
+ 已用时: {0}
+ 详情
+
+ 活跃
+ 暂停
+ 待创建
+ 其他
+ 休眠
+ 已终止
+ 创建/启动
+ 激活
+ 重启
+ 暂停
+ 恢复
+ 强制思考
+ 打开请求
+ 发送
+ 状态
+ 活动
+ 此 Agent 还没有请求流条目。
+ 等待回复
+ 成功
+ 出错
+ 等待模型响应...
+ 请求异常
+ 触发日常反应/独白
+ 触发 AI 交互
+ 调用工具: {0}
+ 【AI 交互记录】 {0}
+ 耗时: {0}ms
+ Tokens: {0}
+ 模型: {0}
+ 来源: {0}
+ 触发意图: {0}
+ 工具调用: {0}
+ 交互内容: {0}
+ 错误详情: {0}
+ 创建智能体失败,请检查服务是否可用。
+ 智能体聊天输入尚未接入,已保留当前草稿。
+
+ AI 请求追踪日志不可用。
+ 暂无 AI 请求记录。
+ 请求: {0}\n状态: {1}\n提示词: {2}\n响应: {3}\n错误: {4}\nToolCall: {5}
+ 请求
+ 系统提示
+ 用户提示
+ 助手上下文
+ 响应
+ 错误
+ 工具调用
+ 空
+ 运行中
+ 已完成
+ 已失败
+
+ 生命周期
+ 运行时状态
+ 游戏状态
+ 世代
+ 服务数
+ 发布时间(UTC)
+ 运行时 ID
+ 最近构建失败
+ 已丢弃过期完成
+ 从未
+ 无
+ 构建中
+ 运行中
+ 已停止
+ 失败
+ 从未发布
+ 运行时已切换,过期结果已丢弃。
+ 批准
+ 拒绝
+ [{0}] {1} 申请执行:{2}
+
+ RimMind: 需要配置 API 密钥
+ RimMind 检测到当前选中的 AI 提供商尚未配置 API 密钥。请在“选项 -> Mod 设置 -> RimMind”中输入您的 API 密钥,以激活殖民者智能体决策、AI 对话与叙事者等功能。
+
+ 显示浮动心智监视窗
+ 在屏幕上显示所有活动殖民者的实时思考与工作流状态条(默认关闭)。
+ AI 报文与上下文检查器
+ 复制完整报文
+ 刷新快照
+ 发送实机测试
+ 发送中...
+ 殖民者列表
+ 场景:
+ 已成功复制完整 AI 请求报文到剪贴板!
+ 检查报文
+ === Prompt Caching (KV-Cache) 前缀稳定性分析 ===
+ • 稳定可缓存前缀 (Static Prefix) : ~{0} tokens ({1:F1}% 缓存占比)
+ - Zone 1 静态公理与指令 : ~{0} tokens (100% 全局复用)
+ - Zone 2 智能体基础档案 : ~{0} tokens (100% 同一小人跨回合复用)
+ - 确定性工具声明列表 : ~{0} tokens (字典序稳定对齐)
+ • 动态易变尾部 (Volatile Tail) : ~{0} tokens ({1:F1}%)
+ • 预估次轮 Cache 命中率 (Estimated Hit) : ~{0:F1}%
+ [✓] 截断边界已严格隔离:时间 (time_of_day)、天气与心情置于尾部,绝不击穿历史 Cache!
+ 小人档案与背景已对齐至 Zone 2 稳定前缀,跨回合 100% 享受 KV-Cache 命中。
+ 恢复默认
+
+ ⚙️ 快捷配置预设
+ 已应用预设:{0}
+ ⚡ 响应优先
+ 快速交互:MaxTokens=600,并发=3,超时=25秒,冷却=15秒。适合即时对话与快节奏智能体。
+ ⚖️ 均衡标准
+ 标准平衡:MaxTokens=800,并发=2,超时=45秒,冷却=30秒。兼顾思考质量与令牌开销。
+ 🛡️ 节能防限流
+ 经济防护:MaxTokens=400,并发=1,超时=60秒,冷却=60秒。适合免费额度或严格限流 API。
+ 一、连接与端点
+ 二、生成与模型行为
+ 三、性能与调度
+ 四、界面与悬浮窗
+ HUD 悬浮窗空闲时收起为微胶囊
+ 当没有待审批请求时,将 HUD 悬浮窗最小化为 120x24 的细长状态胶囊,鼠标悬停或有新审批时自动展开。
+ 启用浮动心智状态监视器
+ 在屏幕边缘浮动显示智能体思维活动与心情波动趋势监视条(实验性)。
+ 打开报文检查与测试工具
+ 打开实时报文检查器,可查看组装后的上下文、测试发送或复制完整 JSON 报文。
+ 待审批: {0}
+ RimMind 待审批悬浮窗(已收起)。悬停展开完整视图。
+ 系统概况与运行状态
+ 快速操作
+ 请求队列
+ 进行中
diff --git a/Languages/English/Keyed/RimMind_Core.xml b/Languages/English/Keyed/RimMind_Core.xml
index 1b8c680d..1ca4123e 100644
--- a/Languages/English/Keyed/RimMind_Core.xml
+++ b/Languages/English/Keyed/RimMind_Core.xml
@@ -1,290 +1,786 @@
-
- AI Request Debug Log
- Search source or response...
- Clear
- Copy Response
- (Click an entry on the left to view details)
- {0} records
- tok
- retry
- wait
- attempt
- queueWait
- (empty)
-
-
- RimMind-Core\n\nClick to toggle registered features on/off\nShift+Click to open mod settings\nCtrl+Click to open debug log window
-
-
- API Config
- Context Filter
- Custom Prompts
-
-
- AI Provider
- Select an AI service provider. Player2 can auto-detect the local desktop app, no manual configuration needed.
- OpenAI Compatible
- Player2
- API Key
- Your AI provider's API key for authentication.
- API Endpoint
- AI service API address. Supports OpenAI-compatible interfaces (e.g. DeepSeek, local Ollama).
- Model Name
- The model ID to use, e.g. gpt-4o-mini, deepseek-chat.
- Force JSON Mode (response_format)
- Append response_format=json_object to the API request, forcing the model to return JSON. Disable this for local models that don't support this parameter.
- Max Tokens
- Maximum tokens per AI response. Higher values allow longer replies but increase cost.
- Reset to Default
- Verbose Logging (Player.log)
- Output detailed AI request and response info to Player.log for troubleshooting.
- Test Connection
- ✓ Connected
- ⚠ API Key not configured
- ✗ Connection failed
- Testing...
- Show
- Hide
-
-
- Player2 is an AI gaming assistant platform. When selected, the system will auto-detect the local Player2 desktop app and authenticate, no API Key needed. If the local app is not installed, you can also enter an API Key manually.
- optional
- Player2 prioritizes local desktop app auto-authentication. Only enter an API Key manually if the local app is not installed.
- Detect Local Player2 App
- Player2 not available: local app not detected and no API Key configured
- RimMind: Player2 desktop app detected! Using automatic authentication (no API key needed).
- RimMind: Player2 desktop app not found. Please start the app or add an API key manually.
-
-
- Model Behavior
- Request Settings
- Debug
- Max Concurrent Requests
- Maximum number of simultaneous AI requests. Higher values improve responsiveness but increase API costs.
- Max Retry Count
- Maximum number of retries after a request failure. Set to 0 to disable retries.
- Request Timeout (seconds)
- Maximum time to wait for an AI response before timing out. Increase for slow models or networks.
- Queue: PAUSED
- Queue: Running
- Queue status has been moved to the "Queue" tab
- Queue
- Queue not initialized (available after loading a save)
- Overall Status
- Active Requests
- Queued Requests
- Local Model
- Busy
- Idle
- Resume Queue
- Pause Queue
- Clear Queues
- Clear Cooldowns
- Per-Mod Queues
- No registered mod queues
- Cooldown
- Ready
- Queued
- Active Request Details
- No active requests
- Queued Request Details
- No queued requests
- Priority
- Attempt
- Elapsed
- Waiting
- This is a test message, please reply in one sentence.
-
-
- Control which game state information is injected into the AI Prompt. Disabling unnecessary fields saves token consumption.
- Quick Presets
- Pawn Info
- Environment
- Reset to Default (Standard)
-
-
- Minimal
- Core info only, least tokens
- Standard
- Balanced detail and cost (Recommended)
- Full
- All fields, highest token cost
- Custom
- Manually select fields
-
-
- Race
- Inject the pawn's race info into AI context.
- Age
- Inject the pawn's age into AI context.
- Gender
- Inject the pawn's gender into AI context.
- Backstory
- Inject the pawn's childhood and adulthood backstories into AI context.
- Ideology (requires Ideology DLC)
- Inject the pawn's ideology and beliefs into AI context.
- Traits
- Inject the pawn's character traits into AI context.
- Skills
- Inject the pawn's skill levels into AI context. Skills below the minimum level are filtered out.
- Min Skill Level
- Skills below this level are not injected into AI context, saving tokens.
- Health
- Inject the pawn's health issues (wounds, diseases, implants, etc.) into AI context.
- Capacities
- Inject the pawn's body capacities (manipulation, moving, sight, etc.) into AI context.
- Mood
- Inject the pawn's current mood percentage into AI context.
- Mood Factors (top 8)
- Inject the top 8 mood-affecting factors into AI context.
- Current Job
- Inject the pawn's current job into AI context.
- Work Assignments
- Inject the pawn's work priority assignments into AI context.
- Weapons & Apparel
- Inject the pawn's equipped weapons and apparel into AI context.
- Carried Items
- Inject the pawn's carried items (inventory) into AI context.
- Location & Temperature
- Inject the pawn's location (indoors/outdoors/room type) and local temperature into AI context.
- Relations
- Inject the pawn's social relationships into AI context.
- Genes (requires Biotech DLC)
- Inject the pawn's gene information into AI context.
- Surroundings
- Inject nearby buildings, items, and animals into AI context.
- Combat Status
- Inject whether the pawn is in combat or drafted into AI context.
-
-
- Game Time
- Inject the current game date and time into AI context.
- Colonist Count
- Inject the colonist and prisoner counts into AI context.
- Colonist Names
- Inject the names of colonists and prisoners into AI context.
- Wealth Level
- Inject the colony wealth and threat level into AI context.
- Food Storage
- Inject the colony's total food nutrition into AI context.
- Season
- Inject the current season into AI context.
- Weather
- Inject the current weather conditions into AI context.
- Threat Level
- Inject the current map threat level into AI context.
-
-
- [Current Map Status]
- Time: {0} {1}:00
- Colonists: {0} ({1})
- Colonists: {0}
- Prisoners: {0} ({1})
- Extreme
- High
- Medium
- Low
- Wealth: {0} (Threat: {1})
- Threat Level: {0}
- Food Storage: ~{0} nutrition
- Season: {0}
- Weather: {0}
-
-
- [{0}'s Status]
- {0}yo
- Childhood: {0}
- Adulthood: {0}
- Background: {0}
- Ideology: {0}{1}
- Mood: {0} (Mental break: {1})
- Mood: {0} (Downed)
- Mood: {0} {1}%
- Mood Factors: {0}
- Full Body
- Health Issues: {0}
- Capacities: {0}
- Skills: {0}
- None
- Current Job: {0}
- Work Assignments: {0}
- Traits: {0}
- Weapon: {0}
- Apparel: {0}
- Carried: {0}
- damaged
- Indoors
- Outdoors
- Location: {0} Temp: {1}°C
- Genes: {0}
- Relations: {0}
- In Combat (target: {0})
- Drafted
- Unknown
- Surroundings: {0}
- Buildings: {0}
- Items: {0}
- Animals: {0}
-
-
- Mental Break
- Downed
- Mood: {0}
- Health: {0}
- Job: {0}
- Location: {0} Temp: {1}°C
- Weapon: {0}
-
-
- D{0} {1}:{2}
-
-
- [RimMind] Connection successful: {0}
- [RimMind] Connection failed: {0}
- [RimMind] Request sent, waiting for response...
-
-
- Edit global custom prompts here. They will be appended to the end of the AI Prompt. Leave empty to skip.
- Global Pawn Prompt
- Custom prompt appended after pawn context (leave empty to skip):
- Global Map Prompt
- Custom prompt appended after map context (leave empty to skip):
-
-
- ({0} earlier records omitted)
-
-
- [Player Custom Pawn Prompt]
- [Player Custom Map Prompt]
-
-
- Only return valid JSON format. Do not output anything else. Do not wrap in markdown code blocks.
- If you cannot determine, return a default value or empty result.
- You are in the RimWorld game world. Do not break the fourth wall. Do not mention game mechanics, "player", or "game".
- Do not break the fourth wall. Do not mention game mechanics, code, data structures, or the concept of "player". Act naturally as your character.
- [Player Custom Rules]
-
-
- Role
- Goal
- Process
- Constraint
- Example
- Output
- Fallback
- Custom
-
-
- RimMind Requests
- [System Blocked] {0}
- No pending requests
- Details
- Drag to resize
-
-
- Approval & Requests
- Clear All
- {0} pending
+
+ RimMind-Core\n\nClick to toggle request overlay\nShift+Click to open mod settings\nCtrl+Click to open the debug center
+
+ API Config
+ Context Filter
+ Custom Prompts
+
+ AI Provider
+ Select an AI service provider. Player2 can auto-detect the local desktop app, no manual configuration needed.
+ OpenAI Compatible
+ Player2
+ Click to switch AI provider or select from popular presets.
+ Custom Endpoint
+ ⚡ OpenCode Go (Subscription / sub2api)
+ 🌟 DeepSeek
+ 🚀 SiliconFlow
+ 🌙 Moonshot AI (Kimi)
+ 🦙 Ollama (Local Deployment)
+ 🤖 OpenAI Official
+ 🌐 Custom OpenAI Compatible
+ 🛡️ Extended Model Service (ModelService Load Balancer / Proxy)
+ 🛡️ Extended Model Service (Not Installed - OpenCode / Codex Proxy)
+ Extended Model Service Active (RimMind-Extension-ModelService)
+ AI requests are managed by the ModelService dispatcher, supporting OpenCode Go subscriptions, Codex local gateway, and multi-endpoint failover / load balancing.
+ ⚙️ Configure Endpoints in Model Service Tab (OpenCode Go / Codex / Nodes)
+ Switch to the Model Service settings tab to manage endpoints, weights, and failover priorities.
+ ⚠ No active endpoints in Model Service. Please add an endpoint in the Model Service tab.
+ API Key
+ Your AI provider's API key for authentication.
+ Saved ({0} chars). Click Show to edit.
+ Click here to enter API Key...
+ API Endpoint
+ AI service API address. Supports OpenAI-compatible interfaces (e.g. DeepSeek, local Ollama).
+ Model Name
+ The model ID to use, e.g. deepseek-v4-flash or gpt-4o-mini.
+ Force JSON Mode (response_format)
+ Append response_format=json_object to the API request, forcing the model to return JSON. Disable this for local models that don't support this parameter.
+ Max Tokens
+ Maximum tokens per AI response. Higher values allow longer replies but increase cost.
+ Temperature
+ Controls randomness. Lower = more deterministic, higher = more creative. Range: 0.0-2.0
+ Verbose Logging (Player.log)
+ Output detailed AI request and response info to Player.log for troubleshooting.
+ Test Connection
+ ⚠ API Key not configured
+ Testing...
+ Show
+ Hide
+
+ Player2 is an AI gaming assistant platform. When selected, the system will auto-detect the local Player2 desktop app and authenticate, no API Key needed. If the local app is not installed, you can also enter an API Key manually.
+ optional
+ Player2 prioritizes local desktop app auto-authentication. Only enter an API Key manually if the local app is not installed.
+ Detect Local Player2 App
+ Player2 not available: local app not detected and no API Key configured
+ Remote API URL
+ Player2 remote service URL. Defaults to https://api.player2.game, usually no need to change.
+ Balance: unknown
+ Refresh Balance
+
+ Agent Control
+ Open the RimMind Debug Center for this pawn's agent.
+ Dev: View Agent
+ Dump Agent internal state to log
+ Active
+ Dormant
+ Paused
+ Terminated
+ RimMind: Player2 desktop app detected! Using automatic authentication (no API key needed).
+ RimMind: Player2 desktop app not found. Please start the app or add an API key manually.
+ Max Concurrent Requests
+ Maximum number of simultaneous AI requests. Higher values improve responsiveness but increase API costs.
+ Daily Activity & Trigger Frequency
+ Unified control for the trigger frequency of colonist inner thoughts, social chit-chat, and colony advisor suggestions.\n\n• Low (0.2x): ~1 event every 3 days colony-wide.\n• Medium / Default (1.0x): ~3 events per day for a baseline of 3 colonists (~1/colonist/day).\n• High (3.0x): Up to ~10 events per day colony-wide.\n\nNote: If autonomous Pawn Agent loops are enabled, requests will naturally increase based on agent proactive goals.
+ Low (~1 per 3 days)
+ Sub-medium (~1 per 2 days)
+ Default (~3 per day for 3 pawns)
+ Active (~6 per day for 3 pawns)
+ Very Active (Up to ~10 per day)
+ Auto-activate Colonist Agents
+ When enabled, new or joining colonists automatically start their autonomous Pawn Agent perception and decision loops without manual activation.
+ Max Retry Count
+ Maximum number of retries after a request failure. Set to 0 to disable retries.
+ Request Timeout (seconds)
+ Maximum time to wait for an AI response before timing out. Increase for slow models or networks.
+ Request Expiry (ticks)
+ Requests older than this tick count are automatically removed from the queue. Prevents stale requests from accumulating. Range: 6000-120000.
+ Behavior History Limit
+ Maximum number of behavior history records retained per agent. Higher values provide more context but increase token usage. Range: 10-500.
+ Queue Process Interval (ticks)
+ Interval in ticks between queue processing cycles. Lower values process requests faster but consume more CPU. Range: 10-300.
+ Default Mod Cooldown (ticks)
+ Default cooldown in ticks between consecutive requests for the same mod. Prevents API rate limiting. Individual mods may override this. Range: 600-36000.
+ Queue: PAUSED
+ Queue: Running
+ Queue status has been moved to the "Queue" tab
+ Queue
+ Queue not initialized (available after loading a save)
+ Overall Status
+ Active Requests
+ Queued Requests
+ Local Model
+ Busy
+ Idle
+ Resume Queue
+ Pause Queue
+ Clear Queues
+ Clear Cooldowns
+ Per-Mod Queues
+ No registered mod queues
+ Cooldown
+ Ready
+ Queued
+ Active Request Details
+ No active requests
+ Queued Request Details
+ No queued requests
+ Priority
+ Attempt
+ Elapsed
+ Waiting
+ This is a test message, please reply in one sentence.
+
+ Control which game state information is injected into the AI Prompt. Disabling unnecessary fields saves token consumption.
+ Quick Presets
+ Pawn Info
+ Environment
+ Reset to Default (Standard)
+ Reset all context filter options to standard recommended defaults.
+
+ Minimal
+ Core info only, least tokens
+ Standard
+ Balanced detail and cost (Recommended)
+ Full
+ All fields, highest token cost
+ Custom
+ Manually select fields
+
+ Race
+ Inject the pawn's race info into AI context.
+ Age
+ Inject the pawn's age into AI context.
+ Gender
+ Inject the pawn's gender into AI context.
+ Backstory
+ Inject the pawn's childhood and adulthood backstories into AI context.
+ Ideology (requires Ideology DLC)
+ Inject the pawn's ideology and beliefs into AI context.
+ Traits
+ Inject the pawn's character traits into AI context.
+ Skills
+ Inject the pawn's skill levels into AI context. Skills below the minimum level are filtered out.
+ Min Skill Level
+ Minimum skill level threshold to include in context. Skills below this level will be omitted.
+ Health
+ Inject the pawn's health issues (wounds, diseases, implants, etc.) into AI context.
+ Capacities
+ Inject the pawn's body capacities (manipulation, moving, sight, etc.) into AI context.
+ Mood
+ Inject the pawn's current mood percentage into AI context.
+ Mood Factors (top 8)
+ Inject the top 8 mood-affecting factors into AI context.
+ Current Job
+ Inject the pawn's current job into AI context.
+ Work Assignments
+ Inject the pawn's work priority assignments into AI context.
+ Weapons & Apparel
+ Inject the pawn's equipped weapons and apparel into AI context.
+ Carried Items
+ Inject the pawn's carried items (inventory) into AI context.
+ Location & Temperature
+ Inject the pawn's location (indoors/outdoors/room type) and local temperature into AI context.
+ Relations
+ Inject the pawn's social relationships into AI context.
+ Genes (requires Biotech DLC)
+ Inject the pawn's gene information into AI context.
+ Surroundings
+ Inject nearby buildings, items, and animals into AI context.
+ Combat Status
+ Inject whether the pawn is in combat or drafted into AI context.
+
+ Game Time
+ Inject the current game date and time into AI context.
+ Colonist Count
+ Inject the colonist and prisoner counts into AI context.
+ Colonist Names
+ Inject the names of colonists and prisoners into AI context.
+ Wealth Level
+ Inject the colony wealth and threat level into AI context.
+ Food Storage
+ Inject the colony's total food nutrition into AI context.
+ Season
+ Inject the current season into AI context.
+ Weather
+ Inject the current weather conditions into AI context.
+ Threat Level
+ Inject the current map threat level into AI context.
+ Context Budget Ratio
+ Controls the maximum ratio of the model's context window allocated for prompt context (recommended: 0.8, leaving a 20% margin for thinking and generation; values above 1.0 are only intended for high-capacity models).
+ Context Diff Lifetime
+ How long (in ticks) context change diffs remain before expiring. Lower values keep context more current; higher values preserve change history longer.
+ Context Calibrate Interval
+ Interval between Flywheel context calibrations (ticks). Shorter intervals update more frequently but cost more CPU. Range: 5000-60000.
+
+ [Current Map Status]
+ Time: {0} {1}:00
+ Colonists: {0} ({1})
+ Colonists: {0}
+ Prisoners: {0} ({1})
+ Extreme
+ High
+ Medium
+ Low
+ Wealth: {0} (Threat: {1})
+ Threat Level: {0}
+ Food Storage: ~{0} nutrition
+ Season: {0}
+ Weather: {0}
+
+ [{0}'s Status]
+ {0}yo
+ Childhood: {0}
+ Adulthood: {0}
+ Background: {0}
+ Ideology: {0}{1}
+ Mood: {0} (Mental break: {1})
+ Mood: {0} (Downed)
+ Mood: {0} {1}%
+ Mood Factors: {0}
+ Full Body
+ Health Issues: {0}
+ Capacities: {0}
+ Skills: {0}
+ None
+ Current Job: {0}
+ Work Assignments: {0}
+ Traits: {0}
+ Weapon: {0}
+ Apparel: {0}
+ Carried: {0}
+ Location: {0} Temp: {1}°C
+ Genes: {0}
+ Relations: {0}
+ In Combat (target: {0})
+ Drafted
+ Unknown
+ Surroundings: {0}
+ Buildings: {0}
+ Items: {0}
+ Animals: {0}
+
+ Mental Break
+ Downed
+ Mood: {0}
+ Health: {0}
+ Job: {0}
+ Location: {0} Temp: {1}°C
+ Weapon: {0}
+
+ [RimMind] Connection successful: {0}
+ [RimMind] Connection failed: {0}
+ [RimMind] Request sent, waiting for response...
+
+ Edit global custom prompts here. They will be appended to the end of the AI Prompt. Leave empty to skip.
+ Global Pawn Prompt
+ Custom prompt appended after pawn context (leave empty to skip):
+ Global Map Prompt
+ Custom prompt appended after map context (leave empty to skip):
+ Clear custom pawn prompt and reset to default.
+ Clear custom map prompt and reset to default.
+ Show All
+ Show all custom prompts for both pawn and map context simultaneously.
+
+ RimMind Requests
+ [System Blocked] {0}
+ No pending requests
+ Details
+ Drag to resize
+
+ Approval & Requests
+ Clear All
+ {0} pending
+ Request queue is paused. Resume it in Settings.
+ API Key not configured. Set it up in Settings.
+ No agents on current map. Create an agent for a pawn first.
+ No requests generated yet. Agents produce requests during think cycles.
+
+ Agent Dialogue
+ Send
+ Player
+ Agent
+ (thinking...)
+ Request {0}: {1}
+ Idle
+ Streaming
+ Completed
+ Failed
+ Discarded (runtime changed)
+
+ You are in the RimWorld game world.\n\nOutput constraints:\n- You must return valid JSON format\n- Do not wrap in markdown code blocks\n- If you cannot determine, return a default value or empty result\n\nBehavior constraints:\n- Do not break the fourth wall\n- Do not mention game mechanics, code, data structures, or the concept of "player"\n- Act naturally as your character
+ You are in the RimWorld game world.
+ {0} says to you: {1}
+ Based on the above context, provide your {0} response.
+
+ Name: {0}
+ ShortName: {0}
+ Description: {0}
+
+ Available commands:
+ - {0}: {1}
+
+ Core rules:
+ - Colonists must survive by managing food, shelter, and mood
+ - Combat is dangerous; avoid unnecessary fights
+ - Relationships affect mood and productivity
+ - Weather and seasons impact survival
+ - Medical needs must be addressed promptly
+
+ Healthy
+ Mental break: {0}
+ Idle
+ Drafted
+ Fighting: {0}
+ Not in combat
+ Pop: {0} Wealth: {1} Threats: {2}
+ Target: {0}
+
+ {0} switched to {1} mode
+
+ Flywheel: auto-apply parameter recommendations
+ Adaptive optimization strategy that automatically adjusts model parameters based on execution history and feedback.
+ Off
+ Log Only
+ Apply with Log
+ Confidence threshold: {0}
+ Auto-apply only when recommendation confidence reaches this threshold
+ Dialogue scenario
+ Decision scenario
+ Personality evaluation scenario
+ Narrative scenario
+ Dark memory generation scenario
+ RimMind v2.0 has breaking changes. Old saves may not be fully compatible. AI request queues and some internal structures have been reorganized. Please start a new game for the best experience.
+
+ Remote Sync
+ Auto Sync
+ Auto Pull on Load
+ Automatically pull remote data when loading a save.
+ Auto Push on Change
+ Automatically push local changes to remote storage.
+ Sync Granularity
+ Sync Memory
+ Include agent memory data in remote sync.
+ Sync Settings
+ Include mod settings in remote sync.
+ Sync Agent Identity
+ Include agent identity and personality data in remote sync.
+ Manual Operations
+ Remote backend not configured. Manual operations require a configured remote storage backend.
+ Pull from Remote
+ Push to Remote
+ Pulling...
+ Pushing...
+ Pull completed successfully.
+ Pull failed: {0}
+ Push completed successfully.
+ Push failed: {0}
+
+ ToolCall Debug
+ No tools registered
+ Tools are registered by sub-mods. Install sub-mods like Actions to populate this list.
+ ID: {0}
+ Description: {0}
+ Parameters Schema:
+ JSON Parameters:
+ Execute
+ Executing...
+ Result:
+ Success ({0}ms)
+ Failed ({0}ms)
+ Category: {0}
+ Select a tool from the list
+
+ Mechanism Status
+ No mechanisms registered
+ Mechanisms are game-state operation surfaces registered by sub-mods. Install sub-mods like Actions to populate this list.
+ Description: {0}
+ Owner: {0}
+ Scope: {0}
+ Risk: {0}
+ Operations: {0}
+ Tools: {0}
+ Filter Scope
+ Filter Risk
+ (no tool mapping)
+
+ Agent Mode Debug
+ No agents on current map
+ Create an agent for a pawn to see its mode info here
+ No modes registered
+ Mode
+ Allowed Tools
+ Should Think
+ Switch Mode
+ Select a pawn
+ Mode Change History
+ No mode changes recorded
+ {0}: {1} → {2}
+ Registered Modes
+ {0} - {1}
+ Pawn List
+ Mode Details
+ Switch to:
+
+ Agent
+ No agent assigned
+ This pawn does not have an agent yet. Click the button below to create one.
+ Create/Get Agent
+ Cannot create agent: factory not available
+ Status
+ Goals
+ Strategy Weights
+ Behavior History
+
+ RimMind Debug Center
+ Overview
+ Agents
+ AI Requests
+ ToolCalls
+ Mechanisms
+ Context Keys
+ Settings
+ Selected pawn
+ No pawn selected
+ Runtime agent states
+ Pending AI requests
+ Agent runtime loop
+ Last loop tick
+ Loop faults
+ pawn
+ scoped
+ Not run
+ Queue: not initialized
+ Queue State
+ Quick Actions
+ Settings
+ Settings
+ The Debug Center provides an entry point only. Full configuration stays in the RimMind Settings window.
+ Open Settings
+ Live AI Connectivity & Model Status
+ Displays current AI provider, endpoint URL, model, and allows initiating a real-time probe test.
+ ⚡ Test AI Request
+ Executes a background test request to verify API connectivity, measuring round-trip latency, HTTP status, and response tokens.
+ Ping Status
+ Not tested
+ Latency
+ Tokens
+ Response Snippet
+ Queue & Cooldown Console
+ Monitor request concurrency and cooldowns, pause queue, flush requests, or reset mod rate limits.
+ All queued requests have been flushed.
+ All mod request cooldowns have been cleared.
+ Pause or resume request queue consumption and background dispatch.
+ Drop all AI requests currently waiting in queue.
+ Reset all mod rate limit and circuit breaker cooldowns.
+ Runtime Settings Tuner
+ Directly tweak and persist core runtime parameters without restarting the game.
+ Maximum number of simultaneous background requests to the AI model (1 ~ 5).
+ Maximum time to wait for AI response before timing out (15s ~ 120s).
+ Enable verbose developer debug logging in console.
+ Offline Mock Mode
+ Simulate offline AI responses for testing without calling remote endpoints.
+ Colonist Agent Inspector
+ View selected colonist's Agent state, inspect payload prompt, or trigger an immediate thinking tick.
+ 👥 Select Colonist
+ Quickly pick a colonist from the current map.
+ Workflow Phase
+ Autonomy Level
+ 🔍 Inspect Payload
+ Open the context payload inspector to view full prompts, memories, and environment data.
+ ⚡ Trigger Agent Tick
+ Reset think cooldown and immediately execute one Agent Tick to prompt decision making.
+ Agent tick triggered for: {0}
+ Agent component not yet initialized for this pawn.
+ ⚡ Activate All Agents
+ Immediately activate autonomous agent perception and action loops for all living dormant colonists.
+ Successfully activated {0} colonist agents!
+
+ ID
+ Status
+ Time
+ Scope
+ Actor
+ Channel
+ Model
+ Summary
+ Request
+ No debug records are available.
+ Waiting
+ Streaming
+ Completed
+ Failed
+ Cancelled
+ No operations
+ {0} ops
+ Priority {0}
+ Effective {0}
+ UpdateCount {0}
+ context
+ score {0}
+ Pawn
+ Map
+ Building
+ Thing
+ World
+ Colony
+ Safe
+ Moderate
+ Dangerous
+ Query
+ Set
+ Add
+ Remove
+ Toggle
+ Trigger
+ List
+ Watch
+ Static
+ Baseline
+ Environment
+ State
+ History
+ Sensor
+ Static
+ Pawn
+ Map
+ Storyteller
+ Scenario
+
+ Agent State Debug
+ No pawn selected
+ Click the 'Create/Get Agent' button below to create an agent for this pawn
+ Pawn: {0} (ID: {1})
+ NPC ID
+ Agent: Not Created
+ Mode
+ Thinking
+ Last Think
+ Create/Get Agent
+ Build Context
+ Send Test Request
+ Request Log
+ ToolCall Debug
+ Mechanism Status
+ Queue State
+ Context Snapshot
+ No Data
+ Building context…
+ Queue: Paused
+ Queue: Running (Active: {0}, Queued: {1})
+ Scope ID
+ Map ID
+ Success Rate
+ Recent Behavior
+ No behavior history
+ Identity
+ Scope Type
+ State
+ Pawn Info
+ Agent State
+ Destroy Agent
+
+ Context Key Debug
+ No registered context keys
+ Context keys are registered by Core and sub-mods. They appear automatically after loading a game save.
+ Filter Layer
+ Filter Owner
+ No keys match the current filter
+ Select a context key from the list
+ Key: {0}
+ Layer: {0}
+ Priority: {0} (Adaptive: {1})
+ Owner Mod: {0}
+ Provider: {0}
+ Scenarios: {0}
+ Duplicate key: {0} (overwritten by {1})
+ Test Duplicates
+ No duplicate keys
+ Registered
+ Not Registered
+ Cache Scope: {0}
+ Override: previously owned by {0}
+
+ Agent Flow Lab
+ No pawn selected
+ Current pawn: {0}
+ Refresh
+ Scope
+ Pawn
+ Map
+ Colony
+ Global
+ This scope is visible for planning, but runtime control is not wired yet. Pawn agents are available now.
+ {0} scope agent. Context building and decision flow are available; full runtime control is limited.
+ Scoped agent: {0}:{1} [{2}]
+ Agent Lifecycle
+ Agent: Exists ({0})
+ Agent: Not Created
+ Create/Get Agent
+ Context Building
+ Build Context Snapshot
+ Estimated Tokens: {0}
+ LLM Request
+ Send Test Request
+ Request Status: {0}
+ Decision Parsing
+ Mechanism Mapping
+ Dry Run Mechanism Mapping
+ Execute Mechanism (Confirmation Required)
+ Confirm execution? This will actually execute game mechanisms and may affect game state.
+ Queue State
+ Error Log
+ Clear Errors
+ No Data
+ No Errors
+ Messages: {0}
+ Offline Mode (Stub)
+ Live Mode (Real API)
+ No API calls. Uses stub response for testing.
+ Sends real API requests. Costs tokens.
+ Using offline stub response
+ Execute (Dry Run Required First)
+ Dry Run: {0} mechanisms registered
+ Decision action: {0}
+ Target mechanism: {0} (risk: {1})
+ No mechanism found for: {0}
+ No parsed decision available for mechanism mapping.
+ Execute blocked: dry run has no mapped write arguments.
+ Execute blocked: mechanism registry is not available.
+ Validation: {0}
+ Passed
+ Failed: {0}
+ Open Logs
+ Request Log
+ ToolCall Debug
+ Mechanism Status
+ Context Keys
+ Agent Progress
+
+ Agent Progress
+ No active agents
+ Create agents for pawns to see their progress here
+ Queue: Paused
+ Queue: Running (Active: {0}, Queued: {1})
+ Idle
+ Perceiving
+ Thinking
+ Acting
+ Recording
+ Elapsed: {0}
+ Details
+
+ Active
+ Paused
+ Pending
+ Other
+ Dormant
+ Terminated
+ Create/Start
+ Activate
+ Restart
+ Pause
+ Resume
+ Force Think
+ Open Requests
+ Send
+ State
+ Activity
+ No request stream entries for this agent yet.
+ Pending
+ Success
+ Error
+ Waiting for model response...
+ Request error
+ Daily reaction/monologue
+ Trigger AI interaction
+ Call tool: {0}
+ [AI Trace Entry] {0}
+ Latency: {0}ms
+ Tokens: {0}
+ Model: {0}
+ Source: {0}
+ Intent: {0}
+ Tools: {0}
+ Content: {0}
+ Error: {0}
+ Failed to create agent. Check that services are available.
+ Agent chat input is not connected yet. Your draft was kept.
+
+ AI request trace log is not available.
+ No AI requests recorded yet.
+ Request: {0}\nState: {1}\nPrompt: {2}\nResponse: {3}\nError: {4}\nToolCalls: {5}
+ Request
+ System Prompt
+ User Prompt
+ Assistant Context
+ Response
+ Error
+ Tool Calls
+ Empty
+ Running
+ Completed
+ Failed
+
+ Lifecycle
+ Runtime state
+ Game state
+ Generation
+ Services
+ Published (UTC)
+ Runtime ID
+ Last build failure
+ Stale completions discarded
+ Never
+ None
+ Building
+ Running
+ Stopped
+ Failed
+ Never published
+ Discarded because the runtime changed.
+ Approve
+ Reject
+ [{0}] {1} requests execution: {2}
+
+ RimMind: API Key Required
+ RimMind detected that the selected AI provider has not been configured with an API key. Please enter your API key in Options -> Mod Settings -> RimMind to activate AI colonist decisions, dialogue, and storytelling features.
+
+ Show Agent Progress Float Window
+ Display real-time thinking and workflow progress bars for all active colonists on screen (disabled by default).
+ AI Context Payload Inspector
+ Copy Full Payload
+ Refresh
+ Send Live Test
+ Sending...
+ Colonists
+ Scenario:
+ Full AI request payload successfully copied to clipboard!
+ Inspect Payload
+ === Prompt Caching (KV-Cache) Prefix Stability ===
+ • Stable Cached Prefix (Static Prefix) : ~{0} tokens ({1:F1}% of prompt)
+ - Zone 1 Static Axioms & Directives : ~{0} tokens (100% globally cached)
+ - Zone 2 Agent Baseline Profile : ~{0} tokens (100% cached per colonist)
+ - Deterministic Tool Declarations : ~{0} tokens (alphabetically aligned)
+ • Volatile Tail (Dynamic Suffix) : ~{0} tokens ({1:F1}%)
+ • Estimated Turn 2+ Cache Hit Rate : ~{0:F1}%
+ [✓] Boundary strictly isolated: time_of_day, weather, and mood are at the tail, never invalidating prefix cache!
+ Colonist profile and backstory aligned to Zone 2 stable prefix, enjoying 100% KV-Cache hits across turns.
+ Reset to Defaults
+
+ ⚙️ Quick Configuration Presets
+ Preset applied: {0}
+ ⚡ Responsive
+ Fast interaction: MaxTokens=600, Concurrency=3, Timeout=25s, Cooldown=15s. Ideal for real-time dialogue and responsive agents.
+ ⚖️ Balanced
+ Standard balance: MaxTokens=800, Concurrency=2, Timeout=45s, Cooldown=30s. Balances reasoning quality and token costs.
+ 🛡️ Eco & Rate Limit Guard
+ Economic guard: MaxTokens=400, Concurrency=1, Timeout=60s, Cooldown=60s. Recommended for free tiers or strict rate limits.
+ 1. Connection & Endpoints
+ 2. Generation & Model Behavior
+ 3. Performance & Scheduling
+ 4. Interface & Overlay
+ HUD Request Overlay Auto-Collapse When Idle
+ When there are no pending approval requests, collapse the HUD overlay into a compact 120x24 status capsule. Expands automatically on hover or when new requests arrive.
+ Enable Floating Mental Monitor
+ Display a floating HUD bar at the screen edge showing colonist thinking activities and mental state trends (Experimental).
+ Open Context Payload Inspector
+ Open the real-time Payload Inspector to view assembled context, test AI connectivity, or copy full request JSON.
+ Pending: {0}
+ RimMind Approval HUD (Collapsed). Hover to expand full view.
+ System Status Summary
+ Quick Actions
+ Request Queue
+ active
diff --git a/LoadFolders.xml b/LoadFolders.xml
new file mode 100644
index 00000000..6ed2ceec
--- /dev/null
+++ b/LoadFolders.xml
@@ -0,0 +1,7 @@
+
+
+
+ /
+ 1.6
+
+
diff --git a/README.md b/README.md
index 952741e2..c57895cf 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ cd RimWorld-RimMind-Mod-Core
4. 如果选择 OpenAI 兼容模式:
- 填写你的 **API Key**
- 填写 **API 端点**(见下方支持的端点列表)
- - 填写 **模型名称**(如 `gpt-4o-mini`、`deepseek-chat`)
+ - 填写 **模型名称**(如 `deepseek-v4-flash`、`gpt-4o-mini`)
5. 如果选择 Player2 模式:
- 安装 Player2 本地应用后可自动检测,无需手动配置
- 也可手动填写 Player2 API Key 使用远程服务
@@ -75,7 +75,7 @@ cd RimWorld-RimMind-Mod-Core
| 服务 | 端点 | 说明 |
|------|------|------|
-| DeepSeek | `https://api.deepseek.com/v1` | deepseek-chat 等模型(默认) |
+| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-v4-flash`(默认) |
| OpenAI | `https://api.openai.com/v1` | GPT-4o-mini 等模型 |
| Ollama (本地) | `http://localhost:11434/v1` | 本地部署的模型 |
| Player2 | 自动检测 / 手动配置 | Player2 本地应用或远程 API |
@@ -89,6 +89,8 @@ cd RimWorld-RimMind-Mod-Core
## 核心功能
+开发入口见 [AGENTS.md](AGENTS.md);上下文构建、缓存与测试地图见 [Context README](Source/Presentation/Context/README.md)。Core 全部测试项目累计少于 1000 个发现用例(参数化数据行逐个计数),以真实行为、失败边界和模块协作为准。
+
### LLM 客户端
兼容 OpenAI / DeepSeek / 本地 Ollama 等所有 OpenAI Chat Completions 格式的 API,同时支持 Player2 服务(本地应用自动检测 + 远程 API)。支持 JSON 强制模式(`response_format: json_object`),本地模型可关闭。
@@ -108,6 +110,31 @@ cd RimWorld-RimMind-Mod-Core
通过"上下文过滤"设置页精确控制哪些游戏信息注入 Prompt,节省 Token。提供最小/标准/完整三种预设,也可自定义勾选 28+ 个选项。
+### Agent 认知架构
+
+每个殖民者作为独立认知主体,遵循 Perceive→Think→Act→Record 循环:
+
+- **Perceive**:5 个 Harmony Patch 将游戏事件(袭击、受伤、心情变化等)转为感知信号,经去重/优先级/冷却过滤后注入 Agent
+- **Think**:Agent 根据感知信号和当前目标,通过 ContextEngine 构建上下文,向 LLM 发送结构化请求
+- **Act**:解析 LLM 响应,执行工具调用(动作、对话、目标调整等)
+- **Record**:记录行为到历史队列,用于后续决策参考
+
+默认装配 Reactive / Proactive 模式,保留主动周期和感知触发。反思、日规划、梦境、社交组织与性格演化没有内置可用策略,不以空实现制造触发;其可选策略合同及 Verse 执行/生命周期完成检查仍保留。
+
+### 统一上下文引擎
+
+ContextEngine 采用 L0-L5 分层构建上下文,支持 Diff 注入与 Tick 过期合并:
+
+- L0 静态层(系统指令、身份)→ L1 基线层(地图、Pawn 信息)→ L2 环境层(天气、时间)→ L3 状态层(健康、心情)→ L4 历史层(对话记录)→ L5 感知层(Sensor 数据)
+- BudgetScheduler 按 Score = W1×优先级 + W2×相关性 调度上下文预算
+- 子模组通过 ContextKeyRegistry.Register 注入自定义上下文 Provider
+
+快照构建统一走 `BuildSnapshotFromEnvelopeAsync`,支持异步 Provider、取消、缓存失效和历史/预算处理;不再提供只执行同步 Provider 的平行构建入口。
+
+### 数据飞轮
+
+内置自动调优系统(Flywheel),持续分析 AI 请求效果并优化上下文参数,让 AI 输出质量随使用时间逐步提升。
+
### 调试工具
- **AI Debug Log**:浮动窗口,查看每次 AI 调用的完整 Prompt + Response
@@ -121,9 +148,10 @@ cd RimWorld-RimMind-Mod-Core
| AI Provider | OpenAI | 选择 OpenAI 兼容 API 或 Player2 |
| API Key | - | 你的 API 密钥(Player2 模式可选) |
| API 端点 | `https://api.deepseek.com/v1` | OpenAI 兼容端点 |
-| 模型名称 | `deepseek-chat` | 任意模型 ID |
+| 模型名称 | `deepseek-v4-flash` | 任意模型 ID |
| 强制 JSON 模式 | 开启 | 不支持的本地模型请关闭 |
| 最大 Token | 800 | 响应长度上限(200-2000) |
+| 默认温度 | 0.7 | 控制输出随机性(0.0-2.0) |
| 最大并发请求数 | 3 | 同时发送请求的上限(1-10) |
| 最大重试次数 | 2 | 请求失败后重试次数(0-5) |
| 请求超时 | 120秒 | 等待 AI 响应的最大时间(10-300秒) |
@@ -132,6 +160,10 @@ cd RimWorld-RimMind-Mod-Core
| 自定义人物提示词 | 空 | 追加在人物上下文末尾 |
| 自定义地图提示词 | 空 | 追加在地图上下文末尾 |
| 上下文过滤器 | 标准 | 28+ 个可选项,三种预设 |
+| 上下文 Diff 生存期 | 3000 ticks | Diff 条目过期时间(300-3000) |
+| 上下文校准间隔 | 30000 ticks | 基线重算间隔(5000-60000) |
+| 飞轮自动应用 | 关闭 | Off / LogOnly / ApplyWithLog |
+| 飞轮置信度阈值 | 0.8 | 自动应用最低置信度(0.5-1.0) |
## 常见问题
@@ -148,7 +180,7 @@ A: API Key 仅存储在本地 RimWorld 设置文件中,不会上传到任何
A: Core 本身不会报错,但所有依赖 AI 的子模组功能将无法工作。
**Q: 推荐用什么模型?**
-A: 推荐使用 `gpt-4o-mini` 或 `deepseek-chat`,性价比高且响应速度快。本地 Ollama 用户可使用 `qwen2.5:7b` 等模型。
+A: 默认推荐 `deepseek-v4-flash`,兼顾价格与响应速度。本地 Ollama 用户可使用 `qwen2.5:7b` 等模型。
## 致谢
@@ -221,7 +253,7 @@ cd RimWorld-RimMind-Mod-Core
4. If using OpenAI-compatible mode:
- Enter your **API Key**
- Enter your **API Endpoint** (see supported endpoints below)
- - Enter your **Model Name** (e.g., `gpt-4o-mini`, `deepseek-chat`)
+ - Enter your **Model Name** (e.g., `deepseek-v4-flash`, `gpt-4o-mini`)
5. If using Player2 mode:
- Install Player2 local app for automatic detection, no manual configuration needed
- Or manually enter a Player2 API Key for remote service
@@ -231,7 +263,7 @@ cd RimWorld-RimMind-Mod-Core
| Service | Endpoint | Notes |
|---------|----------|-------|
-| DeepSeek | `https://api.deepseek.com/v1` | deepseek-chat etc. (default) |
+| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-v4-flash` (default) |
| OpenAI | `https://api.openai.com/v1` | GPT-4o-mini etc. |
| Ollama (local) | `http://localhost:11434/v1` | Locally deployed models |
| Player2 | Auto-detect / Manual | Player2 local app or remote API |
@@ -243,6 +275,9 @@ cd RimWorld-RimMind-Mod-Core
- **Async Request Queue**: All AI requests run on background threads, never blocking the game. Supports automatic retry for transient errors (timeout / 429 / 502 / 503 etc.), serial processing for local models
- **Context Builder**: Automatically collects game state (colonist stats, map info, etc.) for AI prompts
- **Context Filter**: Fine-grained control over what game info gets sent to AI, with Minimal/Standard/Full presets and 28+ configurable options
+- **Agent Cognitive Architecture**: Each colonist as an independent cognitive agent following Perceive→Think→Act→Record cycle, with perception bridge (5 Harmony Patches) converting game events into perception signals
+- **Unified Context Engine**: L0-L5 layered context building with Diff injection and tick-based expiry, BudgetScheduler scoring (W1×priority + W2×relevance), extensible via ContextKeyRegistry
+- **Data Flywheel**: Built-in auto-tuning system that continuously analyzes AI request quality and optimizes context parameters
- **Debug Tools**: AI Debug Log window, request overlay, Dev menu actions (test connection, view context, clear cooldowns, pause/resume queue)
## FAQ
@@ -260,7 +295,7 @@ A: The API Key is stored locally in RimWorld settings files and never uploaded t
A: Core itself won't error, but all AI-dependent sub-module features will be unavailable.
**Q: What model do you recommend?**
-A: `gpt-4o-mini` or `deepseek-chat` for good balance of cost and speed. Ollama users can try `qwen2.5:7b`.
+A: `deepseek-v4-flash` is the default recommendation for a good balance of cost and speed. Ollama users can try `qwen2.5:7b`.
## Acknowledgments
diff --git a/Source/AICoreAPI.cs b/Source/AICoreAPI.cs
deleted file mode 100644
index 4688a309..00000000
--- a/Source/AICoreAPI.cs
+++ /dev/null
@@ -1,626 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using RimMind.Core.Client;
-using RimMind.Core.Client.OpenAI;
-using RimMind.Core.Client.Player2;
-using RimMind.Core.Internal;
-using RimMind.Core.Prompt;
-using RimMind.Core.Settings;
-using RimMind.Core.UI;
-using Verse;
-
-namespace RimMind.Core
-{
- public static class RimMindAPI
- {
- // ── Provider 注册表 ───────────────────────────────────────────────────
-
- private static readonly Dictionary provider, int priority)>
- _staticProviders = new Dictionary, int)>();
-
- private static readonly Dictionary provider, int priority)>
- _dynamicProviders = new Dictionary, int)>();
-
- private static readonly Dictionary provider, int priority)>
- _pawnProviders = new Dictionary, int)>();
-
- private static readonly List<(string tabId, Func labelFn, Action drawFn)>
- _settingsTabs = new List<(string, Func, Action)>();
-
- private static readonly List<(string id, Func isActive, Action toggle)>
- _toggleBehaviors = new List<(string, Func, Action)>();
-
- private static readonly Dictionary> _modCooldownGetters
- = new Dictionary>();
-
- private static Action? _dialogueTriggerFn;
-
- private static readonly Dictionary> _dialogueSkipChecks
- = new Dictionary>();
-
- private static readonly Dictionary> _floatMenuSkipChecks
- = new Dictionary>();
-
- private static readonly Dictionary> _actionSkipChecks
- = new Dictionary>();
-
- // ── 核心请求 API ──────────────────────────────────────────────────────
-
- public static void RequestAsync(AIRequest request, Action onComplete)
- {
- var queue = AIRequestQueue.Instance;
- if (queue == null)
- {
- Log.Error("[RimMind] AIRequestQueue not initialized.");
- return;
- }
- var client = GetClient();
- if (client == null)
- {
- onComplete?.Invoke(AIResponse.Failure(request.RequestId, "AI client not configured."));
- return;
- }
- queue.Enqueue(request, onComplete, client);
- }
-
- public static void RequestImmediate(AIRequest request, Action onComplete)
- {
- var queue = AIRequestQueue.Instance;
- if (queue == null)
- {
- Log.Error("[RimMind] AIRequestQueue not initialized.");
- return;
- }
- var client = GetClient();
- if (client == null)
- {
- onComplete?.Invoke(AIResponse.Failure(request.RequestId, "AI client not configured."));
- return;
- }
- queue.EnqueueImmediate(request, onComplete, client);
- }
-
- public static bool CancelRequest(string requestId)
- => AIRequestQueue.Instance?.CancelRequest(requestId) ?? false;
-
- public static void PauseQueue() => AIRequestQueue.Instance?.PauseQueue();
-
- public static void ResumeQueue() => AIRequestQueue.Instance?.ResumeQueue();
-
- public static bool IsQueuePaused => AIRequestQueue.Instance?.IsPaused ?? false;
-
- public static int ActiveRequestCount => AIRequestQueue.Instance?.ActiveRequestCount ?? 0;
-
- public static IReadOnlyList GetActiveRequests()
- => AIRequestQueue.Instance?.GetActiveRequests() ?? new List();
-
- public static IReadOnlyList GetAllQueuedRequests()
- => AIRequestQueue.Instance?.GetAllQueuedRequests() ?? new List();
-
- public static int TotalQueuedCount => AIRequestQueue.Instance?.TotalQueuedCount ?? 0;
-
- // ── 上下文构建 ────────────────────────────────────────────────────────
-
- public static string BuildMapContext(Map map, bool brief = false)
- => GameContextBuilder.BuildMapContext(map, brief);
-
- public static string BuildPawnContext(Pawn pawn)
- => GameContextBuilder.BuildPawnContext(pawn);
-
- public static string BuildHistoryContext(int maxEntries = 10)
- => GameContextBuilder.BuildHistoryContext(maxEntries);
-
- public static string BuildStaticContext()
- {
- var sb = new StringBuilder();
- foreach (var kvp in _staticProviders)
- {
- try
- {
- string? seg = kvp.Value.provider();
- if (!string.IsNullOrEmpty(seg)) sb.AppendLine(seg);
- }
- catch (Exception ex) { Log.Warning($"[RimMind] StaticProvider '{kvp.Key}' error: {ex.Message}"); }
- }
- return sb.ToString().TrimEnd();
- }
-
- public static string BuildFullPawnPrompt(
- Pawn pawn,
- string? currentQuery = null,
- string[]? excludeProviders = null)
- {
- var sb = new StringBuilder();
-
- foreach (var kvp in _staticProviders)
- {
- try
- {
- string? seg = kvp.Value.provider();
- if (!string.IsNullOrEmpty(seg)) sb.AppendLine(seg);
- }
- catch (Exception ex) { Log.Warning($"[RimMind] StaticProvider '{kvp.Key}' error: {ex.Message}"); }
- }
-
- var ctx = RimMindCoreMod.Settings?.Context;
- foreach (var kvp in _pawnProviders)
- {
- if (ctx?.disabledProviders?.Contains(kvp.Key) == true) continue;
- if (excludeProviders != null && Array.IndexOf(excludeProviders, kvp.Key) >= 0) continue;
- try
- {
- string? seg = kvp.Value.provider(pawn);
- if (!string.IsNullOrEmpty(seg)) sb.AppendLine(seg);
- }
- catch (Exception ex) { Log.Warning($"[RimMind] PawnProvider '{kvp.Key}' error: {ex.Message}"); }
- }
-
- sb.AppendLine(GameContextBuilder.BuildPawnContext(pawn));
-
- if (pawn.Map != null)
- sb.AppendLine(GameContextBuilder.BuildMapContext(pawn.Map, brief: false));
-
- foreach (var kvp in _dynamicProviders)
- {
- try
- {
- string? seg = kvp.Value.provider(currentQuery ?? string.Empty);
- if (!string.IsNullOrEmpty(seg)) sb.AppendLine(seg);
- }
- catch (Exception ex) { Log.Warning($"[RimMind] DynamicProvider '{kvp.Key}' error: {ex.Message}"); }
- }
-
- var settings = RimMindCoreMod.Settings;
- if (settings == null) return sb.ToString().TrimEnd();
- string? customPawn = settings.customPawnPrompt?.Trim();
- if (!string.IsNullOrEmpty(customPawn))
- sb.AppendLine("\n" + "RimMind.Core.Prompt.CustomPawnHeader".Translate() + "\n" + customPawn);
- string? customMap = settings.customMapPrompt?.Trim();
- if (!string.IsNullOrEmpty(customMap))
- sb.AppendLine("\n" + "RimMind.Core.Prompt.CustomMapHeader".Translate() + "\n" + customMap);
-
- return sb.ToString().TrimEnd();
- }
-
- public static string BuildFullPawnPrompt(
- Pawn pawn,
- PromptBudget budget,
- string? currentQuery = null,
- string[]? excludeProviders = null)
- {
- var sections = BuildFullPawnSections(pawn, currentQuery, excludeProviders);
- return budget.ComposeToString(sections);
- }
-
- public static List BuildFullPawnSections(
- Pawn pawn,
- string? currentQuery = null,
- string[]? excludeProviders = null)
- {
- var sections = new List();
- var ctx = RimMindCoreMod.Settings?.Context;
-
- foreach (var kvp in _staticProviders)
- {
- try
- {
- string? seg = kvp.Value.provider();
- if (!string.IsNullOrEmpty(seg))
- sections.Add(new PromptSection(kvp.Key, seg!, kvp.Value.priority));
- }
- catch (Exception ex) { Log.Warning($"[RimMind] StaticProvider '{kvp.Key}' error: {ex.Message}"); }
- }
-
- foreach (var kvp in _pawnProviders)
- {
- if (ctx?.disabledProviders?.Contains(kvp.Key) == true) continue;
- if (excludeProviders != null && Array.IndexOf(excludeProviders, kvp.Key) >= 0) continue;
- try
- {
- string? seg = kvp.Value.provider(pawn);
- if (!string.IsNullOrEmpty(seg))
- sections.Add(new PromptSection(kvp.Key, seg!, kvp.Value.priority));
- }
- catch (Exception ex) { Log.Warning($"[RimMind] PawnProvider '{kvp.Key}' error: {ex.Message}"); }
- }
-
- sections.Add(GameContextBuilder.BuildPawnContextSection(pawn));
-
- if (pawn.Map != null)
- sections.Add(GameContextBuilder.BuildMapContextSection(pawn.Map, brief: false));
-
- foreach (var kvp in _dynamicProviders)
- {
- try
- {
- string? seg = kvp.Value.provider(currentQuery ?? string.Empty);
- if (!string.IsNullOrEmpty(seg))
- sections.Add(new PromptSection(kvp.Key, seg!, kvp.Value.priority));
- }
- catch (Exception ex) { Log.Warning($"[RimMind] DynamicProvider '{kvp.Key}' error: {ex.Message}"); }
- }
-
- var settings = RimMindCoreMod.Settings;
- if (settings != null)
- {
- string? customPawn = settings.customPawnPrompt?.Trim();
- if (!string.IsNullOrEmpty(customPawn))
- sections.Add(new PromptSection("custom_pawn",
- "RimMind.Core.Prompt.CustomPawnHeader".Translate() + "\n" + customPawn,
- PromptSection.PriorityCustom));
- string? customMap = settings.customMapPrompt?.Trim();
- if (!string.IsNullOrEmpty(customMap))
- sections.Add(new PromptSection("custom_map",
- "RimMind.Core.Prompt.CustomMapHeader".Translate() + "\n" + customMap,
- PromptSection.PriorityCustom));
- }
-
- return sections;
- }
-
- // ── 状态查询 ──────────────────────────────────────────────────────────
-
- public static bool IsConfigured() => RimMindCoreMod.Settings.IsConfigured();
-
- // ── Provider 注册(去重/覆盖) ──────────────────────────────────────────
-
- public static void RegisterStaticProvider(string category, Func provider,
- int priority = PromptSection.PriorityAuxiliary, string modId = "", bool overrideExisting = true)
- {
- if (_staticProviders.ContainsKey(category))
- {
- if (!overrideExisting) return;
- _staticProviders[category] = (modId, provider, priority);
- }
- else
- {
- _staticProviders[category] = (modId, provider, priority);
- }
- }
-
- public static void RegisterDynamicProvider(string category, Func provider,
- int priority = PromptSection.PriorityAuxiliary, string modId = "", bool overrideExisting = true)
- {
- if (_dynamicProviders.ContainsKey(category))
- {
- if (!overrideExisting) return;
- _dynamicProviders[category] = (modId, provider, priority);
- }
- else
- {
- _dynamicProviders[category] = (modId, provider, priority);
- }
- }
-
- public static void RegisterPawnContextProvider(string category, Func provider,
- int priority = PromptSection.PriorityAuxiliary, string modId = "", bool overrideExisting = true)
- {
- if (_pawnProviders.ContainsKey(category))
- {
- if (!overrideExisting) return;
- _pawnProviders[category] = (modId, provider, priority);
- }
- else
- {
- _pawnProviders[category] = (modId, provider, priority);
- }
- }
-
- // ── Provider 查询(供外部 Mod 读取 RimMind 数据) ──────────────────────
-
- public static string? GetProviderData(string category, Pawn pawn)
- {
- if (!_pawnProviders.TryGetValue(category, out var entry)) return null;
- var ctx = RimMindCoreMod.Settings?.Context;
- if (ctx?.exposedProviders.Count > 0 && !ctx.exposedProviders.Contains(category)) return null;
- try { return entry.provider(pawn); }
- catch (System.Exception ex) { Log.Warning($"[RimMind] GetProviderData '{category}' error: {ex.Message}"); return null; }
- }
-
- public static string? GetStaticProviderData(string category)
- {
- if (!_staticProviders.TryGetValue(category, out var entry)) return null;
- var ctx = RimMindCoreMod.Settings?.Context;
- if (ctx?.exposedProviders.Count > 0 && !ctx.exposedProviders.Contains(category)) return null;
- try { return entry.provider(); }
- catch (System.Exception ex) { Log.Warning($"[RimMind] GetStaticProviderData '{category}' error: {ex.Message}"); return null; }
- }
-
- public static string? GetDynamicProviderData(string category, string query)
- {
- if (!_dynamicProviders.TryGetValue(category, out var entry)) return null;
- var ctx = RimMindCoreMod.Settings?.Context;
- if (ctx?.exposedProviders.Count > 0 && !ctx.exposedProviders.Contains(category)) return null;
- try { return entry.provider(query); }
- catch (System.Exception ex) { Log.Warning($"[RimMind] GetDynamicProviderData '{category}' error: {ex.Message}"); return null; }
- }
-
- public static List GetRegisteredCategories()
- {
- var all = new HashSet();
- all.UnionWith(_staticProviders.Keys);
- all.UnionWith(_pawnProviders.Keys);
- all.UnionWith(_dynamicProviders.Keys);
-
- var ctx = RimMindCoreMod.Settings?.Context;
- if (ctx?.exposedProviders.Count > 0)
- all.IntersectWith(ctx.exposedProviders);
-
- return all.ToList();
- }
-
- // ── Provider 卸载 ──────────────────────────────────────────────────────
-
- public static void UnregisterPawnContextProvider(string category)
- => _pawnProviders.Remove(category);
-
- public static void UnregisterStaticProvider(string category)
- => _staticProviders.Remove(category);
-
- public static void UnregisterDynamicProvider(string category)
- => _dynamicProviders.Remove(category);
-
- public static void UnregisterModProviders(string modId)
- {
- if (string.IsNullOrEmpty(modId)) return;
- var staticKeys = _staticProviders.Where(kvp => kvp.Value.modId == modId).Select(kvp => kvp.Key).ToList();
- foreach (var key in staticKeys) _staticProviders.Remove(key);
-
- var dynamicKeys = _dynamicProviders.Where(kvp => kvp.Value.modId == modId).Select(kvp => kvp.Key).ToList();
- foreach (var key in dynamicKeys) _dynamicProviders.Remove(key);
-
- var pawnKeys = _pawnProviders.Where(kvp => kvp.Value.modId == modId).Select(kvp => kvp.Key).ToList();
- foreach (var key in pawnKeys) _pawnProviders.Remove(key);
- }
-
- // ── Settings / Toggle / Cooldown ──────────────────────────────────────
-
- public static void RegisterSettingsTab(string tabId, Func labelFn, Action drawFn)
- => _settingsTabs.Add((tabId, labelFn, drawFn));
-
- public static IReadOnlyList<(string tabId, Func labelFn, Action drawFn)>
- SettingsTabs => _settingsTabs;
-
- public static void RegisterToggleBehavior(string id, Func isActive, Action toggle)
- => _toggleBehaviors.Add((id, isActive, toggle));
-
- public static bool IsAnyToggleActive()
- => _toggleBehaviors.Count > 0 && _toggleBehaviors.Any(b => b.isActive());
-
- public static void ToggleAll()
- {
- foreach (var (_, _, toggle) in _toggleBehaviors)
- toggle();
- }
-
- public static bool HasToggleBehaviors => _toggleBehaviors.Count > 0;
-
- public static void RegisterModCooldown(string modId, Func getCooldownTicks)
- => _modCooldownGetters[modId] = getCooldownTicks;
-
- public static Func? GetModCooldownGetter(string modId)
- => _modCooldownGetters.TryGetValue(modId, out var getter) ? getter : null;
-
- public static IReadOnlyDictionary> ModCooldownGetters => _modCooldownGetters;
-
- public static void RegisterDialogueTrigger(Action triggerFn)
- {
- _dialogueTriggerFn = triggerFn;
- }
-
- public static bool CanTriggerDialogue => _dialogueTriggerFn != null;
-
- public static void TriggerDialogue(Pawn pawn, string context, Pawn? recipient = null)
- {
- if (_dialogueTriggerFn == null)
- {
- Log.Warning("[RimMind] TriggerDialogue called but no dialogue trigger registered.");
- return;
- }
- _dialogueTriggerFn(pawn, context, recipient);
- }
-
- // ── SkipCheck API ──────────────────────────────────────────────────
-
- public static void RegisterDialogueSkipCheck(string sourceId, Func skipCheck)
- {
- _dialogueSkipChecks[sourceId] = skipCheck;
- }
-
- public static void UnregisterDialogueSkipCheck(string sourceId)
- => _dialogueSkipChecks.Remove(sourceId);
-
- public static bool ShouldSkipDialogue(Pawn pawn, string triggerType)
- {
- foreach (var kvp in _dialogueSkipChecks.Values.ToList())
- {
- try
- {
- if (kvp(pawn, triggerType)) return true;
- }
- catch (System.Exception ex)
- {
- Log.Warning($"[RimMind] DialogueSkipCheck error: {ex.Message}");
- }
- }
- return false;
- }
-
- public static void RegisterFloatMenuSkipCheck(string sourceId, Func skipCheck)
- {
- _floatMenuSkipChecks[sourceId] = skipCheck;
- }
-
- public static void UnregisterFloatMenuSkipCheck(string sourceId)
- => _floatMenuSkipChecks.Remove(sourceId);
-
- public static bool ShouldSkipFloatMenu()
- {
- foreach (var check in _floatMenuSkipChecks.Values.ToList())
- {
- try
- {
- if (check()) return true;
- }
- catch (System.Exception ex)
- {
- Log.Warning($"[RimMind] FloatMenuSkipCheck error: {ex.Message}");
- }
- }
- return false;
- }
-
- // ── ActionSkipCheck API ──────────────────────────────────────────────
-
- public static void RegisterActionSkipCheck(string sourceId, Func skipCheck)
- {
- _actionSkipChecks[sourceId] = skipCheck;
- }
-
- public static void UnregisterActionSkipCheck(string sourceId)
- => _actionSkipChecks.Remove(sourceId);
-
- public static bool ShouldSkipAction(string intentId)
- {
- foreach (var check in _actionSkipChecks.Values.ToList())
- {
- try
- {
- if (check(intentId)) return true;
- }
- catch (System.Exception ex)
- {
- Log.Warning($"[RimMind] ActionSkipCheck error: {ex.Message}");
- }
- }
- return false;
- }
-
- // ── Incident Cooldown API ────────────────────────────────────────────
-
- private static readonly List _incidentExecutedCallbacks = new List();
-
- public static void RegisterIncidentExecutedCallback(Action callback)
- {
- _incidentExecutedCallbacks.Add(callback);
- }
-
- public static void NotifyIncidentExecuted()
- {
- foreach (var cb in _incidentExecutedCallbacks.ToList())
- {
- try { cb(); }
- catch (System.Exception ex) { Log.Warning($"[RimMind] IncidentExecuted callback error: {ex.Message}"); }
- }
- }
-
- public static void UnregisterIncidentExecutedCallback(Action callback)
- {
- _incidentExecutedCallbacks.Remove(callback);
- }
-
- private static readonly List> _storytellerIncidentSkipChecks = new List>();
-
- public static void RegisterStorytellerIncidentSkipCheck(Func check)
- {
- _storytellerIncidentSkipChecks.Add(check);
- }
-
- public static void UnregisterStorytellerIncidentSkipCheck(Func check)
- {
- _storytellerIncidentSkipChecks.Remove(check);
- }
-
- public static bool ShouldSkipStorytellerIncident()
- {
- foreach (var check in _storytellerIncidentSkipChecks.ToList())
- {
- try { if (check()) return true; }
- catch (System.Exception ex) { Log.Warning($"[RimMind] StorytellerIncidentSkipCheck error: {ex.Message}"); }
- }
- return false;
- }
-
- // ── RequestOverlay API ────────────────────────────────────────────────
-
- public static void RegisterPendingRequest(RequestEntry entry)
- => RequestOverlay.Register(entry);
-
- public static IReadOnlyList GetPendingRequests()
- => RequestOverlay.Pending;
-
- public static void RemovePendingRequest(RequestEntry entry)
- => RequestOverlay.Remove(entry);
-
- // ── 内部 ──────────────────────────────────────────────────────────────
-
- private static IAIClient? GetClient()
- {
- var s = RimMindCoreMod.Settings;
- if (!s.IsConfigured()) return null;
-
- if (s.provider == AIProvider.Player2)
- {
- var cached = EnsurePlayer2Client(s);
- return cached;
- }
-
- return new OpenAIClient(s);
- }
-
- private static Player2Client? _cachedPlayer2Client;
- private static AIProvider _cachedProvider;
- private static bool _player2InitInProgress;
- private static readonly object _player2Lock = new object();
-
- private static Player2Client? EnsurePlayer2Client(RimMindCoreSettings s)
- {
- lock (_player2Lock)
- {
- if (_cachedPlayer2Client != null && _cachedProvider == AIProvider.Player2)
- return _cachedPlayer2Client.IsConfigured() ? _cachedPlayer2Client : null;
-
- if (_player2InitInProgress) return null;
- _player2InitInProgress = true;
- }
-
- Task.Run(async () =>
- {
- try
- {
- var client = await Player2Client.CreateAsync(s);
- lock (_player2Lock)
- {
- _cachedPlayer2Client = client;
- _cachedProvider = AIProvider.Player2;
- _player2InitInProgress = false;
- }
- }
- catch (Exception ex)
- {
- AIRequestQueue.LogFromBackground($"[RimMind] Player2 init failed: {ex.Message}", isWarning: true);
- lock (_player2Lock)
- {
- _player2InitInProgress = false;
- }
- }
- });
-
- return null;
- }
-
- public static void InvalidateClientCache()
- {
- lock (_player2Lock)
- {
- if (_cachedProvider == AIProvider.Player2)
- Player2Client.StopHealthCheck();
- _cachedPlayer2Client = null;
- _cachedProvider = default;
- }
- }
- }
-}
diff --git a/Source/AICoreMod.cs b/Source/AICoreMod.cs
index 45dee7be..0ecc879a 100644
--- a/Source/AICoreMod.cs
+++ b/Source/AICoreMod.cs
@@ -1,30 +1,91 @@
+using System;
using HarmonyLib;
-using RimMind.Core.Settings;
-using RimMind.Core.UI;
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Abstractions;
+using RimMind.Application.Common.Interfaces.Client;
+using RimMind.Application.Common.Interfaces.Context;
+using RimMind.Application.Common.Interfaces.Extension;
+using RimMind.Application.Common.Interfaces.Internal;
+using RimMind.Domain.ValueObjects;
+using RimMind.Application.Features.Json;
+using RimMind.Application.Features.Flywheel;
+using RimMind.Application.Features.Context;
+using RimMind.Presentation.Api;
+using RimMind.Presentation.UI;
+using RimMind.Presentation.Runtime;
+using RimMind.Presentation.Runtime.Services;
+using RimMind.Presentation.Settings;
using UnityEngine;
using Verse;
-namespace RimMind.Core
+namespace RimMind.Presentation
{
- ///
- /// Mod 入口。注册 Harmony,持有全局 Settings 单例。
- ///
+ internal static class BootstrapConstants
+ {
+ internal const string CurrentModVersion = "2.0.0";
+ internal const string HarmonyId = "mcocdaa.RimMindCore";
+ internal const string DomainAssemblyName = "0_RimMindDomain";
+ internal const string ApplicationAssemblyName = "1_RimMindApplication";
+ internal const string CoreAssemblyName = "2_RimMindCore";
+ }
+
public class RimMindCoreMod : Mod
{
+ public static RimMindCoreMod Instance { get; private set; } = null!;
public static RimMindCoreSettings Settings { get; private set; } = null!;
public RimMindCoreMod(ModContentPack content) : base(content)
{
+ Instance = this;
+ AssemblyLoadGuard.AssertAssembliesLoaded();
+ RimMindErrors.OnWarn = (msg, ex) => Verse.Log.Warning(ex != null ? $"[RimMind] {msg}\n{ex}" : $"[RimMind] {msg}");
+ RimMindErrors.OnError = (msg, ex) => Verse.Log.Error(ex != null ? $"[RimMind] {msg}\n{ex}" : $"[RimMind] {msg}");
+
Settings = GetSettings();
- new Harmony("mcocdaa.RimMindCore").PatchAll();
+ var sp = new SettingsProvider(Settings);
- RimMindAPI.RegisterToggleBehavior("request_overlay",
- () => Settings.requestOverlayEnabled,
- () =>
+ ProcessLifecycleEvents.Publisher.Configure(VerseLifecycleEventSink.Instance);
+ RimMindRuntimeHost.Initialize(sp, Settings);
+ var scope = RuntimeServiceHub.Shared.Capture();
+ var runtime = scope.GetRequired();
+
+ if (Settings.SavedModVersion != null &&
+ Settings.SavedModVersion != BootstrapConstants.CurrentModVersion)
+ {
+ LongEventHandler.ExecuteWhenFinished(() =>
{
- Settings.requestOverlayEnabled = !Settings.requestOverlayEnabled;
- Settings.Write();
+ RimMindErrors.Warn(
+ $"[RimMind-Core] Saved mod version mismatch. Old saves may not be fully compatible with v{BootstrapConstants.CurrentModVersion}.");
+ RuntimeServiceHub.Shared.Capture()
+ .GetOptional()?
+ .WindowService?
+ .OpenUpgradeWarning();
});
+ }
+ Settings.SavedModVersion = BootstrapConstants.CurrentModVersion;
+
+ JsonTagExtractor.OnWarning = msg => RimMindErrors.Warn(msg);
+ new Harmony(BootstrapConstants.HarmonyId).PatchAll();
+
+ RimMindAPI.Extensions().Register(new CoreOverlayToggle());
+
+ RimMindAPI.RegisterParameterTuner(new FlywheelBuiltinTuner());
+
+ // Mod constructors run before RimWorld activates LoadedLanguage. Translating here
+ // emits "No active language" errors and leaves scenario descriptions as raw keys.
+ // The registry is not consumed until play-data loading has completed, so defer only
+ // the language-dependent registration to the main-thread completion queue.
+ LongEventHandler.ExecuteWhenFinished(() =>
+ {
+ var currentScope = RuntimeServiceHub.Shared.Capture();
+ ScenarioRegistry.RegisterCoreScenarios(
+ currentScope.GetRequired(),
+ currentScope.GetRequired());
+ });
+
+ // L3: Use instance-based RelevanceTable
+ runtime.RelevanceTable.RegisterCoreRelevance();
+
}
public override string SettingsCategory() => "RimMind";
@@ -34,4 +95,74 @@ public override void DoSettingsWindowContents(Rect inRect)
RimMindCoreSettingsUI.Draw(inRect);
}
}
+
+ internal sealed class CoreOverlayToggle : IToggleBehavior
+ {
+ private readonly RuntimeServiceRef _settings =
+ RuntimeServiceRef.Optional();
+
+ public string Id => "request_overlay";
+ public string OwnerModId => RimMindOwnerConsts.CoreModId;
+ public bool IsActive => _settings.ValueOrDefault?.RequestOverlayEnabled == true;
+
+ public void Toggle()
+ {
+ var settings = _settings.ValueOrDefault;
+ if (settings == null)
+ {
+ RimMindErrors.Warn("[RimMind-Core] Request overlay settings are unavailable while the runtime is stopped.");
+ return;
+ }
+
+ settings.RequestOverlayEnabled = !settings.RequestOverlayEnabled;
+ settings.Persist();
+ }
+ }
+
+ internal static class AssemblyLoadGuard
+ {
+ public static void AssertAssembliesLoaded()
+ {
+ var loaded = AppDomain.CurrentDomain.GetAssemblies();
+ var domain = System.Linq.Enumerable.FirstOrDefault(
+ loaded,
+ a => a.GetName().Name == BootstrapConstants.DomainAssemblyName);
+ var application = System.Linq.Enumerable.FirstOrDefault(
+ loaded,
+ a => a.GetName().Name == BootstrapConstants.ApplicationAssemblyName);
+ var core = System.Linq.Enumerable.FirstOrDefault(
+ loaded,
+ a => a.GetName().Name == BootstrapConstants.CoreAssemblyName);
+
+ if (domain == null)
+ {
+ var msg = $"[RimMind-Core] FATAL: {BootstrapConstants.DomainAssemblyName}.dll not loaded. " +
+ "Check that the dll exists in Assemblies/ folder. " +
+ "If you upgraded from an earlier version, please subscribe to the new mod files.";
+ RimMindErrors.Error(msg);
+ throw new System.InvalidOperationException(msg);
+ }
+
+ if (application == null)
+ {
+ var msg = $"[RimMind-Core] FATAL: {BootstrapConstants.ApplicationAssemblyName}.dll not loaded. " +
+ "Check that the dll exists in Assemblies/ folder.";
+ RimMindErrors.Error(msg);
+ throw new System.InvalidOperationException(msg);
+ }
+
+ if (core == null)
+ {
+ var msg = $"[RimMind-Core] FATAL: {BootstrapConstants.CoreAssemblyName}.dll not loaded. " +
+ "Check that the dll exists in Assemblies/ folder.";
+ RimMindErrors.Error(msg);
+ throw new System.InvalidOperationException(msg);
+ }
+
+ Log.Message($"[RimMind-Core] Assemblies loaded for v{BootstrapConstants.CurrentModVersion}: " +
+ $"Domain={domain.GetName().Version} " +
+ $"Application={application.GetName().Version} " +
+ $"Core={core.GetName().Version}");
+ }
+ }
}
diff --git a/Source/Application/Common/Behaviours/ConcurrentRegistryBase.cs b/Source/Application/Common/Behaviours/ConcurrentRegistryBase.cs
new file mode 100644
index 00000000..b108dc2c
--- /dev/null
+++ b/Source/Application/Common/Behaviours/ConcurrentRegistryBase.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using RimMind.Application.Common.Interfaces.Registry;
+
+namespace RimMind.Application.Common.Behaviours
+{
+ ///
+ /// Base class for concurrent registries. Provides Register/Unregister/FindById/All/UnregisterByOwner.
+ /// Subclasses provide key extractor and owner extractor via constructor.
+ ///
+ public abstract class ConcurrentRegistryBase : IOwnedRegistry
+ where TKey : notnull
+ where TValue : class
+ {
+ protected readonly ConcurrentDictionary _items = new ConcurrentDictionary();
+ private readonly Func _keyExtractor;
+ private readonly Func? _ownerExtractor;
+
+ protected ConcurrentRegistryBase(Func keyExtractor, Func? ownerExtractor = null)
+ {
+ _keyExtractor = keyExtractor ?? throw new ArgumentNullException(nameof(keyExtractor));
+ _ownerExtractor = ownerExtractor;
+ }
+
+ public void Register(TValue item)
+ {
+ if (item == null) return;
+ var key = _keyExtractor(item);
+ _items.AddOrUpdate(key, item, (_, __) => item);
+ }
+
+ public bool Unregister(TKey key) => _items.TryRemove(key, out _);
+
+ public TValue? FindById(TKey key) => _items.TryGetValue(key, out var v) ? v : null;
+
+ public IReadOnlyList All => new List(_items.Values);
+
+ ///
+ public int UnregisterByOwner(string ownerModId)
+ {
+ if (ownerModId == null) throw new ArgumentNullException(nameof(ownerModId));
+ if (_ownerExtractor == null) return 0;
+ var toRemove = new List();
+ foreach (var kvp in _items)
+ {
+ if (_ownerExtractor(kvp.Value) == ownerModId)
+ toRemove.Add(kvp.Key);
+ }
+ foreach (var k in toRemove) _items.TryRemove(k, out _);
+ return toRemove.Count;
+ }
+ }
+}
diff --git a/Source/Application/Common/Behaviours/MiddlewareBase.cs b/Source/Application/Common/Behaviours/MiddlewareBase.cs
new file mode 100644
index 00000000..02f2a539
--- /dev/null
+++ b/Source/Application/Common/Behaviours/MiddlewareBase.cs
@@ -0,0 +1,29 @@
+using System.Threading.Tasks;
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Abstractions;
+using RimMind.Application.Common.Interfaces.Pipeline;
+using RimMind.Application.Common.Models.Pipeline;
+
+namespace RimMind.Application.Common.Behaviours
+{
+ ///
+ /// Base class for IMiddleware implementations. Provides default Id, OwnerModId, and Log property.
+ /// Subclasses only need to override Name, Order, and InvokeAsync.
+ ///
+ public abstract class MiddlewareBase : IMiddleware where TContext : IPipelineContext
+ {
+ public abstract string Name { get; }
+ public abstract int Order { get; }
+ public virtual string Id => Name;
+ public virtual string OwnerModId => RimMindOwnerConsts.CoreModId;
+
+ protected readonly ILogSink? Log;
+
+ protected MiddlewareBase(ILogSink? log = null)
+ {
+ Log = log;
+ }
+
+ public abstract Task InvokeAsync(TContext context, MiddlewareDelegate next);
+ }
+}
diff --git a/Source/Application/Common/Behaviours/MutablePipeline.cs b/Source/Application/Common/Behaviours/MutablePipeline.cs
new file mode 100644
index 00000000..a4e2d799
--- /dev/null
+++ b/Source/Application/Common/Behaviours/MutablePipeline.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using RimMind.Application.Common.Interfaces.Extension;
+using RimMind.Application.Common.Interfaces.Pipeline;
+using RimMind.Application.Common.Models.Pipeline;
+
+namespace RimMind.Application.Common.Behaviours
+{
+ public sealed class MutablePipeline : IPipeline
+ where TContext : IPipelineContext
+ {
+ private volatile IReadOnlyList> _middlewares = new List>();
+ private readonly object _lock = new object();
+
+ // Lazy construction support: extension registry is merged on first ExecuteAsync
+ private IExtensionRegistry>? _extensionRegistry;
+ private volatile bool _extensionsMerged;
+
+ public void Use(IMiddleware middleware)
+ {
+ lock (_lock)
+ {
+ var list = new List>(_middlewares) { middleware };
+ _middlewares = list.OrderBy(m => m.Order).ToList().AsReadOnly();
+ }
+ }
+
+ public void UseRange(IEnumerable> middlewares)
+ {
+ lock (_lock)
+ {
+ var list = new List>(_middlewares);
+ list.AddRange(middlewares);
+ _middlewares = list.OrderBy(m => m.Order).ToList().AsReadOnly();
+ }
+ }
+
+ ///
+ /// Sets the extension registry for lazy middleware merging.
+ /// Extensions are merged on the first ExecuteAsync call, allowing sub-Mods
+ /// to register middlewares after Core initialization.
+ ///
+ public void SetExtensionRegistry(IExtensionRegistry> registry)
+ {
+ _extensionRegistry = registry;
+ _extensionsMerged = false;
+ }
+
+ public async Task ExecuteAsync(TContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ // Lazy merge: on first execution, merge extension middlewares
+ if (!_extensionsMerged && _extensionRegistry != null)
+ {
+ MergeExtensions();
+ }
+
+ var snapshot = _middlewares;
+ int index = 0;
+ async Task Next(TContext ctx)
+ {
+ if (ctx.IsShortCircuited) return;
+ if (index >= snapshot.Count) return;
+ var mw = snapshot[index++];
+ await mw.InvokeAsync(ctx, Next).ConfigureAwait(false);
+ }
+ await Next(context).ConfigureAwait(false);
+ }
+
+ private void MergeExtensions()
+ {
+ lock (_lock)
+ {
+ if (_extensionsMerged) return;
+ var extra = _extensionRegistry?.All ?? Enumerable.Empty>();
+ if (extra.Any())
+ {
+ var list = new List>(_middlewares);
+ list.AddRange(extra);
+ _middlewares = list.OrderBy(m => m.Order).ToList().AsReadOnly();
+ }
+ _extensionsMerged = true;
+ }
+ }
+ }
+
+ public static class PipelineFactory
+ {
+ ///
+ /// Builds a pipeline with default middlewares. Extension middlewares are
+ /// lazily merged on first ExecuteAsync if a registry is provided.
+ ///
+ public static IPipeline Build(
+ IReadOnlyList> defaults,
+ IExtensionRegistry>? extensions = null)
+ where TContext : IPipelineContext
+ {
+ var pipeline = new MutablePipeline();
+ pipeline.UseRange(defaults);
+ if (extensions != null)
+ {
+ pipeline.SetExtensionRegistry(extensions);
+ }
+ return pipeline;
+ }
+ }
+}
diff --git a/Source/Application/Common/Constants/RimMindOwnerConsts.cs b/Source/Application/Common/Constants/RimMindOwnerConsts.cs
new file mode 100644
index 00000000..e629c9df
--- /dev/null
+++ b/Source/Application/Common/Constants/RimMindOwnerConsts.cs
@@ -0,0 +1,43 @@
+namespace RimMind.Application.Common.Constants
+{
+ ///
+ /// OwnerModId 统一常量。所有 Core 内置 IExtension 实现应引用此类,
+ /// 避免散落的字符串字面量导致 UnregisterByOwner 查询不一致。
+ ///
+ ///
+ /// 已知不一致(待后续统一):
+ /// - "RimMind.Core" :用于 ScopedAgent.cs、ToolManifest.cs(2 处)
+ /// - "Core" :用于 OutputGuardrailMiddleware.cs、CoreContextProviders.cs 的 ownerMod 参数(~21 处)
+ /// 这两种写法暂不修改,避免破坏存档序列化或潜在的 UnregisterByOwner 查询。
+ /// 后续应统一为 。
+ ///
+ public static class RimMindOwnerConsts
+ {
+ /// Core 模组的 OwnerModId 标识。
+ public const string CoreModId = "RimMindCore";
+
+ /// Actions 模组的 OwnerModId 标识。
+ public const string ActionsModId = "RimMindActions";
+
+ /// Advisor 模组的 OwnerModId 标识。
+ public const string AdvisorModId = "RimMindAdvisor";
+
+ /// Dialogue 模组的 OwnerModId 标识。
+ public const string DialogueModId = "RimMindDialogue";
+
+ /// Memory 模组的 OwnerModId 标识。
+ public const string MemoryModId = "RimMindMemory";
+
+ /// Personality 模组的 OwnerModId 标识。
+ public const string PersonalityModId = "RimMindPersonality";
+
+ /// Storyteller 模组的 OwnerModId 标识。
+ public const string StorytellerModId = "RimMindStoryteller";
+
+ /// Bridge-RimTalk 模组的 OwnerModId 标识。
+ public const string BridgeRimTalkModId = "RimMindBridgeRimTalk";
+
+ /// Bridge-RimChat 模组的 OwnerModId 标识。
+ public const string BridgeRimChatModId = "RimMindBridgeRimChat";
+ }
+}
diff --git a/Source/Application/Common/Defaults/AgentBusCoreSubscriber.cs b/Source/Application/Common/Defaults/AgentBusCoreSubscriber.cs
new file mode 100644
index 00000000..b38d2268
--- /dev/null
+++ b/Source/Application/Common/Defaults/AgentBusCoreSubscriber.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Threading;
+using RimMind.Application.Common.Interfaces;
+using RimMind.Application.Common.Interfaces.Abstractions;
+using RimMind.Domain.Events;
+
+namespace RimMind.Application.Common.Defaults;
+
+public sealed class AgentBusCoreSubscriber : IDisposable
+{
+ private readonly IAgentBus _eventBus;
+ private readonly ILogSink _logSink;
+ private readonly string _perceptionKey;
+ private readonly string _actionKey;
+ private readonly string _modeChangedKey;
+ private readonly string _lifecycleKey;
+ private readonly string _decisionKey;
+ private readonly string _goalKey;
+ private int _disposed;
+
+ public AgentBusCoreSubscriber(IAgentBus eventBus, ILogSink logSink)
+ {
+ _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
+ _logSink = logSink;
+ _perceptionKey = eventBus.Subscribe(OnPerception);
+ _actionKey = eventBus.Subscribe(OnAction);
+ _modeChangedKey = eventBus.Subscribe(OnModeChanged);
+ _lifecycleKey = eventBus.Subscribe(OnLifecycle);
+ _decisionKey = eventBus.Subscribe(OnDecision);
+ _goalKey = eventBus.Subscribe(OnGoal);
+ }
+
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) != 0)
+ return;
+
+ _eventBus.Unsubscribe(_perceptionKey);
+ _eventBus.Unsubscribe(_actionKey);
+ _eventBus.Unsubscribe(_modeChangedKey);
+ _eventBus.Unsubscribe(_lifecycleKey);
+ _eventBus.Unsubscribe(_decisionKey);
+ _eventBus.Unsubscribe(_goalKey);
+ }
+
+ private void OnPerception(PerceptionEvent e)
+ {
+ _logSink.Message($"[AgentBus] Perception: NpcId={e.NpcId}, PawnId={e.PawnId}, Type={e.PerceptionType}");
+ }
+
+ private void OnAction(ActionEvent e)
+ {
+ _logSink.Message($"[AgentBus] Action: NpcId={e.NpcId}, PawnId={e.PawnId}, Name={e.ActionName}, Success={e.Success}");
+ }
+
+ private void OnModeChanged(AgentModeChangedEvent e)
+ {
+ _logSink.Message($"[AgentBus] ModeChanged: NpcId={e.NpcId}, PawnId={e.PawnId}, {e.OldMode}->{e.NewMode}");
+ }
+
+ private void OnLifecycle(AgentLifecycleEvent e)
+ {
+ _logSink.Message($"[AgentBus] Lifecycle: NpcId={e.NpcId}, PawnId={e.PawnId}, {e.PreviousState}->{e.NewState}");
+ }
+
+ private void OnDecision(DecisionEvent e)
+ {
+ _logSink.Message($"[AgentBus] Decision: NpcId={e.NpcId}, PawnId={e.PawnId}, Type={e.DecisionType}");
+ }
+
+ private void OnGoal(GoalEvent e)
+ {
+ _logSink.Message($"[AgentBus] Goal: NpcId={e.NpcId}, PawnId={e.PawnId}, Status={e.Status}, Desc={e.GoalDescription}");
+ }
+}
diff --git a/Source/Application/Common/Defaults/NullAgentActionBridge.cs b/Source/Application/Common/Defaults/NullAgentActionBridge.cs
new file mode 100644
index 00000000..23454c4b
--- /dev/null
+++ b/Source/Application/Common/Defaults/NullAgentActionBridge.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Extension;
+using RimMind.Domain.Llm;
+
+namespace RimMind.Application.Common.Defaults
+{
+ public sealed class NullAgentActionBridge : IAgentActionBridge
+ {
+ public static readonly NullAgentActionBridge Instance = new NullAgentActionBridge();
+
+ public string Id => "null-agent-action-bridge";
+ public string OwnerModId => RimMindOwnerConsts.CoreModId;
+
+ public void ExecuteAction(string npcId, string actionName, string[]? args = null) { }
+ public bool CanExecute(string npcId, string actionName) => false;
+ public bool CanExecute(object pawn, string action) => false;
+ public void Execute(object pawn, string action, string? targetName = null) { }
+ public List? GetAvailableTools(object pawn) => null;
+ }
+}
diff --git a/Source/Application/Common/Defaults/NullDialogueTrigger.cs b/Source/Application/Common/Defaults/NullDialogueTrigger.cs
new file mode 100644
index 00000000..5543a38e
--- /dev/null
+++ b/Source/Application/Common/Defaults/NullDialogueTrigger.cs
@@ -0,0 +1,14 @@
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Extension;
+
+namespace RimMind.Application.Common.Defaults
+{
+ public sealed class NullDialogueTrigger : IDialogueTrigger
+ {
+ public static readonly NullDialogueTrigger Instance = new NullDialogueTrigger();
+
+ public string Id => "null.dialogue-trigger";
+ public string OwnerModId => RimMindOwnerConsts.CoreModId;
+ public void Trigger(object pawn, string context, object? recipient) { }
+ }
+}
diff --git a/Source/Application/Common/Defaults/NullIncidentExecutedListener.cs b/Source/Application/Common/Defaults/NullIncidentExecutedListener.cs
new file mode 100644
index 00000000..767c1a76
--- /dev/null
+++ b/Source/Application/Common/Defaults/NullIncidentExecutedListener.cs
@@ -0,0 +1,14 @@
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Extension;
+
+namespace RimMind.Application.Common.Defaults
+{
+ public sealed class NullIncidentExecutedListener : IIncidentExecutedListener
+ {
+ public static readonly NullIncidentExecutedListener Instance = new NullIncidentExecutedListener();
+
+ public string Id => "null.incident-executed-listener";
+ public string OwnerModId => RimMindOwnerConsts.CoreModId;
+ public void OnIncidentExecuted() { }
+ }
+}
diff --git a/Source/Application/Common/Defaults/NullModCooldown.cs b/Source/Application/Common/Defaults/NullModCooldown.cs
new file mode 100644
index 00000000..380d130e
--- /dev/null
+++ b/Source/Application/Common/Defaults/NullModCooldown.cs
@@ -0,0 +1,14 @@
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Extension;
+
+namespace RimMind.Application.Common.Defaults
+{
+ public sealed class NullModCooldown : IModCooldown
+ {
+ public static readonly NullModCooldown Instance = new NullModCooldown();
+
+ public string Id => "null.mod-cooldown";
+ public string OwnerModId => RimMindOwnerConsts.CoreModId;
+ public int CooldownTicks => 0;
+ }
+}
diff --git a/Source/Application/Common/Defaults/NullSkipCheck.cs b/Source/Application/Common/Defaults/NullSkipCheck.cs
new file mode 100644
index 00000000..c843989a
--- /dev/null
+++ b/Source/Application/Common/Defaults/NullSkipCheck.cs
@@ -0,0 +1,15 @@
+using RimMind.Application.Common.Constants;
+using RimMind.Application.Common.Interfaces.Extension;
+
+namespace RimMind.Application.Common.Defaults
+{
+ public sealed class NullSkipCheck : ISkipCheck
+ {
+ public static readonly NullSkipCheck Instance = new NullSkipCheck();
+
+ public string Id => "null.skip-check";
+ public string OwnerModId => RimMindOwnerConsts.CoreModId;
+ public SkipCheckKind Kind => (SkipCheckKind)(-1);
+ public bool ShouldSkip(in SkipCheckArgs args) => false;
+ }
+}
diff --git a/Source/Application/Common/Extensions/KeyMetaExtensions.cs b/Source/Application/Common/Extensions/KeyMetaExtensions.cs
new file mode 100644
index 00000000..4f2e7073
--- /dev/null
+++ b/Source/Application/Common/Extensions/KeyMetaExtensions.cs
@@ -0,0 +1,14 @@
+using RimMind.Application.Common.Interfaces.Context;
+using RimMind.Domain.ValueObjects;
+
+namespace RimMind.Application.Common.Extensions
+{
+ public static class KeyMetaExtensions
+ {
+ public static ContextProviderDef? GetProviderDef(this KeyMeta meta)
+ => meta.Def as ContextProviderDef;
+
+ public static bool HasAsyncProvider(this KeyMeta meta)
+ => meta.Def is ContextProviderDef;
+ }
+}
diff --git a/Source/Application/Common/Helpers/AIProviderRegistry.cs b/Source/Application/Common/Helpers/AIProviderRegistry.cs
new file mode 100644
index 00000000..3a24db01
--- /dev/null
+++ b/Source/Application/Common/Helpers/AIProviderRegistry.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Linq;
+using RimMind.Application.Common.Interfaces.Client;
+using RimMind.Application.Common.Interfaces.Extension;
+using RimMind.Application.Common.Interfaces.Internal;
+
+namespace RimMind.Application.Common.Helpers
+{
+ public static class AIProviderRegistry
+ {
+ private static readonly string FallbackProvider = "openai";
+
+ public static IReadOnlyList GetAllProviderIds(IExtensionRegistry? registry = null)
+ {
+ if (registry == null || registry.All.Count == 0)
+ return new List { FallbackProvider };
+ return registry.All.Select(f => f.ProviderId).ToList();
+ }
+
+ public static string GetDefaultProviderId(IExtensionRegistry? registry = null)
+ {
+ var ids = GetAllProviderIds(registry);
+ return ids.Count > 0 ? ids[0] : FallbackProvider;
+ }
+
+ public static bool IsProviderRegistered(string providerId, IExtensionRegistry? registry = null)
+ {
+ if (string.IsNullOrEmpty(providerId)) return false;
+ return registry?.FindById(providerId) != null;
+ }
+
+ public static bool RequiresApiKey(string providerId, IExtensionRegistry? registry = null)
+ {
+ if (string.IsNullOrEmpty(providerId)) return true;
+ if (registry != null)
+ {
+ var factory = registry.FindById(providerId);
+ if (factory != null) return factory.RequiresApiKey;
+ }
+ if (providerId == "player2" || providerId == "extended_service")
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Abstractions/ILogSink.cs b/Source/Application/Common/Interfaces/Abstractions/ILogSink.cs
new file mode 100644
index 00000000..cb8049c6
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Abstractions/ILogSink.cs
@@ -0,0 +1,10 @@
+namespace RimMind.Application.Common.Interfaces.Abstractions
+{
+ public interface ILogSink
+ {
+ void Message(string msg);
+ void Warning(string msg);
+ void Error(string msg);
+ void LogFromBackground(string msg, bool isWarning = false);
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Abstractions/IPathProvider.cs b/Source/Application/Common/Interfaces/Abstractions/IPathProvider.cs
new file mode 100644
index 00000000..fe8b5721
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Abstractions/IPathProvider.cs
@@ -0,0 +1,7 @@
+namespace RimMind.Application.Common.Interfaces.Abstractions
+{
+ public interface IPathProvider
+ {
+ string SaveDataFolderPath { get; }
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Abstractions/IThreadChecker.cs b/Source/Application/Common/Interfaces/Abstractions/IThreadChecker.cs
new file mode 100644
index 00000000..3b837636
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Abstractions/IThreadChecker.cs
@@ -0,0 +1,7 @@
+namespace RimMind.Application.Common.Interfaces.Abstractions
+{
+ public interface IThreadChecker
+ {
+ bool IsMainThread { get; }
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Abstractions/ITickProvider.cs b/Source/Application/Common/Interfaces/Abstractions/ITickProvider.cs
new file mode 100644
index 00000000..7c68fbea
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Abstractions/ITickProvider.cs
@@ -0,0 +1,7 @@
+namespace RimMind.Application.Common.Interfaces.Abstractions
+{
+ public interface ITickProvider
+ {
+ int TicksGame { get; }
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Abstractions/ITranslationService.cs b/Source/Application/Common/Interfaces/Abstractions/ITranslationService.cs
new file mode 100644
index 00000000..9217d323
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Abstractions/ITranslationService.cs
@@ -0,0 +1,10 @@
+namespace RimMind.Application.Common.Interfaces.Abstractions
+{
+ public interface ITranslationService
+ {
+ string Translate(string key);
+ string Translate(string key, object arg0);
+ string Translate(string key, object arg0, object arg1);
+ string Translate(string key, object arg0, object arg1, object arg2);
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Agent/IActionExecutor.cs b/Source/Application/Common/Interfaces/Agent/IActionExecutor.cs
new file mode 100644
index 00000000..9187cd61
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Agent/IActionExecutor.cs
@@ -0,0 +1,22 @@
+using RimMind.Domain.Agent.Modes;
+using RimMind.Domain.Common;
+using RimMind.Domain.ValueObjects;
+
+namespace RimMind.Application.Common.Interfaces.Agent
+{
+ ///
+ /// Maps AgentDecision to concrete game actions via Mechanisms.
+ ///
+ public interface IActionExecutor
+ {
+ ///
+ /// Execute an AgentDecision by finding and invoking the corresponding Mechanism.
+ ///
+ Result ExecuteDecision(AgentDecision decision, int pawnId);
+
+ ///
+ /// Check if an action intent can be mapped to a registered Mechanism.
+ ///
+ bool CanExecute(string actionIntent);
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Agent/IAgentActionBridgeAccessor.cs b/Source/Application/Common/Interfaces/Agent/IAgentActionBridgeAccessor.cs
new file mode 100644
index 00000000..a3f2be64
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Agent/IAgentActionBridgeAccessor.cs
@@ -0,0 +1,9 @@
+using RimMind.Application.Common.Interfaces.Extension;
+
+namespace RimMind.Application.Common.Interfaces.Agent
+{
+ public interface IAgentActionBridgeAccessor
+ {
+ IAgentActionBridge Current { get; }
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Agent/IAgentActiveChecker.cs b/Source/Application/Common/Interfaces/Agent/IAgentActiveChecker.cs
new file mode 100644
index 00000000..57b2f261
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Agent/IAgentActiveChecker.cs
@@ -0,0 +1,7 @@
+namespace RimMind.Application.Common.Interfaces.Agent
+{
+ public interface IAgentActiveChecker
+ {
+ bool IsAgentActive(string pawnThingId);
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Agent/IAgentBehavior.cs b/Source/Application/Common/Interfaces/Agent/IAgentBehavior.cs
new file mode 100644
index 00000000..525752dc
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Agent/IAgentBehavior.cs
@@ -0,0 +1,13 @@
+using RimMind.Application.Common.Models.Agent;
+
+namespace RimMind.Application.Common.Interfaces.Agent
+{
+ ///
+ /// Behavior recording methods for an agent — goal removal and behavior record persistence.
+ ///
+ public interface IAgentBehavior
+ {
+ bool RemoveGoal(string goalDescription);
+ void RecordBehavior(BehaviorRecordDto record);
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Agent/IAgentControl.cs b/Source/Application/Common/Interfaces/Agent/IAgentControl.cs
new file mode 100644
index 00000000..0be3871e
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Agent/IAgentControl.cs
@@ -0,0 +1,15 @@
+namespace RimMind.Application.Common.Interfaces.Agent
+{
+ ///
+ /// Composite interface aggregating all agent concerns.
+ /// Inherit from a sub-interface when only a subset of functionality is needed:
+ /// — read-only state queries
+ /// — lifecycle control (tick, transition, cleanup)
+ /// — behavior recording
+ /// — identity and basic info
+ /// — pending job consumption
+ ///
+ public interface IAgentControl : IAgentState, IAgentLifecycle, IAgentBehavior, IAgentInfo, IJobProvider
+ {
+ }
+}
diff --git a/Source/Application/Common/Interfaces/Agent/IAgentIdentity.cs b/Source/Application/Common/Interfaces/Agent/IAgentIdentity.cs
new file mode 100644
index 00000000..ef6f913a
--- /dev/null
+++ b/Source/Application/Common/Interfaces/Agent/IAgentIdentity.cs
@@ -0,0 +1,17 @@
+namespace RimMind.Application.Common.Interfaces.Agent
+{
+ ///
+ /// Agent identity interface located in Application/Common/Interfaces so sub-mods can reference it
+ /// via 1_RimMindApplication.dll without depending on the Presentation layer or Verse.
+ /// The IExposable implementation lives in the Presentation layer.
+ ///
+ public interface IAgentIdentity
+ {
+ string NpcId { get; }
+ int PawnId { get; }
+ string DisplayName { get; }
+ System.Collections.Generic.List Motivations { get; }
+ System.Collections.Generic.List PersonalityTraits { get; }
+ System.Collections.Generic.List