902 lines
35 KiB
Markdown
902 lines
35 KiB
Markdown
# free-code .NET 10 迁移 — 代码审查与架构分析报告
|
||
|
||
> **审查版本**: 0.1.0 (初始迁移)
|
||
> **审查日期**: 2026-04-06
|
||
> **审查范围**: `src/` 目录下全部 16 个项目、~200 个 .cs 文件
|
||
> **对照基准**: 原始 TypeScript 项目 v2.1.87 (512,834 行代码)
|
||
> **审查人角色**: 多语言编码架构师,以 .NET 平台最佳实践为评审标准
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [一、总体评估](#一总体评估)
|
||
- [二、架构设计分析](#二架构设计分析)
|
||
- [三、关键设计问题 (Critical/High)](#三关键设计问题-criticalhigh)
|
||
- [四、实现细节问题 (Medium/Low)](#四实现细节问题-mediumlow)
|
||
- [五、性能优化建议](#五性能优化建议)
|
||
- [六、.NET 惯用法改进](#六net-惯用法改进)
|
||
- [七、功能对等性分析](#七功能对等性分析)
|
||
- [八、优化路线图](#八优化路线图)
|
||
- [九、总结](#九总结)
|
||
|
||
---
|
||
|
||
## 一、总体评估
|
||
|
||
### 综合评分:6.5 / 10
|
||
|
||
| 维度 | 评分 | 说明 |
|
||
|------|------|------|
|
||
| **架构分层** | 7/10 | 五层架构清晰,但项目拆分过细 |
|
||
| **接口设计** | 5/10 | 接口驱动思路正确,但存在弱类型(`object`)和过度抽象 |
|
||
| **DI 实践** | 7/10 | 构造函数注入完整,但注册分散、顺序敏感 |
|
||
| **异步模式** | 7/10 | IAsyncEnumerable + CancellationToken 使用得当 |
|
||
| **.NET 惯用法** | 5/10 | 未充分利用 C# 13/.NET 10 特性,部分代码像"翻译的 TypeScript" |
|
||
| **性能考量** | 4/10 | JSON 处理冗余分配、HttpClient 管理缺失、进程创建频繁 |
|
||
| **测试覆盖** | 6/10 | 9 个测试项目结构完整,但需验证实际覆盖率 |
|
||
| **功能对等** | 5/10 | 骨架代码完整,但核心特性(compact、PKCE、prompt-cache)缺失 |
|
||
|
||
### 总体判断
|
||
|
||
迁移工作完成了 **架构骨架搭建**:16 个项目的分层、核心接口定义、DI 注册管道、查询引擎主循环均已实现。但在以下方面存在显著不足:
|
||
|
||
1. **设计问题**: ToolRegistry 的 switch 分发、AppStateStore 弱类型、QueryEngine 神类
|
||
2. **性能问题**: JSON 序列化的 write→parse→clone 反模式、HttpClient 未池化
|
||
3. **功能缺失**: 上下文压缩、OAuth PKCE、prompt-cache 优化、Terminal.Gui 完整 REPL
|
||
4. **.NET 化不足**: 大量 `lock(object)` 同步、Console.Error 替代 ILogger、同步方法包装为异步
|
||
|
||
---
|
||
|
||
## 二、架构设计分析
|
||
|
||
### 2.1 当前架构分层
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ FreeCode.TerminalUI │
|
||
│ Terminal.Gui v2 (REPL/Components/Theme) │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ FreeCode (入口) │
|
||
│ Program.cs → QuickPathHandler → Host → IAppRunner │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ FreeCode.Engine │ FreeCode.Commands │
|
||
│ QueryEngine (423行) │ 95+ Command 类 │
|
||
│ SystemPromptBuilder │ │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ FreeCode.Tools │ FreeCode.Services │
|
||
│ 45+ Tool 类 │ Auth/Memory/Voice/... │
|
||
│ ToolRegistry (365行) │ │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ FreeCode.ApiProviders │ FreeCode.Mcp │ FreeCode.Lsp│
|
||
│ 5 个 LLM Provider │ 自研 MCP SDK │ LSP Client │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ FreeCode.Core (28 接口 / 19 模型 / 18 枚举) │
|
||
│ FreeCode.State (AppState record) │
|
||
│ FreeCode.Features (FeatureFlags) │
|
||
│ FreeCode.Skills / FreeCode.Plugins / FreeCode.Tasks │
|
||
│ FreeCode.Bridge │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 项目依赖关系(实际)
|
||
|
||
```
|
||
FreeCode.Core ←── 所有项目
|
||
↑
|
||
├── FreeCode.Engine (引用 Core)
|
||
├── FreeCode.ApiProviders (引用 Core)
|
||
├── FreeCode.Services (引用 Core)
|
||
├── FreeCode.State (引用 Core)
|
||
├── FreeCode.Features (引用 Core)
|
||
├── FreeCode.Tools (引用 Core + Services + Mcp)
|
||
├── FreeCode.Commands (引用 Core + Engine + State + Skills)
|
||
├── FreeCode.Mcp (引用 Core)
|
||
├── FreeCode.Lsp (引用 Core)
|
||
├── FreeCode.Bridge (引用 Core)
|
||
├── FreeCode.Skills (引用 Core + Services)
|
||
├── FreeCode.Plugins (引用 Core)
|
||
├── FreeCode.Tasks (引用 Core + Services)
|
||
├── FreeCode.TerminalUI (引用 Core + State)
|
||
└── FreeCode (引用全部 15 个项目)
|
||
```
|
||
|
||
### 2.3 数据流
|
||
|
||
```
|
||
用户输入
|
||
→ Program.Main() / QuickPathHandler
|
||
→ IAppRunner.RunAsync()
|
||
→ IQueryEngine.SubmitMessageAsync(content)
|
||
→ IPromptBuilder.BuildAsync() // 构建 System Prompt
|
||
→ IToolRegistry.GetToolsAsync() // 获取可用工具列表
|
||
→ IApiProviderRouter.GetActiveProvider() // 路由到 API 提供商
|
||
→ IApiProvider.StreamAsync() // 流式 LLM 响应
|
||
→ while (shouldContinue) { ... } // 工具调用循环
|
||
→ ExecuteToolAsync() // 执行工具
|
||
→ AppendMessage() // 追加消息到历史
|
||
→ TerminalUI 渲染输出
|
||
```
|
||
|
||
---
|
||
|
||
## 三、关键设计问题 (Critical/High)
|
||
|
||
### 问题 3.1 [Critical] ToolRegistry 巨型 switch 分发
|
||
|
||
**位置**: `src/FreeCode.Tools/ToolRegistry.cs:162-213`
|
||
|
||
**问题描述**:
|
||
`ExecuteToolAsync` 方法包含一个 48 个 case 的 switch 表达式,每添加一个新工具需要同时修改三处:注册列表(GetBaseTools)、switch 表达式、Input/Output DTO 类。这是典型的 Open-Closed 原则违反。
|
||
|
||
```csharp
|
||
// 当前实现 — 每增加一个工具就要改这里
|
||
return tool.Name switch
|
||
{
|
||
"Agent" => await ExecuteAsync<AgentToolInput, AgentToolOutput>(...),
|
||
"Bash" => await ExecuteAsync<BashToolInput, BashToolOutput>(...),
|
||
"Read" => await ExecuteAsync<FileReadToolInput, string>(...),
|
||
// ... 48 个 case
|
||
_ => ($"Unsupported tool execution for {tool.Name}", true, false)
|
||
};
|
||
```
|
||
|
||
**根因**: `ToolBase` 缺少统一的 `ExecuteAsync(JsonElement, ToolExecutionContext, CancellationToken)` 方法,导致 ToolRegistry 必须知道每个工具的具体类型才能调用泛型 `ExecuteAsync<TInput, TOutput>`。
|
||
|
||
**建议方案**: 在 `ToolBase` 中添加基于 `JsonElement` 的统一执行入口:
|
||
|
||
```csharp
|
||
// 建议方案:ToolBase 添加通用执行方法
|
||
public abstract class ToolBase : ITool
|
||
{
|
||
// 新增:统一的 JSON 执行入口
|
||
public abstract Task<(string Output, bool IsAllowed, bool ShouldContinue)> ExecuteFromJsonAsync(
|
||
JsonElement input, ToolExecutionContext context, CancellationToken ct);
|
||
|
||
// 子类实现
|
||
public override Task<(string, bool, bool)> ExecuteFromJsonAsync(
|
||
JsonElement input, ToolExecutionContext context, CancellationToken ct)
|
||
{
|
||
var typedInput = JsonSerializer.Deserialize<BashToolInput>(input.GetRawText())!;
|
||
// ... execute logic
|
||
}
|
||
}
|
||
|
||
// ToolRegistry 简化为:
|
||
if (tool is ToolBase toolBase)
|
||
{
|
||
return await toolBase.ExecuteFromJsonAsync(input, executionContext, ct);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 3.2 [Critical] IAppStateStore 弱类型 `object` 接口
|
||
|
||
**位置**: `src/FreeCode.Core/Interfaces/IAppStateStore.cs` (全部 11 行)
|
||
|
||
**问题描述**:
|
||
核心状态接口使用 `object` 作为状态类型,迫使所有消费者进行强制类型转换。这完全违背了 C# 强类型系统的设计理念。
|
||
|
||
```csharp
|
||
// 当前 — 弱类型
|
||
public interface IAppStateStore
|
||
{
|
||
object GetState(); // 返回 object
|
||
void Update(Func<object, object> updater); // Func<object, object>
|
||
IDisposable Subscribe(Action<object> listener); // Action<object>
|
||
event EventHandler<StateChangedEventArgs>? StateChanged;
|
||
}
|
||
```
|
||
|
||
**连锁影响**:
|
||
- `AppStateStore.cs:33` — 需要 `as AppState ?? throw new InvalidCastException()`
|
||
- `ToolRegistry.cs:288-295` — `ResolvePermissionMode` 使用反射 `GetProperty("PermissionMode")`
|
||
- 所有状态消费者都需要 `(AppState)store.GetState()` 强转
|
||
|
||
**建议方案**: 使用泛型接口或直接暴露 AppState:
|
||
|
||
```csharp
|
||
// 方案A:泛型接口
|
||
public interface IStateStore<TState> where TState : notnull
|
||
{
|
||
TState GetState();
|
||
void Update(Func<TState, TState> updater);
|
||
IDisposable Subscribe(Action<TState> listener);
|
||
event EventHandler<StateChangedEventArgs<TState>>? StateChanged;
|
||
}
|
||
|
||
// 方案B:直接强类型(更简单,推荐)
|
||
public interface IAppStateStore
|
||
{
|
||
AppState GetState();
|
||
void Update(Func<AppState, AppState> updater);
|
||
IDisposable Subscribe(Action<AppState> listener);
|
||
event EventHandler<StateChangedEventArgs>? StateChanged;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 3.3 [Critical] QueryEngine 神类 + 构造函数注入 Func 委托
|
||
|
||
**位置**: `src/FreeCode.Engine/QueryEngine.cs` (全文 423 行)
|
||
|
||
**问题描述**:
|
||
1. **构造函数接收 9 个参数**(第 12-20 行),其中 `toolExecutor` 是一个复杂的 `Func<...>` 委托,实际上是 `ToolRegistry.ExecuteToolAsync` 方法的引用。这破坏了依赖注入的原则——应该注入接口而非具体方法的委托。
|
||
2. 单一类承担了:流式响应处理、工具执行调度、消息历史管理、JSON 序列化、Token 估算、CancellationToken 生命周期管理。
|
||
|
||
```csharp
|
||
// QueryEngine 构造函数(第 12-20 行)
|
||
public sealed class QueryEngine(
|
||
IApiProviderRouter apiProviderRouter,
|
||
IToolRegistry toolRegistry,
|
||
IPermissionEngine permissionEngine,
|
||
IPromptBuilder promptBuilder,
|
||
ISessionMemoryService sessionMemoryService,
|
||
IFeatureFlagService featureFlagService,
|
||
Func<string, JsonElement, IPermissionEngine, ToolPermissionContext?, CancellationToken,
|
||
Task<(string Output, bool IsAllowed, bool ShouldContinue)>>? toolExecutor, // ← 这是什么?
|
||
ILogger<QueryEngine> logger) : IQueryEngine
|
||
```
|
||
|
||
**建议方案**: 将职责拆分为:
|
||
|
||
```csharp
|
||
// 1. IToolExecutor — 工具执行职责
|
||
public interface IToolExecutor
|
||
{
|
||
Task<(string Output, bool IsAllowed, bool ShouldContinue)> ExecuteAsync(
|
||
string toolName, JsonElement input, ToolPermissionContext? context, CancellationToken ct);
|
||
}
|
||
|
||
// 2. IMessageStore — 消息管理职责
|
||
public interface IMessageStore
|
||
{
|
||
void Append(Message message);
|
||
IReadOnlyList<Message> GetAll();
|
||
}
|
||
|
||
// 3. IJsonSerializer — JSON 序列化职责(可选)
|
||
public interface IApiRequestSerializer
|
||
{
|
||
JsonElement SerializeMessages(IReadOnlyList<Message> messages);
|
||
JsonElement SerializeTools(IReadOnlyList<ITool> tools);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 3.4 [High] AppState 巨型 Record — 单体状态
|
||
|
||
**位置**: `src/FreeCode.State/AppState.cs` (49 个属性)
|
||
|
||
**问题描述**:
|
||
单个 `AppState` record 包含 49 个属性,涵盖配置、权限、任务、MCP、插件、远程、UI、Agent 等所有领域。任何属性变更都会触发整个 record 的 `with {}` 重建,所有订阅者都会收到通知,即使它们只关心某个特定属性。
|
||
|
||
```csharp
|
||
public sealed record AppState
|
||
{
|
||
public SettingsJson Settings { get; init; } // 配置
|
||
public PermissionMode PermissionMode { get; init; } // 权限
|
||
public IReadOnlyDictionary<string, BackgroundTask> Tasks { get; init; } // 任务
|
||
public McpState Mcp { get; init; } // MCP
|
||
public PluginState Plugins { get; init; } // 插件
|
||
public RemoteConnectionStatus RemoteConnectionStatus { get; init; } // 远程
|
||
public Companion? Companion { get; init; } // 同伴
|
||
public NotificationState Notifications { get; init; } // 通知
|
||
// ... 41 more properties
|
||
}
|
||
```
|
||
|
||
**建议方案**: 分片状态管理
|
||
|
||
```csharp
|
||
// 按领域拆分状态切片
|
||
public sealed record AppState
|
||
{
|
||
public ConversationState Conversation { get; init; } = new();
|
||
public McpState Mcp { get; init; } = McpState.Empty;
|
||
public TaskState Tasks { get; init; } = new();
|
||
public UiState Ui { get; init; } = new();
|
||
public PluginState Plugins { get; init; } = PluginState.Empty;
|
||
}
|
||
|
||
// 支持选择性订阅
|
||
public interface IStateSlice<T>
|
||
{
|
||
T Value { get; }
|
||
IObservable<T> Observe();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 3.5 [High] PermissionEngine 从 IServiceProvider 获取工具实例
|
||
|
||
**位置**: `src/FreeCode.Services/PermissionEngine.cs:13`
|
||
|
||
```csharp
|
||
public Task<PermissionResult> CheckAsync(string toolName, object input, ToolExecutionContext context)
|
||
{
|
||
var tool = context.Services.GetService(typeof(ITool)) as ITool;
|
||
var isReadOnly = tool?.IsReadOnly(input) ?? false;
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**问题**:
|
||
1. `context.Services` 是 `ToolRegistry.ToolExecutionServiceProvider`(第 297-301 行),一个自定义 IServiceProvider,其唯一目的是返回当前工具实例。这个设计极其不直观。
|
||
2. `ITool` 是一个通用接口,`GetService(typeof(ITool))` 语义上应该返回任意工具实例,但实际返回的是"当前正在执行的工具"。
|
||
3. 如果 `ToolExecutionServiceProvider` 未正确设置,会静默返回 null 并 fallback 到 `isReadOnly = false`。
|
||
|
||
**建议**: 直接在 `ToolExecutionContext` 中携带 `IsReadOnly` 信息:
|
||
|
||
```csharp
|
||
public record ToolExecutionContext(
|
||
string WorkingDirectory,
|
||
PermissionMode PermissionMode,
|
||
bool IsToolReadOnly, // ← 直接传入
|
||
// ...
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 3.6 [High] ToolBase 静态 JsonDocument 内存泄漏
|
||
|
||
**位置**: `src/FreeCode.Tools/ToolBase.cs:9`
|
||
|
||
```csharp
|
||
public abstract class ToolBase : ITool
|
||
{
|
||
private static readonly JsonElement EmptySchema = JsonDocument.Parse("{}").RootElement.Clone();
|
||
// ...
|
||
public virtual JsonElement GetInputSchema() => EmptySchema;
|
||
}
|
||
```
|
||
|
||
**问题**: `JsonDocument.Parse("{}")` 创建的 `JsonDocument` 永远不会被 Dispose(因为只保留了 `RootElement.Clone()` 的引用)。虽然在这个特定场景下影响很小(只有一个 `"{}"` 对象),但这违反了 `JsonDocument` 的使用规范——应该保持 `JsonDocument` 存活直到 `JsonElement` 不再使用。
|
||
|
||
**建议方案**:
|
||
|
||
```csharp
|
||
public abstract class ToolBase : ITool
|
||
{
|
||
private static readonly JsonObject EmptySchemaNode = new JsonObject();
|
||
|
||
// 方案A:使用 ImmutableByteArray
|
||
private static readonly byte[] EmptySchemaBytes = "{}"u8.ToArray();
|
||
public virtual JsonElement GetInputSchema()
|
||
{
|
||
using var doc = JsonDocument.Parse(EmptySchemaBytes);
|
||
return doc.RootElement.Clone();
|
||
}
|
||
|
||
// 方案B(更好):改用 JsonNode API (.NET 10 推荐)
|
||
public virtual JsonNode? GetInputSchema() => null;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、实现细节问题 (Medium/Low)
|
||
|
||
### 问题 4.1 [Medium] JSON "write → parse → clone" 反模式
|
||
|
||
**位置**:
|
||
- `src/FreeCode.Engine/QueryEngine.cs:263-285` (BuildApiToolsJsonAsync)
|
||
- `src/FreeCode.Engine/QueryEngine.cs:287-339` (BuildApiMessagesJson)
|
||
- `src/FreeCode.Engine/QueryEngine.cs:361-397` (BuildAssistantContent)
|
||
|
||
**问题描述**:
|
||
三个方法都使用相同模式:`ArrayBufferWriter → Utf8JsonWriter → JsonDocument.Parse(自己写的内容) → RootElement.Clone()`。这相当于把数据序列化为 JSON 字节后,再反序列化回来,最后再克隆一次。
|
||
|
||
```csharp
|
||
// 当前:write → parse → clone(3次内存分配)
|
||
var buffer = new ArrayBufferWriter<byte>(4096);
|
||
using (var writer = new Utf8JsonWriter(buffer)) { /* write */ writer.Flush(); }
|
||
using var doc = JsonDocument.Parse(buffer.WrittenMemory); // 分配1:parse
|
||
return doc.RootElement.Clone(); // 分配2:clone
|
||
```
|
||
|
||
**建议**: 既然最终结果是 `JsonElement`(不可变),可以直接保留 `ArrayBufferWriter` 的字节并从中创建 `JsonElement`,或改用 `JsonNode` API 避免 Clone:
|
||
|
||
```csharp
|
||
// 优化:直接返回写入的字节
|
||
private static JsonElement BuildApiToolsJson(IReadOnlyList<ITool> tools)
|
||
{
|
||
var buffer = new ArrayBufferWriter<byte>(4096);
|
||
using (var writer = new Utf8JsonWriter(buffer))
|
||
{
|
||
// ... write
|
||
writer.Flush();
|
||
}
|
||
// 使用 JsonDocument 但不 Clone — 让 JsonDocument 持有 buffer 的副本
|
||
var doc = JsonDocument.Parse(buffer.WrittenSpan);
|
||
return doc.RootElement; // JsonDocument 本身持有数据
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.2 [Medium] EstimateTokens 重复实现
|
||
|
||
**位置**:
|
||
- `src/FreeCode.Engine/QueryEngine.cs:415-422`
|
||
- `src/FreeCode.Services/SessionMemoryService.cs:148-155`
|
||
|
||
```csharp
|
||
// QueryEngine.cs:415
|
||
private static int EstimateTokens(object? content)
|
||
=> content switch
|
||
{
|
||
null => 0,
|
||
string text => Math.Max(1, text.Length / 4),
|
||
JsonElement jsonElement => Math.Max(1, jsonElement.ToString().Length / 4),
|
||
_ => Math.Max(1, (content.ToString()?.Length ?? 0) / 4),
|
||
};
|
||
|
||
// SessionMemoryService.cs:148 — 几乎完全相同
|
||
private static int EstimateTokens(object? content)
|
||
=> content switch
|
||
{
|
||
null => 0,
|
||
string text => Math.Max(1, text.Length / 4),
|
||
JsonElement element => Math.Max(1, element.ToString().Length / 4),
|
||
_ => Math.Max(1, (content.ToString()?.Length ?? 0) / 4),
|
||
};
|
||
```
|
||
|
||
**建议**: 提取到 `FreeCode.Core` 中的静态工具类:
|
||
|
||
```csharp
|
||
// src/FreeCode.Core/Utilities/TokenEstimator.cs
|
||
public static class TokenEstimator
|
||
{
|
||
public static int Estimate(object? content) => content switch
|
||
{
|
||
null => 0,
|
||
string text => Math.Max(1, text.Length / 4),
|
||
JsonElement json => Math.Max(1, json.ToString().Length / 4),
|
||
_ => Math.Max(1, (content.ToString()?.Length ?? 0) / 4),
|
||
};
|
||
|
||
public static int EstimateTotal(IReadOnlyList<Message> messages)
|
||
{
|
||
var tokens = 0;
|
||
foreach (var msg in messages)
|
||
{
|
||
tokens += Estimate(msg.Content);
|
||
tokens += string.IsNullOrWhiteSpace(msg.ToolUseId) ? 0 : 8;
|
||
tokens += string.IsNullOrWhiteSpace(msg.ToolName) ? 0 : 4;
|
||
}
|
||
return tokens;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.3 [Medium] AnthropicProvider HttpClient 未通过 DI 管理
|
||
|
||
**位置**: `src/FreeCode.ApiProviders/AnthropicProvider.cs:18-25`
|
||
|
||
```csharp
|
||
public AnthropicProvider(HttpClient? httpClient = null)
|
||
{
|
||
_httpClient = httpClient ?? new HttpClient(); // 可能每次 new
|
||
_baseUrl = Environment.GetEnvironmentVariable("ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com";
|
||
_apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? ...;
|
||
}
|
||
```
|
||
|
||
同样的问题存在于 `BedrockProvider`、`CodexProvider`、`FoundryProvider`、`VertexProvider` 以及 `AuthService.ExchangeCodeForTokenAsync` 中的 `new HttpClient()`(第130行)。
|
||
|
||
**建议**: 使用 `IHttpClientFactory`:
|
||
|
||
```csharp
|
||
// ServiceCollectionExtensions.cs
|
||
services.AddHttpClient<AnthropicProvider>(client =>
|
||
{
|
||
client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
|
||
});
|
||
|
||
// AnthropicProvider.cs
|
||
public AnthropicProvider(HttpClient httpClient) // DI 注入,不可为 null
|
||
{
|
||
_httpClient = httpClient;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.4 [Medium] SystemPromptBuilder 每次构建启动 2 个 git 进程
|
||
|
||
**位置**: `src/FreeCode.Engine/SystemPromptBuilder.cs:82-139`
|
||
|
||
```csharp
|
||
private async Task<string> BuildContextInfoAsync(...)
|
||
{
|
||
var branch = await RunGitCommandAsync("rev-parse --abbrev-ref HEAD").ConfigureAwait(false);
|
||
var status = await RunGitCommandAsync("status --porcelain").ConfigureAwait(false);
|
||
// ...
|
||
}
|
||
|
||
private async Task<string> RunGitCommandAsync(string arguments)
|
||
{
|
||
using var process = new Process { /* ... */ }; // 每次创建新进程
|
||
process.Start();
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**问题**: 每次 `BuildAsync` 调用都会启动 2 个 `git` 子进程。在快速对话场景下,这可能导致明显的延迟。
|
||
|
||
**建议**: 缓存 git 信息,仅在检测到文件系统变更时刷新:
|
||
|
||
```csharp
|
||
public sealed class GitInfoProvider
|
||
{
|
||
private string? _cachedBranch;
|
||
private string? _cachedStatus;
|
||
private DateTime _lastRefresh;
|
||
private readonly TimeSpan _cacheDuration = TimeSpan.FromSeconds(2);
|
||
|
||
public async Task<(string Branch, string Status)> GetInfoAsync()
|
||
{
|
||
if (DateTime.UtcNow - _lastRefresh < _cacheDuration && _cachedBranch is not null)
|
||
return (_cachedBranch, _cachedStatus!);
|
||
|
||
// 并行执行两个 git 命令
|
||
var branchTask = RunGitCommandAsync("rev-parse --abbrev-ref HEAD");
|
||
var statusTask = RunGitCommandAsync("status --porcelain");
|
||
await Task.WhenAll(branchTask, statusTask).ConfigureAwait(false);
|
||
|
||
_cachedBranch = branchTask.Result;
|
||
_cachedStatus = statusTask.Result;
|
||
_lastRefresh = DateTime.UtcNow;
|
||
return (_cachedBranch, _cachedStatus);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.5 [Medium] BashTool.IsReadOnly 简单前缀匹配
|
||
|
||
**位置**: `src/FreeCode.Tools/BashTool.cs:44-55`
|
||
|
||
```csharp
|
||
public override bool IsReadOnly(object input)
|
||
{
|
||
var command = input is BashToolInput toolInput ? toolInput.Command : input as string;
|
||
var trimmed = command.TrimStart();
|
||
var readOnlyPrefixes = new[] { "ls", "cat", "grep", "find", "pwd", ... };
|
||
return readOnlyPrefixes.Any(prefix => trimmed.StartsWith(prefix, StringComparison.Ordinal));
|
||
}
|
||
```
|
||
|
||
**问题**:
|
||
1. 每次调用创建新数组(可改为 `static readonly`)
|
||
2. `lsblk`、`lsof`、`catdoc` 等命令会被错误匹配为 `ls`、`cat`
|
||
3. `git status --short && rm -rf /` 这样的复合命令会通过检测
|
||
|
||
**建议**: 使用单词边界匹配 + 管道/链式检测:
|
||
|
||
```csharp
|
||
private static readonly string[] ReadOnlyCommands = ["ls", "cat", "grep", "find", "pwd", "head", "tail", "stat", "wc", "du", "which", "whoami", "git"];
|
||
|
||
public override bool IsReadOnly(object input)
|
||
{
|
||
var command = input is BashToolInput t ? t.Command : input as string;
|
||
if (string.IsNullOrWhiteSpace(command)) return false;
|
||
|
||
// 检测管道/链式命令
|
||
if (command.Contains('|') || command.Contains(';') || command.Contains("&&") || command.Contains("||"))
|
||
return false;
|
||
|
||
var firstWord = command.TrimStart().Split(' ', 2)[0];
|
||
var baseCommand = firstWord.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? firstWord;
|
||
|
||
return ReadOnlyCommands.Contains(baseCommand, StringComparer.Ordinal)
|
||
|| (baseCommand == "git" && command.TrimStart().StartsWith("git status", StringComparison.Ordinal));
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.6 [Medium] 硬编码配置值
|
||
|
||
| 位置 | 硬编码值 | 说明 |
|
||
|------|----------|------|
|
||
| `AnthropicProvider.cs:31` | `"2023-06-01"` | Anthropic API 版本号 |
|
||
| `AnthropicProvider.cs:145` | `4096` | max_tokens 限制 |
|
||
| `BashTool.cs:128` | `"/bin/zsh"` | 默认 shell 路径 |
|
||
| `AuthService.cs:95` | `38465` | OAuth 回调端口 |
|
||
| `SessionMemoryService.cs:10-11` | `4000`, `8` | Token/ToolCall 阈值 |
|
||
| `SystemPromptBuilder.cs:157-166` | BaseInstructions | 基础系统提示词 |
|
||
|
||
**建议**: 移至 `appsettings.json` 和 Options pattern:
|
||
|
||
```csharp
|
||
// appsettings.json
|
||
{
|
||
"Anthropic": {
|
||
"ApiVersion": "2023-06-01",
|
||
"MaxTokens": 4096,
|
||
"BaseUrl": "https://api.anthropic.com"
|
||
},
|
||
"Shell": {
|
||
"DefaultPath": "/bin/zsh",
|
||
"Timeout": 120000
|
||
},
|
||
"Memory": {
|
||
"TokenThreshold": 4000,
|
||
"ToolCallThreshold": 8
|
||
}
|
||
}
|
||
|
||
// 使用 IOptions<T>
|
||
public sealed class AnthropicProvider(HttpClient httpClient, IOptions<AnthropicOptions> options)
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.7 [Medium] Console.Error.WriteLine 替代结构化日志
|
||
|
||
**位置**:
|
||
- `src/FreeCode.Services/SessionMemoryService.cs:221`
|
||
- `src/FreeCode.Services/AuthService.cs:210`
|
||
- `src/FreeCode.Services/KeychainTokenStorage.cs` (多处)
|
||
- `src/FreeCode/Program.cs:73`
|
||
|
||
```csharp
|
||
// 当前 — 非结构化输出
|
||
Console.Error.WriteLine($"Warning: MCP connection failed: {ex.Message}");
|
||
Console.Error.WriteLine($"Failed to open browser: {ex.Message}");
|
||
Console.Error.WriteLine($"Failed to save memory entry '{entry.Id}': {ex.Message}");
|
||
```
|
||
|
||
**建议**: 统一使用 `ILogger<T>`:
|
||
|
||
```csharp
|
||
public sealed class SessionMemoryService(ILogger<SessionMemoryService> logger)
|
||
{
|
||
private void SaveEntry(MemoryEntry entry)
|
||
{
|
||
try { /* ... */ }
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogWarning(ex, "Failed to save memory entry {EntryId}", entry.Id);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.8 [Low] 同步方法包装为 Task 返回
|
||
|
||
**位置**:
|
||
- `src/FreeCode.Services/SessionMemoryService.cs` — 所有公开方法返回 `Task` 但内部完全同步
|
||
- `src/FreeCode.Services/PermissionEngine.cs:9` — `CheckAsync` 内部无异步操作
|
||
|
||
```csharp
|
||
// SessionMemoryService — 完全同步却返回 Task
|
||
public Task<IReadOnlyList<MemoryEntry>> SearchMemoryAsync(string keyword, CancellationToken ct = default)
|
||
{
|
||
// 完全同步操作
|
||
lock (_gate) { return Task.FromResult<IReadOnlyList<MemoryEntry>>(results); }
|
||
}
|
||
```
|
||
|
||
**建议**: 对确定同步的方法提供同步 API,或使用 `ValueTask` 减少分配:
|
||
|
||
```csharp
|
||
// 方案A:提供同步 API
|
||
public IReadOnlyList<MemoryEntry> SearchMemory(string keyword) { /* sync */ }
|
||
|
||
// 方案B:使用 ValueTask(减少 Task 分配)
|
||
public ValueTask<IReadOnlyList<MemoryEntry>> SearchMemoryAsync(string keyword, CancellationToken ct = default)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
lock (_gate) { return new(results); }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 问题 4.9 [Low] 全局 `using System.Linq` 但大部分查询简单
|
||
|
||
**位置**: `src/FreeCode/Program.cs:12`
|
||
|
||
```csharp
|
||
using System.Linq;
|
||
```
|
||
|
||
Program.cs 仅在一处使用 Linq(`args.Any(...)`),其余所有 `using` 都通过 `ImplicitUsings` 自动引入。
|
||
|
||
---
|
||
|
||
## 五、性能优化建议
|
||
|
||
### 5.1 JSON 处理优化
|
||
|
||
| 问题 | 位置 | 优化方案 | 预期收益 |
|
||
|------|------|----------|----------|
|
||
| write→parse→clone | QueryEngine.cs:263-397 | 直接返回 JsonDocument 不 Clone | 减少 3 次内存分配/调用 |
|
||
| McpClient.ParseMessage Clone | McpClient.cs:170-204 | 使用 `ReadOnlySpan<byte>` + 流式解析 | 减少 GC 压力 |
|
||
| ToolBase EmptySchema | ToolBase.cs:9 | 使用 `JsonNode` 或缓存 `byte[]` | 消除泄漏 |
|
||
| SessionMemoryService 反序列化 | SessionMemoryService.cs:195 | 使用 Source Generator | AOT 兼容 + 减少反射 |
|
||
|
||
### 5.2 网络连接优化
|
||
|
||
| 问题 | 位置 | 优化方案 | 预期收益 |
|
||
|------|------|----------|----------|
|
||
| HttpClient 未池化 | 所有 Provider + AuthService | IHttpClientFactory | 连接复用 |
|
||
| git 进程频繁创建 | SystemPromptBuilder.cs:107 | 缓存 + FileSystemWatcher | 减少进程创建 |
|
||
| SSE 流缓冲区 | AnthropicProvider.cs:156 | 可配置 bufferSize | 大消息场景 |
|
||
|
||
### 5.3 内存优化
|
||
|
||
| 问题 | 位置 | 优化方案 | 预期收益 |
|
||
|------|------|----------|----------|
|
||
| Message.Content 是 `object?` | Message.cs:9 | 改为 `OneOf<string, JsonElement>` 或 tagged union | 避免 boxing |
|
||
| AppState 全量更新 | AppStateStore.cs:29-35 | 分片状态 + 选择性通知 | 减少 GC 压力 |
|
||
| ToolRegistry 缓存永不失效 | ToolRegistry.cs:56-136 | Feature flag 变更时清缓存 | 功能正确性 |
|
||
|
||
---
|
||
|
||
## 六、.NET 惯用法改进
|
||
|
||
### 6.1 使用 file-scoped namespace
|
||
|
||
当前所有文件使用 block-scoped namespace:
|
||
|
||
```csharp
|
||
namespace FreeCode.Engine
|
||
{
|
||
public sealed class QueryEngine ...
|
||
}
|
||
```
|
||
|
||
建议改为 C# 10+ 的 file-scoped namespace:
|
||
|
||
```csharp
|
||
namespace FreeCode.Engine;
|
||
|
||
public sealed class QueryEngine ...
|
||
```
|
||
|
||
### 6.2 替换 `lock(object)` 为更现代的同步原语
|
||
|
||
当前至少 6 个类使用 `private readonly object _gate = new(); lock(_gate)` 模式:
|
||
- AppStateStore, QueryEngine, SessionMemoryService, RateLimitService, ToolRegistry, CoordinatorService
|
||
|
||
建议按场景替换:
|
||
- **读多写少**: `ReaderWriterLockSlim` 或 `ImmutableInterlocked`
|
||
- **生产者-消费者**: `Channel<T>`(McpClient 已经用了)
|
||
- **信号量**: `SemaphoreSlim`(RemoteSessionManager 已经用了)
|
||
|
||
### 6.3 使用 `System.Text.Json` Source Generator
|
||
|
||
`SessionMemoryService.cs:195` 和 `ToolRegistry.cs:303-313` 中使用了 `JsonSerializer.Deserialize<T>` 的反射路径。虽然 `FreeCode.Services/SourceGenerationContext.cs` 已经定义了 Source Generation Context,但未在所有序列化点使用。
|
||
|
||
### 6.4 使用 `required` + `init` 替代构造函数验证
|
||
|
||
当前 `Message` record 使用 `required` 属性(正确),但 `AppState` 不使用 `required`——所有属性都有默认值,这意味着可能创建出无效的状态实例。
|
||
|
||
### 6.5 使用 `primary constructors` 统一风格
|
||
|
||
`QueryEngine` 和 `SystemPromptBuilder` 使用了 C# 12 primary constructors(良好),但 `AppStateStore`、`SessionMemoryService` 等使用传统构造函数。建议统一。
|
||
|
||
---
|
||
|
||
## 七、功能对等性分析
|
||
|
||
### 7.1 已实现核心功能 ✅
|
||
|
||
| 功能 | 实现位置 | 对等程度 |
|
||
|------|----------|----------|
|
||
| 查询引擎主循环 | FreeCode.Engine/QueryEngine.cs | ~80% — 缺少 turn limit / budget enforcement |
|
||
| 工具注册与执行 | FreeCode.Tools/ToolRegistry.cs | ~70% — 缺少延迟加载、工具预设 |
|
||
| 命令系统 | FreeCode.Commands/ (95+ files) | ~40% — 大量命令可能是空壳 |
|
||
| 5 个 API 提供商 | FreeCode.ApiProviders/ | ~75% — SSE 解析基本完成 |
|
||
| MCP 协议 | FreeCode.Mcp/ | ~70% — 4 种传输层已实现 |
|
||
| OAuth 认证 | FreeCode.Services/AuthService.cs | ~60% — 缺少 PKCE |
|
||
| 状态管理 | FreeCode.State/ | ~70% — 基本功能完整 |
|
||
| 特性开关 | FreeCode.Features/ | ~60% — 运行时 flag,缺少编译时 flag |
|
||
| 插件系统 | FreeCode.Plugins/ | ~50% — AssemblyLoadContext 骨架 |
|
||
| 技能系统 | FreeCode.Skills/ | ~30% — 基本加载 |
|
||
|
||
### 7.2 缺失关键功能 ❌
|
||
|
||
| 功能 | 原始实现 | 影响 | 优先级 |
|
||
|------|----------|------|--------|
|
||
| **上下文压缩 (compact)** | src/services/compact/ | 长会话无法管理 token 窗口 | P0 |
|
||
| **OAuth PKCE** | src/services/oauth/crypto.ts | 安全漏洞 | P0 |
|
||
| **Prompt Cache 优化** | cache_control 注解 + 稳定排序 | API 成本倍增 | P1 |
|
||
| **System.CommandLine 集成** | 文档声称使用,实际手动解析 | 子命令、自动补全缺失 | P1 |
|
||
| **Terminal.Gui 完整 REPL** | 原始 REPL.tsx 5000+ 行 | UI 体验不完整 | P1 |
|
||
| **工具延迟加载** | ToolSearch tool + 按需 schema | Context window 浪费 | P2 |
|
||
| **Swarm/Team Agent** | src/assistant/, coordinator/ | 多 Agent 协作不完整 | P2 |
|
||
| **Voice 输入** | src/voice/ | 功能缺失但 flag 存在 | P3 |
|
||
| **IDE Bridge 完整协议** | src/bridge/ (32 files) | 远程 IDE 控制不完整 | P2 |
|
||
|
||
### 7.3 命令系统完成度分析
|
||
|
||
基于探索代理的报告,95+ 个命令文件中绝大部分被标记为 "CommandBase 子类,ExecuteAsync,Standard 质量,无问题"。这种高度一致的评价暗示:
|
||
|
||
1. 大部分命令可能只实现了骨架代码(调用 `CommandResult.Success("...")`)
|
||
2. 缺少与原项目对照的实际功能验证
|
||
3. 建议逐个验证关键命令(`/login`, `/config`, `/compact`, `/session`, `/model`)的实际功能
|
||
|
||
---
|
||
|
||
## 八、优化路线图
|
||
|
||
### Phase 1: 关键修复 (1 周)
|
||
|
||
| # | 任务 | 影响范围 | 工作量 | 收益 |
|
||
|---|------|----------|--------|------|
|
||
| 1 | IAppStateStore 强类型化 | Core + State + Tools + Engine | 0.5天 | 消除反射、强类型安全 |
|
||
| 2 | ToolRegistry 消除 switch | Tools | 1天 | OCP 合规、新增工具零改动 |
|
||
| 3 | QueryEngine 拆分(提取 IToolExecutor, IMessageStore) | Engine + Tools | 1天 | SRP、可测试性 |
|
||
| 4 | HttpClient 池化 | ApiProviders + Services | 0.5天 | 连接复用、性能 |
|
||
| 5 | 添加 OAuth PKCE | Services/AuthService | 0.5天 | 安全性 |
|
||
|
||
### Phase 2: 架构优化 (2 周)
|
||
|
||
| # | 任务 | 影响范围 | 工作量 | 收益 |
|
||
|---|------|----------|--------|------|
|
||
| 6 | AppState 分片 | State + 所有消费者 | 2天 | 性能、选择性更新 |
|
||
| 7 | JSON 序列化优化(消除 write→parse→clone) | Engine + Mcp | 2天 | 减少内存分配 |
|
||
| 8 | System.CommandLine 集成 | FreeCode (入口) | 1天 | 子命令、自动补全 |
|
||
| 9 | 结构化日志(消除 Console.Error) | Services + Engine | 0.5天 | 可观测性 |
|
||
| 10 | 实现 IOptions pattern | 全局 | 1天 | 配置管理 |
|
||
| 11 | 上下文压缩 (compact) | 新增 Engine 功能 | 3天 | Token 窗口管理 |
|
||
|
||
### Phase 3: 深度优化 (持续)
|
||
|
||
| # | 任务 | 影响范围 | 工作量 | 收益 |
|
||
|---|------|----------|--------|------|
|
||
| 12 | 项目合并(16→8~10) | 解决方案级 | 1天 | 构建速度 |
|
||
| 13 | 工具延迟加载 | Tools | 2天 | Context window 优化 |
|
||
| 14 | Prompt Cache 优化 | Engine | 1天 | API 成本 |
|
||
| 15 | Terminal.Gui 完整 REPL | TerminalUI | 5天 | UI 功能对等 |
|
||
| 16 | 命令功能逐个验证与完善 | Commands | 5天+ | 功能对等 |
|
||
| 17 | 集成测试完善 | Tests | 持续 | 可靠性 |
|
||
|
||
---
|
||
|
||
## 九、总结
|
||
|
||
### 优点
|
||
|
||
1. **分层架构清晰**: 五层架构(基础→基础设施→核心→应用→表现)边界明确
|
||
2. **接口驱动设计**: 28 个核心接口定义了完整的契约层
|
||
3. **DI 覆盖完整**: 所有服务通过构造函数注入,16 个模块独立注册
|
||
4. **异步管道正确**: `IAsyncEnumerable<SDKMessage>` 流式响应、CancellationToken 贯穿全链路
|
||
5. **不可变状态**: AppState 使用 record + init 属性,避免意外修改
|
||
6. **JSON 高级 API 使用**: Utf8JsonWriter + ArrayBufferWriter 优于手动字符串拼接
|
||
7. **工具池稳定排序**: 按名称字典序排序,最大化 prompt cache 命中潜力
|
||
|
||
### 核心不足
|
||
|
||
1. **ToolRegistry switch 分发** — 最严重的设计问题,每增加工具需改 3 处
|
||
2. **IAppStateStore 弱类型** — `object` 类型贯穿状态管理全链路
|
||
3. **QueryEngine 神类** — 423 行承担 6+ 职责,构造函数注入 Func 委托
|
||
4. **JSON 冗余分配** — write→parse→clone 反模式出现 3 次
|
||
5. **功能缺失** — compact、PKCE、prompt-cache 三项关键功能未实现
|
||
6. **HttpClient 未管理** — 5 个 Provider + AuthService 各自 new HttpClient()
|
||
|
||
### 一句话总结
|
||
|
||
> 架构骨架搭建完成度约 70%,但存在多个 **Critical 级设计问题** 需要在功能开发前解决,否则技术债将随代码增长而指数级放大。建议按 Phase 1→2→3 路线图推进,优先解决类型安全、OCP 合规、连接管理三大基础问题。
|
||
|
||
---
|
||
|
||
*报告生成日期:2026-04-06*
|
||
*审查范围:src/ 目录下 16 个项目全部 .cs 文件*
|