AI 编辑器 CC for Unity
About OpenClaw
最近的龙虾神乎其神呀,OpenClaw 被众多人吹捧,一个 7 * 24 小时持续运行的 AI Agent 系统。
但是你了解它背后的秘密吗?其实很简单。
OpenClaw 的架构由三个模块组成:Agent Loop、Tools、Gateway。
-
• Agent Loop:龙虾的大脑,它负责决策和思考,它会根据当前任务判断下一步要做什么,并在需要的时候调用各种工具来完成行动。 -
• Tools:龙虾的手脚,为Agent Loop提供各种能力,比如浏览网页、执行命令、调用 API、处理文件等,让龙虾真正具备干活的能力。 -
• Gateway:龙虾的身体,它让整个系统能够持续在线,接收来自不同渠道的消息(比如 Telegram、飞书等),并把任务交给Agent Loop处理,再把结果返回给用户。
其中 Agent Loop、Tools 并不是 OpenClaw 独有的,Claude Code 和 Codex 都有,Gateway 是 OpenClaw 独有的模块。
Gateway 只是持续在线通信的手段,并不在我们关心的范围。Agent Loop、Tools 的实现才是我们着重关注的。
而在 Claude Code 中也有,因此这里我打算在 Unity 中做一个极简版的 Claude Code。
带大家了解其背后的秘密,其实 AI 应用层它极其简单,一步一步实现出来,也许会震碎你三观…
和引擎一样,人工智能的底层逻辑才是 AI 的命脉,后续面向 AI-Infra 才是我个人开发实践的重点。
这篇文章也是我从游戏引擎过度人工智能的开始,后续会把学习的重心放在 AI 上,待 AI 小有所成后,回过头来继续游戏引擎、继续图形学。到时候用超魔法打败魔法~
回归正题,即《AI 编辑器 MCP for Unity》的后续之作,这次要开发的是 《AI 编辑器 Claude Code for Unity》。
Anthropic SDK CSharp
最初的打算是使用 C++ 来实现一个 Claude Code CLI,但考虑到时间成本,之前也已经实现了 MCP for Unity,而且 Anthropic SDK 没有 C++ 版(可以自己实现),有 C# 版本,如果能配合 CC for Unity,便可以轻松打造一个超强的智能体。
通过将 MCP for Unity 与 CC for Unity 结合使用,两者相辅相成,能够帮助开发者深入理解如何开发 AI 工具,并形成一个完整的闭环。
我只是抛砖引玉,如果感兴趣的话,可以沿着这个思路开发下去,做出自己心仪的 AI 产品。
Anthropic SDK 是 Anthropic 官方提供的开发工具包,旨在帮助开发者将 Claude 大语言模型无缝集成到自己的应用程序中。
这里提供 C# 版本的下载地址:https://github.com/anthropics/anthropic-sdk-csharp
当前最新的版本是 v12.8.0,但我下载的是 v10.4.0。
我使用的是 Unity 6.3 版本,因为当前 Unity 只支持 C# 9 版本,而 Anthropic SDK 已经支持到了 C# 12。本以为下载低版本的做降级会轻松些,事后发现我想多了…
不得不说为 Anthropic SDK 做降级还是花了一些心思的,但不是课程的重点,因此这里只做一下总结!
Unity 中使用 Anthropic SDK 的相关修改:
-
• 代码降级到 C# 9 -
• 删除 Microsoft.Extensions.AI 依赖 -
• 清理不需要的目录(examples、tests等) -
• 下载必需的 DLL 文件(System.Text.Json.dll、System.Collections.Immutable.dll、System.Memory.dll、System.Buffers.dll 等)如何下载 DLL?使用 NuGet 命令行: nuget install System.Text.Json -Version 4.7.2
nuget install System.Collections.Immutable -Version 8.0.0 -
• System.Text.Json 兼容性修复
改动还是蛮多的…
Claude Code for Unity
在后续的开发过程中,你会发现,这里只用到了 Anthropic SDK 的 token 验证和模型交互功能。
其余的都是自己实现的,这样跟着看下来,你会对 AI Agent 开发有个整体的认知。
而且这部分代码可以完全剥离 Unity 依赖,你也可以单独提炼出来在 dotnet 以命令行去实现。
Agent Loop
Agent Loop,是 AI 接到一个任务之后,自己一步一步把事情做完的机制。
实践上它就是一个循环体,是一个和 AI 不停交互的过程,这就是 Agent Loop。
因为我想一次性在文章讲完,虽然代码不复杂但还是有些体量的,就直接贴全码了,不讲究开发阶段过程了
AgentLoop.cs
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
namespaceClaudeCodeForUnity
{
publicclassAgentLoop
{
privatereadonly AnthropicClient _client;
privatereadonlystring _model;
privatereadonlystring _systemPrompt;
privatereadonly ToolManager _skillAgent;
privatereadonly BackgroundManager _backgroundTaskManager;
private ToolManager.SkillType _skillType;
private List<MessageParam> _conversationHistory;
privatelong _totalInputTokens;
privatelong _totalOutputTokens;
publicevent System.Action<long, long, long> OnTokenUsageUpdated;
publicAgentLoop(AnthropicClient client, string model, string systemPrompt,
ToolManager skillAgent, ToolManager.SkillType skillType = ToolManager.SkillType.Code,
BackgroundManager backgroundTaskManager = null)
{
_client = client;
_model = model;
_systemPrompt = systemPrompt;
_skillAgent = skillAgent;
_skillType = skillType;
_backgroundTaskManager = backgroundTaskManager;
_conversationHistory = new List<MessageParam>();
}
public IReadOnlyList<MessageParam> ConversationHistory => _conversationHistory;
publicvoidClearHistory()
{
_conversationHistory.Clear();
}
publicasync Task<string> ProcessMessageAsync(string userMessage, CancellationToken cancellationToken = default)
{
// 添加用户消息到历史
_conversationHistory.Add(new MessageParam
{
Role = Role.User,
Content = userMessage
});
// 运行代理循环
var result = await RunAgentLoopAsync(cancellationToken);
// 提取最终的文本响应
return ExtractTextResponse(result);
}
privateasync Task<List<ContentBlockParam>> RunAgentLoopAsync(CancellationToken cancellationToken = default)
{
var tools = _skillAgent.GetToolsForSkill(_skillType);
while (true)
{
// 检查取消请求
cancellationToken.ThrowIfCancellationRequested();
// 0. 排空后台任务通知队列,将完成的任务结果注入对话
if (_backgroundTaskManager != null)
{
var notifications = _backgroundTaskManager.DrainNotifications();
if (notifications.Count > 0 && _conversationHistory.Count > 0)
{
var notifText = string.Join("\n",
notifications.Select(n => $"[bg:{n.TaskId}] {n.Status}: {n.Result}"));
// 注入后台结果作为用户消息
_conversationHistory.Add(new MessageParam
{
Role = Role.User,
Content = $"<background-results>\n{notifText}\n</background-results>"
});
// 助手确认
_conversationHistory.Add(new MessageParam
{
Role = Role.Assistant,
Content = "Noted background results."
});
UnityEngine.Debug.Log($"[Background Notifications] Injected {notifications.Count} completed task(s)");
}
}
// 1. 调用模型
var response = await _client.Messages.Create(new MessageCreateParams
{
Model = _model,
Messages = _conversationHistory,
System = _systemPrompt,
Tools = tools.Select(t => (ToolUnion)t).ToList(),
MaxTokens = 4096
});
// 更新 token 使用统计
if (response.Usage != null)
{
_totalInputTokens += response.Usage.InputTokens;
_totalOutputTokens += response.Usage.OutputTokens;
var totalTokens = _totalInputTokens + _totalOutputTokens;
// 触发事件通知UI更新
OnTokenUsageUpdated?.Invoke(_totalInputTokens, _totalOutputTokens, totalTokens);
UnityEngine.Debug.Log($"[Token Usage] Input: {response.Usage.InputTokens}, Output: {response.Usage.OutputTokens}, Total Session: {totalTokens}");
}
// 2. 收集工具调用和文本输出
var toolCalls = new List<ToolUseBlock>();
var textOutput = new List<string>();
foreach (var block in response.Content)
{
if (block.TryPickText(outvar text))
textOutput.Add(text.Text);
if (block.TryPickThinking(outvar thinking))
{
// 记录思考内容(可选显示)
UnityEngine.Debug.Log($"[AI Thinking] {thinking.Thinking}");
}
if (block.TryPickToolUse(outvar toolUse))
toolCalls.Add(toolUse);
}
// 3. 如果没有工具调用,任务完成
if (response.StopReason != StopReason.ToolUse)
{
// 将助手响应添加到历史
var assistantContent = ConvertContentBlocks(response.Content);
_conversationHistory.Add(new MessageParam
{
Role = Role.Assistant,
Content = assistantContent
});
return assistantContent;
}
// 4. 执行每个工具并收集结果
var toolResults = new List<ToolResultBlockParam>();
foreach (var toolCall in toolCalls)
{
var output = await _skillAgent.ExecuteToolAsync(toolCall.Name, toolCall.Input);
toolResults.Add(new ToolResultBlockParam
{
ToolUseID = toolCall.ID,
Content = output
});
}
// 5. 将助手消息和工具结果添加到历史,继续循环
var assistantBlocks = ConvertContentBlocks(response.Content);
_conversationHistory.Add(new MessageParam
{
Role = Role.Assistant,
Content = assistantBlocks
});
_conversationHistory.Add(new MessageParam
{
Role = Role.User,
Content = toolResults.Select(r => (ContentBlockParam)r).ToList()
});
}
}
privatestringExtractTextResponse(List<ContentBlockParam> contentBlocks)
{
var textResponse = new System.Text.StringBuilder();
foreach (var block in contentBlocks)
{
if (block.TryPickText(outvar textBlock))
{
textResponse.AppendLine(textBlock.Text);
}
// 可选:显示思考内容(DeepSeek Reasoner)
// else if (block.TryPickThinking(out var thinkingBlock))
// {
// textResponse.AppendLine($"[Thinking: {thinkingBlock.Thinking}]");
// }
}
return textResponse.ToString().Trim();
}
private List<ContentBlockParam> ConvertContentBlocks(IEnumerable<ContentBlock> contentBlocks)
{
var result = new List<ContentBlockParam>();
foreach (var block in contentBlocks)
{
if (block.TryPickText(outvar textBlock))
{
result.Add(new TextBlockParam { Text = textBlock.Text });
}
elseif (block.TryPickThinking(outvar thinkingBlock))
{
// 处理 thinking 块(DeepSeek Reasoner 需要)
result.Add(new ThinkingBlockParam
{
Signature = thinkingBlock.Signature,
Thinking = thinkingBlock.Thinking
});
}
elseif (block.TryPickToolUse(outvar toolUseBlock))
{
result.Add(new ToolUseBlockParam
{
ID = toolUseBlock.ID,
Name = toolUseBlock.Name,
Input = toolUseBlock.Input
});
}
}
return result;
}
publicvoidSetSystemPrompt(string systemPrompt)
{
// 注意:系统提示在每次API调用时传递,这里只是存储
// 实际使用时需要在RunAgentLoopAsync中传递
}
publicvoidSetSkillType(ToolManager.SkillType skillType)
{
// 技能类型影响可用工具,需要在下次循环中生效
_skillType = skillType;
}
}
}
Bash Agent
Bash 就是一切 – 最简 Agent 的哲学。
构建了各种复杂的 Agent 系统后,我们追问一个根本问题:Agent 的本质是什么?
答案令人惊讶地简单:一个工具 + 一个循环 = 完整的 Agent 能力。
Unix 哲学说”一切皆文件,一切皆可管道”。Bash 是通往这个世界的大门,让我们来实现一个 Bash Agent 吧!
BashAgent.cs
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Linq;
using System.Threading;
namespaceClaudeCodeForUnity
{
publicclassBashAgent
{
privatereadonlystring _workingDirectory;
privatereadonlyint _timeoutSeconds;
publicBashAgent(string workingDirectory, int timeoutSeconds = 60)
{
_workingDirectory = workingDirectory;
_timeoutSeconds = timeoutSeconds;
}
privateasync Task WaitForProcessExitAsync(Process process, CancellationToken cancellationToken)
{
while (!process.HasExited && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
}
if (!process.HasExited)
{
process.Kill();
}
}
publicasync Task<string> ExecuteAsync(string command)
{
// 安全检查 - 阻止危险命令
string[] dangerousCommands = { "rm -rf /", "sudo", "shutdown", "reboot", "> /dev/" };
if (dangerousCommands.Any(d => command.Contains(d)))
return"Error: 危险命令已阻止";
try
{
usingvar process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"{command.Replace("\"", "\\\"")}\"",
WorkingDirectory = _workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
usingvar cts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds));
await WaitForProcessExitAsync(process, cts.Token);
var output = (stdout + stderr).Trim();
returnstring.IsNullOrEmpty(output) ? "(no output)" : SafeSubstring(output, 50000);
}
catch (OperationCanceledException)
{
return$"Error: 命令超时 ({_timeoutSeconds}s)";
}
catch (Exception ex)
{
return$"Error: {ex.Message}";
}
}
publicstringExecute(string command)
{
// 安全检查
string[] dangerousCommands = { "rm -rf /", "sudo", "shutdown", "reboot", "> /dev/" };
if (dangerousCommands.Any(d => command.Contains(d)))
return"Error: 危险命令已阻止";
try
{
usingvar process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"{command.Replace("\"", "\\\"")}\"",
WorkingDirectory = _workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
// 设置超时
if (!process.WaitForExit(_timeoutSeconds * 1000))
{
process.Kill();
return"Error: 命令超时";
}
var output = (stdout + stderr).Trim();
returnstring.IsNullOrEmpty(output) ? "(no output)" : SafeSubstring(output, 50000);
}
catch (Exception ex)
{
return$"Error: {ex.Message}";
}
}
privatestringSafeSubstring(string text, int maxLength)
{
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;
return text.Substring(0, maxLength);
}
}
}
File Agent
模型即代理 – 4 个工具覆盖 90% 场景。
|
|
|
|
bash |
|
|
read_file |
|
|
write_file |
|
|
edit_file |
|
|
因此产生了 File Agent,实现一些文件读写操作。
FileAgent.cs
using System;
using System.IO;
using System.Linq;
namespaceClaudeCodeForUnity
{
publicclassFileAgent
{
privatereadonlystring _workingDirectory;
publicFileAgent(string workingDirectory)
{
_workingDirectory = workingDirectory;
}
publicstringReadFile(string relativePath, int? limit = null)
{
try
{
var fullPath = GetSafePath(relativePath);
var lines = File.ReadAllLines(fullPath);
if (limit.HasValue && limit.Value < lines.Length)
{
lines = lines.Take(limit.Value)
.Concat(new[] { $"... ({lines.Length - limit.Value} more lines)" })
.ToArray();
}
var result = string.Join("\n", lines);
return SafeSubstring(result, 50000);
}
catch (Exception ex)
{
return$"Error: {ex.Message}";
}
}
publicstringWriteFile(string relativePath, string content)
{
try
{
var fullPath = GetSafePath(relativePath);
var dir = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
File.WriteAllText(fullPath, content);
return$"Wrote {content.Length} bytes to {relativePath}";
}
catch (Exception ex)
{
return$"Error: {ex.Message}";
}
}
publicstringEditFile(string relativePath, string oldText, string newText)
{
try
{
var fullPath = GetSafePath(relativePath);
var fileContent = File.ReadAllText(fullPath);
if (!fileContent.Contains(oldText))
return$"Error: 在 {relativePath} 中未找到文本";
// 只替换第一次出现,保证安全
var index = fileContent.IndexOf(oldText);
if (index == -1)
return$"Error: 在 {relativePath} 中未找到文本";
var newContent = fileContent.Substring(0, index) +
newText +
fileContent.Substring(index + oldText.Length);
File.WriteAllText(fullPath, newContent);
return$"Edited {relativePath}";
}
catch (Exception ex)
{
return$"Error: {ex.Message}";
}
}
publicboolFileExists(string relativePath)
{
try
{
var fullPath = GetSafePath(relativePath);
return File.Exists(fullPath);
}
catch
{
returnfalse;
}
}
///<exception cref="InvalidOperationException">路径逃逸工作目录</exception>
privatestringGetSafePath(string relativePath)
{
var fullPath = Path.GetFullPath(Path.Combine(_workingDirectory, relativePath));
if (!fullPath.StartsWith(_workingDirectory))
thrownew InvalidOperationException($"路径逃逸工作区: {relativePath}");
return fullPath;
}
privatestringSafeSubstring(string text, int maxLength)
{
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;
return text.Substring(0, maxLength);
}
}
}
Todo Manager
当下的 AI 编辑器,都提供了规划计划的能力,OK~ 接下来我们也要实现此功能了。
TodoManager.cs
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
namespaceClaudeCodeForUnity
{
publicclassTodoManager
{
private List<TodoItem> _items = new List<TodoItem>();
publicevent System.Action OnTodoListUpdated;
publicstringUpdate(JsonElement itemsJson)
{
var validated = new List<TodoItem>();
var inProgressCount = 0;
foreach (var item in itemsJson.EnumerateArray())
{
var content = item.TryGetProperty("content", outvar c) ? c.GetString()?.Trim() ?? "" : "";
var status = item.TryGetProperty("status", outvar s) ? s.GetString()?.ToLower() ?? "pending" : "pending";
var activeForm = item.TryGetProperty("activeForm", outvar a) ? a.GetString()?.Trim() ?? "" : "";
if (string.IsNullOrEmpty(content))
thrownew System.InvalidOperationException($"Item: content required");
if (status isnot ("pending"or"in_progress"or"completed"))
thrownew System.InvalidOperationException($"Item: invalid status '{status}'");
// 如果 activeForm 为空,使用 content 作为默认值
if (string.IsNullOrEmpty(activeForm))
activeForm = content;
if (status == "in_progress")
inProgressCount++;
validated.Add(new TodoItem(content, status, activeForm));
}
if (validated.Count > 20)
thrownew System.InvalidOperationException("Max 20 todos allowed");
if (inProgressCount > 1)
thrownew System.InvalidOperationException("Only one task can be in_progress at a time");
_items = validated;
// 触发更新事件
OnTodoListUpdated?.Invoke();
return Render();
}
publicstringRender()
{
if (_items.Count == 0)
return"No todos.";
var sb = new StringBuilder();
foreach (var item in _items)
{
var mark = item.Status switch
{
"completed" => "[x]",
"in_progress" => "[>]",
_ => "[ ]"
};
if (item.Status == "in_progress")
sb.AppendLine($"{mark}{item.Content} <- {item.ActiveForm}");
else
sb.AppendLine($"{mark}{item.Content}");
}
var completed = _items.Count(t => t.Status == "completed");
sb.AppendLine($"\n({completed}/{_items.Count} completed)");
return sb.ToString();
}
publicvoidClear()
{
_items.Clear();
OnTodoListUpdated?.Invoke();
}
public IReadOnlyList<TodoItem> Items => _items;
}
publicclassTodoItem
{
publicstring Content { get; }
publicstring Status { get; }
publicstring ActiveForm { get; }
publicTodoItem(string content, string status, string activeForm)
{
Content = content;
Status = status;
ActiveForm = activeForm;
}
}
}
Sub Agent
有了计划功能的加入,我们要设计对应的三个子代理模式了,它们分其责,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SubAgentManager.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
namespaceClaudeCodeForUnity
{
publicclassSubAgentManager
{
privatereadonlystring _workingDirectory;
privatereadonly AnthropicClient _client;
privatereadonlystring _model;
publicclassSubAgentConfig
{
publicstring Name { get; set; }
publicstring Description { get; set; }
publicstring[] AllowedTools { get; set; }
publicstring SystemPrompt { get; set; }
publicSubAgentConfig(string name, string description, string[] allowedTools, string systemPrompt)
{
Name = name;
Description = description;
AllowedTools = allowedTools;
SystemPrompt = systemPrompt;
}
}
privatereadonly Dictionary<string, SubAgentConfig> _agentConfigs = new()
{
["explore"] = new SubAgentConfig(
"explore",
"只读代理,用于探索代码、查找文件、搜索",
new[] { "bash", "read_file" },
"你是一个探索代理。搜索和分析,但不要修改文件。返回简洁的摘要。"
),
["code"] = new SubAgentConfig(
"code",
"完整代理,用于实现功能和修复bug",
new[] { "*" }, // 所有工具
"你是一个编程代理。高效地实现请求的更改。"
),
["plan"] = new SubAgentConfig(
"plan",
"规划代理,用于设计实现策略",
new[] { "bash", "read_file" },
"你是一个规划代理。分析代码库并输出编号的实现计划。不要做更改。"
)
};
publicSubAgentManager(string workingDirectory, AnthropicClient client, string model)
{
_workingDirectory = workingDirectory;
_client = client;
_model = model;
}
publicasync Task<string> ExecuteSubAgentAsync(string description, string prompt, string agentType,
List<Tool> availableTools)
{
if (!_agentConfigs.TryGetValue(agentType, outvar config))
return$"Error: 未知agent类型 '{agentType}'";
// 过滤工具列表
var filteredTools = FilterTools(availableTools, config.AllowedTools);
// 创建子代理系统提示
var subSystemPrompt = $@"你是一个位于 {_workingDirectory} 的 {agentType} 子代理。
{config.SystemPrompt}
完成任务并返回清晰简洁的摘要。";
// 创建独立的对话历史
var subHistory = new List<MessageParam>
{
new MessageParam { Role = Role.User, Content = prompt }
};
var startTime = DateTime.Now;
var toolCount = 0;
// 运行子代理循环
while (true)
{
var response = await _client.Messages.Create(new MessageCreateParams
{
Model = _model,
Messages = subHistory,
System = subSystemPrompt,
Tools = filteredTools.Select(t => (ToolUnion)t).ToList(),
MaxTokens = 4096
});
// 如果没有工具调用,返回最终文本
if (response.StopReason != StopReason.ToolUse)
{
var elapsed = (DateTime.Now - startTime).TotalSeconds;
return FormatResult(response, toolCount, elapsed);
}
// 执行工具调用
var toolCalls = response.Content
.Where(c => c.TryPickToolUse(out _))
.Select(c => { c.TryPickToolUse(outvar tu); return tu!; })
.ToList();
var toolResults = new List<ToolResultBlockParam>();
foreach (var toolCall in toolCalls)
{
toolCount++;
// 注意:这里需要实际的工具执行逻辑
// 由于工具执行依赖于主代理的ToolManager,这里返回模拟结果
var output = $"[子代理工具执行: {toolCall.Name}]";
toolResults.Add(new ToolResultBlockParam
{
ToolUseID = toolCall.ID,
Content = output
});
}
// 更新对话历史
var assistantBlocks = response.Content.Select<ContentBlock, ContentBlockParam>(c =>
{
if (c.TryPickText(outvar t)) returnnew TextBlockParam { Text = t.Text };
if (c.TryPickThinking(outvar thinking)) returnnew ThinkingBlockParam
{
Signature = thinking.Signature,
Thinking = thinking.Thinking
};
if (c.TryPickToolUse(outvar tu)) returnnew ToolUseBlockParam
{
ID = tu.ID,
Name = tu.Name,
Input = tu.Input
};
thrownew InvalidOperationException("Unknown content block type");
}).ToList();
subHistory.Add(new MessageParam { Role = Role.Assistant, Content = assistantBlocks });
subHistory.Add(new MessageParam
{
Role = Role.User,
Content = toolResults.Select(r => (ContentBlockParam)r).ToList()
});
}
}
private List<Tool> FilterTools(List<Tool> allTools, string[] allowedTools)
{
if (allowedTools.Contains("*"))
return allTools;
return allTools.Where(t => allowedTools.Contains(t.Name)).ToList();
}
privatestringFormatResult(Message response, int toolCount, double elapsedSeconds)
{
var textBlocks = response.Content
.Where(c => c.TryPickText(out _))
.Select(c => { c.TryPickText(outvar t); return t!.Text; })
.ToList();
var result = string.Join("", textBlocks);
return$"[子代理完成] 工具调用: {toolCount}次, 耗时: {elapsedSeconds:F1}秒\n\n{result}";
}
public Dictionary<string, string> GetAgentDescriptions()
{
return _agentConfigs.ToDictionary(
kv => kv.Key,
kv => kv.Value.Description
);
}
publicboolAgentTypeExists(string agentType)
{
return _agentConfigs.ContainsKey(agentType);
}
publicvoidAddAgentConfig(string name, string description, string[] allowedTools, string systemPrompt)
{
_agentConfigs[name] = new SubAgentConfig(name, description, allowedTools, systemPrompt);
}
}
}
Skill Agent
Agent Skills 确实是最近AI领域最火爆的概念之一。也是 OpenClaw 的“灵魂”与“工具箱”。
工具是能力,技能是知识。Skills 机制便是知识外部化。
SKILL.md 标准
skills/
├── pdf/
│ └── SKILL.md # 必需:YAML frontmatter + Markdown
├── mcp-builder/
│ ├── SKILL.md
│ └── references/ # 可选:参考文档
└── code-review/
├── SKILL.md
└── scripts/ # 可选:辅助脚本
SKILL.md 格式
---
name: my-skill
description: 简短描述,用于触发判断。
---
# My Skill
## 什么时候使用
当用户需要...
## 如何使用
### 模式 1: ...
```bash
example command
具体细节这里就不做讲解,网上很多现成的 Skill ,直接拿来主义即可。
SkillLoader.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespaceClaudeCodeForUnity
{
publicclassSkillLoader
{
publicclassSkill
{
publicstring Name { get; }
publicstring Description { get; }
publicstring Body { get; }
publicstring Path { get; }
publicstring Directory { get; }
publicSkill(string name, string description, string body, string path, string directory)
{
Name = name;
Description = description;
Body = body;
Path = path;
Directory = directory;
}
}
privatereadonlystring _skillsDirectory;
privatereadonly Dictionary<string, Skill> _skills = new Dictionary<string, Skill>();
publicSkillLoader(string skillsDirectory)
{
_skillsDirectory = skillsDirectory;
LoadSkills();
}
private Skill ParseSkillMd(string path)
{
var content = File.ReadAllText(path);
// 匹配 YAML frontmatter: ---\n...\n---\n
var match = Regex.Match(content, @"^---\s*\n(.*?)\n---\s*\n(.*)$",
RegexOptions.Singleline);
if (!match.Success)
{
UnityEngine.Debug.LogWarning($"[SkillLoader] Invalid SKILL.md format: {path}");
returnnull;
}
var frontmatter = match.Groups[1].Value;
var body = match.Groups[2].Value.Trim();
// 解析 YAML frontmatter
var metadata = new Dictionary<string, string>();
foreach (var line in frontmatter.Split('\n'))
{
var colonIndex = line.IndexOf(':');
if (colonIndex > 0)
{
var key = line.Substring(0, colonIndex).Trim();
varvalue = line.Substring(colonIndex + 1).Trim().Trim('"', '\'');
metadata[key] = value;
}
}
if (!metadata.TryGetValue("name", outvar name) ||
!metadata.TryGetValue("description", outvar description))
{
UnityEngine.Debug.LogWarning($"[SkillLoader] Missing name or description in: {path}");
returnnull;
}
var directory = System.IO.Path.GetDirectoryName(path);
returnnew Skill(name, description, body, path, directory);
}
privatevoidLoadSkills()
{
if (!System.IO.Directory.Exists(_skillsDirectory))
{
UnityEngine.Debug.LogWarning($"[SkillLoader] Skills directory not found: {_skillsDirectory}");
return;
}
var skillDirs = System.IO.Directory.GetDirectories(_skillsDirectory);
foreach (var skillDir in skillDirs)
{
var skillMdPath = System.IO.Path.Combine(skillDir, "SKILL.md");
if (!File.Exists(skillMdPath))
continue;
var skill = ParseSkillMd(skillMdPath);
if (skill != null)
{
_skills[skill.Name] = skill;
UnityEngine.Debug.Log($"[SkillLoader] Loaded skill: {skill.Name} - {skill.Description}");
}
}
UnityEngine.Debug.Log($"[SkillLoader] Total skills loaded: {_skills.Count}");
}
publicstringGetDescriptions()
{
if (_skills.Count == 0)
return"(no skills available)";
var descriptions = _skills.Select(kv => $"- {kv.Key}: {kv.Value.Description}");
returnstring.Join("\n", descriptions);
}
publicstringGetSkillContent(string name)
{
if (!_skills.TryGetValue(name, outvar skill))
returnnull;
var content = $"# Skill: {skill.Name}\n\n{skill.Body}";
// 检查可用资源
var resources = new List<string>();
var resourceFolders = new[]
{
("scripts", "Scripts"),
("references", "References"),
("assets", "Assets")
};
foreach (var (folder, label) in resourceFolders)
{
var folderPath = System.IO.Path.Combine(skill.Directory, folder);
if (System.IO.Directory.Exists(folderPath))
{
var files = System.IO.Directory.GetFiles(folderPath)
.Select(System.IO.Path.GetFileName);
if (files.Any())
resources.Add($"{label}: {string.Join(", ", files)}");
}
}
// 添加资源信息
if (resources.Count > 0)
{
content += $"\n\n**Available resources in {skill.Directory}:**\n";
content += string.Join("\n", resources.Select(r => $"- {r}"));
}
return content;
}
public IEnumerable<string> ListSkills()
{
return _skills.Keys;
}
publicint SkillCount => _skills.Count;
publicboolHasSkill(string name)
{
return _skills.ContainsKey(name);
}
}
}
SkillAgent.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespaceClaudeCodeForUnity
{
publicclassSkillAgent
{
privatereadonly SkillLoader _skillLoader;
publicSkillAgent(SkillLoader skillLoader)
{
_skillLoader = skillLoader ?? thrownew ArgumentNullException(nameof(skillLoader));
}
publicstringGetSkillDescriptions()
{
return _skillLoader.GetDescriptions();
}
public IEnumerable<string> ListSkills()
{
return _skillLoader.ListSkills();
}
publicboolHasSkill(string skillName)
{
return _skillLoader.HasSkill(skillName);
}
publicstringLoadSkill(string skillName)
{
if (string.IsNullOrEmpty(skillName))
{
return"Error: skill name cannot be empty";
}
var content = _skillLoader.GetSkillContent(skillName);
if (content == null)
{
var available = string.Join(", ", ListSkills());
return$"Error: Unknown skill '{skillName}'. Available: {(string.IsNullOrEmpty(available) ? "none" : available)}";
}
// 关键机制:以 tool_result 形式注入,不修改系统提示(保持 prompt cache)
return$@"<skill-loaded name=""{skillName}"">
{content}
</skill-loaded>
按照上面技能中的指令完成用户的任务。";
}
publicint SkillCount => _skillLoader.SkillCount;
public SkillLoader SkillLoader => _skillLoader;
}
}
Task Manager
任务管理实际要解决的是上下文压缩和记忆功能。
上下文窗口有限,但智能体会话可以无限。
三层压缩在不同粒度上解决这个问题:
-
• MicroCompact(替换旧工具输出)- 静默替换 -
• AutoCompact(接近限制时 LLM 摘要)- 阈值触发 -
• Manual Compact(用户触发)- 手动触发
分层方法让每一层在各自的粒度上独立运作,从静默的逐轮清理到完整的对话重置。
有了压缩功能,要面对另一个问题,当对话被压缩时,所有的任务状态都丢失了。
这里使用文件系统是最简单的持久化方案:无需数据库,无需序列化框架,JSON + 文件目录就是一个功能完整的任务看板。
TaskManager.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
namespaceClaudeCodeForUnity
{
publicclassTaskManager
{
privatereadonly AnthropicClient _client;
privatereadonlystring _model;
privatereadonly TodoManager _todoManager;
privatereadonlystring _workDir;
privatereadonlystring _transcriptDir;
privatereadonlystring _tasksDir;
// 压缩参数
privateconstint TOKEN_THRESHOLD = 50000; // Token 阈值
privateconstint KEEP_RECENT = 3; // 保留最近 N 个工具结果
// 工具结果追踪器: 记录哪些消息包含 tool_result 及其 ID 和工具名
privatereadonly List<(int msgIdx, List<(string toolUseId, string toolName)> tools)> _toolResultTracker
= new List<(int, List<(string, string)>)>();
// 任务 ID 计数器
privateint _nextTaskId;
publicevent Action<string, string> OnCompactCompleted;
publicevent Action<int> OnTokenEstimateUpdated;
publicevent Action<int, string> OnTaskCreated;
publicevent Action<int, string> OnTaskUpdated;
publicTaskManager(AnthropicClient client, string model, TodoManager todoManager, string workDir)
{
_client = client ?? thrownew ArgumentNullException(nameof(client));
_model = model ?? thrownew ArgumentNullException(nameof(model));
_todoManager = todoManager ?? thrownew ArgumentNullException(nameof(todoManager));
_workDir = workDir ?? Directory.GetCurrentDirectory();
_transcriptDir = Path.Combine(_workDir, ".transcripts");
_tasksDir = Path.Combine(_workDir, ".tasks");
// 初始化任务系统
Directory.CreateDirectory(_tasksDir);
_nextTaskId = GetMaxTaskId() + 1;
UnityEngine.Debug.Log($"[TaskManager] Initialized with tasks directory: {_tasksDir}");
UnityEngine.Debug.Log($"[TaskManager] Next task ID: {_nextTaskId}");
}
privatestatic JsonElement SerializeToElement<T>(T value)
{
var json = JsonSerializer.Serialize(value);
usingvar doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
publicintEstimateTokens(List<MessageParam> messages)
{
var json = JsonSerializer.Serialize(messages);
var estimate = json.Length / 4;
OnTokenEstimateUpdated?.Invoke(estimate);
return estimate;
}
publicvoidMicroCompact(List<MessageParam> messages)
{
if (_toolResultTracker.Count <= KEEP_RECENT)
return;
var toClear = _toolResultTracker.Take(_toolResultTracker.Count - KEEP_RECENT).ToList();
foreach (var (msgIdx, tools) in toClear)
{
if (msgIdx >= messages.Count)
continue;
// 用包含占位符内容的新 ToolResultBlockParam 替换整条消息
var newBlocks = tools.Select(t => (ContentBlockParam)new ToolResultBlockParam
{
ToolUseID = t.toolUseId,
Content = $"[Previous: used {t.toolName}]"
}).ToList();
messages[msgIdx] = new MessageParam
{
Role = Role.User,
Content = newBlocks
};
}
UnityEngine.Debug.Log($"[MicroCompact] Compressed {toClear.Count} old tool results");
}
publicasync Task<string> AutoCompactAsync(List<MessageParam> messages)
{
// 1. 保存完整记录到磁盘
Directory.CreateDirectory(_transcriptDir);
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var transcriptPath = Path.Combine(_transcriptDir, $"transcript_{timestamp}.jsonl");
var lines = messages.Select(m => JsonSerializer.Serialize(m));
await File.WriteAllLinesAsync(transcriptPath, lines);
UnityEngine.Debug.Log($"[AutoCompact] Transcript saved: {transcriptPath}");
// 2. 让 LLM 总结对话
var conversationText = JsonSerializer.Serialize(messages);
if (conversationText.Length > 80000)
conversationText = conversationText.Substring(0, 80000);
var summaryResponse = await _client.Messages.Create(new MessageCreateParams
{
Model = _model,
Messages = new List<MessageParam>
{
new MessageParam
{
Role = Role.User,
Content = "Summarize this conversation for continuity. Include: " +
"1) What was accomplished, 2) Current state, 3) Key decisions made. " +
"Be concise but preserve critical details.\n\n" + conversationText
}
},
MaxTokens = 2000
});
var summary = string.Join("", summaryResponse.Content
.Where(c => c.TryPickText(out _))
.Select(c => { c.TryPickText(outvar t); return t!.Text; }));
// 3. 替换所有消息为压缩摘要
messages.Clear();
_toolResultTracker.Clear();
messages.Add(new MessageParam
{
Role = Role.User,
Content = $"[Conversation compressed. Transcript: {transcriptPath}]\n\n{summary}"
});
messages.Add(new MessageParam
{
Role = Role.Assistant,
Content = "Understood. I have the context from the summary. Continuing."
});
// 4. 清除 TodoManager 的任务列表 - 这是关键的记忆清除
_todoManager.Clear();
UnityEngine.Debug.Log("[AutoCompact] TodoManager cleared - memory reset");
// 5. 触发压缩完成事件
OnCompactCompleted?.Invoke(transcriptPath, summary);
return summary;
}
publicasync Task<string> ManualCompactAsync(List<MessageParam> messages, string focus = "")
{
UnityEngine.Debug.Log($"[ManualCompact] Triggered{(string.IsNullOrEmpty(focus) ? "" : $" with focus: {focus}")}");
returnawait AutoCompactAsync(messages);
}
publicboolShouldAutoCompact(List<MessageParam> messages)
{
var tokens = EstimateTokens(messages);
return tokens > TOKEN_THRESHOLD;
}
publicvoidTrackToolResult(int msgIdx, List<(string toolUseId, string toolName)> toolInfos)
{
_toolResultTracker.Add((msgIdx, toolInfos));
}
publicint TrackedResultCount => _toolResultTracker.Count;
publicvoidClearTrackers()
{
_toolResultTracker.Clear();
}
publicstring[] GetTranscripts()
{
if (!Directory.Exists(_transcriptDir))
return Array.Empty<string>();
return Directory.GetFiles(_transcriptDir, "transcript_*.jsonl")
.OrderByDescending(f => f)
.ToArray();
}
publicintCleanupOldTranscripts(int keepCount = 10)
{
var transcripts = GetTranscripts();
if (transcripts.Length <= keepCount)
return0;
var toDelete = transcripts.Skip(keepCount).ToArray();
foreach (varfilein toDelete)
{
try
{
File.Delete(file);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Failed to delete transcript {file}: {ex.Message}");
}
}
UnityEngine.Debug.Log($"[CleanupTranscripts] Deleted {toDelete.Length} old transcripts");
return toDelete.Length;
}
#region v6 持久化任务系统
privateintGetMaxTaskId()
{
var files = Directory.GetFiles(_tasksDir, "task_*.json");
if (files.Length == 0)
return0;
return files
.Select(f =>
{
var fileName = Path.GetFileNameWithoutExtension(f);
var parts = fileName.Split('_');
return parts.Length >= 2 && int.TryParse(parts[1], outvar id) ? id : 0;
})
.DefaultIfEmpty(0)
.Max();
}
private Dictionary<string, JsonElement> LoadTask(int taskId)
{
var path = Path.Combine(_tasksDir, $"task_{taskId}.json");
if (!File.Exists(path))
thrownew InvalidOperationException($"Task {taskId} not found");
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
}
privatevoidSaveTask(Dictionary<string, JsonElement> task)
{
var taskId = task["id"].GetInt32();
var path = Path.Combine(_tasksDir, $"task_{taskId}.json");
var json = JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}
publicstringCreateTask(string subject, string description = "", int? sessionId = null)
{
var task = new Dictionary<string, JsonElement>
{
["id"] = SerializeToElement(_nextTaskId),
["subject"] = SerializeToElement(subject),
["description"] = SerializeToElement(description),
["status"] = SerializeToElement("pending"),
["blockedBy"] = SerializeToElement(new List<int>()),
["blocks"] = SerializeToElement(new List<int>()),
["owner"] = SerializeToElement(""),
["sessionId"] = SerializeToElement(sessionId ?? 0),
["createdAt"] = SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds())
};
SaveTask(task);
UnityEngine.Debug.Log($"[TaskCreate] #{_nextTaskId}: {subject} (Session: {sessionId ?? 0})");
OnTaskCreated?.Invoke(_nextTaskId, subject);
_nextTaskId++;
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
}
publicstringUpdateTask(int taskId, string status = null, List<int> addBlockedBy = null, List<int> addBlocks = null)
{
var task = LoadTask(taskId);
// 更新状态
if (!string.IsNullOrEmpty(status))
{
if (status != "pending" && status != "in_progress" && status != "completed")
thrownew InvalidOperationException($"Invalid status: {status}");
task["status"] = SerializeToElement(status);
UnityEngine.Debug.Log($"[TaskUpdate] #{taskId} -> {status}");
// 如果任务完成,清除依赖关系
if (status == "completed")
ClearDependency(taskId);
OnTaskUpdated?.Invoke(taskId, status);
}
// 添加 blockedBy 依赖
if (addBlockedBy != null && addBlockedBy.Count > 0)
{
var current = GetIntList(task, "blockedBy");
current.AddRange(addBlockedBy);
task["blockedBy"] = SerializeToElement(current.Distinct().ToList());
}
// 添加 blocks 依赖
if (addBlocks != null && addBlocks.Count > 0)
{
var current = GetIntList(task, "blocks");
current.AddRange(addBlocks);
task["blocks"] = SerializeToElement(current.Distinct().ToList());
// 反向更新:将当前任务添加到被阻塞任务的 blockedBy 列表
foreach (var blockedId in addBlocks)
{
try
{
var blocked = LoadTask(blockedId);
var blockedBy = GetIntList(blocked, "blockedBy");
if (!blockedBy.Contains(taskId))
{
blockedBy.Add(taskId);
blocked["blockedBy"] = SerializeToElement(blockedBy);
SaveTask(blocked);
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Failed to update blocked task {blockedId}: {ex.Message}");
}
}
}
task["updatedAt"] = SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
SaveTask(task);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
}
privatevoidClearDependency(int completedId)
{
var files = Directory.GetFiles(_tasksDir, "task_*.json");
foreach (varfilein files)
{
try
{
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
var blockedBy = GetIntList(task, "blockedBy");
if (blockedBy.Remove(completedId))
{
task["blockedBy"] = SerializeToElement(blockedBy);
SaveTask(task);
UnityEngine.Debug.Log($"[ClearDependency] Removed #{completedId} from task #{task["id"].GetInt32()}");
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Failed to process task file {file}: {ex.Message}");
}
}
}
publicstringGetTask(int taskId)
{
var task = LoadTask(taskId);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
}
publicstringListAllTasks(int? sessionId = null)
{
var files = Directory.GetFiles(_tasksDir, "task_*.json").OrderBy(f => f).ToArray();
if (files.Length == 0)
return"No tasks.";
var lines = new List<string>();
foreach (varfilein files)
{
try
{
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
// 如果指定了会话 ID,则过滤任务
if (sessionId.HasValue)
{
var taskSessionId = task.TryGetValue("sessionId", outvar sid) ? sid.GetInt32() : 0;
if (taskSessionId != sessionId.Value)
continue;
}
var taskId = task["id"].GetInt32();
var subject = task["subject"].GetString();
var status = task["status"].GetString();
var blockedBy = GetIntList(task, "blockedBy");
var marker = status switch
{
"pending" => "[ ]",
"in_progress" => "[>]",
"completed" => "[x]",
_ => "[?]"
};
var blockedInfo = blockedBy.Count > 0
? $" (blocked by: [{string.Join(", ", blockedBy)}])"
: "";
lines.Add($"{marker} #{taskId}: {subject}{blockedInfo}");
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Failed to read task file {file}: {ex.Message}");
}
}
return lines.Count > 0 ? string.Join("\n", lines) : "No tasks.";
}
privatestaticList<int> GetIntList(Dictionary<string, JsonElement> dict, string key)
{
if (!dict.TryGetValue(key, outvar element))
returnnew List<int>();
if (element.ValueKind == JsonValueKind.Array)
{
return element.EnumerateArray()
.Where(e => e.ValueKind == JsonValueKind.Number)
.Select(e => e.GetInt32())
.ToList();
}
returnnew List<int>();
}
publicvoidDeleteTask(int taskId)
{
var path = Path.Combine(_tasksDir, $"task_{taskId}.json");
if (File.Exists(path))
{
File.Delete(path);
UnityEngine.Debug.Log($"[TaskDelete] Deleted task #{taskId}");
}
}
publicintCleanupCompletedTasks()
{
var files = Directory.GetFiles(_tasksDir, "task_*.json");
var deletedCount = 0;
foreach (varfilein files)
{
try
{
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
if (task["status"].GetString() == "completed")
{
File.Delete(file);
deletedCount++;
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Failed to process task file {file}: {ex.Message}");
}
}
if (deletedCount > 0)
UnityEngine.Debug.Log($"[CleanupTasks] Deleted {deletedCount} completed tasks");
return deletedCount;
}
publicstringGetTaskStats()
{
var files = Directory.GetFiles(_tasksDir, "task_*.json");
if (files.Length == 0)
return"No tasks.";
var pending = 0;
var inProgress = 0;
var completed = 0;
foreach (varfilein files)
{
try
{
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
var status = task["status"].GetString();
switch (status)
{
case"pending": pending++; break;
case"in_progress": inProgress++; break;
case"completed": completed++; break;
}
}
catch { }
}
return$"Tasks: {files.Length} total | {pending} pending | {inProgress} in progress | {completed} completed";
}
#endregion
}
}
Session Manager
有了任务后,这里我们添加会话管理,这样可以处理多个会话功能:
SessionManager.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Anthropic.Models.Messages;
namespaceClaudeCodeForUnity
{
publicclassSessionManager
{
privatereadonlystring _sessionsDir;
privateint _nextSessionId;
private Session _currentSession;
publicevent Action<int, string> OnSessionCreated;
publicevent Action<int, string> OnSessionSwitched;
publicevent Action<int> OnSessionUpdated;
publicevent Action<int, int, string> OnTaskCreated;
publicevent Action<int, int, string> OnTaskUpdated;
publicSessionManager(string workDir)
{
_sessionsDir = Path.Combine(workDir, ".sessions");
Directory.CreateDirectory(_sessionsDir);
_nextSessionId = GetMaxSessionId() + 1;
// 加载或创建默认会话
var existingSessions = GetAllSessions();
if (existingSessions.Count > 0)
{
// 加载最近使用的会话
_currentSession = existingSessions.OrderByDescending(s => s.UpdatedAt).First();
UnityEngine.Debug.Log($"[SessionManager] Loaded session #{_currentSession.Id}: {_currentSession.Name}");
}
else
{
// 创建默认会话
_currentSession = CreateSessionInternal("Default Session");
UnityEngine.Debug.Log($"[SessionManager] Created default session #{_currentSession.Id}");
}
}
public Session CurrentSession => _currentSession;
public Session CreateSession(string name = null)
{
if (string.IsNullOrWhiteSpace(name))
name = $"Session {DateTime.Now:yyyy-MM-dd HH:mm}";
var session = CreateSessionInternal(name);
OnSessionCreated?.Invoke(session.Id, session.Name);
return session;
}
private Session CreateSessionInternal(string name)
{
var session = new Session
{
Id = _nextSessionId++,
Name = name,
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
// 创建会话目录
var sessionDir = GetSessionDirectory(session.Id);
Directory.CreateDirectory(sessionDir);
// 保存会话元数据
SaveSession(session);
// 初始化空任务列表
var tasksPath = GetTasksPath(session.Id);
File.WriteAllText(tasksPath, "[]");
// 初始化空历史文件
var historyPath = GetHistoryPath(session.Id);
File.WriteAllText(historyPath, "");
UnityEngine.Debug.Log($"[SessionManager] Session directory created: {sessionDir}");
return session;
}
publicboolSwitchToSession(int sessionId)
{
var session = LoadSession(sessionId);
if (session == null)
returnfalse;
_currentSession = session;
UnityEngine.Debug.Log($"[SessionManager] Switched to session #{sessionId}: {session.Name}");
OnSessionSwitched?.Invoke(sessionId, session.Name);
returntrue;
}
publicvoidRenameCurrentSession(string newName)
{
if (_currentSession == null) return;
_currentSession.Name = newName;
_currentSession.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
SaveSession(_currentSession);
OnSessionUpdated?.Invoke(_currentSession.Id);
}
publicboolDeleteSession(int sessionId)
{
if (_currentSession != null && _currentSession.Id == sessionId)
{
UnityEngine.Debug.LogWarning($"[SessionManager] Cannot delete current session #{sessionId}");
returnfalse;
}
var sessionDir = GetSessionDirectory(sessionId);
try
{
// 删除整个会话目录(包含所有文件)
if (Directory.Exists(sessionDir))
{
Directory.Delete(sessionDir, true); // recursive = true
UnityEngine.Debug.Log($"[SessionManager] Deleted session directory: {sessionDir}");
}
returntrue;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[SessionManager] Failed to delete session #{sessionId}: {ex.Message}");
returnfalse;
}
}
publicvoidSaveConversationHistory(List<MessageParam> messages)
{
if (_currentSession == null) return;
var historyPath = GetHistoryPath(_currentSession.Id);
var lines = messages.Select(m => JsonSerializer.Serialize(m));
File.WriteAllLines(historyPath, lines);
_currentSession.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
SaveSession(_currentSession);
}
public List<MessageParam> LoadConversationHistory()
{
if (_currentSession == null)
returnnew List<MessageParam>();
var historyPath = GetHistoryPath(_currentSession.Id);
if (!File.Exists(historyPath))
returnnew List<MessageParam>();
try
{
var lines = File.ReadAllLines(historyPath);
return lines
.Where(line => !string.IsNullOrWhiteSpace(line))
.Select(line => JsonSerializer.Deserialize<MessageParam>(line))
.ToList();
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[SessionManager] Failed to load conversation history: {ex.Message}");
returnnew List<MessageParam>();
}
}
public List<Session> GetAllSessions()
{
var sessions = new List<Session>();
var sessionDirs = Directory.GetDirectories(_sessionsDir, "session_*");
foreach (var sessionDir in sessionDirs)
{
try
{
var sessionFile = Path.Combine(sessionDir, "session.json");
if (File.Exists(sessionFile))
{
var json = File.ReadAllText(sessionFile);
var session = JsonSerializer.Deserialize<Session>(json);
if (session != null)
sessions.Add(session);
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[SessionManager] Failed to load session from {sessionDir}: {ex.Message}");
}
}
return sessions.OrderByDescending(s => s.UpdatedAt).ToList();
}
publicstringGetCurrentSessionTaskSummary()
{
if (_currentSession == null)
return"No active session";
var tasks = LoadCurrentSessionTasks();
return$"Session: {_currentSession.Name} | Tasks: {tasks.Count}";
}
#region 私有方法
privateintGetMaxSessionId()
{
var sessionDirs = Directory.GetDirectories(_sessionsDir, "session_*");
if (sessionDirs.Length == 0)
return0;
return sessionDirs
.Select(dir =>
{
var dirName = Path.GetFileName(dir);
var parts = dirName.Split('_');
return parts.Length >= 2 && int.TryParse(parts[1], outvar id) ? id : 0;
})
.DefaultIfEmpty(0)
.Max();
}
privatestringGetSessionDirectory(int sessionId)
{
return Path.Combine(_sessionsDir, $"session_{sessionId}");
}
privatestringGetSessionPath(int sessionId)
{
return Path.Combine(GetSessionDirectory(sessionId), "session.json");
}
privatestringGetHistoryPath(int sessionId)
{
return Path.Combine(GetSessionDirectory(sessionId), "history.jsonl");
}
privatestringGetTasksPath(int sessionId)
{
return Path.Combine(GetSessionDirectory(sessionId), "tasks.json");
}
privatevoidSaveSession(Session session)
{
// 确保会话目录存在
var sessionDir = GetSessionDirectory(session.Id);
Directory.CreateDirectory(sessionDir);
// 保存会话元数据
var path = GetSessionPath(session.Id);
var json = JsonSerializer.Serialize(session, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}
private Session LoadSession(int sessionId)
{
var path = GetSessionPath(sessionId);
if (!File.Exists(path))
returnnull;
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Session>(json);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[SessionManager] Failed to load session #{sessionId}: {ex.Message}");
returnnull;
}
}
#region 任务管理
private List<SessionTask> LoadCurrentSessionTasks()
{
if (_currentSession == null)
returnnew List<SessionTask>();
var tasksPath = GetTasksPath(_currentSession.Id);
if (!File.Exists(tasksPath))
returnnew List<SessionTask>();
try
{
var json = File.ReadAllText(tasksPath);
return JsonSerializer.Deserialize<List<SessionTask>>(json) ?? new List<SessionTask>();
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[SessionManager] Failed to load tasks: {ex.Message}");
returnnew List<SessionTask>();
}
}
privatevoidSaveCurrentSessionTasks(List<SessionTask> tasks)
{
if (_currentSession == null) return;
var tasksPath = GetTasksPath(_currentSession.Id);
var json = JsonSerializer.Serialize(tasks, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(tasksPath, json);
_currentSession.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
SaveSession(_currentSession);
}
public SessionTask CreateTask(string subject, string description = "")
{
if (_currentSession == null)
thrownew InvalidOperationException("No active session");
var tasks = LoadCurrentSessionTasks();
var nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
var task = new SessionTask
{
Id = nextId,
Subject = subject,
Description = description,
Status = "pending",
BlockedBy = new List<int>(),
Blocks = new List<int>(),
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
tasks.Add(task);
SaveCurrentSessionTasks(tasks);
UnityEngine.Debug.Log($"[SessionManager] Task #{nextId} created in session #{_currentSession.Id}: {subject}");
OnTaskCreated?.Invoke(_currentSession.Id, nextId, subject);
return task;
}
public SessionTask UpdateTask(int taskId, string status = null, List<int> addBlockedBy = null, List<int> addBlocks = null)
{
if (_currentSession == null)
thrownew InvalidOperationException("No active session");
var tasks = LoadCurrentSessionTasks();
var task = tasks.FirstOrDefault(t => t.Id == taskId);
if (task == null)
thrownew InvalidOperationException($"Task #{taskId} not found in current session");
// 更新状态
if (!string.IsNullOrEmpty(status))
{
if (status != "pending" && status != "in_progress" && status != "completed")
thrownew InvalidOperationException($"Invalid status: {status}");
task.Status = status;
UnityEngine.Debug.Log($"[SessionManager] Task #{taskId} -> {status}");
// 如果任务完成,清除依赖
if (status == "completed")
{
foreach (var t in tasks)
{
t.BlockedBy.Remove(taskId);
}
}
OnTaskUpdated?.Invoke(_currentSession.Id, taskId, status);
}
// 添加 blockedBy 依赖
if (addBlockedBy != null && addBlockedBy.Count > 0)
{
foreach (var id in addBlockedBy)
{
if (!task.BlockedBy.Contains(id))
task.BlockedBy.Add(id);
}
}
// 添加 blocks 依赖
if (addBlocks != null && addBlocks.Count > 0)
{
foreach (var id in addBlocks)
{
if (!task.Blocks.Contains(id))
task.Blocks.Add(id);
// 反向更新
var blockedTask = tasks.FirstOrDefault(t => t.Id == id);
if (blockedTask != null && !blockedTask.BlockedBy.Contains(taskId))
{
blockedTask.BlockedBy.Add(taskId);
}
}
}
task.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
SaveCurrentSessionTasks(tasks);
return task;
}
public SessionTask GetTask(int taskId)
{
if (_currentSession == null)
thrownew InvalidOperationException("No active session");
var tasks = LoadCurrentSessionTasks();
return tasks.FirstOrDefault(t => t.Id == taskId);
}
public List<SessionTask> ListTasks()
{
return LoadCurrentSessionTasks();
}
publicboolDeleteTask(int taskId)
{
if (_currentSession == null)
returnfalse;
var tasks = LoadCurrentSessionTasks();
var task = tasks.FirstOrDefault(t => t.Id == taskId);
if (task == null)
returnfalse;
tasks.Remove(task);
SaveCurrentSessionTasks(tasks);
UnityEngine.Debug.Log($"[SessionManager] Task #{taskId} deleted");
returntrue;
}
publicstringGetTaskStats()
{
var tasks = LoadCurrentSessionTasks();
if (tasks.Count == 0)
return"No tasks";
var pending = tasks.Count(t => t.Status == "pending");
var inProgress = tasks.Count(t => t.Status == "in_progress");
var completed = tasks.Count(t => t.Status == "completed");
return$"Tasks: {tasks.Count} total | {pending} pending | {inProgress} in progress | {completed} completed";
}
#endregion
#endregion
}
publicclassSession
{
publicint Id { get; set; }
publicstring Name { get; set; }
publiclong CreatedAt { get; set; }
publiclong UpdatedAt { get; set; }
publicstringGetFormattedCreatedAt()
{
return DateTimeOffset.FromUnixTimeSeconds(CreatedAt).ToLocalTime().ToString("yyyy-MM-dd HH:mm");
}
publicstringGetFormattedUpdatedAt()
{
return DateTimeOffset.FromUnixTimeSeconds(UpdatedAt).ToLocalTime().ToString("yyyy-MM-dd HH:mm");
}
}
publicclassSessionTask
{
publicint Id { get; set; }
publicstring Subject { get; set; }
publicstring Description { get; set; }
publicstring Status { get; set; }
public List<int> BlockedBy { get; set; }
public List<int> Blocks { get; set; }
publiclong CreatedAt { get; set; }
publiclong UpdatedAt { get; set; }
}
}
Background Manager
后台任务 – 永不阻塞的智能体:在真实项目中,构建、测试、部署动辄数分钟。让智能体干等着是极大的浪费。
在独立线程中运行命令,在每次 LLM 调用前排空通知队列,使智能体永远不会因长时间运行的操作而阻塞。
BackgroundManager.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespaceClaudeCodeForUnity
{
publicclassBackgroundManager
{
publicclassBackgroundNotification
{
publicstring TaskId { get; }
publicstring Status { get; }
publicstring Command { get; }
publicstring Result { get; }
publicBackgroundNotification(string taskId, string status, string command, string result)
{
TaskId = taskId;
Status = status;
Command = command;
Result = result;
}
}
privateclassTaskInfo
{
publicstring Status { get; }
publicstring Result { get; }
publicstring Command { get; }
publicTaskInfo(string status, string result, string command)
{
Status = status;
Result = result;
Command = command;
}
}
privatereadonly ConcurrentDictionary<string, TaskInfo> _tasks = new();
privatereadonly ConcurrentQueue<BackgroundNotification> _notificationQueue = new();
privatereadonlystring _workingDirectory;
publicBackgroundManager(string workingDirectory)
{
_workingDirectory = workingDirectory;
}
publicstringRun(string command)
{
var taskId = Guid.NewGuid().ToString().Substring(0, 8);
_tasks[taskId] = new TaskInfo("running", null, command);
var thread = new Thread(() => ExecuteTask(taskId, command))
{
IsBackground = true
};
thread.Start();
var preview = command.Length > 80 ? command.Substring(0, 80) + "..." : command;
return$"Background task {taskId} started: {preview}";
}
privatevoidExecuteTask(string taskId, string command)
{
string output;
string status;
try
{
usingvar proc = new Process();
proc.StartInfo = new ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"{command.Replace("\"", "\\\"")}\"",
WorkingDirectory = _workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
proc.Start();
// 5 分钟超时
if (!proc.WaitForExit(300_000))
{
proc.Kill();
output = "Error: Timeout (300s)";
status = "timeout";
}
else
{
var stdout = proc.StandardOutput.ReadToEnd();
var stderr = proc.StandardError.ReadToEnd();
output = (stdout + stderr).Trim();
output = string.IsNullOrEmpty(output) ? "(no output)" : output;
// 限制输出大小
if (output.Length > 50000)
output = output.Substring(0, 50000) + "\n... (output truncated)";
status = "completed";
}
}
catch (Exception ex)
{
output = $"Error: {ex.Message}";
status = "error";
}
// 更新任务状态
_tasks[taskId] = new TaskInfo(status, output, command);
// 添加通知到队列
var commandPreview = command.Length > 80 ? command.Substring(0, 80) + "..." : command;
var resultPreview = output.Length > 500 ? output.Substring(0, 500) + "..." : output;
_notificationQueue.Enqueue(new BackgroundNotification(
taskId,
status,
commandPreview,
resultPreview
));
UnityEngine.Debug.Log($"[BackgroundTask {taskId}] {status}: {commandPreview}");
}
publicstringCheck(string taskId = null)
{
if (taskId != null)
{
if (!_tasks.TryGetValue(taskId, outvar task))
return$"Error: Unknown task {taskId}";
var commandPreview = task.Command.Length > 60
? task.Command.Substring(0, 60) + "..."
: task.Command;
var result = task.Result ?? "(running)";
return$"[{task.Status}] {commandPreview}\n{result}";
}
// 返回所有任务
if (_tasks.IsEmpty)
return"No background tasks.";
var taskList = _tasks.Select(kv =>
{
var commandPreview = kv.Value.Command.Length > 60
? kv.Value.Command.Substring(0, 60) + "..."
: kv.Value.Command;
return$"{kv.Key}: [{kv.Value.Status}] {commandPreview}";
});
returnstring.Join("\n", taskList);
}
public List<BackgroundNotification> DrainNotifications()
{
var result = new List<BackgroundNotification>();
while (_notificationQueue.TryDequeue(outvar notification))
{
result.Add(notification);
}
return result;
}
publicintGetActiveTaskCount()
{
return _tasks.Count(kv => kv.Value.Status == "running");
}
publicvoidClearCompletedTasks()
{
var completedTasks = _tasks.Where(kv => kv.Value.Status != "running")
.Select(kv => kv.Key)
.ToList();
foreach (var taskId in completedTasks)
{
_tasks.TryRemove(taskId, out _);
}
UnityEngine.Debug.Log($"[BackgroundManager] Cleared {completedTasks.Count} completed tasks");
}
}
}
Tool Manager
工具管理类,是对上述功能的整合。
TextManager.cs
using System.Collections.Generic;
namespaceClaudeCodeForUnity
{
publicclassTextManager
{
publicenum Language
{
Chinese,
English
}
private Language _currentLanguage = Language.Chinese;
privatereadonly Dictionary<string, Dictionary<Language, string>> _texts;
publicTextManager()
{
_texts = new Dictionary<string, Dictionary<Language, string>>();
InitializeTexts();
}
publicvoidSetLanguage(Language language)
{
_currentLanguage = language;
}
public Language CurrentLanguage => _currentLanguage;
publicstringGet(string key)
{
if (_texts.TryGetValue(key, outvar translations))
{
if (translations.TryGetValue(_currentLanguage, outvar text))
return text;
}
UnityEngine.Debug.LogWarning($"[TextManager] Missing text for key: {key}");
return$"[{key}]";
}
publicstringGetFormat(string key, paramsobject[] args)
{
var template = Get(key);
try
{
returnstring.Format(template, args);
}
catch (System.FormatException ex)
{
UnityEngine.Debug.LogWarning($"[TextManager] Format error for key '{key}': {ex.Message}");
return template;
}
}
publicvoidSet(string key, string chineseText, string englishText)
{
if (!_texts.ContainsKey(key))
_texts[key] = new Dictionary<Language, string>();
_texts[key][Language.Chinese] = chineseText;
_texts[key][Language.English] = englishText;
}
privatevoidInitializeTexts()
{
// UI 文本
Set("window.title", "Claude Code", "Claude Code");
Set("window.mode", "模式:", "Mode:");
Set("window.send", "发送", "Send");
Set("window.stop", "停止", "Stop");
Set("window.stopping", "停止中...", "Stopping...");
Set("window.session", "会话", "Session");
Set("window.session.default", "默认会话", "Default Session");
// 会话管理
Set("session.new", "新建会话", "New Session");
Set("session.rename", "重命名会话", "Rename Session");
Set("session.rename.hint", "💡 重命名会话:使用 /rename <新名称>", "💡 To rename session, use: /rename <new name>");
Set("session.rename.hint2", "或告诉代理:\"将此会话重命名为 <新名称>\"", "Or tell the agent: \"Rename this session to <new name>\"");
Set("session.delete", "删除会话", "Delete Session");
Set("session.delete.confirm", "确定要删除会话吗?", "Are you sure you want to delete this session?");
Set("session.delete.warning", "此操作无法撤销!", "This action cannot be undone!");
Set("session.delete.title", "删除会话", "Delete Session");
Set("session.delete.message", "确定要删除会话 '{0}' 吗?\n\n这将永久删除:\n- 对话历史\n- {1} 个任务\n- 会话数据\n\n此操作无法撤销!", "Are you sure you want to delete session '{0}'?\n\nThis will permanently delete:\n- Conversation history\n- {1} task(s)\n- Session data\n\nThis action cannot be undone!");
Set("session.delete.button_yes", "是的,删除", "Yes, Delete");
Set("session.delete.button_cancel", "取消", "Cancel");
Set("session.no_active", "❌ 没有活动会话可删除", "❌ No active session to delete");
Set("session.cannot_delete_last", "❌ 无法删除最后一个会话。请先创建新会话。", "❌ Cannot delete the last session. Create a new session first.");
Set("session.delete.cancelled", "ℹ️ 会话删除已取消", "ℹ️ Session deletion cancelled");
Set("session.deleted", "🗑️ 会话 '{0}' 删除成功 ({1} 个任务已移除)", "🗑️ Session '{0}' deleted successfully ({1} task(s) removed)");
Set("session.delete_failed", "❌ 删除会话 '{0}' 失败", "❌ Failed to delete session '{0}'");
Set("session.created", "📂 新会话已创建: {0}", "📂 New session created: {0}");
Set("session.switched", "📂 已切换到会话: {0}", "📂 Switched to session: {0}");
// 状态消息
Set("status.initializing", "初始化中...", "Initializing...");
Set("status.ready", "就绪", "Ready");
Set("status.connecting", "连接中...", "Connecting...");
Set("status.error", "错误", "Error");
Set("status.idle", "空闲", "Idle");
Set("status.task_cancelled", "任务已取消", "Task cancelled");
Set("status.task_cancelling", "正在取消任务...", "Cancelling task...");
Set("status.thinking", "思考中...", "Thinking...");
// 欢迎消息
Set("welcome.message", "欢迎使用 Claude Code!", "Welcome to Claude Code!");
Set("welcome.model", "模型:", "Model:");
Set("welcome.directory", "目录:", "Directory:");
Set("welcome.session", "当前会话:", "Current Session:");
Set("welcome.session.hint", "会话:每个会话存储独立任务。使用上方工具栏切换会话。",
"Sessions: Each session stores separate tasks. Switch sessions using the toolbar above.");
Set("welcome.tasks.hint", "持久化任务:创建的任务会保存到 .sessions/ 目录,即使对话压缩也不会丢失!",
"Persistent Tasks: Created tasks are saved to .sessions/ and survive conversation compression!");
// 任务相关
Set("task.create", "创建任务", "Create Task");
Set("task.update", "更新任务", "Update Task");
Set("task.list", "任务列表", "Task List");
Set("task.list.persistent", "持久化任务", "Persistent Tasks");
Set("task.get", "获取任务", "Get Task");
Set("task.no_tasks", "暂无任务。", "No tasks.");
Set("task.pending", "待办", "Pending");
Set("task.in_progress", "进行中", "In Progress");
Set("task.completed", "已完成", "Completed");
// 后台任务
Set("background.run", "后台运行", "Background Run");
Set("background.check", "检查后台任务", "Check Background");
Set("background.no_tasks", "没有后台任务。", "No background tasks.");
Set("background.timeout", "超时", "Timeout");
Set("background.running", "运行中", "Running");
Set("background.completed", "已完成", "Completed");
Set("background.error", "错误", "Error");
// 技能相关
Set("skill.load", "加载技能", "Load Skill");
Set("skill.no_skills", "暂无可用技能。", "No skills available.");
Set("skill.loaded", "技能已加载", "Skill loaded");
Set("skill.unknown", "未知技能", "Unknown skill");
Set("skill.available", "可用技能:", "Available skills:");
// 工具相关
Set("tool.bash", "运行shell命令", "Run shell command");
Set("tool.read_file", "读取文件内容", "Read file content");
Set("tool.write_file", "写入内容到文件", "Write content to file");
Set("tool.edit_file", "替换文件中的精确文本", "Replace exact text in file");
Set("tool.executing", "工具执行中...", "Tool executing...");
Set("tool.status", "工具: {0}", "Tool: {0}");
Set("tool.status_with_details", "工具: {0} - {1}", "Tool: {0} - {1}");
Set("tool.status_with_progress", "工具: {0} ({1}/{2})", "Tool: {0} ({1}/{2})");
// 错误消息
Set("error.init_failed", "模块初始化失败", "Module initialization failed");
Set("error.connection_failed", "连接失败", "Connection failed");
Set("error.unknown_tool", "未知工具", "Unknown tool");
Set("error.missing_param", "缺少必需参数", "Missing required parameter");
Set("error.file_not_found", "文件未找到", "File not found");
Set("error.permission_denied", "权限被拒绝", "Permission denied");
// 压缩相关
Set("compact.auto", "自动压缩", "Auto Compact");
Set("compact.manual", "手动压缩", "Manual Compact");
Set("compact.completed", "🗜️ 对话已压缩。记录: {0}", "🗜️ Conversation compacted. Transcript: {0}");
Set("compact.summary", "摘要: {0}", "Summary: {0}");
Set("compact.clearing_memory", "清理内存...", "Clearing memory...");
// Token 使用
Set("token.input", "输入", "Input");
Set("token.output", "输出", "Output");
Set("token.total", "总计", "Total");
Set("token.usage", "Token 使用", "Token Usage");
// 模式
Set("mode.explore", "探索", "Explore");
Set("mode.code", "编程", "Code");
Set("mode.plan", "规划", "Plan");
}
public IEnumerable<string> GetAllKeys()
{
return _texts.Keys;
}
}
}
ToolManager
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
namespaceClaudeCodeForUnity
{
publicclassToolManager
{
privatereadonlystring _workingDirectory;
privatereadonly BashAgent _bashAgent;
privatereadonly FileAgent _fileAgent;
privatereadonly TodoManager _todoManager;
privatereadonly TaskManager _taskManager;
privatereadonly SessionManager _sessionManager;
privatereadonly SubAgentManager _subAgentManager;
privatereadonly BackgroundManager _backgroundTaskManager;
privatereadonly SkillAgent _skillAgent;
// Helper method to serialize to JsonElement for .NET Standard 2.1 compatibility
privatestatic JsonElement SerializeToElement<T>(T value)
{
var json = JsonSerializer.Serialize(value);
usingvar doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
publicenum SkillType
{
Explore,
Code,
Plan
}
publicToolManager(string workingDirectory, BashAgent bashAgent, FileAgent fileAgent,
TodoManager todoManager, TaskManager taskManager = null,
SessionManager sessionManager = null, SubAgentManager subAgentManager = null,
BackgroundManager backgroundTaskManager = null, SkillAgent skillAgent = null)
{
_workingDirectory = workingDirectory;
_bashAgent = bashAgent;
_fileAgent = fileAgent;
_todoManager = todoManager;
_taskManager = taskManager;
_sessionManager = sessionManager;
_subAgentManager = subAgentManager;
_backgroundTaskManager = backgroundTaskManager;
_skillAgent = skillAgent;
}
public List<Tool> GetToolsForSkill(SkillType skillType)
{
var tools = new List<Tool>();
// 基础工具
var bashTool = CreateBashTool();
var readFileTool = CreateReadFileTool();
var writeFileTool = CreateWriteFileTool();
var editFileTool = CreateEditFileTool();
var todoTool = CreateTodoTool();
// SubAgent Task工具(如果有subAgentManager)
Tool subAgentTaskTool = null;
if (_subAgentManager != null)
{
subAgentTaskTool = CreateTaskTool();
}
// 持久化任务工具(如果有sessionManager)
Tool taskCreateTool = null;
Tool taskUpdateTool = null;
Tool taskListTool = null;
Tool taskGetTool = null;
if (_sessionManager != null)
{
taskCreateTool = CreateTaskCreateTool();
taskUpdateTool = CreateTaskUpdateTool();
taskListTool = CreateTaskListTool();
taskGetTool = CreateTaskGetTool();
}
// 后台任务工具(如果有backgroundTaskManager)
Tool backgroundRunTool = null;
Tool checkBackgroundTool = null;
if (_backgroundTaskManager != null)
{
backgroundRunTool = CreateBackgroundRunTool();
checkBackgroundTool = CreateCheckBackgroundTool();
}
// 技能工具(如果有skillAgent)
Tool skillTool = null;
if (_skillAgent != null)
{
skillTool = CreateSkillTool();
}
switch (skillType)
{
case SkillType.Explore:
// 只读探索:bash和read_file
tools.Add(bashTool);
tools.Add(readFileTool);
break;
case SkillType.Code:
// 完整编程:所有工具
tools.Add(bashTool);
tools.Add(readFileTool);
tools.Add(writeFileTool);
tools.Add(editFileTool);
tools.Add(todoTool);
if (subAgentTaskTool != null)
tools.Add(subAgentTaskTool);
// 添加持久化任务工具
if (taskCreateTool != null)
{
tools.Add(taskCreateTool);
tools.Add(taskUpdateTool);
tools.Add(taskListTool);
tools.Add(taskGetTool);
}
// 添加后台任务工具
if (backgroundRunTool != null)
{
tools.Add(backgroundRunTool);
tools.Add(checkBackgroundTool);
}
// 添加技能工具
if (skillTool != null)
{
tools.Add(skillTool);
}
break;
case SkillType.Plan:
// 规划:bash和read_file(不修改)
tools.Add(bashTool);
tools.Add(readFileTool);
tools.Add(todoTool);
if (subAgentTaskTool != null)
tools.Add(subAgentTaskTool);
// 添加持久化任务工具
if (taskListTool != null)
{
tools.Add(taskListTool);
tools.Add(taskGetTool);
}
break;
}
return tools;
}
publicasync Task<string> ExecuteToolAsync(string toolName, IReadOnlyDictionary<string, JsonElement> args)
{
return toolName switch
{
"bash" => await ExecuteBashAsync(args),
"read_file" => ExecuteReadFile(args),
"write_file" => ExecuteWriteFile(args),
"edit_file" => ExecuteEditFile(args),
"TodoWrite" => ExecuteTodoWrite(args),
"Task" => await ExecuteTaskAsync(args),
"task_create" => ExecuteTaskCreate(args),
"task_update" => ExecuteTaskUpdate(args),
"task_list" => ExecuteTaskList(args),
"task_get" => ExecuteTaskGet(args),
"background_run" => ExecuteBackgroundRun(args),
"check_background" => ExecuteCheckBackground(args),
"Skill" => ExecuteSkill(args),
_ => $"Unknown tool: {toolName}"
};
}
#region 工具创建方法
private Tool CreateBashTool()
{
returnnew Tool
{
Name = "bash",
Description = "运行shell命令。用于: ls, find, grep, git, npm, dotnet等。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["command"] = SerializeToElement(new { type = "string", description = "要执行的命令" })
},
Required = new List<string> { "command" }
}
};
}
private Tool CreateReadFileTool()
{
returnnew Tool
{
Name = "read_file",
Description = "读取文件内容。返回UTF-8文本。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["path"] = SerializeToElement(new { type = "string", description = "文件的相对路径" }),
["limit"] = SerializeToElement(new { type = "integer", description = "最大读取行数(默认: 全部)" })
},
Required = new List<string> { "path" }
}
};
}
private Tool CreateWriteFileTool()
{
returnnew Tool
{
Name = "write_file",
Description = "将内容写入文件。如果需要会创建父目录。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["path"] = SerializeToElement(new { type = "string", description = "文件的相对路径" }),
["content"] = SerializeToElement(new { type = "string", description = "要写入的内容" })
},
Required = new List<string> { "path", "content" }
}
};
}
private Tool CreateEditFileTool()
{
returnnew Tool
{
Name = "edit_file",
Description = "替换文件中的精确文本。用于局部编辑。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["path"] = SerializeToElement(new { type = "string", description = "文件的相对路径" }),
["old_text"] = SerializeToElement(new { type = "string", description = "要查找的精确文本(必须完全匹配)" }),
["new_text"] = SerializeToElement(new { type = "string", description = "替换文本" })
},
Required = new List<string> { "path", "old_text", "new_text" }
}
};
}
private Tool CreateTodoTool()
{
returnnew Tool
{
Name = "TodoWrite",
Description = @"更新结构化任务列表。用于规划和追踪进度。
规则:
1. 最多20项任务
2. 同时只能有1个任务状态为 in_progress
3. 每个任务需要 content(任务描述)、status(状态)、activeForm(进行时描述)
示例:
{
""items"": [
{""content"": ""读取配置文件"", ""status"": ""completed"", ""activeForm"": ""Reading config file""},
{""content"": ""解析JSON数据"", ""status"": ""in_progress"", ""activeForm"": ""Parsing JSON data""},
{""content"": ""写入输出文件"", ""status"": ""pending"", ""activeForm"": ""Writing output file""}
]
}",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["items"] = SerializeToElement(new
{
type = "array",
description = "完整的任务列表(替换现有)",
items = new
{
type = "object",
properties = new
{
content = new { type = "string", description = "任务描述(必填)" },
status = new { type = "string", @enum = new[] { "pending", "in_progress", "completed" }, description = "任务状态:pending(待处理)、in_progress(进行中)、completed(已完成)" },
activeForm = new { type = "string", description = "现在进行时描述,如'Reading file'、'Parsing data'(必填)" }
},
required = new[] { "content", "status", "activeForm" }
}
})
},
Required = new List<string> { "items" }
}
};
}
private Tool CreateTaskTool()
{
var agentDescriptions = _subAgentManager?.GetAgentDescriptions() ?? new Dictionary<string, string>();
var descriptionText = string.Join("\n", agentDescriptions.Select(kv => $"- {kv.Key}: {kv.Value}"));
returnnew Tool
{
Name = "Task",
Description = $"为专注的子任务生成子代理。\n\n可用代理类型:\n{descriptionText}",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["description"] = SerializeToElement(new { type = "string", description = "简短任务名(3-5词)用于进度显示" }),
["prompt"] = SerializeToElement(new { type = "string", description = "给子代理的详细指令" }),
["agent_type"] = SerializeToElement(new { type = "string", @enum = agentDescriptions.Keys.ToArray() })
},
Required = new List<string> { "description", "prompt", "agent_type" }
}
};
}
private Tool CreateTaskCreateTool()
{
returnnew Tool
{
Name = "task_create",
Description = "创建一个新的持久化任务。任务会保存到 .tasks/ 目录,即使对话压缩也不会丢失。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["subject"] = SerializeToElement(new { type = "string", description = "任务主题(必填)" }),
["description"] = SerializeToElement(new { type = "string", description = "任务详细描述(可选)" })
},
Required = new List<string> { "subject" }
}
};
}
private Tool CreateTaskUpdateTool()
{
returnnew Tool
{
Name = "task_update",
Description = "更新任务状态或依赖关系。完成任务时会自动清除其他任务的阻塞关系。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["task_id"] = SerializeToElement(new { type = "integer", description = "任务 ID(必填)" }),
["status"] = SerializeToElement(new { type = "string", @enum = new[] { "pending", "in_progress", "completed" }, description = "新状态(可选)" }),
["addBlockedBy"] = SerializeToElement(new { type = "array", items = new { type = "integer" }, description = "添加阻塞依赖:此任务被哪些任务阻塞(可选)" }),
["addBlocks"] = SerializeToElement(new { type = "array", items = new { type = "integer" }, description = "添加阻塞关系:此任务阻塞哪些任务(可选)" })
},
Required = new List<string> { "task_id" }
}
};
}
private Tool CreateTaskListTool()
{
returnnew Tool
{
Name = "task_list",
Description = "列出所有持久化任务及其状态和依赖关系。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>(),
Required = new List<string>()
}
};
}
private Tool CreateTaskGetTool()
{
returnnew Tool
{
Name = "task_get",
Description = "获取指定任务的完整详情(JSON格式)。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["task_id"] = SerializeToElement(new { type = "integer", description = "任务 ID" })
},
Required = new List<string> { "task_id" }
}
};
}
private Tool CreateBackgroundRunTool()
{
returnnew Tool
{
Name = "background_run",
Description = "在后台线程运行命令。适用于长时间运行的操作(如 build, test, install)。立即返回 task_id。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["command"] = SerializeToElement(new { type = "string", description = "要在后台执行的命令" })
},
Required = new List<string> { "command" }
}
};
}
private Tool CreateCheckBackgroundTool()
{
returnnew Tool
{
Name = "check_background",
Description = "检查后台任务状态。省略 task_id 返回所有任务列表。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["task_id"] = SerializeToElement(new { type = "string", description = "任务 ID(可选)" })
},
Required = new List<string>()
}
};
}
private Tool CreateSkillTool()
{
var skillDescriptions = _skillAgent != null ? _skillAgent.GetSkillDescriptions() : "(no skills available)";
returnnew Tool
{
Name = "Skill",
Description = $@"加载技能以获取任务的专业知识。
可用技能:
{skillDescriptions}
何时使用:
- 用户任务匹配技能描述时立即使用
- 在尝试特定领域工作(PDF、MCP 等)之前
技能内容将注入对话,给你详细指令和资源访问。",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["skill"] = SerializeToElement(new { type = "string", description = "要加载的技能名称" })
},
Required = new List<string> { "skill" }
}
};
}
#endregion
#region 工具执行方法
privateasync Task<string> ExecuteBashAsync(IReadOnlyDictionary<string, JsonElement> args)
{
var command = args["command"].GetString();
if (string.IsNullOrEmpty(command))
return"Error: command is required";
returnawait _bashAgent.ExecuteAsync(command);
}
privatestringExecuteReadFile(IReadOnlyDictionary<string, JsonElement> args)
{
var path = args["path"].GetString();
if (string.IsNullOrEmpty(path))
return"Error: path is required";
var limit = args.TryGetValue("limit", outvar limitElement) ? limitElement.GetInt32() : (int?)null;
return _fileAgent.ReadFile(path, limit);
}
privatestringExecuteWriteFile(IReadOnlyDictionary<string, JsonElement> args)
{
var path = args["path"].GetString();
var content = args["content"].GetString();
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(content))
return"Error: path and content are required";
return _fileAgent.WriteFile(path, content);
}
privatestringExecuteEditFile(IReadOnlyDictionary<string, JsonElement> args)
{
var path = args["path"].GetString();
var oldText = args["old_text"].GetString();
var newText = args["new_text"].GetString();
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(oldText) || string.IsNullOrEmpty(newText))
return"Error: path, old_text and new_text are required";
return _fileAgent.EditFile(path, oldText, newText);
}
privatestringExecuteTodoWrite(IReadOnlyDictionary<string, JsonElement> args)
{
var items = args["items"];
return _todoManager.Update(items);
}
privateasync Task<string> ExecuteTaskAsync(IReadOnlyDictionary<string, JsonElement> args)
{
if (_subAgentManager == null)
return"Error: SubAgentManager not available";
var description = args["description"].GetString();
var prompt = args["prompt"].GetString();
var agentType = args["agent_type"].GetString();
if (string.IsNullOrEmpty(description) || string.IsNullOrEmpty(prompt) || string.IsNullOrEmpty(agentType))
return"Error: description, prompt and agent_type are required";
// 获取当前可用的工具列表
var currentTools = GetToolsForSkill(SkillType.Code);
try
{
returnawait _subAgentManager.ExecuteSubAgentAsync(description, prompt, agentType, currentTools);
}
catch (Exception ex)
{
return$"Error executing sub-agent: {ex.Message}";
}
}
privatestringExecuteTaskCreate(IReadOnlyDictionary<string, JsonElement> args)
{
if (_sessionManager == null)
return"Error: SessionManager not available";
var subject = args["subject"].GetString();
if (string.IsNullOrEmpty(subject))
return"Error: subject is required";
var description = args.TryGetValue("description", outvar desc) ? desc.GetString() : "";
try
{
// 在当前会话中创建任务
var task = _sessionManager.CreateTask(subject, description);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception ex)
{
return$"Error creating task: {ex.Message}";
}
}
privatestringExecuteTaskUpdate(IReadOnlyDictionary<string, JsonElement> args)
{
if (_sessionManager == null)
return"Error: SessionManager not available";
if (!args.TryGetValue("task_id", outvar idElement))
return"Error: task_id is required";
var taskId = idElement.GetInt32();
var status = args.TryGetValue("status", outvar statusElement) ? statusElement.GetString() : null;
List<int> addBlockedBy = null;
if (args.TryGetValue("addBlockedBy", outvar blockedByElement) && blockedByElement.ValueKind == JsonValueKind.Array)
{
addBlockedBy = blockedByElement.EnumerateArray().Select(e => e.GetInt32()).ToList();
}
List<int> addBlocks = null;
if (args.TryGetValue("addBlocks", outvar blocksElement) && blocksElement.ValueKind == JsonValueKind.Array)
{
addBlocks = blocksElement.EnumerateArray().Select(e => e.GetInt32()).ToList();
}
try
{
var task = _sessionManager.UpdateTask(taskId, status, addBlockedBy, addBlocks);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception ex)
{
return$"Error updating task: {ex.Message}";
}
}
privatestringExecuteTaskList(IReadOnlyDictionary<string, JsonElement> _)
{
if (_sessionManager == null)
return"Error: SessionManager not available";
try
{
var tasks = _sessionManager.ListTasks();
if (tasks.Count == 0)
return"No tasks.";
var lines = new List<string>();
foreach (var task in tasks)
{
var marker = task.Status switch
{
"pending" => "[ ]",
"in_progress" => "[>]",
"completed" => "[x]",
_ => "[?]"
};
var blockedInfo = task.BlockedBy != null && task.BlockedBy.Count > 0
? $" (blocked by: [{string.Join(", ", task.BlockedBy)}])"
: "";
lines.Add($"{marker} #{task.Id}: {task.Subject}{blockedInfo}");
}
returnstring.Join("\n", lines);
}
catch (Exception ex)
{
return$"Error listing tasks: {ex.Message}";
}
}
privatestringExecuteTaskGet(IReadOnlyDictionary<string, JsonElement> args)
{
if (_sessionManager == null)
return"Error: SessionManager not available";
if (!args.TryGetValue("task_id", outvar idElement))
return"Error: task_id is required";
var taskId = idElement.GetInt32();
try
{
var task = _sessionManager.GetTask(taskId);
if (task == null)
return$"Task #{taskId} not found in current session";
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception ex)
{
return$"Error getting task: {ex.Message}";
}
}
privatestringExecuteBackgroundRun(IReadOnlyDictionary<string, JsonElement> args)
{
if (_backgroundTaskManager == null)
return"Error: BackgroundManager not available";
if (!args.TryGetValue("command", outvar commandElement))
return"Error: command is required";
var command = commandElement.GetString();
if (string.IsNullOrEmpty(command))
return"Error: command cannot be empty";
try
{
return _backgroundTaskManager.Run(command);
}
catch (Exception ex)
{
return$"Error starting background task: {ex.Message}";
}
}
privatestringExecuteCheckBackground(IReadOnlyDictionary<string, JsonElement> args)
{
if (_backgroundTaskManager == null)
return"Error: BackgroundManager not available";
try
{
var taskId = args.TryGetValue("task_id", outvar idElement)
? idElement.GetString()
: null;
return _backgroundTaskManager.Check(taskId);
}
catch (Exception ex)
{
return$"Error checking background tasks: {ex.Message}";
}
}
privatestringExecuteSkill(IReadOnlyDictionary<string, JsonElement> args)
{
if (_skillAgent == null)
return"Error: SkillAgent not available";
if (!args.TryGetValue("skill", outvar skillElement))
return"Error: skill name is required";
var skillName = skillElement.GetString();
try
{
// 委托给 SkillAgent 处理
return _skillAgent.LoadSkill(skillName);
}
catch (Exception ex)
{
return$"Error loading skill: {ex.Message}";
}
}
#endregion
publicstaticstringGetSkillDescription(SkillType skillType)
{
return skillType switch
{
SkillType.Explore => "只读代理,用于探索代码、查找文件、搜索",
SkillType.Code => "完整代理,用于实现功能和修复bug",
SkillType.Plan => "规划代理,用于设计实现策略",
_ => "未知技能类型"
};
}
publicstatic Dictionary<SkillType, string> GetAllSkillDescriptions()
{
returnnew Dictionary<SkillType, string>
{
{ SkillType.Explore, GetSkillDescription(SkillType.Explore) },
{ SkillType.Code, GetSkillDescription(SkillType.Code) },
{ SkillType.Plan, GetSkillDescription(SkillType.Plan) }
};
}
}
}
UIToolkit
接下来简单做个界面来承载这些功能,UI 使用的 UIToolkit。
虽然使用的是 Anthropic SDK,但这里使用的大预言模式是 DeepSeek。
ClaudeConfig.cs
using System.IO;
using UnityEditor;
using UnityEngine;
namespaceClaudeCodeForUnity
{
[System.Serializable]
publicclassClaudeConfig
{
public ApiConfig api;
public UiConfig ui;
public PathsConfig paths;
[System.Serializable]
publicclassApiConfig
{
publicstring baseUrl;
publicstring apiKey;
publicstring model;
}
[System.Serializable]
publicclassUiConfig
{
publicstring language;
publicstring theme;
}
[System.Serializable]
publicclassPathsConfig
{
publicstring skillsDirectory;
publicstring sessionsDirectory;
}
}
publicclassConfigManager
{
privatestatic ClaudeConfig _config;
privatestaticreadonlystring ConfigFilePath = Path.Combine(
Application.dataPath,
"Claude Code for Unity",
"Editor",
"config",
"claude_config.json"
);
// 默认配置常量
privateconststring DEFAULT_BASE_URL = "https://api.deepseek.com/anthropic";
privateconststring DEFAULT_API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
privateconststring DEFAULT_MODEL = "deepseek-reasoner";
// EditorPrefs 键名
privateconststring PREF_BASE_URL = "ClaudeCode.BaseUrl";
privateconststring PREF_API_KEY = "ClaudeCode.ApiKey";
privateconststring PREF_MODEL = "ClaudeCode.Model";
staticConfigManager()
{
LoadConfig();
}
privatestaticvoidLoadConfig()
{
try
{
if (File.Exists(ConfigFilePath))
{
var json = File.ReadAllText(ConfigFilePath);
_config = JsonUtility.FromJson<ClaudeConfig>(json);
UnityEngine.Debug.Log($"[ConfigManager] Loaded config from: {ConfigFilePath}");
}
else
{
// 创建默认配置
_config = new ClaudeConfig
{
api = new ClaudeConfig.ApiConfig
{
baseUrl = DEFAULT_BASE_URL,
apiKey = DEFAULT_API_KEY,
model = DEFAULT_MODEL
},
ui = new ClaudeConfig.UiConfig
{
language = "Chinese",
theme = "dark"
},
paths = new ClaudeConfig.PathsConfig
{
skillsDirectory = "Assets/Claude Code for Unity/Editor/skills",
sessionsDirectory = ".sessions"
}
};
SaveConfig();
UnityEngine.Debug.Log($"[ConfigManager] Created default config at: {ConfigFilePath}");
}
}
catch (System.Exception ex)
{
UnityEngine.Debug.LogError($"[ConfigManager] Failed to load config: {ex.Message}");
// 使用默认配置
_config = new ClaudeConfig
{
api = new ClaudeConfig.ApiConfig
{
baseUrl = DEFAULT_BASE_URL,
apiKey = DEFAULT_API_KEY,
model = DEFAULT_MODEL
},
ui = new ClaudeConfig.UiConfig
{
language = "Chinese",
theme = "dark"
},
paths = new ClaudeConfig.PathsConfig
{
skillsDirectory = "Assets/Claude Code for Unity/Editor/skills",
sessionsDirectory = ".sessions"
}
};
}
}
publicstaticvoidSaveConfig()
{
try
{
var json = JsonUtility.ToJson(_config, true);
File.WriteAllText(ConfigFilePath, json);
UnityEngine.Debug.Log($"[ConfigManager] Saved config to: {ConfigFilePath}");
}
catch (System.Exception ex)
{
UnityEngine.Debug.LogError($"[ConfigManager] Failed to save config: {ex.Message}");
}
}
publicstaticvoidReloadConfig()
{
_config = null;
_workingDirectory = null;
_skillsDirectory = null;
_sessionsDirectory = null;
LoadConfig();
UnityEngine.Debug.Log("[ConfigManager] Configuration reloaded from file");
}
publicstaticstring BaseUrl
{
get
{
if (EditorPrefs.HasKey(PREF_BASE_URL))
return EditorPrefs.GetString(PREF_BASE_URL);
return _config?.api?.baseUrl ?? DEFAULT_BASE_URL;
}
}
publicstaticstring ApiKey
{
get
{
if (EditorPrefs.HasKey(PREF_API_KEY))
return EditorPrefs.GetString(PREF_API_KEY);
return _config?.api?.apiKey ?? DEFAULT_API_KEY;
}
}
publicstaticstring Model
{
get
{
if (EditorPrefs.HasKey(PREF_MODEL))
return EditorPrefs.GetString(PREF_MODEL);
return _config?.api?.model ?? DEFAULT_MODEL;
}
}
publicstaticstring Language => _config?.ui?.language ?? "Chinese";
publicstaticvoidSetApiConfig(string baseUrl, string apiKey, string model)
{
EditorPrefs.SetString(PREF_BASE_URL, baseUrl);
EditorPrefs.SetString(PREF_API_KEY, apiKey);
EditorPrefs.SetString(PREF_MODEL, model);
}
publicstaticvoidResetToDefaults()
{
EditorPrefs.DeleteKey(PREF_BASE_URL);
EditorPrefs.DeleteKey(PREF_API_KEY);
EditorPrefs.DeleteKey(PREF_MODEL);
}
// 目录路径
privatestaticstring _workingDirectory;
privatestaticstring _skillsDirectory;
privatestaticstring _sessionsDirectory;
publicstaticstring WorkingDirectory
{
get
{
if (string.IsNullOrEmpty(_workingDirectory))
{
_workingDirectory = System.Environment.CurrentDirectory;
}
return _workingDirectory;
}
}
publicstaticstring SkillsDirectory
{
get
{
if (string.IsNullOrEmpty(_skillsDirectory))
{
var configPath = _config?.paths?.skillsDirectory ?? "Assets/Claude Code for Unity/Editor/skills";
// 如果是相对路径,基于 Application.dataPath 解析
if (!Path.IsPathRooted(configPath))
{
// 如果路径以 "Assets/" 开头,替换为实际的 dataPath
if (configPath.StartsWith("Assets/") || configPath.StartsWith("Assets\\"))
{
configPath = configPath.Substring("Assets/".Length);
_skillsDirectory = Path.Combine(Application.dataPath, configPath);
}
else
{
_skillsDirectory = Path.Combine(WorkingDirectory, configPath);
}
}
else
{
_skillsDirectory = configPath;
}
}
return _skillsDirectory;
}
}
publicstaticstring SessionsDirectory
{
get
{
if (string.IsNullOrEmpty(_sessionsDirectory))
{
var configPath = _config?.paths?.sessionsDirectory ?? ".sessions";
// 如果是相对路径,基于 WorkingDirectory 解析
if (!Path.IsPathRooted(configPath))
{
_sessionsDirectory = Path.Combine(WorkingDirectory, configPath);
}
else
{
_sessionsDirectory = configPath;
}
}
return _sessionsDirectory;
}
}
publicstaticstringGetRelativePath(string fullPath)
{
if (fullPath.StartsWith(WorkingDirectory))
{
return fullPath.Substring(WorkingDirectory.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
return fullPath;
}
publicstaticvoidEnsureDirectoryExists(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
}
}
在 Unity Editor 启动是就加载配置文件。
ClaudeCodeInitializer.cs
using UnityEditor;
using UnityEngine;
namespaceClaudeCodeForUnity
{
[InitializeOnLoad]
publicstaticclassClaudeCodeInitializer
{
staticClaudeCodeInitializer()
{
// 延迟执行,确保 Unity 编辑器完全启动
EditorApplication.delayCall += Initialize;
}
privatestaticvoidInitialize()
{
Debug.Log("[Claude Code] Initializing...");
try
{
// 1. 触发 ConfigManager 的静态构造函数,加载配置文件
var baseUrl = ConfigManager.BaseUrl;
var model = ConfigManager.Model;
var language = ConfigManager.Language;
Debug.Log($"[Claude Code] Configuration loaded successfully");
Debug.Log($" - API Base URL: {baseUrl}");
Debug.Log($" - Model: {model}");
Debug.Log($" - Language: {language}");
Debug.Log($" - Skills Directory: {ConfigManager.SkillsDirectory}");
Debug.Log($" - Sessions Directory: {ConfigManager.SessionsDirectory}");
// 2. 确保必要的目录存在
ConfigManager.EnsureDirectoryExists(ConfigManager.SessionsDirectory);
Debug.Log($"[Claude Code] Session directory verified: {ConfigManager.SessionsDirectory}");
// 3. 如果需要,可以在这里进行其他初始化工作
// 例如:检查技能目录、验证配置等
Debug.Log("[Claude Code] Initialization complete");
}
catch (System.Exception ex)
{
Debug.LogError($"[Claude Code] Initialization failed: {ex.Message}");
Debug.LogException(ex);
}
}
}
}
一个简单的状态栏
StatusBar.cs
using UnityEngine;
using UnityEngine.UIElements;
namespaceClaudeCodeForUnity
{
///<summary>
/// 状态栏组件,参考VSCode Claude Code状态栏
///</summary>
publicclassStatusBar : VisualElement
{
private Label _modelLabel;
private Label _connectionLabel;
private Label _tokenLabel;
private Label _statusLabel;
///<summary>
/// 创建状态栏实例
///</summary>
publicStatusBar()
{
// 状态栏样式
AddToClassList("status-bar");
style.flexDirection = FlexDirection.Row;
style.justifyContent = Justify.SpaceBetween;
style.paddingLeft = 12;
style.paddingRight = 12;
style.paddingTop = 4;
style.paddingBottom = 4;
style.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1.0f);
style.borderTopWidth = 1;
style.borderTopColor = new Color(0.3f, 0.3f, 0.3f, 1.0f);
// 左侧:模型信息
var leftContainer = new VisualElement();
leftContainer.style.flexDirection = FlexDirection.Row;
leftContainer.style.alignItems = Align.Center;
_modelLabel = CreateStatusLabel("Model: Unknown");
_modelLabel.name = "model-label";
leftContainer.Add(_modelLabel);
Add(leftContainer);
// 中间:连接状态
var centerContainer = new VisualElement();
centerContainer.style.flexDirection = FlexDirection.Row;
centerContainer.style.alignItems = Align.Center;
_connectionLabel = CreateStatusLabel("🔗 Connecting...");
_connectionLabel.name = "connection-label";
centerContainer.Add(_connectionLabel);
Add(centerContainer);
// 右侧:Token使用和状态
var rightContainer = new VisualElement();
rightContainer.style.flexDirection = FlexDirection.Row;
rightContainer.style.alignItems = Align.Center;
_tokenLabel = CreateStatusLabel("Tokens: 0/0");
_tokenLabel.name = "token-label";
rightContainer.Add(_tokenLabel);
_statusLabel = CreateStatusLabel("Ready");
_statusLabel.name = "status-label";
_statusLabel.style.marginLeft = 12;
rightContainer.Add(_statusLabel);
Add(rightContainer);
}
///<summary>
/// 创建状态标签
///</summary>
private Label CreateStatusLabel(string text)
{
var label = new Label(text);
label.AddToClassList("status-label");
label.style.fontSize = 11;
label.style.unityFontStyleAndWeight = FontStyle.Normal;
label.style.color = new Color(0.7f, 0.7f, 0.7f, 1.0f);
return label;
}
///<summary>
/// 更新模型显示
///</summary>
publicvoidUpdateModel(string modelName)
{
_modelLabel.text = $"Model: {modelName}";
}
///<summary>
/// 更新连接状态
///</summary>
publicvoidUpdateConnectionStatus(bool connected, string message = null)
{
if (connected)
{
_connectionLabel.text = "🔗 Connected";
_connectionLabel.style.color = new Color(0.2f, 0.8f, 0.2f, 1.0f);
}
else
{
_connectionLabel.text = message ?? "🔌 Disconnected";
_connectionLabel.style.color = new Color(0.8f, 0.2f, 0.2f, 1.0f);
}
}
///<summary>
/// 更新Token使用情况
///</summary>
publicvoidUpdateTokenUsage(int used, int total)
{
_tokenLabel.text = $"Tokens: {used}/{total}";
// 根据使用率改变颜色
if (total > 0)
{
float usage = (float)used / total;
if (usage > 0.9f)
_tokenLabel.style.color = new Color(0.8f, 0.2f, 0.2f, 1.0f); // 红色
elseif (usage > 0.7f)
_tokenLabel.style.color = new Color(0.8f, 0.8f, 0.2f, 1.0f); // 黄色
else
_tokenLabel.style.color = new Color(0.2f, 0.8f, 0.2f, 1.0f); // 绿色
}
}
///<summary>
/// 更新状态消息
///</summary>
publicvoidUpdateStatus(string status, StatusType type = StatusType.Info)
{
_statusLabel.text = status;
switch (type)
{
case StatusType.Info:
_statusLabel.style.color = new Color(0.7f, 0.7f, 0.7f, 1.0f);
break;
case StatusType.Warning:
_statusLabel.style.color = new Color(0.8f, 0.8f, 0.2f, 1.0f);
break;
case StatusType.Error:
_statusLabel.style.color = new Color(0.8f, 0.2f, 0.2f, 1.0f);
break;
case StatusType.Success:
_statusLabel.style.color = new Color(0.2f, 0.8f, 0.2f, 1.0f);
break;
}
}
///<summary>
/// 显示工具执行状态
///</summary>
publicvoidShowToolExecution(string toolName, int current, int total)
{
if (total > 0)
{
UpdateStatus($"🔧 {toolName} ({current}/{total})", StatusType.Info);
}
else
{
UpdateStatus($"🔧 {toolName}", StatusType.Info);
}
}
///<summary>
/// 显示思考状态
///</summary>
publicvoidShowThinking()
{
UpdateStatus("💭 Thinking...", StatusType.Info);
}
///<summary>
/// 显示空闲状态
///</summary>
publicvoidShowIdle()
{
UpdateStatus("✅ Ready", StatusType.Success);
}
///<summary>
/// 状态类型
///</summary>
publicenum StatusType
{
Info,
Warning,
Error,
Success
}
}
}
UI 样式文件:
ClaudeCodeWindow.uss
/* 根容器 - Claude Code 深色主题 */
.unity-editor-window {
background-color: rgb(30, 30, 30);
}
/* 标题栏 */
.header {
background-color: rgb(37, 37, 38);
padding: 8px12px;
border-bottom-width: 1px;
border-bottom-color: rgb(60, 60, 60);
flex-direction: row;
align-items: center;
}
.title {
font-size: 13px;
-unity-font-style: bold;
color: rgb(204, 204, 204);
}
/* 会话工具栏 */
.session-toolbar {
flex-direction: row;
align-items: center;
padding-left: 8px;
padding-right: 8px;
padding-top: 4px;
padding-bottom: 4px;
background-color: rgb(51, 51, 57);
border-bottom-width: 1px;
border-bottom-color: rgb(76, 76, 76);
}
.session-icon {
margin-right: 6px;
font-size: 14px;
}
.session-selector {
flex-grow: 1;
min-width: 200px;
}
.session-button {
width: 30px;
height: 22px;
margin-left: 4px;
}
/* 聊天滚动视图 - Timeline 样式 */
.chat-scroll-view {
flex-grow: 1;
background-color: rgb(30, 30, 30);
padding: 0;
}
.chat-scroll-view > .unity-scroll-view__content-container {
padding: 0;
}
/* 消息容器基础样式 */
.message-container {
margin-bottom: 0;
padding: 10px16px;
background-color: transparent;
flex-direction: row;
align-items: flex-start;
}
.message-container:hover {
background-color: rgba(255, 255, 255, 0.03);
}
/* 消息头像/图标区域 */
.message-icon {
width: 20px;
height: 20px;
margin-right: 8px;
margin-top: 2px;
-unity-text-align: middle-center;
font-size: 14px;
flex-shrink: 0;
}
/* 用户消息 - 右对齐 */
.user-message {
flex-direction: row-reverse;
justify-content: flex-start;
border-right-width: 3px;
border-right-color: rgb(0, 122, 204);
background-color: rgba(0, 122, 204, 0.08);
}
.user-message.message-icon {
margin-right: 0;
margin-left: 8px;
}
.user-message.message-content {
color: rgb(230, 230, 230);
white-space: normal;
-unity-text-align: upper-right;
}
/* 助手消息 - 左对齐 */
.assistant-message {
background-color: transparent;
}
/* 助手消息的内容容器 - 浅绿色圆角框包围整个 markdown 内容 */
.assistant-content {
border-width: 2px;
border-color: rgb(144, 238, 144);
border-radius: 10px;
background-color: rgba(144, 238, 144, 0.08);
padding: 14px16px;
flex-direction: column;
flex-shrink: 0;
margin-top: 6px;
margin-bottom: 6px;
}
/* 助手消息中的普通文本 */
.assistant-message.message-content {
color: rgb(204, 204, 204);
white-space: normal;
padding: 0;
margin: 0;
}
/* Markdown 代码块内容样式(在绿框内部)*/
.markdown-code-block {
background-color: transparent;
border-width: 0;
padding: 4px;
margin: 0;
color: rgb(220, 220, 220);
font-size: 13px;
-unity-font-style: normal;
white-space: pre;
line-height: 1.5;
}
/* ScrollView 在绿框内 */
.assistant-content.unity-scroll-view {
background-color: transparent;
border-width: 0;
}
.assistant-content.unity-scroll-view__content-container {
background-color: transparent;
}
/* 系统消息 - 居中 */
.system-message {
border-left-width: 0;
background-color: transparent;
padding: 6px16px;
justify-content: center;
}
.system-message.message-icon {
color: rgb(106, 153, 85);
}
.system-message.message-content {
color: rgb(140, 140, 140);
white-space: normal;
font-size: 12px;
-unity-text-align: middle-center;
}
/* 错误消息 - 居中 */
.error-message {
border-left-width: 3px;
border-left-color: rgb(244, 71, 71);
background-color: rgba(244, 71, 71, 0.1);
}
.error-message.message-content {
color: rgb(244, 150, 150);
white-space: normal;
}
/* Thinking 状态样式 - 左对齐 */
.thinking-message {
border-left-width: 3px;
border-left-color: rgb(139, 92, 246);
background-color: transparent;
}
.thinking-message.message-content {
color: rgb(140, 140, 140);
white-space: normal;
-unity-font-style: italic;
}
/* 工具调用消息 - 左对齐 */
.tool-message {
border-left-width: 3px;
border-left-color: rgb(106, 153, 85);
background-color: rgba(106, 153, 85, 0.05);
padding: 8px16px;
}
.tool-message.message-content {
color: rgb(180, 180, 180);
font-size: 12px;
}
/* 消息内容 */
.message-content {
font-size: 13px;
padding: 0;
white-space: normal;
line-height: 1.4;
flex-grow: 1;
}
/* 工具指示点(绿点) */
.tool-dot {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: rgb(106, 153, 85);
margin-right: 6px;
margin-top: 5px;
flex-shrink: 0;
}
/* 输入区域 */
.input-container {
background-color: rgb(37, 37, 38);
padding: 10px12px;
border-top-width: 1px;
border-top-color: rgb(60, 60, 60);
flex-direction: column;
flex-shrink: 0;
}
/* 输入行 */
.input-row {
flex-direction: row;
flex-grow: 1;
}
/* 模式选择器容器 */
.mode-selector-container {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
padding-bottom: 6px;
}
.mode-label {
font-size: 12px;
color: rgb(180, 180, 180);
margin-right: 8px;
min-width: 40px;
}
.mode-selector {
min-width: 100px;
max-width: 120px;
height: 24px;
}
.mode-selector > .unity-base-field__input {
background-color: rgb(60, 60, 60);
border-color: rgb(80, 80, 80);
border-width: 1px;
border-radius: 3px;
padding: 2px6px;
color: rgb(204, 204, 204);
font-size: 11px;
height: 24px;
}
.mode-selector > .unity-base-field__input:hover {
border-color: rgb(100, 100, 100);
background-color: rgb(65, 65, 65);
}
.mode-selector > .unity-base-field__input:focus {
border-color: rgb(0, 122, 204);
}
.input-field {
flex-grow: 1;
min-height: 36px;
max-height: 100px;
background-color: rgb(60, 60, 60);
border-color: rgb(80, 80, 80);
border-width: 1px;
border-radius: 4px;
padding: 8px10px;
color: rgb(204, 204, 204);
font-size: 13px;
margin-right: 8px;
flex-shrink: 1;
}
.input-field:focus {
border-color: rgb(0, 122, 204);
border-width: 1px;
background-color: rgb(50, 50, 50);
}
.input-field > .unity-base-text-field__input {
background-color: transparent;
border-width: 0;
padding: 0;
color: rgb(204, 204, 204);
white-space: normal;
}
/* 发送按钮 */
.send-button {
background-color: rgb(0, 122, 204);
border-radius: 4px;
padding: 8px20px;
color: rgb(255, 255, 255);
font-size: 13px;
border-width: 0;
min-width: 60px;
height: 36px;
flex-shrink: 0;
align-self: flex-start;
}
.send-button:hover {
background-color: rgb(28, 151, 234);
}
.send-button:active {
background-color: rgb(0, 100, 170);
}
.send-button:disabled {
background-color: rgb(60, 60, 60);
color: rgb(120, 120, 120);
}
/* 状态栏样式 */
.status-bar {
background-color: rgb(0, 122, 204);
border-top-width: 0;
padding-left: 12px;
padding-right: 12px;
padding-top: 3px;
padding-bottom: 3px;
flex-direction: row;
justify-content: space-between;
height: 22px;
}
.status-label {
font-size: 11px;
-unity-font-style: normal;
color: rgb(255, 255, 255);
}
/* 工具执行指示器容器 */
.tool-indicator {
background-color: rgba(106, 153, 85, 0.15);
border-left-width: 3px;
border-left-color: rgb(106, 153, 85);
padding: 6px12px;
margin: 0;
flex-direction: row;
align-items: center;
}
.tool-indicator-label {
font-size: 12px;
color: rgb(180, 180, 180);
margin-left: 4px;
}
/* 进度条 */
.progress-bar {
height: 2px;
background-color: rgb(0, 122, 204);
margin-top: 0;
}
/* Todo 消息容器 */
.todo-message-container {
background-color: rgba(106, 153, 85, 0.08);
border-left-width: 3px;
border-left-color: rgb(106, 153, 85);
}
.todo-list-content {
padding: 4px0;
}
.todo-status-mark {
font-size: 12px;
color: rgb(180, 180, 180);
font-family: monospace;
}
.todo-item-text {
font-size: 12px;
color: rgb(204, 204, 204);
white-space: normal;
line-height: 1.4;
}
.todo-progress-text {
font-size: 11px;
color: rgb(140, 140, 140);
margin-top: 4px;
}
ClaudeCodeWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Anthropic;
using Anthropic.Models.Messages;
using Anthropic.Core;
namespaceClaudeCodeForUnity
{
publicclassClaudeCodeWindow : EditorWindow
{
private ScrollView _chatScrollView;
private TextField _inputField;
private Button _sendButton;
private StatusBar _statusBar;
private VisualElement _toolIndicator;
private DropdownField _modeSelector;
private VisualElement _currentTodoMessage;
private VisualElement _currentTaskMessage;
private DropdownField _sessionSelector;
private CancellationTokenSource _cancellationTokenSource;
privatebool _isTaskRunning;
private AnthropicClient _client;
private AgentLoop _agentLoop;
private BashAgent _bashAgent;
private FileAgent _fileAgent;
private TodoManager _todoManager;
private TaskManager _taskManager;
private SessionManager _sessionManager;
private ToolManager _toolManager;
private SubAgentManager _subAgentManager;
private BackgroundManager _backgroundTaskManager;
private SkillLoader _skillLoader;
private SkillAgent _skillAgent;
private TextManager _textManager;
privatestring _currentBaseUrl;
privatestring _currentApiKey;
privatestring _currentModel;
privatestring _currentSubAgentMode = "code";
[MenuItem("Window/Claude Code")]
publicstaticvoidShowWindow()
{
ClaudeCodeWindow window = GetWindow<ClaudeCodeWindow>();
window.titleContent = new GUIContent("Claude Code");
window.minSize = new Vector2(500, 600);
}
publicvoidCreateGUI()
{
_currentBaseUrl = ConfigManager.BaseUrl;
_currentApiKey = ConfigManager.ApiKey;
_currentModel = ConfigManager.Model;
_textManager = new TextManager();
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Column;
root.style.flexGrow = 1;
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Claude Code for Unity/Editor/UI/ClaudeCodeWindow.uss");
if (styleSheet != null)
root.styleSheets.Add(styleSheet);
root.Add(CreateHeader());
root.Add(CreateSessionToolbar());
_chatScrollView = CreateChatScrollView();
root.Add(_chatScrollView);
_toolIndicator = CreateToolIndicator();
root.Add(_toolIndicator);
root.Add(CreateInputContainer());
_statusBar = new StatusBar();
root.Add(_statusBar);
InitializeModules();
AddSystemMessage(_textManager.Get("welcome.message"));
AddSystemMessage($"{_textManager.Get("welcome.model")}{_currentModel} | {_textManager.Get("welcome.directory")}{ConfigManager.WorkingDirectory}");
if (_sessionManager != null)
{
AddSystemMessage($"📂 {_textManager.Get("welcome.session")}{_sessionManager.CurrentSession.Name}");
AddSystemMessage(_textManager.Get("welcome.session.hint"));
}
AddSystemMessage(_textManager.Get("welcome.tasks.hint"));
}
privatevoidInitializeModules()
{
try
{
_statusBar.UpdateStatus($"🔄 {_textManager.Get("status.initializing")}", StatusBar.StatusType.Info);
_statusBar.UpdateConnectionStatus(false);
_client = new AnthropicClient(new ClientOptions
{
AuthToken = _currentApiKey,
BaseUrl = new Uri(_currentBaseUrl),
HttpClient = new System.Net.Http.HttpClient()
});
var workingDirectory = ConfigManager.WorkingDirectory;
_bashAgent = new BashAgent(workingDirectory);
_fileAgent = new FileAgent(workingDirectory);
_todoManager = new TodoManager();
_todoManager.OnTodoListUpdated += OnTodoListUpdated;
_sessionManager = new SessionManager(workingDirectory);
_sessionManager.OnSessionCreated += OnSessionCreated;
_sessionManager.OnSessionSwitched += OnSessionSwitched;
_sessionManager.OnSessionUpdated += OnSessionUpdated;
_sessionManager.OnTaskCreated += OnTaskCreatedInSession;
_sessionManager.OnTaskUpdated += OnTaskUpdatedInSession;
_taskManager = new TaskManager(_client, _currentModel, _todoManager, workingDirectory);
_taskManager.OnCompactCompleted += OnCompactCompleted;
_backgroundTaskManager = new BackgroundManager(workingDirectory);
_skillLoader = new SkillLoader(ConfigManager.SkillsDirectory);
_skillAgent = new SkillAgent(_skillLoader);
_subAgentManager = new SubAgentManager(workingDirectory, _client, _currentModel);
_toolManager = new ToolManager(workingDirectory, _bashAgent, _fileAgent, _todoManager,
_taskManager, _sessionManager, _subAgentManager, _backgroundTaskManager, _skillAgent);
var systemPrompt = CreateSystemPrompt();
_agentLoop = new AgentLoop(_client, _currentModel, systemPrompt, _toolManager,
ToolManager.SkillType.Code, _backgroundTaskManager);
_agentLoop.OnTokenUsageUpdated += OnTokenUsageUpdated;
_statusBar.UpdateModel(_currentModel);
_statusBar.UpdateConnectionStatus(true);
_statusBar.UpdateTokenUsage(0, 4096);
_statusBar.ShowIdle();
RefreshSessionSelector();
RefreshTaskMessage();
Debug.Log($"[Claude Code] Initialized - Session: {_sessionManager.CurrentSession.Name}, Model: {_currentModel}");
}
catch (Exception ex)
{
Debug.LogError($"{_textManager.Get("error.init_failed")}: {ex.Message}");
Debug.LogException(ex);
_statusBar.UpdateStatus($"{_textManager.Get("error.init_failed")}: {ex.Message}", StatusBar.StatusType.Error);
_statusBar.UpdateConnectionStatus(false, _textManager.Get("error.connection_failed"));
}
}
privatestringCreateSystemPrompt()
{
var workingDirectory = ConfigManager.WorkingDirectory;
var sessionInfo = _sessionManager != null
? $"\n当前会话: {_sessionManager.CurrentSession.Name} (ID: {_sessionManager.CurrentSession.Id})"
: "";
var skillInfo = _skillAgent != null && _skillAgent.SkillCount > 0
? $@"
**可用技能**(任务匹配时用 Skill 工具调用):
{_skillAgent.GetSkillDescriptions()}"
: "";
return$@"你是一个位于 {workingDirectory} 的编程代理。{sessionInfo}
循环: 规划 -> 使用工具行动 -> 报告。
{skillInfo}
你可以使用以下工具:
- bash: 运行shell命令(快速命令)
- read_file: 读取文件内容
- write_file: 写入内容到文件
- edit_file: 替换文件中的精确文本
- TodoWrite: 更新短期任务列表(临时,压缩时清空)
- Task: 为专注的子任务生成子代理
- task_create: 创建持久化任务(自动关联到当前会话)
- task_update: 更新任务状态或依赖关系
- task_list: 列出当前会话的所有任务
- task_get: 获取任务详情
- background_run: 在后台运行长时间命令(如 build, test, install)
- check_background: 检查后台任务状态
- Skill: 加载特定领域的专业知识(当任务匹配技能描述时使用)
会话与任务系统:
- 每个会话有独立的任务列表
- 使用 task_create 创建的任务会自动关联到当前会话
- task_list 默认只显示当前会话的任务
- 切换会话时,会显示对应会话的任务
任务创建示例:
task_create(subject=""实现新功能X"", description=""详细说明"")
task_update(task_id=1, status=""in_progress"")
task_update(task_id=2, status=""completed"")
任务状态流转:
pending(待办) -> in_progress(进行中) -> completed(已完成)
后台任务使用:
- 对可能超过几秒的命令使用 background_run(如 build, test, npm install)
- 快速命令使用 bash
- 后台任务完成时,结果会自动注入对话(<background-results>)
技能使用:
- 当用户任务匹配技能描述时立即使用 Skill 工具
- 技能内容将注入对话,提供详细指令和最佳实践
- 技能不修改系统提示(保持 prompt cache 效率)
规则:
- 行动优先,不要只是解释。
- 不要凭空想象文件路径。如果不确定,先用 bash ls/find 查看。
- 最小化修改。不要过度工程化。
- 对多步骤计划,首先用 task_create 创建任务,然后逐步完成
- 完成后,总结做了什么改动。";
}
#region UI创建方法
privatevoidUpdateAgentSkillType()
{
if (_agentLoop == null) return;
var skillType = _currentSubAgentMode switch
{
"explore" => ToolManager.SkillType.Explore,
"plan" => ToolManager.SkillType.Plan,
"code" => ToolManager.SkillType.Code,
_ => ToolManager.SkillType.Code
};
_agentLoop.SetSkillType(skillType);
}
private VisualElement CreateHeader()
{
var header = new VisualElement();
header.AddToClassList("header");
var titleLabel = new Label("Claude Code");
titleLabel.AddToClassList("title");
header.Add(titleLabel);
return header;
}
private VisualElement CreateSessionToolbar()
{
var toolbar = new VisualElement();
toolbar.AddToClassList("session-toolbar");
// 会话图标
var sessionIcon = new Label("📂");
sessionIcon.AddToClassList("session-icon");
// 会话选择器
_sessionSelector = new DropdownField(_textManager.Get("window.session"));
_sessionSelector.AddToClassList("session-selector");
_sessionSelector.choices = new List<string> { _textManager.Get("window.session.default") };
_sessionSelector.value = _textManager.Get("window.session.default");
_sessionSelector.RegisterValueChangedCallback(evt =>
{
OnSessionDropdownChanged(evt.newValue);
});
// 新建会话按钮
var newSessionButton = new Button(() => CreateNewSession());
newSessionButton.text = "➕";
newSessionButton.AddToClassList("session-button");
// 重命名会话按钮
var renameButton = new Button(() => RenameCurrentSession());
renameButton.text = "✏️";
renameButton.AddToClassList("session-button");
// 删除会话按钮
var deleteButton = new Button(() => DeleteCurrentSession());
deleteButton.text = "🗑️";
deleteButton.AddToClassList("session-button");
toolbar.Add(sessionIcon);
toolbar.Add(_sessionSelector);
toolbar.Add(newSessionButton);
toolbar.Add(renameButton);
toolbar.Add(deleteButton);
return toolbar;
}
privatevoidOnSessionDropdownChanged(string selectedValue)
{
if (_sessionManager == null) return;
var sessions = _sessionManager.GetAllSessions();
var selectedIndex = _sessionSelector.choices.IndexOf(selectedValue);
if (selectedIndex >= 0 && selectedIndex < sessions.Count)
{
var session = sessions[selectedIndex];
if (session.Id != _sessionManager.CurrentSession.Id)
{
_sessionManager.SwitchToSession(session.Id);
}
}
}
privatevoidCreateNewSession()
{
if (_sessionManager == null) return;
var newName = $"Session {DateTime.Now:HH:mm:ss}";
var session = _sessionManager.CreateSession(newName);
// 自动切换到新会话
_sessionManager.SwitchToSession(session.Id);
}
privatevoidRenameCurrentSession()
{
if (_sessionManager == null) return;
// 简单实现:通过系统消息提示
AddSystemMessage(_textManager.Get("session.rename.hint"));
AddSystemMessage(_textManager.Get("session.rename.hint2"));
}
privatevoidDeleteCurrentSession()
{
if (_sessionManager == null) return;
var currentSession = _sessionManager.CurrentSession;
if (currentSession == null)
{
AddSystemMessage(_textManager.Get("session.no_active"));
return;
}
// 检查是否是最后一个会话
var allSessions = _sessionManager.GetAllSessions();
if (allSessions.Count <= 1)
{
AddSystemMessage(_textManager.Get("session.cannot_delete_last"));
return;
}
// 确认删除
var sessionName = currentSession.Name;
var tasks = _sessionManager.ListTasks();
var taskCount = tasks.Count;
// 确认删除会话
var deleteSession = EditorUtility.DisplayDialog(
_textManager.Get("session.delete.title"),
_textManager.GetFormat("session.delete.message", sessionName, taskCount),
_textManager.Get("session.delete.button_yes"),
_textManager.Get("session.delete.button_cancel")
);
if (!deleteSession)
{
AddSystemMessage(_textManager.Get("session.delete.cancelled"));
return;
}
var sessionIdToDelete = currentSession.Id;
// 切换到另一个会话
var otherSession = allSessions.FirstOrDefault(s => s.Id != sessionIdToDelete);
if (otherSession != null)
{
_sessionManager.SwitchToSession(otherSession.Id);
}
// 删除会话(会自动删除所有关联文件,包括任务)
if (_sessionManager.DeleteSession(sessionIdToDelete))
{
AddSystemMessage(_textManager.GetFormat("session.deleted", sessionName, taskCount));
// 刷新 UI
RefreshSessionSelector();
RefreshTaskMessage();
}
else
{
AddSystemMessage(_textManager.GetFormat("session.delete_failed", sessionName));
}
}
private ScrollView CreateChatScrollView()
{
var scrollView = new ScrollView(ScrollViewMode.Vertical);
scrollView.AddToClassList("chat-scroll-view");
return scrollView;
}
private VisualElement CreateToolIndicator()
{
var container = new VisualElement();
container.AddToClassList("tool-indicator");
container.style.display = DisplayStyle.None; // 默认隐藏
// 绿点指示器
var dot = new VisualElement();
dot.AddToClassList("tool-dot");
var label = new Label(_textManager.Get("tool.executing"));
label.AddToClassList("tool-indicator-label");
container.Add(dot);
container.Add(label);
return container;
}
privatevoidOnTodoListUpdated()
{
EditorApplication.delayCall += () =>
{
if (_todoManager == null) return;
// 移除旧的 Todo 消息(如果存在)
if (_currentTodoMessage != null && _chatScrollView.Contains(_currentTodoMessage))
{
_chatScrollView.Remove(_currentTodoMessage);
}
// 添加新的 Todo 消息
_currentTodoMessage = AddTodoMessage();
};
}
privatevoidOnTokenUsageUpdated(long inputTokens, long outputTokens, long totalTokens)
{
EditorApplication.delayCall += () =>
{
if (_statusBar != null)
{
// 假设最大 token 为 128k (128000)
_statusBar.UpdateTokenUsage((int)totalTokens, 128000);
}
};
}
privatevoidOnTaskCreatedInSession(int _, int __, string ___)
{
EditorApplication.delayCall += () =>
{
RefreshTaskMessage();
};
}
privatevoidOnTaskUpdatedInSession(int _, int __, string ___)
{
EditorApplication.delayCall += () =>
{
RefreshTaskMessage();
};
}
privatevoidOnCompactCompleted(string transcriptPath, string summary)
{
EditorApplication.delayCall += () =>
{
AddSystemMessage(_textManager.GetFormat("compact.completed", transcriptPath));
AddSystemMessage(_textManager.GetFormat("compact.summary", summary));
};
}
privatevoidOnSessionCreated(int _, string name)
{
EditorApplication.delayCall += () =>
{
AddSystemMessage(_textManager.GetFormat("session.created", name));
RefreshSessionSelector();
};
}
privatevoidOnSessionSwitched(int _, string name)
{
EditorApplication.delayCall += () =>
{
// 清空当前聊天显示
_chatScrollView.Clear();
// 加载会话的对话历史(如果需要显示)
AddSystemMessage(_textManager.GetFormat("session.switched", name));
// 刷新任务显示(显示当前会话的任务)
RefreshTaskMessage();
};
}
privatevoidOnSessionUpdated(int _)
{
EditorApplication.delayCall += () =>
{
RefreshSessionSelector();
};
}
privatevoidRefreshSessionSelector()
{
if (_sessionSelector == null || _sessionManager == null) return;
var sessions = _sessionManager.GetAllSessions();
var choices = sessions.Select(s => $"{s.Name} ({s.GetFormattedUpdatedAt()})").ToList();
_sessionSelector.choices = choices;
var currentSession = _sessionManager.CurrentSession;
if (currentSession != null && choices.Count > 0)
{
var currentChoice = $"{currentSession.Name} ({currentSession.GetFormattedUpdatedAt()})";
if (choices.Contains(currentChoice))
{
_sessionSelector.value = currentChoice;
}
}
}
privatevoidRefreshTaskMessage()
{
if (_taskManager == null) return;
// 移除旧的任务消息
if (_currentTaskMessage != null && _chatScrollView != null && _chatScrollView.Contains(_currentTaskMessage))
{
_chatScrollView.Remove(_currentTaskMessage);
_currentTaskMessage = null;
}
// 添加新的任务消息
_currentTaskMessage = AddTaskMessage();
}
private VisualElement AddTodoMessage()
{
if (_todoManager == null || _todoManager.Items.Count == 0)
returnnull;
var messageContainer = CreateMessageContainer("todo-message-container");
// 图标
var icon = new Label("📋");
icon.AddToClassList("message-icon");
// 内容容器
var contentContainer = new VisualElement();
contentContainer.AddToClassList("todo-list-content");
contentContainer.style.flexGrow = 1;
contentContainer.style.flexDirection = FlexDirection.Column;
var items = _todoManager.Items;
// 添加每个任务项
foreach (var item in items)
{
var itemElement = new VisualElement();
itemElement.style.flexDirection = FlexDirection.Row;
itemElement.style.marginBottom = 4;
// 状态标记
var statusMark = item.Status switch
{
"completed" => "[✓]",
"in_progress" => "[▶]",
_ => "[ ]"
};
var statusLabel = new Label(statusMark);
statusLabel.AddToClassList("todo-status-mark");
statusLabel.style.marginRight = 6;
statusLabel.style.minWidth = 30;
// 任务内容
var contentText = item.Status == "in_progress"
? $"{item.Content} ← {item.ActiveForm}"
: item.Content;
var contentLabel = new Label(contentText);
contentLabel.AddToClassList("todo-item-text");
contentLabel.style.flexGrow = 1;
// 根据状态设置样式
if (item.Status == "completed")
{
contentLabel.style.color = new Color(0.5f, 0.5f, 0.5f);
contentLabel.style.unityTextOutlineColor = new Color(0.4f, 0.4f, 0.4f);
}
elseif (item.Status == "in_progress")
{
contentLabel.style.color = new Color(0.7f, 0.7f, 1f);
}
itemElement.Add(statusLabel);
itemElement.Add(contentLabel);
contentContainer.Add(itemElement);
}
// 进度统计
var completed = items.Count(t => t.Status == "completed");
var progressLabel = new Label($"\n({completed}/{items.Count} completed)");
progressLabel.AddToClassList("todo-progress-text");
progressLabel.style.color = new Color(0.6f, 0.6f, 0.6f);
progressLabel.style.fontSize = 11;
contentContainer.Add(progressLabel);
messageContainer.Add(icon);
messageContainer.Add(contentContainer);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
return messageContainer;
}
private VisualElement AddTaskMessage()
{
if (_sessionManager == null)
returnnull;
// 从 SessionManager 获取当前会话的任务
var tasks = _sessionManager.ListTasks();
if (tasks == null || tasks.Count == 0)
returnnull;
var messageContainer = CreateMessageContainer("task-message-container");
// 图标
var icon = new Label("📁");
icon.AddToClassList("message-icon");
// 内容容器
var contentContainer = new VisualElement();
contentContainer.AddToClassList("task-list-content");
contentContainer.style.flexGrow = 1;
contentContainer.style.flexDirection = FlexDirection.Column;
// 标题
var titleLabel = new Label(_textManager.Get("task.list.persistent"));
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
titleLabel.style.marginBottom = 4;
titleLabel.style.color = new Color(0.8f, 0.8f, 1f);
contentContainer.Add(titleLabel);
// 添加每个任务
foreach (var task in tasks)
{
var itemElement = new VisualElement();
itemElement.style.flexDirection = FlexDirection.Row;
itemElement.style.marginBottom = 3;
// 状态标记
var marker = task.Status switch
{
"pending" => "[ ]",
"in_progress" => "[>]",
"completed" => "[x]",
_ => "[?]"
};
// 阻塞信息
var blockedInfo = task.BlockedBy != null && task.BlockedBy.Count > 0
? $" (blocked by: [{string.Join(", ", task.BlockedBy)}])"
: "";
var taskText = $"{marker} #{task.Id}: {task.Subject}{blockedInfo}";
var itemLabel = new Label(taskText);
itemLabel.AddToClassList("task-item-text");
itemLabel.style.flexGrow = 1;
itemLabel.style.fontSize = 12;
// 根据状态着色
if (task.Status == "completed")
{
itemLabel.style.color = new Color(0.5f, 0.8f, 0.5f); // 绿色 - 完成
}
elseif (task.Status == "in_progress")
{
itemLabel.style.color = new Color(0.7f, 0.7f, 1f); // 蓝色 - 进行中
}
elseif (task.BlockedBy != null && task.BlockedBy.Count > 0)
{
itemLabel.style.color = new Color(1f, 0.7f, 0.4f); // 橙色 - 阻塞
}
else
{
itemLabel.style.color = new Color(0.8f, 0.8f, 0.8f); // 白色 - 待办
}
itemElement.Add(itemLabel);
contentContainer.Add(itemElement);
}
// 统计信息
var stats = _sessionManager.GetTaskStats();
var statsLabel = new Label($"\n{stats}");
statsLabel.style.color = new Color(0.6f, 0.6f, 0.6f);
statsLabel.style.fontSize = 11;
statsLabel.style.unityFontStyleAndWeight = FontStyle.Italic;
contentContainer.Add(statsLabel);
messageContainer.Add(icon);
messageContainer.Add(contentContainer);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
return messageContainer;
}
private VisualElement CreateInputContainer()
{
var inputContainer = new VisualElement();
inputContainer.AddToClassList("input-container");
// 创建模式选择器区域(左下角)
var modeContainer = new VisualElement();
modeContainer.AddToClassList("mode-selector-container");
var modeLabel = new Label(_textManager.Get("window.mode"));
modeLabel.AddToClassList("mode-label");
_modeSelector = new DropdownField();
_modeSelector.AddToClassList("mode-selector");
_modeSelector.choices = new List<string> { "explore", "code", "plan" };
_modeSelector.value = _currentSubAgentMode;
_modeSelector.RegisterValueChangedCallback(evt =>
{
_currentSubAgentMode = evt.newValue;
UpdateAgentSkillType();
Debug.Log($"SubAgent 模式已切换为: {_currentSubAgentMode}");
});
modeContainer.Add(modeLabel);
modeContainer.Add(_modeSelector);
// 创建输入行(输入框 + 发送按钮)
var inputRow = new VisualElement();
inputRow.AddToClassList("input-row");
_inputField = new TextField();
_inputField.multiline = true;
_inputField.AddToClassList("input-field");
_inputField.RegisterCallback<KeyDownEvent>(OnInputKeyDown);
_sendButton = new Button(OnSendButtonClicked);
_sendButton.text = "Send";
_sendButton.AddToClassList("send-button");
inputRow.Add(_inputField);
inputRow.Add(_sendButton);
inputContainer.Add(modeContainer);
inputContainer.Add(inputRow);
return inputContainer;
}
#endregion
#region 事件处理
privatevoidOnInputKeyDown(KeyDownEvent evt)
{
// Ctrl+Enter 发送消息
if (evt.keyCode == KeyCode.Return && evt.ctrlKey)
{
OnSendButtonClicked();
evt.StopPropagation();
}
}
privateasyncvoidOnSendButtonClicked()
{
// 如果任务正在运行,停止任务
if (_isTaskRunning)
{
StopTask();
return;
}
string userMessage = _inputField.value?.Trim();
if (string.IsNullOrEmpty(userMessage))
{
return;
}
// 清空输入框
_inputField.value = string.Empty;
// 显示用户消息
AddUserMessage(userMessage);
// 创建新的 CancellationTokenSource
_cancellationTokenSource = new System.Threading.CancellationTokenSource();
_isTaskRunning = true;
// 切换按钮为停止模式
_sendButton.text = _textManager.Get("window.stop");
_sendButton.style.backgroundColor = new Color(0.96f, 0.28f, 0.28f); // 红色
// 更新状态栏
_statusBar.ShowThinking();
VisualElement thinkingMessage = null;
try
{
// 显示思考中的占位符
thinkingMessage = AddThinkingMessage();
// 处理消息并获取响应(支持取消)
var response = await _agentLoop.ProcessMessageAsync(userMessage, _cancellationTokenSource.Token);
// 移除思考中的占位符
if (thinkingMessage != null && _chatScrollView.Contains(thinkingMessage))
{
_chatScrollView.Remove(thinkingMessage);
}
// 显示助手响应
AddAssistantMessage(response);
// 更新状态栏
_statusBar.ShowIdle();
}
catch (OperationCanceledException)
{
// 任务被取消
if (thinkingMessage != null && _chatScrollView.Contains(thinkingMessage))
{
_chatScrollView.Remove(thinkingMessage);
}
AddSystemMessage($"⚠️ {_textManager.Get("status.task_cancelled")}");
_statusBar.UpdateStatus(_textManager.Get("status.task_cancelled"), StatusBar.StatusType.Warning);
Debug.Log("[AgentLoop] Task cancelled by user");
}
catch (Exception ex)
{
// 移除思考中的占位符
if (thinkingMessage != null && _chatScrollView.Contains(thinkingMessage))
{
_chatScrollView.Remove(thinkingMessage);
}
AddErrorMessage($"{_textManager.Get("status.error")}: {ex.Message}");
Debug.LogException(ex);
// 更新状态栏显示错误
_statusBar.UpdateStatus($"{_textManager.Get("status.error")}: {ex.Message}", StatusBar.StatusType.Error);
}
finally
{
// 恢复发送按钮状态
_isTaskRunning = false;
_sendButton.text = _textManager.Get("window.send");
_sendButton.style.backgroundColor = new Color(0f, 0.478f, 0.8f); // 蓝色
_sendButton.SetEnabled(true);
// 清理 CancellationTokenSource
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
_inputField.Focus();
}
}
privatevoidStopTask()
{
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
{
Debug.Log("[ClaudeCodeWindow] Stopping task...");
_cancellationTokenSource.Cancel();
// 立即更新 UI 状态
_sendButton.text = "Stopping...";
_sendButton.SetEnabled(false);
}
}
#endregion
#region 消息显示方法
privatevoidAddUserMessage(string message)
{
var messageContainer = CreateMessageContainer("user-message");
// 用户图标
var icon = new Label("👤");
icon.AddToClassList("message-icon");
var label = new Label(message);
label.AddToClassList("message-content");
messageContainer.Add(icon);
messageContainer.Add(label);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
}
private VisualElement AddAssistantMessage(string message)
{
var messageContainer = CreateMessageContainer("assistant-message");
// 助手图标
var icon = new Label("🤖");
icon.AddToClassList("message-icon");
// 创建主容器(不带框)
var mainContainer = new VisualElement();
mainContainer.style.flexDirection = FlexDirection.Column;
mainContainer.style.flexGrow = 1;
// 解析 markdown 并添加内容(会自动分离普通文本和代码块)
ParseMarkdownContent(mainContainer, message);
messageContainer.Add(icon);
messageContainer.Add(mainContainer);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
return messageContainer;
}
privatevoidParseMarkdownContent(VisualElement container, string message)
{
// 检测是否包含 markdown 标题(以 # 开头的行)
var lines = message.Split('\n');
int markdownStartIndex = -1;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].TrimStart();
if (line.StartsWith("# "))
{
markdownStartIndex = i;
break;
}
}
// 如果找到了 markdown 标题
if (markdownStartIndex > 0)
{
// 前面的文字(说明文字,不加框)
var beforeLines = lines.Take(markdownStartIndex).ToArray();
var beforeText = string.Join("\n", beforeLines).Trim();
if (!string.IsNullOrEmpty(beforeText))
{
var textLabel = new Label(beforeText);
textLabel.AddToClassList("message-content");
container.Add(textLabel);
}
// 后面的 markdown 内容(加绿框)
var markdownLines = lines.Skip(markdownStartIndex).ToArray();
var markdownContent = string.Join("\n", markdownLines);
var markdownContainer = new VisualElement();
markdownContainer.AddToClassList("assistant-content");
var scrollView = new ScrollView(ScrollViewMode.Vertical);
scrollView.style.maxHeight = 600;
scrollView.style.flexGrow = 1;
var codeLabel = new Label(markdownContent);
codeLabel.AddToClassList("markdown-code-block");
codeLabel.enableRichText = false;
codeLabel.style.whiteSpace = WhiteSpace.Pre;
scrollView.Add(codeLabel);
markdownContainer.Add(scrollView);
container.Add(markdownContainer);
}
else
{
// 没有 markdown 标题,使用旧逻辑处理代码块
var codeBlockPattern = @"```([\w]*)\s*\n([\s\S]*?)\n```";
var matches = System.Text.RegularExpressions.Regex.Matches(message, codeBlockPattern);
if (matches.Count == 0)
{
// 没有代码块,直接显示普通文本
var textLabel = new Label(message);
textLabel.AddToClassList("message-content");
container.Add(textLabel);
return;
}
int lastIndex = 0;
foreach (System.Text.RegularExpressions.Match match in matches)
{
// 添加代码块之前的普通文本(不加框)
if (match.Index > lastIndex)
{
var beforeText = message[lastIndex..match.Index].Trim();
if (!string.IsNullOrEmpty(beforeText))
{
var textLabel = new Label(beforeText);
textLabel.AddToClassList("message-content");
container.Add(textLabel);
}
}
// 创建绿色框容器
var markdownContainer = new VisualElement();
markdownContainer.AddToClassList("assistant-content");
var codeContent = match.Groups[2].Value;
var scrollView = new ScrollView(ScrollViewMode.Vertical);
scrollView.style.maxHeight = 600;
scrollView.style.flexGrow = 1;
var codeLabel = new Label(codeContent);
codeLabel.AddToClassList("markdown-code-block");
codeLabel.enableRichText = false;
codeLabel.style.whiteSpace = WhiteSpace.Pre;
scrollView.Add(codeLabel);
markdownContainer.Add(scrollView);
container.Add(markdownContainer);
lastIndex = match.Index + match.Length;
}
// 添加最后一个代码块之后的文本
if (lastIndex < message.Length)
{
var afterText = message[lastIndex..].Trim();
if (!string.IsNullOrEmpty(afterText))
{
var textLabel = new Label(afterText);
textLabel.AddToClassList("message-content");
container.Add(textLabel);
}
}
}
}
private VisualElement AddThinkingMessage()
{
var messageContainer = CreateMessageContainer("thinking-message");
// 思考图标
var icon = new Label("💭");
icon.AddToClassList("message-icon");
var label = new Label(_textManager.Get("status.thinking"));
label.AddToClassList("message-content");
messageContainer.Add(icon);
messageContainer.Add(label);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
return messageContainer;
}
privatevoidAddSystemMessage(string message)
{
var messageContainer = CreateMessageContainer("system-message");
// 系统图标
var icon = new Label("ℹ️");
icon.AddToClassList("message-icon");
var label = new Label(message);
label.AddToClassList("message-content");
messageContainer.Add(icon);
messageContainer.Add(label);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
}
privatevoidAddErrorMessage(string message)
{
var messageContainer = CreateMessageContainer("error-message");
// 错误图标
var icon = new Label("❌");
icon.AddToClassList("message-icon");
var label = new Label(message);
label.AddToClassList("message-content");
messageContainer.Add(icon);
messageContainer.Add(label);
_chatScrollView.Add(messageContainer);
ScrollToBottom();
}
private VisualElement CreateMessageContainer(string className)
{
var container = new VisualElement();
container.AddToClassList("message-container");
container.AddToClassList(className);
return container;
}
privatevoidScrollToBottom()
{
// 延迟滚动以确保布局已更新
EditorApplication.delayCall += () =>
{
_chatScrollView.scrollOffset = new Vector2(0, _chatScrollView.contentContainer.layout.height);
};
}
#endregion
privatevoidOnDestroy()
{
if (_client != null)
{
// 清理客户端资源
_client.HttpClient?.Dispose();
}
// 取消订阅事件
if (_todoManager != null)
{
_todoManager.OnTodoListUpdated -= OnTodoListUpdated;
}
if (_agentLoop != null)
{
_agentLoop.OnTokenUsageUpdated -= OnTokenUsageUpdated;
}
}
}
}
至此我们完成了一个极简的 Claude Code for Unity 的编写,结合之前的 MCP for Unity,你便可以在 Unity 中为所欲为了~
从 AI 工具的使用者到开发者,再到基建的搭建者——我们正在跨越最关键的一步。接下来,才是真正的硬核之旅:从零手搓大模型!(C++ 实现)
脚踢AI数学,手撕深度学习,生吞大模型……牛皮先吹下了,剩下的,就用代码和汗水来填坑吧!
已经开始带着六岁的小儿子学 C 语言编程了,小家伙还算感兴趣,磕磕绊绊也能写出第一个 “Hello World ”!
我这半吊子编程水平,总算有了传承的着落。,希望他今后在图形学、引擎、AI 领域早有所成。
夜雨聆风