init easy-code
This commit is contained in:
commit
e25ac591a7
482
.gitignore
vendored
Normal file
482
.gitignore
vendored
Normal file
@ -0,0 +1,482 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
23
Directory.Build.props
Normal file
23
Directory.Build.props
Normal file
@ -0,0 +1,23 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
<EnableAotAnalyzer>true</EnableAotAnalyzer>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<NoWarn>CS1591;CS1998;CS0168;CS0219;IL2026;IL2062;IL2070;IL2072;IL2075;IL2087;IL2118;IL2126;IL3050</NoWarn>
|
||||
<Version>0.1.0</Version>
|
||||
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))">
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
337
docs/UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md
Normal file
337
docs/UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md
Normal file
@ -0,0 +1,337 @@
|
||||
# Terminal.Gui 终端 UI
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 所属模块: [UI 与扩展设计](UI与扩展设计.md)
|
||||
> 原始代码: `../../src/screens/REPL.tsx` + `../../src/components/`(50+ React/Ink 组件)
|
||||
> 交叉参考: [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本模块用 Terminal.Gui 重新实现原始项目的 React/Ink 终端 UI 层。原始 `REPL.tsx` 是约 800 行的 React 函数式组件,依赖 Ink 的虚拟 DOM 将 JSX 渲染到终端。.NET 重写将其拆分为 `TerminalApp`(应用入口与主题)和 `REPLScreen`(主交互窗口),并将 50+ Ink 组件中最关键的两个(`PermissionDialog`、`CompanionSpriteView`)映射为 Terminal.Gui 的 `Dialog` 和 `View` 子类。
|
||||
|
||||
---
|
||||
|
||||
## 15.1 应用启动
|
||||
|
||||
`TerminalApp` 是整个 UI 的入口,对应原始 `cli.tsx` 到 `REPL.tsx` 的初始化链路。
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Terminal.Gui 应用入口
|
||||
/// 对应原始 cli.tsx → REPL.tsx 的初始化
|
||||
/// </summary>
|
||||
public static class TerminalApp
|
||||
{
|
||||
public static void Run(IHost host)
|
||||
{
|
||||
Application.Init();
|
||||
Application.UseSystemConsole = true;
|
||||
|
||||
// 全局主题
|
||||
ApplyTheme();
|
||||
|
||||
// 创建主窗口
|
||||
var top = Application.Top;
|
||||
var replScreen = new REPLScreen(host.Services);
|
||||
top.Add(replScreen);
|
||||
|
||||
// 全局键绑定
|
||||
Application.KeyBindings.Add(Key.Esc, Command.Quit);
|
||||
Application.KeyBindings.Add(Key.CtrlMask | Key.C, Command.Cancel);
|
||||
|
||||
Application.Run(top);
|
||||
Application.Shutdown();
|
||||
}
|
||||
|
||||
private static void ApplyTheme()
|
||||
{
|
||||
var colors = Colors.ColorSchemes["Base"];
|
||||
colors.Normal = new Attribute(Color.White, Color.Black);
|
||||
colors.Focus = new Attribute(Color.Green, Color.Black);
|
||||
colors.HotNormal = new Attribute(Color.BrightCyan, Color.Black);
|
||||
colors.HotFocus = new Attribute(Color.BrightGreen, Color.Black);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
Terminal.Gui 的 `Application.Init()` 对应 Ink 的 `render()` 调用。`UseSystemConsole = true` 确保在大多数终端模拟器下使用原生控制台输出,避免 Terminal.Gui 的 curses 后端与某些终端的兼容性问题。主题颜色选取刻意与原始 Ink 样式保持一致:白色前景、黑色背景、绿色高亮。
|
||||
|
||||
---
|
||||
|
||||
## 15.2 REPL 主屏幕
|
||||
|
||||
`REPLScreen` 对应原始 `REPL.tsx` 的全部交互逻辑,包括消息流渲染、斜杠命令路由和状态订阅。
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// REPL 主屏幕 — Terminal.Gui 版本
|
||||
/// 对应原始 REPL.tsx(~800 行 React/Ink 组件)
|
||||
/// </summary>
|
||||
public sealed class REPLScreen : Window
|
||||
{
|
||||
private readonly IQueryEngine _queryEngine;
|
||||
private readonly IAppStateStore _stateStore;
|
||||
private readonly ICommandRegistry _commands;
|
||||
private readonly IBackgroundTaskManager _taskManager;
|
||||
private readonly TextView _promptInput;
|
||||
private readonly ListView _messageList;
|
||||
private readonly StatusBar _statusBar;
|
||||
private readonly Label _spinner;
|
||||
private readonly IDisposable _stateSubscription;
|
||||
|
||||
public REPLScreen(IServiceProvider services) : base("free-code")
|
||||
{
|
||||
_queryEngine = services.GetRequiredService<IQueryEngine>();
|
||||
_stateStore = services.GetRequiredService<IAppStateStore>();
|
||||
_commands = services.GetRequiredService<ICommandRegistry>();
|
||||
_taskManager = services.GetRequiredService<IBackgroundTaskManager>();
|
||||
|
||||
// 布局: 上方消息列表 + 下方输入框 + 底部状态栏
|
||||
var messagesFrame = new FrameView("Messages")
|
||||
{
|
||||
X = 0, Y = 0,
|
||||
Width = Dim.Fill(),
|
||||
Height = Dim.Fill() - 5
|
||||
};
|
||||
|
||||
_messageList = new ListView()
|
||||
{
|
||||
X = 0, Y = 0,
|
||||
Width = Dim.Fill(),
|
||||
Height = Dim.Fill(),
|
||||
};
|
||||
messagesFrame.Add(_messageList);
|
||||
|
||||
_spinner = new Label("●")
|
||||
{
|
||||
X = 0, Y = Pos.Bottom(messagesFrame),
|
||||
Visible = false
|
||||
};
|
||||
|
||||
_promptInput = new TextView()
|
||||
{
|
||||
X = 2, Y = Pos.Bottom(messagesFrame),
|
||||
Width = Dim.Fill(),
|
||||
Height = 4,
|
||||
};
|
||||
_promptInput.KeyPress += OnPromptKeyPress;
|
||||
|
||||
_statusBar = new StatusBar()
|
||||
{
|
||||
X = 0, Y = Pos.Bottom(_promptInput),
|
||||
};
|
||||
|
||||
Add(messagesFrame, _spinner, _promptInput, _statusBar);
|
||||
|
||||
// 订阅状态变更
|
||||
_stateSubscription = _stateStore.Subscribe(OnStateChanged);
|
||||
}
|
||||
|
||||
private void OnPromptKeyPress(KeyEventEventArgs e)
|
||||
{
|
||||
// Enter 提交, Shift+Enter 换行
|
||||
if (e.KeyEvent.Key == Key.Enter && !e.KeyEvent.IsShift)
|
||||
{
|
||||
e.Handled = true;
|
||||
var input = _promptInput.Text.ToString()?.Trim();
|
||||
if (string.IsNullOrEmpty(input)) return;
|
||||
|
||||
// 斜杠命令检测
|
||||
if (input.StartsWith('/'))
|
||||
_ = HandleCommandAsync(input);
|
||||
else
|
||||
_ = HandleQueryAsync(input);
|
||||
|
||||
_promptInput.Text = "";
|
||||
}
|
||||
|
||||
// Ctrl+C 取消
|
||||
if (e.KeyEvent.Key == (Key.CtrlMask | Key.C))
|
||||
{
|
||||
_ = _queryEngine.CancelAsync();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleQueryAsync(string input)
|
||||
{
|
||||
_spinner.Visible = true;
|
||||
_spinner.Text = "◉ Thinking...";
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var msg in _queryEngine.SubmitMessageAsync(input))
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case SDKMessage.StreamingDelta delta:
|
||||
UpdateAssistantMessage(delta.Text);
|
||||
break;
|
||||
case SDKMessage.ToolUseStart tus:
|
||||
AddToolUseMessage(tus.Name, tus.Input);
|
||||
break;
|
||||
case SDKMessage.ToolUseResult tur:
|
||||
UpdateToolResult(tur.ToolUseId, tur.Output);
|
||||
break;
|
||||
case SDKMessage.AssistantMessage:
|
||||
_spinner.Visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AddSystemMessage("[Cancelled]");
|
||||
}
|
||||
|
||||
_spinner.Visible = false;
|
||||
}
|
||||
|
||||
private async Task HandleCommandAsync(string input)
|
||||
{
|
||||
var parts = input.Split(' ', 2);
|
||||
var commandName = parts[0][1..]; // 去掉 '/'
|
||||
var args = parts.Length > 1 ? parts[1] : null;
|
||||
|
||||
var commands = await _commands.GetEnabledCommandsAsync();
|
||||
var command = commands.FirstOrDefault(c =>
|
||||
c.Name == commandName || c.Aliases?.Contains(commandName) == true);
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
AddSystemMessage($"Unknown command: /{commandName}");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await command.ExecuteAsync(
|
||||
new CommandContext(_stateStore, _queryEngine), args);
|
||||
if (result.Output != null)
|
||||
AddSystemMessage(result.Output);
|
||||
}
|
||||
|
||||
private void OnStateChanged(AppState state)
|
||||
{
|
||||
// 更新状态栏(须切回主线程)
|
||||
Application.MainLoop.Invoke(() =>
|
||||
{
|
||||
var model = state.MainLoopModelForSession ?? "claude-sonnet-4-6";
|
||||
var bgTasks = state.Tasks.Values
|
||||
.Count(t => t.Status is TaskStatus.Running or TaskStatus.Pending);
|
||||
_statusBar.Text =
|
||||
$" Model: {model} | Tasks: {bgTasks} | Dir: {Directory.GetCurrentDirectory()}";
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
原始 `REPL.tsx` 通过 React 状态(`useState`/`useReducer`)驱动 UI 更新。`REPLScreen` 将这套推送模型转换为 `IAppStateStore.Subscribe` 订阅回调,并通过 `Application.MainLoop.Invoke` 确保 UI 变更在主线程执行,与 Ink 的 React reconciler 在语义上等价。
|
||||
|
||||
消息流处理通过 `await foreach` 消费 `IQueryEngine.SubmitMessageAsync` 返回的 `IAsyncEnumerable<SDKMessage>`,完整保留原始流式渲染语义。Ctrl+C 调用 `_queryEngine.CancelAsync()` 对应原始组件中的 `AbortController` 取消机制。
|
||||
|
||||
---
|
||||
|
||||
## 15.3 组件库
|
||||
|
||||
对应原始 `../../src/components/` 目录中的 50+ React/Ink 组件。以下列出两个核心组件的 .NET 实现。
|
||||
|
||||
### PermissionDialog
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 权限对话框 — 用户审批工具使用
|
||||
/// 对应原始 PermissionRequest 组件
|
||||
/// </summary>
|
||||
public sealed class PermissionDialog : Dialog
|
||||
{
|
||||
public PermissionResponse Response { get; private set; }
|
||||
|
||||
public PermissionDialog(string toolName, string description, string input)
|
||||
: base($"Permission: {toolName}", 60, 12)
|
||||
{
|
||||
var descLabel = new Label(description) { X = 1, Y = 1, Width = Dim.Fill() - 2 };
|
||||
var inputLabel = new Label(Truncate(input, 200)) { X = 1, Y = 3, Width = Dim.Fill() - 2 };
|
||||
|
||||
var allowBtn = new Button("Allow (y)") { X = 5, Y = 8 };
|
||||
var denyBtn = new Button("Deny (n)") { X = 25, Y = 8 };
|
||||
var alwaysBtn = new Button("Always Allow (a)") { X = 40, Y = 8 };
|
||||
|
||||
allowBtn.Clicked += () => { Response = PermissionResponse.AllowOnce; RequestStop(); };
|
||||
denyBtn.Clicked += () => { Response = PermissionResponse.Deny; RequestStop(); };
|
||||
alwaysBtn.Clicked += () => { Response = PermissionResponse.AllowAlways; RequestStop(); };
|
||||
|
||||
Add(descLabel, inputLabel, allowBtn, denyBtn, alwaysBtn);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
原始 `PermissionRequest` 组件通过 React props 回调传递用户选择。`PermissionDialog` 改用 `PermissionResponse` 属性,调用方在 `Application.Run(dialog)` 返回后读取该属性,取得与原始回调语义等价的结果,但完全同步。
|
||||
|
||||
### CompanionSpriteView
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 同伴精灵渲染 — ASCII art sprite
|
||||
/// 对应原始 CompanionSprite 组件
|
||||
/// </summary>
|
||||
public sealed class CompanionSpriteView : View
|
||||
{
|
||||
private readonly Companion _companion;
|
||||
|
||||
public CompanionSpriteView(Companion companion)
|
||||
{
|
||||
_companion = companion;
|
||||
Width = 20;
|
||||
Height = 10;
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
var sprite = CompanionSpriteRenderer.Render(_companion);
|
||||
Move(0, 0);
|
||||
for (var i = 0; i < sprite.Length; i++)
|
||||
{
|
||||
var line = sprite[i];
|
||||
Driver.AddStr(line);
|
||||
Move(0, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
原始 `CompanionSprite` 是纯渲染组件,通过 Ink 的 `<Text>` 将 ASCII art 输出到终端。`CompanionSpriteView` 重写 `Draw()` 方法,用 Terminal.Gui 的 `Driver.AddStr` 逐行写入,保持与原始输出完全相同的视觉效果。`CompanionSpriteRenderer.Render` 静态方法封装精灵图案生成逻辑,独立于 UI 层测试。
|
||||
|
||||
---
|
||||
|
||||
## 组件映射一览
|
||||
|
||||
| 原始 Ink 组件 | .NET Terminal.Gui 实现 | 说明 |
|
||||
|---|---|---|
|
||||
| `REPL.tsx` | `REPLScreen : Window` | 主屏幕 |
|
||||
| `PermissionRequest` | `PermissionDialog : Dialog` | 工具审批对话框 |
|
||||
| `CompanionSprite` | `CompanionSpriteView : View` | ASCII 精灵渲染 |
|
||||
| `StatusBar` | `StatusBar`(Terminal.Gui 内置)| 底部状态栏 |
|
||||
| `Spinner` | `Label`(动态文本)| 思考中指示器 |
|
||||
| `MessageList` | `ListView` | 消息历史列表 |
|
||||
| `PromptInput` | `TextView` | 多行输入框 |
|
||||
|
||||
其余 43+ 组件(`FileDiff`、`TokenBudget`、`HistoryPicker` 等)由对应的特性开关控制加载,在各自模块文档中描述。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [UI 与扩展设计 — 总览](UI与扩展设计.md)
|
||||
- [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md)
|
||||
- [UI 与扩展设计 — 特性开关系统](UI与扩展设计-特性开关系统.md)
|
||||
265
docs/UI与扩展设计/UI与扩展设计-技能系统.md
Normal file
265
docs/UI与扩展设计/UI与扩展设计-技能系统.md
Normal file
@ -0,0 +1,265 @@
|
||||
# 技能系统
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 所属模块: [UI 与扩展设计](UI与扩展设计.md)
|
||||
> 原始代码: `../../src/skills/`
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
技能系统允许用户以 Markdown 文件的形式定义可复用的指令集,通过斜杠命令或提示注入的方式触发。原始实现扫描 `.free-code/skills/` 目录,解析每个文件的 YAML frontmatter 获取元数据,并将 Markdown 正文作为 System Prompt 内容注入查询引擎。
|
||||
|
||||
.NET 重写通过 `ISkillLoader` 接口封装这一流程,用 `SkillDefinition` record 描述单条技能,用 `SkillHooks` record 描述生命周期钩子。`SkillLoader` 是默认实现,负责目录扫描、YAML 解析、结果缓存和执行委托。
|
||||
|
||||
---
|
||||
|
||||
## 16.1 接口与数据模型
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 技能加载器 — 从 .free-code/skills/ 目录加载 SKILL.md
|
||||
/// 对应原始 skill system 的目录扫描 + frontmatter 解析
|
||||
/// </summary>
|
||||
public interface ISkillLoader
|
||||
{
|
||||
Task<IReadOnlyList<SkillDefinition>> LoadAllSkillsAsync();
|
||||
Task<SkillDefinition?> LoadSkillAsync(string skillName);
|
||||
Task ExecuteSkillAsync(SkillDefinition skill, string? args);
|
||||
}
|
||||
```
|
||||
|
||||
### SkillDefinition
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 技能定义 — 对应一个 SKILL.md 文件的完整解析结果
|
||||
/// </summary>
|
||||
public sealed record SkillDefinition
|
||||
{
|
||||
/// <summary>技能名称,来自 frontmatter 或文件名</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>技能描述,用于斜杠命令帮助文本</summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>Markdown 正文,作为 System Prompt 注入</summary>
|
||||
public required string Content { get; init; }
|
||||
|
||||
/// <summary>技能允许使用的工具名称列表</summary>
|
||||
public IReadOnlyList<string> Tools { get; init; } = [];
|
||||
|
||||
/// <summary>覆盖默认模型(如 "claude-haiku-4-5")</summary>
|
||||
public string? Model { get; init; }
|
||||
|
||||
/// <summary>技能参数定义(参数名 → 描述)</summary>
|
||||
public IReadOnlyDictionary<string, string>? Arguments { get; init; }
|
||||
|
||||
/// <summary>生命周期钩子</summary>
|
||||
public SkillHooks? Hooks { get; init; }
|
||||
|
||||
/// <summary>磁盘路径,仅供调试</summary>
|
||||
public string? FilePath { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
### SkillHooks
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 技能生命周期钩子
|
||||
/// 对应原始 skill hooks 机制
|
||||
/// </summary>
|
||||
public sealed record SkillHooks
|
||||
{
|
||||
/// <summary>执行前的提示注入片段</summary>
|
||||
public string? PreExecute { get; init; }
|
||||
|
||||
/// <summary>执行后的提示注入片段</summary>
|
||||
public string? PostExecute { get; init; }
|
||||
|
||||
/// <summary>发生错误时的提示注入片段</summary>
|
||||
public string? OnError { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
`SkillDefinition` 使用 `sealed record` 确保不可变性:一旦从磁盘解析完成,技能定义在整个会话生命周期内不会改变。`Tools` 字段对应原始 frontmatter 中的 `tools` 数组,允许技能声明自己需要哪些工具,`ToolRegistry` 在组装工具池时可据此过滤。
|
||||
|
||||
---
|
||||
|
||||
## 16.2 SkillLoader 实现
|
||||
|
||||
```csharp
|
||||
public sealed class SkillLoader : ISkillLoader
|
||||
{
|
||||
private readonly string[] _skillDirectories;
|
||||
private List<SkillDefinition>? _cached;
|
||||
private readonly ILogger<SkillLoader> _logger;
|
||||
|
||||
public SkillLoader(ILogger<SkillLoader> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var cwd = Directory.GetCurrentDirectory();
|
||||
|
||||
// 扫描顺序: 项目级 → 用户级
|
||||
_skillDirectories =
|
||||
[
|
||||
Path.Combine(cwd, ".free-code", "skills"),
|
||||
Path.Combine(home, ".free-code", "skills"),
|
||||
];
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SkillDefinition>> LoadAllSkillsAsync()
|
||||
{
|
||||
if (_cached != null) return _cached;
|
||||
_cached = [];
|
||||
|
||||
foreach (var dir in _skillDirectories)
|
||||
{
|
||||
if (!Directory.Exists(dir)) continue;
|
||||
foreach (var file in Directory.GetFiles(dir, "*.md", SearchOption.AllDirectories))
|
||||
{
|
||||
try
|
||||
{
|
||||
var skill = await ParseSkillFileAsync(file);
|
||||
if (skill != null) _cached.Add(skill);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load skill from {File}", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _cached;
|
||||
}
|
||||
|
||||
public async Task<SkillDefinition?> LoadSkillAsync(string skillName)
|
||||
{
|
||||
var all = await LoadAllSkillsAsync();
|
||||
return all.FirstOrDefault(s =>
|
||||
string.Equals(s.Name, skillName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 SKILL.md — YAML frontmatter + Markdown body
|
||||
/// </summary>
|
||||
private static async Task<SkillDefinition?> ParseSkillFileAsync(string filePath)
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(filePath);
|
||||
if (!content.StartsWith("---")) return null;
|
||||
|
||||
var endIdx = content.IndexOf("\n---", 3);
|
||||
if (endIdx < 0) return null;
|
||||
|
||||
var frontmatter = content[3..endIdx];
|
||||
var body = content[(endIdx + 4)..].TrimStart();
|
||||
|
||||
// 解析 YAML frontmatter
|
||||
var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build();
|
||||
var meta = deserializer.Deserialize<Dictionary<string, object>>(frontmatter);
|
||||
|
||||
return new SkillDefinition
|
||||
{
|
||||
Name = meta.GetValueOrDefault("name",
|
||||
Path.GetFileNameWithoutExtension(filePath))?.ToString() ?? "",
|
||||
Description = meta.GetValueOrDefault("description")?.ToString(),
|
||||
Content = body,
|
||||
Tools = (meta.GetValueOrDefault("tools") as List<object>)
|
||||
?.Select(o => o.ToString() ?? "")
|
||||
.ToList() ?? [],
|
||||
Model = meta.GetValueOrDefault("model")?.ToString(),
|
||||
FilePath = filePath,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task ExecuteSkillAsync(SkillDefinition skill, string? args)
|
||||
{
|
||||
// 技能内容注入 System Prompt,参数拼接到末尾,由 QueryEngine 执行
|
||||
var fullPrompt = skill.Content;
|
||||
if (args != null)
|
||||
fullPrompt += $"\n\nArguments: {args}";
|
||||
|
||||
// 通过 QueryEngine 提交,渲染到 UI
|
||||
var engine = /* 从 IServiceProvider 解析 */;
|
||||
await foreach (var msg in engine.SubmitMessageAsync(fullPrompt))
|
||||
{
|
||||
// UI 渲染逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录扫描与优先级
|
||||
|
||||
`SkillLoader` 按以下顺序扫描两个目录,先找到的技能名称优先:
|
||||
|
||||
| 优先级 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| 1(高)| `<当前工作目录>/.free-code/skills/` | 项目级技能,随代码库版本管理 |
|
||||
| 2(低)| `~/.free-code/skills/` | 用户级技能,跨项目共享 |
|
||||
|
||||
同名技能以项目级为准,允许项目级覆盖用户级的默认行为。
|
||||
|
||||
---
|
||||
|
||||
## SKILL.md 文件格式
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-review
|
||||
description: 对当前分支的变更进行代码审查
|
||||
tools:
|
||||
- Bash
|
||||
- Read
|
||||
model: claude-sonnet-4-6
|
||||
---
|
||||
|
||||
你是一位严谨的代码审查员。分析提供的代码变更,重点关注:
|
||||
- 潜在的 bug 和边界条件
|
||||
- 安全漏洞
|
||||
- 性能问题
|
||||
- 代码风格与项目规范的一致性
|
||||
|
||||
输出格式:按严重性分组,每条问题标注文件和行号。
|
||||
```
|
||||
|
||||
frontmatter 字段说明:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `name` | `string` | 否 | 技能名称,默认使用文件名 |
|
||||
| `description` | `string` | 否 | 显示在斜杠命令帮助中的描述 |
|
||||
| `tools` | `string[]` | 否 | 允许使用的工具,空则使用全部 |
|
||||
| `model` | `string` | 否 | 覆盖默认模型 |
|
||||
| `arguments` | `object` | 否 | 参数定义(名称 → 描述) |
|
||||
|
||||
---
|
||||
|
||||
## 生命周期钩子执行顺序
|
||||
|
||||
```
|
||||
ExecuteSkillAsync(skill, args)
|
||||
│
|
||||
├── 1. hooks.PreExecute → 注入到提示前缀
|
||||
├── 2. skill.Content → 主提示内容
|
||||
├── 3. args → 用户传入参数
|
||||
│
|
||||
└── QueryEngine.SubmitMessageAsync(fullPrompt)
|
||||
│
|
||||
└── [成功] hooks.PostExecute
|
||||
└── [失败] hooks.OnError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [UI 与扩展设计 — 总览](UI与扩展设计.md)
|
||||
- [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md)
|
||||
- [UI 与扩展设计 — 插件系统](UI与扩展设计-插件系统.md)
|
||||
280
docs/UI与扩展设计/UI与扩展设计-插件系统.md
Normal file
280
docs/UI与扩展设计/UI与扩展设计-插件系统.md
Normal file
@ -0,0 +1,280 @@
|
||||
# 插件系统
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 所属模块: [UI 与扩展设计](UI与扩展设计.md)
|
||||
> 原始代码: `../../src/plugins/`
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
插件系统允许第三方代码在运行时扩展 free-code 的能力,包括注册新技能、新斜杠命令和新 MCP 服务器配置。原始实现通过 Node.js 的动态 `import()` 加载插件模块,利用模块缓存隔离各插件的命名空间。
|
||||
|
||||
.NET 重写用 `AssemblyLoadContext`(ALC)替代动态导入,实现更强的隔离性和确定性卸载。每个插件运行在独立的 `PluginLoadContext` 实例中,其依赖项从插件目录优先解析,不与宿主进程或其他插件共享程序集版本。`PluginManager` 管理所有插件的生命周期,支持安装、卸载、启用和禁用操作。
|
||||
|
||||
---
|
||||
|
||||
## 17.1 接口定义
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 插件管理器 — AssemblyLoadContext 隔离加载
|
||||
/// 对应原始 plugin system
|
||||
/// </summary>
|
||||
public interface IPluginManager
|
||||
{
|
||||
Task<IReadOnlyList<LoadedPlugin>> LoadAllPluginsAsync();
|
||||
Task InstallPluginAsync(string pluginId);
|
||||
Task UninstallPluginAsync(string pluginId);
|
||||
Task EnablePluginAsync(string pluginId);
|
||||
Task DisablePluginAsync(string pluginId);
|
||||
Task RefreshAsync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17.2 LoadedPlugin 与 PluginManifest
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 已加载的插件实例描述
|
||||
/// </summary>
|
||||
public sealed record LoadedPlugin
|
||||
{
|
||||
/// <summary>插件唯一标识符(目录名)</summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>插件显示名称</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>版本字符串</summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>来源:marketplace | local | github</summary>
|
||||
public required string Source { get; init; }
|
||||
|
||||
/// <summary>主程序集路径</summary>
|
||||
public required string AssemblyPath { get; init; }
|
||||
|
||||
/// <summary>是否启用</summary>
|
||||
public bool IsEnabled { get; init; }
|
||||
|
||||
/// <summary>插件清单</summary>
|
||||
public PluginManifest Manifest { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件清单 — plugin.json 反序列化目标
|
||||
/// 描述插件向宿主暴露的所有扩展点
|
||||
/// </summary>
|
||||
public sealed record PluginManifest
|
||||
{
|
||||
/// <summary>插件提供的技能列表</summary>
|
||||
public IReadOnlyList<SkillDefinition> Skills { get; init; } = [];
|
||||
|
||||
/// <summary>插件提供的斜杠命令列表</summary>
|
||||
public IReadOnlyList<ICommand> Commands { get; init; } = [];
|
||||
|
||||
/// <summary>插件注册的 MCP 服务器配置(服务器名 → 配置)</summary>
|
||||
public IReadOnlyDictionary<string, ScopedMcpServerConfig> McpServers { get; init; }
|
||||
= new Dictionary<string, ScopedMcpServerConfig>();
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
`PluginManifest` 直接对应 `plugin.json` 的顶层结构,三个扩展点(技能、命令、MCP 服务器)分别映射为 `SkillLoader`、`CommandRegistry`、`McpClientManager` 的动态注册入口。插件卸载时,这三个注册表各自移除该插件贡献的条目。
|
||||
|
||||
---
|
||||
|
||||
## 17.3 PluginLoadContext
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 可卸载的程序集加载上下文
|
||||
/// 对应原始动态 import() 的隔离语义
|
||||
/// </summary>
|
||||
public sealed class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
|
||||
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
// 优先从插件目录解析,回退到默认加载上下文
|
||||
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
return assemblyPath != null
|
||||
? LoadFromAssemblyPath(assemblyPath)
|
||||
: null; // null 触发回退到默认 ALC
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
return libraryPath != null
|
||||
? LoadUnmanagedDllFromPath(libraryPath)
|
||||
: IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设计意图**
|
||||
|
||||
`isCollectible: true` 是关键参数。普通 `AssemblyLoadContext` 加载的程序集在整个进程生命周期内无法卸载。可回收 ALC 允许在所有对该上下文中类型的强引用归零后,GC 完整回收其托管内存和元数据,这是 `/plugin uninstall` 命令实现热卸载的基础。
|
||||
|
||||
`AssemblyDependencyResolver` 读取插件主程序集旁边的 `.deps.json` 文件,自动解析所有传递依赖的路径。插件作者只需将所有依赖打包到插件目录,无需关心宿主进程中已有哪些程序集版本。
|
||||
|
||||
---
|
||||
|
||||
## 17.4 PluginManager 实现
|
||||
|
||||
```csharp
|
||||
public sealed class PluginManager : IPluginManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PluginLoadContext> _contexts = new();
|
||||
private readonly ConcurrentDictionary<string, LoadedPlugin> _plugins = new();
|
||||
|
||||
public async Task<IReadOnlyList<LoadedPlugin>> LoadAllPluginsAsync()
|
||||
{
|
||||
var pluginDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".free-code", "plugins");
|
||||
|
||||
if (!Directory.Exists(pluginDir)) return [];
|
||||
|
||||
foreach (var dir in Directory.GetDirectories(pluginDir))
|
||||
{
|
||||
var manifestPath = Path.Combine(dir, "plugin.json");
|
||||
if (!File.Exists(manifestPath)) continue;
|
||||
|
||||
var manifest = JsonSerializer.Deserialize<PluginManifest>(
|
||||
await File.ReadAllTextAsync(manifestPath));
|
||||
|
||||
var dllPath = Directory.GetFiles(dir, "*.dll").FirstOrDefault();
|
||||
if (dllPath == null) continue;
|
||||
|
||||
var context = new PluginLoadContext(dllPath);
|
||||
var assembly = context.LoadFromAssemblyPath(dllPath);
|
||||
|
||||
_contexts[dir] = context;
|
||||
_plugins[dir] = new LoadedPlugin
|
||||
{
|
||||
Id = Path.GetFileName(dir),
|
||||
Name = manifest?.Skills.FirstOrDefault()?.Name
|
||||
?? Path.GetFileName(dir),
|
||||
Version = "1.0",
|
||||
Source = "local",
|
||||
AssemblyPath = dllPath,
|
||||
IsEnabled = true,
|
||||
};
|
||||
}
|
||||
|
||||
return _plugins.Values.ToList();
|
||||
}
|
||||
|
||||
public async Task UninstallPluginAsync(string pluginId)
|
||||
{
|
||||
if (_contexts.TryRemove(pluginId, out var context))
|
||||
{
|
||||
context.Unload(); // 触发 GC 可回收流程
|
||||
}
|
||||
_plugins.TryRemove(pluginId, out _);
|
||||
}
|
||||
|
||||
public Task RefreshAsync()
|
||||
{
|
||||
// 重新扫描目录,加载新插件,卸载已删除的
|
||||
return LoadAllPluginsAsync();
|
||||
}
|
||||
|
||||
public Task InstallPluginAsync(string pluginId)
|
||||
{
|
||||
// 从 marketplace / GitHub 下载到 ~/.free-code/plugins/<id>/
|
||||
// 然后调用 LoadAllPluginsAsync 触发加载
|
||||
throw new NotImplementedException("待实现:marketplace 下载逻辑");
|
||||
}
|
||||
|
||||
public Task EnablePluginAsync(string pluginId)
|
||||
{
|
||||
if (_plugins.TryGetValue(pluginId, out var plugin))
|
||||
_plugins[pluginId] = plugin with { IsEnabled = true };
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisablePluginAsync(string pluginId)
|
||||
{
|
||||
if (_plugins.TryGetValue(pluginId, out var plugin))
|
||||
_plugins[pluginId] = plugin with { IsEnabled = false };
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 插件目录结构
|
||||
|
||||
```
|
||||
~/.free-code/plugins/
|
||||
└── my-plugin/
|
||||
├── plugin.json # 插件清单
|
||||
├── MyPlugin.dll # 主程序集
|
||||
├── MyPlugin.deps.json # 依赖清单(供 AssemblyDependencyResolver 使用)
|
||||
└── SomeDependency.dll # 插件私有依赖
|
||||
```
|
||||
|
||||
### plugin.json 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"name": "my-skill",
|
||||
"description": "自定义技能示例",
|
||||
"content": "你是一个帮助用户完成特定任务的助手。"
|
||||
}
|
||||
],
|
||||
"commands": [],
|
||||
"mcpServers": {
|
||||
"my-mcp": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "my-mcp-server"],
|
||||
"scope": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生命周期图
|
||||
|
||||
```
|
||||
LoadAllPluginsAsync()
|
||||
│
|
||||
├── 扫描 ~/.free-code/plugins/ 目录
|
||||
├── 读取 plugin.json → 反序列化 PluginManifest
|
||||
├── 创建 PluginLoadContext(dllPath)
|
||||
├── LoadFromAssemblyPath(dllPath)
|
||||
└── 注册到 _plugins / _contexts
|
||||
|
||||
UninstallPluginAsync(id)
|
||||
│
|
||||
├── context.Unload() [ALC 标记为待回收]
|
||||
├── 移除 _contexts[id] [WeakReference 归零后 GC 回收]
|
||||
└── 移除 _plugins[id] [插件对象消失]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [UI 与扩展设计 — 总览](UI与扩展设计.md)
|
||||
- [UI 与扩展设计 — 技能系统](UI与扩展设计-技能系统.md)
|
||||
- [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md)
|
||||
- [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md)
|
||||
270
docs/UI与扩展设计/UI与扩展设计-特性开关系统.md
Normal file
270
docs/UI与扩展设计/UI与扩展设计-特性开关系统.md
Normal file
@ -0,0 +1,270 @@
|
||||
# 特性开关系统
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 所属模块: [UI 与扩展设计](UI与扩展设计.md)
|
||||
> 原始代码: `scripts/build.ts` `feature()` 编译时开关 + 运行时 GrowthBook
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
特性开关系统控制 free-code 的 88 个功能标志,其中 54 个可编译、34 个损坏。原始实现有两个层次:编译时用 `scripts/build.ts` 的 `feature()` 函数注入 `#define`,运行时用 GrowthBook SDK 从云端拉取实验配置。
|
||||
|
||||
.NET 重写将这两个层次统一到同一套接口下。`FeatureFlags` 静态类持有全部 54 个常量,供编译时 `#if` 和运行时查询共同使用;`IFeatureFlagService` 抽象运行时查询;`FeatureFlagService` 从 `appsettings.json` 的 `FeatureFlags` 配置节读取初始值,取代原始 GrowthBook 的云端下发机制,确保零网络依赖。
|
||||
|
||||
---
|
||||
|
||||
## 18.1 FeatureFlags 常量清单
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 特性开关常量 — 编译时 #if + 运行时查询
|
||||
/// 对应原始 88 个 feature flags(54 可编译,34 损坏)
|
||||
/// </summary>
|
||||
public static class FeatureFlags
|
||||
{
|
||||
// ===== 交互与 UI =====
|
||||
public const string Ultraplan = "ULTRAPLAN";
|
||||
public const string Ultrathink = "ULTRATHINK";
|
||||
public const string VoiceMode = "VOICE_MODE";
|
||||
public const string TokenBudget = "TOKEN_BUDGET";
|
||||
public const string HistoryPicker = "HISTORY_PICKER";
|
||||
public const string MessageActions = "MESSAGE_ACTIONS";
|
||||
public const string QuickSearch = "QUICK_SEARCH";
|
||||
public const string ShotStats = "SHOT_STATS";
|
||||
|
||||
// ===== Agent、记忆与规划 =====
|
||||
public const string BuiltinExplorePlanAgents = "BUILTIN_EXPLORE_PLAN_AGENTS";
|
||||
public const string VerificationAgent = "VERIFICATION_AGENT";
|
||||
public const string AgentTriggers = "AGENT_TRIGGERS";
|
||||
public const string AgentTriggersRemote = "AGENT_TRIGGERS_REMOTE";
|
||||
public const string ExtractMemories = "EXTRACT_MEMORIES";
|
||||
public const string CompactionReminders = "COMPACTION_REMINDERS";
|
||||
public const string CachedMicrocompact = "CACHED_MICROCOMPACT";
|
||||
public const string TeamMem = "TEAMMEM";
|
||||
|
||||
// ===== 工具与基础设施 =====
|
||||
public const string BridgeMode = "BRIDGE_MODE";
|
||||
public const string BashClassifier = "BASH_CLASSIFIER";
|
||||
public const string PromptCacheBreakDetection = "PROMPT_CACHE_BREAK_DETECTION";
|
||||
|
||||
// ===== 其余 35 个可编译标志 =====
|
||||
public const string Buddy = "BUDDY";
|
||||
public const string V2Todo = "V2_TODO";
|
||||
public const string EnableLspTool = "ENABLE_LSP_TOOL";
|
||||
public const string ContextCollapse = "CONTEXT_COLLAPSE";
|
||||
public const string TokenBudgetWarning = "TOKEN_BUDGET_WARNING";
|
||||
public const string MemoryAutoExtract = "MEMORY_AUTO_EXTRACT";
|
||||
public const string CompactHistory = "COMPACT_HISTORY";
|
||||
public const string PermissionClassifier = "PERMISSION_CLASSIFIER";
|
||||
public const string AsyncToolExecution = "ASYNC_TOOL_EXECUTION";
|
||||
public const string Speculation = "SPECULATION";
|
||||
public const string PromptSuggestion = "PROMPT_SUGGESTION";
|
||||
public const string SkillImprovement = "SKILL_IMPROVEMENT";
|
||||
public const string AgentSwarm = "ENABLE_AGENT_SWARMS";
|
||||
public const string WorkflowTool = "WORKFLOW_TOOL";
|
||||
public const string MonitorTool = "MONITOR_TOOL";
|
||||
public const string McpSkills = "MCP_SKILLS";
|
||||
public const string ChicagoMcp = "CHICAGO_MCP";
|
||||
public const string EnableDeskApi = "ENABLE_DESK_API";
|
||||
public const string Heapdump = "HEAPDUMP";
|
||||
public const string EffortFlag = "EFFORT";
|
||||
public const string Advisor = "ADVISOR";
|
||||
public const string ThinkingBudget = "THINKING_BUDGET";
|
||||
public const string RemoteEnv = "REMOTE_ENV";
|
||||
public const string NotifyTool = "NOTIFY_TOOL";
|
||||
public const string CaptureTool = "CAPTURE_TOOL";
|
||||
public const string ReplayUserMessages = "REPLAY_USER_MESSAGES";
|
||||
public const string TelemetryFlush = "TELEMETRY_FLUSH";
|
||||
public const string StripUnusedImports = "STRIP_UNUSED_IMPORTS";
|
||||
public const string ToolResultStorage = "TOOL_RESULT_STORAGE";
|
||||
public const string McpStreaming = "MCP_STREAMING";
|
||||
public const string PlanMode = "PLAN_MODE";
|
||||
public const string PlanModeV2 = "PLAN_MODE_V2";
|
||||
public const string RemoteControl = "REMOTE_CONTROL";
|
||||
public const string ConsentedRemoteControl = "CONSENTED_REMOTE_CONTROL";
|
||||
public const string CcrV2 = "CCR_V2";
|
||||
public const string EnableSandbox = "ENABLE_SANDBOX";
|
||||
public const string DiffBasedEdit = "DIFF_BASED_EDIT";
|
||||
public const string OutputTruncation = "OUTPUT_TRUNCATION";
|
||||
public const string WebSearchTool = "WEB_SEARCH_TOOL";
|
||||
public const string PythonTool = "PYTHON_TOOL";
|
||||
public const string NotebookEditTool = "NOTEBOOK_EDIT_TOOL";
|
||||
public const string DedupeToolCalls = "DEDUPE_TOOL_CALLS";
|
||||
public const string IdeIntegration = "IDE_INTEGRATION";
|
||||
public const string RateLimitUi = "RATE_LIMIT_UI";
|
||||
public const string TokenCounting = "TOKEN_COUNTING";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 标志分类一览
|
||||
|
||||
| 类别 | 标志数量 | 说明 |
|
||||
|---|---|---|
|
||||
| 交互与 UI | 8 | 提示词增强、历史记录、语音模式等 |
|
||||
| Agent、记忆与规划 | 8 | 多 Agent、记忆提取、规划模式等 |
|
||||
| 工具与基础设施 | 3 | IDE 桥接、分类器、缓存检测 |
|
||||
| 其余可编译标志 | 35 | 各类实验功能 |
|
||||
| **合计可编译** | **54** | |
|
||||
| 损坏标志(不含)| 34 | 需重建,详见 FEATURES.md |
|
||||
|
||||
---
|
||||
|
||||
## 18.2 IFeatureFlagService 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 运行时特性开关查询接口
|
||||
/// </summary>
|
||||
public interface IFeatureFlagService
|
||||
{
|
||||
/// <summary>查询指定标志是否启用</summary>
|
||||
bool IsEnabled(string flag);
|
||||
|
||||
/// <summary>动态设置标志状态(用于测试或运行时覆盖)</summary>
|
||||
void SetFlag(string flag, bool enabled);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18.3 FeatureFlagService 实现
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 运行时特性开关实现
|
||||
/// 编译时使用 #if FLAG_NAME,运行时使用 IsEnabled()
|
||||
/// 对应原始 GrowthBook SDK 的本地替代
|
||||
/// </summary>
|
||||
public sealed class FeatureFlagService : IFeatureFlagService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, bool> _flags = new();
|
||||
|
||||
public FeatureFlagService(IConfiguration config)
|
||||
{
|
||||
// 从 appsettings.json 的 FeatureFlags 节加载
|
||||
var section = config.GetSection("FeatureFlags");
|
||||
foreach (var child in section.GetChildren())
|
||||
_flags[child.Key] = bool.Parse(child.Value ?? "false");
|
||||
}
|
||||
|
||||
public bool IsEnabled(string flag)
|
||||
=> _flags.GetValueOrDefault(flag, false);
|
||||
|
||||
public void SetFlag(string flag, bool enabled)
|
||||
=> _flags[flag] = enabled;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 双模式设计
|
||||
|
||||
特性开关系统同时支持两种工作模式,对应原始实现的两个不同层次。
|
||||
|
||||
### 编译时模式(`#if`)
|
||||
|
||||
```csharp
|
||||
// 编译时完全消除死代码,减小二进制体积
|
||||
#if ULTRAPLAN
|
||||
services.AddSingleton<IUltraplanService, UltraplanService>();
|
||||
#endif
|
||||
|
||||
#if VOICE_MODE
|
||||
services.AddSingleton<IVoiceService, VoiceService>();
|
||||
#endif
|
||||
```
|
||||
|
||||
编译时标志通过 MSBuild 属性注入,对应原始 `build.ts` 的 `feature()` 函数:
|
||||
|
||||
```xml
|
||||
<!-- Directory.Build.props -->
|
||||
<PropertyGroup Condition="'$(FEATURE_ULTRAPLAN)' == 'true'">
|
||||
<DefineConstants>$(DefineConstants);ULTRAPLAN</DefineConstants>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
构建命令示例:
|
||||
|
||||
```bash
|
||||
# 全功能构建(对应原始 build:dev:full)
|
||||
dotnet build -p:FEATURE_ULTRAPLAN=true -p:FEATURE_VOICE_MODE=true ...
|
||||
|
||||
# 仅启用语音模式(对应原始默认构建)
|
||||
dotnet build -p:FEATURE_VOICE_MODE=true
|
||||
```
|
||||
|
||||
### 运行时模式(`IsEnabled()`)
|
||||
|
||||
```csharp
|
||||
// 不重新编译就能切换,由配置文件驱动
|
||||
if (_features.IsEnabled(FeatureFlags.TokenBudget))
|
||||
{
|
||||
// 显示 token 预算组件
|
||||
}
|
||||
|
||||
if (_features.IsEnabled(FeatureFlags.ExtractMemories))
|
||||
{
|
||||
await _memoryService.TryExtractAsync(messages);
|
||||
}
|
||||
```
|
||||
|
||||
`appsettings.json` 配置示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"FeatureFlags": {
|
||||
"TOKEN_BUDGET": true,
|
||||
"HISTORY_PICKER": true,
|
||||
"EXTRACT_MEMORIES": false,
|
||||
"ULTRAPLAN": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 原始实现对比
|
||||
|
||||
| 原始(TypeScript)| .NET 重写 | 说明 |
|
||||
|---|---|---|
|
||||
| `scripts/build.ts` `feature("FLAG")` | MSBuild `-p:FEATURE_FLAG=true` | 编译时开关 |
|
||||
| `#if defined(FLAG)` in bundled output | `#if FLAG_NAME` in C# | 死代码消除 |
|
||||
| GrowthBook SDK(云端下发)| `FeatureFlagService`(配置文件)| 运行时开关 |
|
||||
| `growthbook.isOn("flag-name")` | `_features.IsEnabled(FeatureFlags.Xxx)` | 查询 API |
|
||||
| 34 损坏标志(缺少资源/类型)| 保留常量,实现体标记 TODO | 向后兼容占位 |
|
||||
|
||||
**关键差异**:.NET 重写移除了 GrowthBook 的网络依赖。原始实现在每次启动时联系 GrowthBook 服务器更新标志状态,这是一个隐式的遥测上报渠道。`FeatureFlagService` 完全本地化,配置通过 `appsettings.json` 或环境变量覆盖,无任何出站请求。
|
||||
|
||||
---
|
||||
|
||||
## DI 注册
|
||||
|
||||
```csharp
|
||||
// Program.cs 或服务注册扩展方法
|
||||
services.AddSingleton<IFeatureFlagService, FeatureFlagService>();
|
||||
|
||||
// 使用示例(构造函数注入)
|
||||
public sealed class SomeService(IFeatureFlagService features)
|
||||
{
|
||||
public void DoWork()
|
||||
{
|
||||
if (features.IsEnabled(FeatureFlags.AsyncToolExecution))
|
||||
{
|
||||
// 异步工具执行路径
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [UI 与扩展设计 — 总览](UI与扩展设计.md)
|
||||
- [UI 与扩展设计 — Terminal.Gui 终端 UI](UI与扩展设计-Terminal-Gui终端UI.md)
|
||||
- [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [测试与构建](../测试与构建/)
|
||||
- [FEATURES.md](../../../FEATURES.md)(完整 88 个标志审计)
|
||||
95
docs/UI与扩展设计/UI与扩展设计.md
Normal file
95
docs/UI与扩展设计/UI与扩展设计.md
Normal file
@ -0,0 +1,95 @@
|
||||
# UI 与扩展设计 — 总览
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 配套文档: [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md) | [核心模块设计](../核心模块设计/核心模块设计.md) | [基础设施设计](../基础设施设计/) | [服务子系统设计](../服务子系统设计/)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
UI 与扩展设计覆盖 free-code .NET 10 重写中所有面向用户的终端界面层,以及三个扩展机制:技能、插件和特性开关。
|
||||
|
||||
原始 TypeScript 实现以 React/Ink 渲染终端 UI,用文件系统约定管理技能,用动态 `import()` 加载插件,用编译时 `feature()` 函数控制特性开关。.NET 重写将这四个关注点各自封装为独立模块:Terminal.Gui 替代 Ink 提供声明式终端 UI,`ISkillLoader` 替代目录扫描约定,`AssemblyLoadContext` 替代动态导入实现插件隔离,`IFeatureFlagService` 在运行时桥接编译时标志与配置驱动开关。
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
REPLScreen (Terminal.Gui Window)
|
||||
│
|
||||
├── PermissionDialog 工具使用审批
|
||||
├── CompanionSpriteView ASCII 精灵渲染
|
||||
│
|
||||
├── ISkillLoader 技能加载与执行
|
||||
│ └── SkillDefinition + SkillHooks
|
||||
│
|
||||
├── IPluginManager 插件生命周期管理
|
||||
│ └── PluginLoadContext (AssemblyLoadContext)
|
||||
│
|
||||
└── IFeatureFlagService 特性开关查询
|
||||
└── FeatureFlags (54+ 常量)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 子模块列表
|
||||
|
||||
### [Terminal.Gui 终端 UI](UI与扩展设计-Terminal-Gui终端UI.md)
|
||||
|
||||
覆盖 `TerminalApp` 应用启动与主题配置、`REPLScreen` 主交互屏幕的完整实现,以及 `PermissionDialog` 和 `CompanionSpriteView` 组件。
|
||||
|
||||
原始来源: `../../src/screens/REPL.tsx`, `../../src/components/`(50+ React/Ink 组件)
|
||||
|
||||
---
|
||||
|
||||
### [技能系统](UI与扩展设计-技能系统.md)
|
||||
|
||||
覆盖 `ISkillLoader` 接口、`SkillDefinition` 数据模型、`SkillHooks` 生命周期钩子,以及 `SkillLoader` 的目录扫描与 YAML frontmatter 解析实现。
|
||||
|
||||
原始来源: `../../src/skills/`
|
||||
|
||||
---
|
||||
|
||||
### [插件系统](UI与扩展设计-插件系统.md)
|
||||
|
||||
覆盖 `IPluginManager` 接口、`PluginLoadContext` 可卸载程序集加载上下文、`PluginManager` 的完整实现,以及 `PluginManifest` 清单结构。
|
||||
|
||||
原始来源: `../../src/plugins/`
|
||||
|
||||
---
|
||||
|
||||
### [特性开关系统](UI与扩展设计-特性开关系统.md)
|
||||
|
||||
覆盖 `FeatureFlags` 完整常量清单(54+ 标志)、`IFeatureFlagService` 接口,以及 `FeatureFlagService` 的配置驱动实现。说明编译时 `#if` 与运行时 `IsEnabled()` 的双模式设计。
|
||||
|
||||
原始来源: `scripts/build.ts` `feature()` 编译时开关 + 运行时 GrowthBook
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
**Terminal.Gui 替代 React/Ink**
|
||||
|
||||
原始项目深度依赖 React 组件模型和 Ink 的虚拟 DOM 终端渲染。.NET 重写选用 Terminal.Gui,它提供与 Ink 语义最接近的声明式布局(`Pos`/`Dim`)、事件驱动模型和色彩主题系统,同时完全原生于 .NET 运行时,无需 Node.js 桥接。
|
||||
|
||||
**技能作为 System Prompt 注入**
|
||||
|
||||
技能系统不引入自定义执行运行时。技能的 Markdown 内容直接注入 `QueryEngine` 的 System Prompt,参数拼接到提示末尾,其余执行逻辑全部复用主查询管道。这与原始实现的语义完全一致,且不新增任何抽象层。
|
||||
|
||||
**插件隔离通过 `isCollectible: true`**
|
||||
|
||||
`PluginLoadContext` 以 `isCollectible: true` 创建,允许在 `UninstallPluginAsync` 调用后通过 GC 完整回收插件的所有托管内存和类型元数据。这在语义上等价于原始 TypeScript 的动态 `import()` 隔离,但提供更强的确定性卸载保证。
|
||||
|
||||
**特性开关双模式并存**
|
||||
|
||||
编译时标志(`#if ULTRAPLAN`)用于完全消除死代码,减小最终二进制体积。运行时标志(`IFeatureFlagService.IsEnabled`)用于不重新编译就能切换的开关,由 `appsettings.json` 中的 `FeatureFlags` 配置节驱动,取代原始 GrowthBook 的云端下发机制。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md)
|
||||
- [服务子系统设计](../服务子系统设计/)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
30
docs/UI与扩展设计/reference/原始代码映射-UI与扩展.md
Normal file
30
docs/UI与扩展设计/reference/原始代码映射-UI与扩展.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 原始代码映射:UI 与扩展设计
|
||||
|
||||
> 交叉引用:详见《UI 与扩展设计》总览文档中的模块分层、命名约定与扩展边界说明。
|
||||
|
||||
## .NET 类型 → 原始 TypeScript 源文件映射
|
||||
|
||||
| .NET 类型/模块 | 原始 TypeScript 源文件 | 对应关系说明 |
|
||||
|---|---|---|
|
||||
| TerminalApp | `../../../src/screens/REPL.tsx` + `../../../src/components/` | 对应终端主入口与交互循环;UI 逻辑主要集中在 REPL 屏幕,配套的 Ink/React 组件负责局部渲染与交互。 |
|
||||
| REPLScreen | `../../../src/screens/REPL.tsx` | 直接对应 REPL 主屏幕实现,是终端 UI 的核心承载层。 |
|
||||
| PermissionDialog | `../../../src/components/` | 对应权限确认类对话框组件,属于终端 UI 中的交互式弹窗/提示层。 |
|
||||
| CompanionSpriteView | `../../../src/components/` | 对应陪伴角色/装饰性展示组件,通常作为界面中的独立视图单元实现。 |
|
||||
| ISkillLoader | `../../../src/skills/` | 对应技能加载抽象接口,用于定义技能发现、解析与装载能力。 |
|
||||
| SkillDefinition | `../../../src/skills/` | 对应技能定义模型,承载技能元数据、入口与执行相关描述。 |
|
||||
| SkillHooks | `../../../src/skills/` | 对应技能生命周期钩子集合,用于扩展技能执行流程。 |
|
||||
| SkillLoader | `../../../src/skills/` | 对应技能加载器实现,负责读取并实例化技能配置。 |
|
||||
| IPluginManager | `../../../src/plugins/` | 对应插件管理抽象接口,定义插件注册、启用与调度能力。 |
|
||||
| PluginLoadContext | `../../../src/plugins/` | 对应插件加载上下文,封装插件初始化与运行时依赖。 |
|
||||
| PluginManager | `../../../src/plugins/` | 对应插件管理器实现,负责插件生命周期管理。 |
|
||||
| LoadedPlugin | `../../../src/plugins/` | 对应已加载插件实例模型,表示运行时插件对象。 |
|
||||
| PluginManifest | `../../../src/plugins/` | 对应插件清单/描述文件模型,保存插件名称、版本、入口等信息。 |
|
||||
| FeatureFlags | `scripts/build.ts` 的 `feature()` + 运行时 GrowthBook | 对应构建期特性开关与运行时实验能力的统一抽象。 |
|
||||
| IFeatureFlagService | `scripts/build.ts` 的 `feature()` + 运行时 GrowthBook | 对应特性开关服务接口,既覆盖编译期注入,也覆盖运行时读取。 |
|
||||
| FeatureFlagService | `scripts/build.ts` 的 `feature()` + 运行时 GrowthBook | 对应特性开关服务实现,负责聚合构建时与运行时的开关结果。 |
|
||||
|
||||
## .NET 命名约定说明
|
||||
|
||||
- `.NET 类型/模块` 列优先使用 C# 语义化命名,表示面向对象层的抽象、服务或模型。
|
||||
- `原始 TypeScript 源文件` 列仅记录最接近的源代码位置;当实现分散时,用 `+` 标明多个来源。
|
||||
- `对应关系说明` 以职责映射为准,不强求 1:1 文件对应,便于后续从 TypeScript 迁移到 .NET 结构时保持概念一致。
|
||||
968
docs/free-code 项目结构完整分析报告.md
Normal file
968
docs/free-code 项目结构完整分析报告.md
Normal file
@ -0,0 +1,968 @@
|
||||
# free-code 项目结构完整分析报告
|
||||
|
||||
## 一、项目概览
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **名称** | free-code (Claude Code 的开源重建版本) |
|
||||
| **版本** | 2.1.87 |
|
||||
| **来源** | Anthropic Claude Code CLI 的源码快照(2026-03-31 npm 分发包 source map 暴露后重建) |
|
||||
| **运行时** | Bun >= 1.3.11 |
|
||||
| **语言** | TypeScript (ESNext, JSX: react-jsx) |
|
||||
| **总代码量** | ~1,997 个 TS/TSX 文件,共 ~512,834 行代码 |
|
||||
| **核心框架** | React 19 + Ink 6 (终端 UI) |
|
||||
|
||||
### 三大修改方向
|
||||
|
||||
1. **遥测移除** — 所有 OpenTelemetry/gRPC、GrowthBook 分析、Sentry 报告等外发遥测全部替换为空实现(stub)
|
||||
2. **安全提示护栏移除** — 移除了 Anthropic 注入的额外限制性系统提示
|
||||
3. **实验性功能解锁** — 88 个 feature flag 中 54 个可编译通过并启用
|
||||
|
||||
---
|
||||
|
||||
## 二、目录结构与代码量
|
||||
|
||||
| 目录 | 文件数 | 代码行数 | 职责 |
|
||||
|---|---|---|---|
|
||||
| `../src/utils/` | 577 | 178,924 | 工具函数(模型管理、认证、设置、权限等) |
|
||||
| `../src/components/` | 400 | 81,808 | Ink/React 终端 UI 组件 |
|
||||
| `../src/services/` | 147 | 54,351 | API 客户端、OAuth、MCP、分析(已 stub) |
|
||||
| `../src/tools/` | 210 | 51,068 | 40+ Agent 工具实现 |
|
||||
| `../src/commands/` | 218 | 26,526 | 70+ 斜杠命令 |
|
||||
| `../src/ink/` | 98 | 19,844 | Ink 框架内部(终端渲染引擎) |
|
||||
| `../src/hooks/` | 104 | 19,205 | React hooks |
|
||||
| `../src/bridge/` | 32 | 12,614 | IDE 远程控制桥接 |
|
||||
| `../src/cli/` | 21 | 12,408 | CLI 参数解析和命令分发 |
|
||||
| `../src/screens/` | 3 | 5,981 | 主界面 REPL |
|
||||
| `../src/entrypoints/` | 11 | 4,162 | 入口点 |
|
||||
| `../src/skills/` | 50 | 4,092 | 技能系统 |
|
||||
| `../src/native-ts/` | 4 | 4,081 | 原生模块绑定 |
|
||||
| `../src/types/` | 12 | 3,468 | 类型定义 |
|
||||
| `../src/tasks/` | 14 | 3,288 | 后台任务管理 |
|
||||
| `../src/keybindings/` | 14 | 3,159 | 快捷键配置 |
|
||||
| `../src/constants/` | 22 | 2,725 | 常量定义 |
|
||||
| 其他 (18 个目录) | — | — | vim、状态、迁移、MCP 等 |
|
||||
|
||||
### 完整目录列表
|
||||
|
||||
```
|
||||
../src/
|
||||
assistant/ # 助手会话管理
|
||||
bootstrap/ # 启动引导
|
||||
bridge/ # IDE 远程控制桥接(32 文件)
|
||||
buddy/ # Buddy 功能
|
||||
cli/ # CLI 参数解析和快速路径分发(21 文件)
|
||||
commands/ # 70+ 斜杠命令实现(218 文件)
|
||||
components/ # Ink/React 终端 UI 组件(400 文件)
|
||||
constants/ # 常量定义(22 文件)
|
||||
context/ # React 上下文
|
||||
coordinator/ # 协调器模式
|
||||
daemon/ # 后台守护进程
|
||||
entrypoints/ # 入口点(11 文件)
|
||||
environment-runner/ # BYOC 环境运行器
|
||||
hooks/ # React hooks(104 文件)
|
||||
ink/ # Ink 终端渲染引擎(98 文件)
|
||||
jobs/ # 作业系统
|
||||
keybindings/ # 快捷键配置(14 文件)
|
||||
memdir/ # 记忆目录管理
|
||||
migrations/ # 数据迁移(11 文件)
|
||||
moreright/ # 扩展权限
|
||||
native-ts/ # 原生 TypeScript 模块(4 文件)
|
||||
outputStyles/ # 输出样式
|
||||
plugins/ # 插件系统(2 文件)
|
||||
proactive/ # 主动任务
|
||||
query/ # 查询管道(4 文件)
|
||||
remote/ # 远程会话管理(4 文件)
|
||||
schemas/ # JSON Schema
|
||||
screens/ # 主界面屏幕(3 文件)
|
||||
self-hosted-runner/ # 自托管运行器
|
||||
server/ # 服务器模式(11 文件)
|
||||
services/ # 服务层(147 文件)
|
||||
skills/ # 技能系统(50 文件)
|
||||
ssh/ # SSH 远程会话
|
||||
state/ # 应用状态管理(6 文件)
|
||||
tasks/ # 后台任务类型(14 文件)
|
||||
tools/ # Agent 工具实现(210 文件)
|
||||
types/ # 类型定义(12 文件)
|
||||
upstreamproxy/ # 上游代理
|
||||
utils/ # 工具函数(577 文件)
|
||||
vendor/ # 第三方内联代码
|
||||
vim/ # Vim 编辑模式(5 文件)
|
||||
voice/ # 语音输入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、核心架构
|
||||
|
||||
### 3.1 启动流程
|
||||
|
||||
```
|
||||
cli.tsx (快速路径分发)
|
||||
├─ --version → 直接输出,零模块加载
|
||||
├─ --dump-system-prompt → 转储系统提示(feature-gated)
|
||||
├─ --computer-use-mcp → Chrome/Computer Use 模式
|
||||
├─ --daemon-worker → 后台守护进程
|
||||
├─ remote-control → 远程控制桥接
|
||||
└─ 默认交互模式 → main.tsx
|
||||
├─ 并行初始化(MDM、Keychain 预取)
|
||||
├─ Commander.js CLI 解析
|
||||
├─ 插件/技能加载
|
||||
├─ MCP 服务器初始化
|
||||
└─ REPL.tsx (主 UI)
|
||||
```
|
||||
|
||||
**入口文件详解:**
|
||||
|
||||
- **../src/entrypoints/cli.tsx** — 主入口,实现零导入快速路径优化
|
||||
- 所有导入都是动态的,以最小化模块评估
|
||||
- 特殊模式处理:Chrome 集成、daemon、bridge、后台会话、模板任务等
|
||||
- 环境特定优化(CCR 容器分配 8GB 堆内存)
|
||||
|
||||
- **../src/main.tsx** — 完整初始化流程
|
||||
- 带性能分析检查点的重型初始化
|
||||
- 并行启动优化(MDM 原始读取、Keychain 预取)
|
||||
- Commander.js CLI 设置
|
||||
- 会话管理和恢复
|
||||
- 插件/技能初始化
|
||||
- MCP 服务器管理
|
||||
- 分析和遥测设置
|
||||
|
||||
### 3.2 查询引擎 (`../src/QueryEngine.ts`)
|
||||
|
||||
`QueryEngine` 是整个 LLM 交互的核心:
|
||||
|
||||
- **异步生成器模式** — `submitMessage()` 以 async generator 流式输出 SDK 消息
|
||||
- **系统提示组装** — 整合自定义提示、记忆机制、思考配置
|
||||
- **权限管理** — `canUseTool()` 包装,追踪拒绝情况
|
||||
- **对话循环** — 多轮对话,带 budget/turn 限制
|
||||
- **会话持久化** — API 响应前保存会话,防崩溃丢数据
|
||||
- **Snip-boundary 回放** — 长会话的内存管理
|
||||
- **结构化输出执行** — 带重试限制
|
||||
- **错误追踪** — 基于 watermark 的 turn 范围界定
|
||||
|
||||
**查询管道:**
|
||||
|
||||
```
|
||||
User Input → processUserInput() → QueryEngine.submitMessage()
|
||||
↓
|
||||
System Prompt Assembly
|
||||
↓
|
||||
Permission Checking
|
||||
↓
|
||||
LLM API Call (query.ts)
|
||||
↓
|
||||
Stream Response → SDK Messages
|
||||
```
|
||||
|
||||
### 3.3 工具注册 (`../src/tools.ts`)
|
||||
|
||||
`getAllBaseTools()` 注册 80+ 工具,通过以下方式过滤:
|
||||
- Feature flag 条件编译
|
||||
- 权限上下文过滤
|
||||
- 模式过滤(SIMPLE/REPL/普通)
|
||||
- 工具预设配置
|
||||
|
||||
关键函数:
|
||||
- `getAllBaseTools()` — 所有工具的真实来源
|
||||
- `getTools()` — 按权限上下文和模式过滤
|
||||
- `assembleToolPool()` — 组合内置 + MCP 工具,去重
|
||||
- 稳定排序以确保 prompt-cache 效率
|
||||
|
||||
### 3.4 命令注册 (`../src/commands.ts`)
|
||||
|
||||
`getCommands()` 加载 ~150 个命令,来源包括:
|
||||
- 内置命令
|
||||
- Bundled skills
|
||||
- 插件 skills
|
||||
- MCP 命令
|
||||
- Workflow 命令
|
||||
|
||||
关键特性:
|
||||
- 通过 lodash memoization 实现延迟求值
|
||||
- 多源命令组合
|
||||
- 技能缓存与粒度失效
|
||||
- `REMOTE_SAFE_COMMANDS` / `BRIDGE_SAFE_COMMANDS` 安全过滤
|
||||
- `meetsAvailabilityRequirement()` 基于认证/提供商的过滤
|
||||
|
||||
### 3.5 REPL 屏幕 (`../src/screens/REPL.tsx`)
|
||||
|
||||
- 基于 React 的 Ink TUI,支持虚拟滚动
|
||||
- 基于消息的状态管理,不可变更新
|
||||
- 复杂的权限处理(auto-mode、sandbox、swarm)
|
||||
- 实时流式 LLM 响应
|
||||
- 多模式输入处理(normal、vim、search)
|
||||
- 成本追踪和预算执行
|
||||
- 后台任务管理
|
||||
- Swarm/Teammate 多 Agent 协调
|
||||
|
||||
---
|
||||
|
||||
## 四、五大 API 提供商支持
|
||||
|
||||
| 提供商 | 环境变量 | 认证方式 |
|
||||
|---|---|---|
|
||||
| **Anthropic(默认)** | — | `ANTHROPIC_API_KEY` 或 OAuth |
|
||||
| **OpenAI Codex** | `CLAUDE_CODE_USE_OPENAI=1` | OpenAI OAuth |
|
||||
| **AWS Bedrock** | `CLAUDE_CODE_USE_BEDROCK=1` | AWS credentials |
|
||||
| **Google Vertex AI** | `CLAUDE_CODE_USE_VERTEX=1` | gcloud ADC |
|
||||
| **Anthropic Foundry** | `CLAUDE_CODE_USE_FOUNDRY=1` | API Key |
|
||||
|
||||
**API 客户端架构:**
|
||||
|
||||
`../src/services/api/client.ts` 是统一的 API 客户端工厂,根据配置自动路由到不同提供商。
|
||||
|
||||
**Codex 适配器:**
|
||||
|
||||
`../src/services/api/codex-fetch-adapter.ts` 负责将 Anthropic 消息格式转换为 OpenAI Codex 格式:
|
||||
- Anthropic `base64` 图像 schema 映射到 Codex `input_image` 载荷
|
||||
- `tool_result` 项路由为顶层 `function_call_output` 对象
|
||||
- 剥离 Anthropic 专有的 `cache_control` 注解
|
||||
- 拦截 Codex `response.reasoning.delta` SSE 帧并包装为 Anthropic `<thinking>` 事件
|
||||
- 绑定 `response.completed` 事件以追踪 token 使用量
|
||||
|
||||
### 支持的模型
|
||||
|
||||
**Anthropic (Direct API) — 默认:**
|
||||
|
||||
| Model | ID |
|
||||
|---|---|
|
||||
| Claude Opus 4.6 | `claude-opus-4-6` |
|
||||
| Claude Sonnet 4.6 | `claude-sonnet-4-6` |
|
||||
| Claude Haiku 4.5 | `claude-haiku-4-5` |
|
||||
|
||||
**OpenAI Codex:**
|
||||
|
||||
| Model | ID |
|
||||
|---|---|
|
||||
| GPT-5.3 Codex (recommended) | `gpt-5.3-codex` |
|
||||
| GPT-5.4 | `gpt-5.4` |
|
||||
| GPT-5.4 Mini | `gpt-5.4-mini` |
|
||||
|
||||
### 环境变量参考
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
||||
| `ANTHROPIC_AUTH_TOKEN` | Auth token (alternative) |
|
||||
| `ANTHROPIC_MODEL` | Override default model |
|
||||
| `ANTHROPIC_BASE_URL` | Custom API endpoint |
|
||||
| `ANTHROPIC_DEFAULT_OPUS_MODEL` | Custom Opus model ID |
|
||||
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | Custom Sonnet model ID |
|
||||
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Custom Haiku model ID |
|
||||
| `CLAUDE_CODE_OAUTH_TOKEN` | OAuth token via env |
|
||||
| `CLAUDE_CODE_API_KEY_HELPER_TTL_MS` | API key helper cache TTL |
|
||||
| `AWS_REGION` / `AWS_DEFAULT_REGION` | AWS region for Bedrock |
|
||||
| `ANTHROPIC_BEDROCK_BASE_URL` | Custom Bedrock endpoint |
|
||||
| `ANTHROPIC_FOUNDRY_API_KEY` | Foundry API key |
|
||||
|
||||
---
|
||||
|
||||
## 五、Feature Flag 系统
|
||||
|
||||
### 构建系统 (`scripts/build.ts`)
|
||||
|
||||
- 使用 `bun:bundle` 的 `feature()` 宏实现编译时死代码消除
|
||||
- Bun 编译带字节码生成
|
||||
- 宏系统提供构建时常量(VERSION、BUILD_TIME 等)
|
||||
|
||||
### 构建变体
|
||||
|
||||
| 命令 | 输出 | Feature Flags | 描述 |
|
||||
|---|---|---|---|
|
||||
| `bun run build` | `./cli` | 仅 VOICE_MODE | 生产构建 |
|
||||
| `bun run build:dev` | `./cli-dev` | 仅 VOICE_MODE | 开发版 |
|
||||
| `bun run build:dev:full` | `./cli-dev` | 全部 54 个实验性 flag | 完整解锁构建 |
|
||||
| `bun run compile` | `./dist/cli` | 仅 VOICE_MODE | 替代输出路径 |
|
||||
| 自定义 | 自定义 | 指定 flags | `bun run ./scripts/build.ts --feature=ULTRAPLAN --feature=ULTRATHINK` |
|
||||
|
||||
### 54 个可工作 Feature Flags
|
||||
|
||||
#### 交互与 UI(14 个)
|
||||
|
||||
| Flag | 描述 |
|
||||
|---|---|
|
||||
| `AWAY_SUMMARY` | 离开键盘摘要行为 |
|
||||
| `HISTORY_PICKER` | 交互式提示历史选择器 |
|
||||
| `HOOK_PROMPTS` | 将 prompt/request 文本传入 hook 执行流程 |
|
||||
| `KAIROS_BRIEF` | 简短转录布局和 BriefTool UX |
|
||||
| `KAIROS_CHANNELS` | 频道通知和回调 |
|
||||
| `LODESTONE` | 深度链接/协议注册相关流程 |
|
||||
| `MESSAGE_ACTIONS` | 消息操作入口点 |
|
||||
| `NEW_INIT` | 新版 `/init` 决策路径 |
|
||||
| `QUICK_SEARCH` | 提示快速搜索 |
|
||||
| `SHOT_STATS` | 额外的 shot-distribution 统计视图 |
|
||||
| `TOKEN_BUDGET` | Token 预算追踪和警告 UI |
|
||||
| `ULTRAPLAN` | 远程多 Agent 规划(Opus 级别) |
|
||||
| `ULTRATHINK` | 深度思考模式 |
|
||||
| `VOICE_MODE` | 语音切换、听写、语音通知和 UI |
|
||||
|
||||
#### Agent/记忆/规划(10 个)
|
||||
|
||||
| Flag | 描述 |
|
||||
|---|---|
|
||||
| `AGENT_MEMORY_SNAPSHOT` | 存储 Agent 记忆快照状态 |
|
||||
| `AGENT_TRIGGERS` | 本地 cron/触发器工具 |
|
||||
| `AGENT_TRIGGERS_REMOTE` | 远程触发器工具路径 |
|
||||
| `BUILTIN_EXPLORE_PLAN_AGENTS` | 内置 explore/plan agent 预设 |
|
||||
| `CACHED_MICROCOMPACT` | 通过查询和 API 流程缓存的微压缩状态 |
|
||||
| `COMPACTION_REMINDERS` | 压缩和附件流程的提醒 |
|
||||
| `EXTRACT_MEMORIES` | 查询后记忆提取 hooks |
|
||||
| `PROMPT_CACHE_BREAK_DETECTION` | 压缩/查询流程中的缓存中断检测 |
|
||||
| `TEAMMEM` | 团队记忆文件和 watcher hooks |
|
||||
| `VERIFICATION_AGENT` | 验证 Agent 指导 |
|
||||
|
||||
#### 工具/权限/远程(13 个)
|
||||
|
||||
| Flag | 描述 |
|
||||
|---|---|
|
||||
| `BASH_CLASSIFIER` | 分类器辅助的 bash 权限决策 |
|
||||
| `BRIDGE_MODE` | 远程控制 / REPL 桥接命令 |
|
||||
| `CCR_AUTO_CONNECT` | CCR 自动连接默认路径 |
|
||||
| `CCR_MIRROR` | 仅出站 CCR 镜像会话 |
|
||||
| `CCR_REMOTE_SETUP` | 远程设置命令路径 |
|
||||
| `CHICAGO_MCP` | Computer-use MCP 集成 |
|
||||
| `CONNECTOR_TEXT` | 连接器文本块处理 |
|
||||
| `MCP_RICH_OUTPUT` | 更丰富的 MCP UI 渲染 |
|
||||
| `NATIVE_CLIPBOARD_IMAGE` | 原生 macOS 剪贴板图像快速路径 |
|
||||
| `POWERSHELL_AUTO_MODE` | PowerShell 自动模式权限处理 |
|
||||
| `TREE_SITTER_BASH` | tree-sitter bash 解析器后端 |
|
||||
| `TREE_SITTER_BASH_SHADOW` | tree-sitter bash 影子发布路径 |
|
||||
| `UNATTENDED_RETRY` | API 重试流程中的无人值守重试 |
|
||||
|
||||
#### 支撑性 Flag(17 个)
|
||||
|
||||
| Flag | 描述 |
|
||||
|---|---|
|
||||
| `ABLATION_BASELINE` | CLI 消融/基线入口开关 |
|
||||
| `ALLOW_TEST_VERSIONS` | 允许原生安装程序中的测试版本 |
|
||||
| `ANTI_DISTILLATION_CC` | 添加反蒸馏请求元数据 |
|
||||
| `BREAK_CACHE_COMMAND` | 注入 break-cache 命令路径 |
|
||||
| `COWORKER_TYPE_TELEMETRY` | 添加协作者类型遥测字段 |
|
||||
| `DOWNLOAD_USER_SETTINGS` | 启用设置同步拉取路径 |
|
||||
| `DUMP_SYSTEM_PROMPT` | 启用系统提示转储路径 |
|
||||
| `FILE_PERSISTENCE` | 启用文件持久化管道 |
|
||||
| `HARD_FAIL` | 启用更严格的失败/日志行为 |
|
||||
| `IS_LIBC_GLIBC` | 强制 glibc 环境检测 |
|
||||
| `IS_LIBC_MUSL` | 强制 musl 环境检测 |
|
||||
| `NATIVE_CLIENT_ATTESTATION` | 添加原生证明标记文本 |
|
||||
| `PERFETTO_TRACING` | 启用 perfetto 追踪 hooks |
|
||||
| `SKILL_IMPROVEMENT` | 启用技能改进 hooks |
|
||||
| `SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED` | 自动更新禁用时跳过更新检测 |
|
||||
| `SLOW_OPERATION_LOGGING` | 启用慢操作日志 |
|
||||
| `UPLOAD_USER_SETTINGS` | 启用设置同步推送路径 |
|
||||
|
||||
### 运行时注意事项
|
||||
|
||||
部分 flag 虽然编译通过但有运行时限制:
|
||||
|
||||
- `VOICE_MODE` — 需要 claude.ai OAuth 和本地录制后端
|
||||
- `NATIVE_CLIPBOARD_IMAGE` — 仅在 macOS 且存在 `image-processor-napi` 时加速
|
||||
- `BRIDGE_MODE`、`CCR_*` — 运行时需要 claude.ai OAuth + GrowthBook 权限检查
|
||||
- `KAIROS_BRIEF`、`KAIROS_CHANNELS` — 仅暴露已存在的 brief/channel 特定接口
|
||||
- `CHICAGO_MCP` — 运行时仍需 `@ant/computer-use-*` 外部包
|
||||
|
||||
### 34 个不可编译 Flag
|
||||
|
||||
#### 易修复(15 个)— 缺少单个文件或资产
|
||||
|
||||
| Flag | 缺失内容 |
|
||||
|---|---|
|
||||
| `AUTO_THEME` | `../src/utils/systemThemeWatcher.js`(OSC 11 watcher) |
|
||||
| `BG_SESSIONS` | `../src/cli/bg.js` |
|
||||
| `BUDDY` | `../src/commands/buddy/index.js` |
|
||||
| `BUILDING_CLAUDE_APPS` | `../src/claude-api/csharp/claude-api.md`(文档资产) |
|
||||
| `COMMIT_ATTRIBUTION` | `../src/utils/attributionHooks.js` |
|
||||
| `FORK_SUBAGENT` | `../src/commands/fork/index.js` |
|
||||
| `HISTORY_SNIP` | `../src/commands/force-snip.js` |
|
||||
| `KAIROS_GITHUB_WEBHOOKS` | `../src/tools/SubscribePRTool/SubscribePRTool.js` |
|
||||
| `KAIROS_PUSH_NOTIFICATION` | `../src/tools/PushNotificationTool/PushNotificationTool.js` |
|
||||
| `MCP_SKILLS` | `../src/skills/mcpSkills.js` |
|
||||
| `MEMORY_SHAPE_TELEMETRY` | `../src/memdir/memoryShapeTelemetry.js` |
|
||||
| `OVERFLOW_TEST_TOOL` | `../src/tools/OverflowTestTool/OverflowTestTool.js` |
|
||||
| `RUN_SKILL_GENERATOR` | `../src/runSkillGenerator.js` |
|
||||
| `TEMPLATES` | `../src/cli/handlers/templateJobs.js` |
|
||||
| `TORCH` | `../src/commands/torch.js` |
|
||||
| `TRANSCRIPT_CLASSIFIER` | `../src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt` |
|
||||
|
||||
#### 中等修复(16 个)— 缺少较大子系统部分
|
||||
|
||||
| Flag | 缺失内容 |
|
||||
|---|---|
|
||||
| `BYOC_ENVIRONMENT_RUNNER` | `../src/environment-runner/main.js` |
|
||||
| `CONTEXT_COLLAPSE` | `../src/tools/CtxInspectTool/CtxInspectTool.js` |
|
||||
| `COORDINATOR_MODE` | `../src/coordinator/workerAgent.js` |
|
||||
| `DAEMON` | `../src/daemon/workerRegistry.js` |
|
||||
| `DIRECT_CONNECT` | `../src/server/parseConnectUrl.js` |
|
||||
| `EXPERIMENTAL_SKILL_SEARCH` | `../src/services/skillSearch/localSearch.js` |
|
||||
| `MONITOR_TOOL` | `../src/tools/MonitorTool/MonitorTool.js` |
|
||||
| `REACTIVE_COMPACT` | `../src/services/compact/reactiveCompact.js` |
|
||||
| `REVIEW_ARTIFACT` | `../src/hunter.js` |
|
||||
| `SELF_HOSTED_RUNNER` | `../src/self-hosted-runner/main.js` |
|
||||
| `SSH_REMOTE` | `../src/ssh/createSSHSession.js` |
|
||||
| `TERMINAL_PANEL` | `../src/tools/TerminalCaptureTool/TerminalCaptureTool.js` |
|
||||
| `UDS_INBOX` | `../src/utils/udsMessaging.js` |
|
||||
| `WEB_BROWSER_TOOL` | `../src/tools/WebBrowserTool/WebBrowserTool.js` |
|
||||
| `WORKFLOW_SCRIPTS` | `../src/commands/workflows/index.js` 及多个相关文件 |
|
||||
|
||||
#### 大型缺失(3 个)— 需要重建完整子系统
|
||||
|
||||
| Flag | 缺失内容 |
|
||||
|---|---|
|
||||
| `KAIROS` | `../src/assistant/index.js` 及大部分助手栈 |
|
||||
| `KAIROS_DREAM` | `../src/dream.js` 及 dream-task 行为 |
|
||||
| `PROACTIVE` | `../src/proactive/index.js` 及主动任务/工具栈 |
|
||||
|
||||
---
|
||||
|
||||
## 六、核心子系统详解
|
||||
|
||||
### 6.1 状态管理 (`../src/state/`)
|
||||
|
||||
自定义 Store 模式(非 Redux/Zustand):
|
||||
|
||||
**核心文件:**
|
||||
|
||||
- **store.ts** — `getState()/setState()/subscribe()` 基础实现
|
||||
- 可选 `onChange` 回调用于状态变更追踪
|
||||
- Listener set 用于 React 风格订阅
|
||||
|
||||
- **AppStateStore.ts** — 450+ 行的类型定义,涵盖:
|
||||
- 设置和配置
|
||||
- 任务状态管理(tasks、agentNameRegistry)
|
||||
- MCP 状态(clients、tools、commands、resources)
|
||||
- 插件状态(enabled、disabled、errors、installation status)
|
||||
- 远程桥接状态(connection、session、polling)
|
||||
- UI 状态(expandedView、footerSelection、spinnerTip)
|
||||
- 权限上下文和模式
|
||||
- 文件历史和归属追踪
|
||||
- Tungsten (tmux) 集成状态
|
||||
- Computer Use (chicago MCP) 状态
|
||||
- Team/swarm 上下文和收件箱管理
|
||||
|
||||
- **AppState.tsx** — React 集成:
|
||||
- `AppStateProvider` 上下文组件
|
||||
- `useAppState(selector)` — 使用 `useSyncExternalStore` 的优化 hook
|
||||
- `useSetAppState()` — 非订阅 setter
|
||||
- `useAppStateStore()` — 直接 store 访问
|
||||
- `useAppStateMaybeOutsideOfProvider()` — 可选上下文的安全版本
|
||||
|
||||
- **selectors.ts** — 纯选择器函数:
|
||||
- `getViewedTeammateTask()` — 获取当前查看的 teammate
|
||||
- `getActiveAgentForInput()` — 确定输入路由(leader/viewed/named)
|
||||
|
||||
- **onChangeAppState.ts** — 状态变更副作用处理器:
|
||||
- 权限模式同步到 CCR/SDK
|
||||
- 模型设置持久化
|
||||
- 展开视图持久化
|
||||
- 设置变更时清除认证缓存
|
||||
|
||||
### 6.2 服务层 (`../src/services/`)
|
||||
|
||||
28 个主要服务区域:
|
||||
|
||||
#### API 客户端 (`../src/services/api/`)
|
||||
|
||||
- **client.ts** — 统一 API 客户端工厂,支持五大提供商
|
||||
- **codex-fetch-adapter.ts** — Anthropic → OpenAI Codex 格式转换
|
||||
- OAuth token 刷新、自定义 headers、CCH 签名、代理支持
|
||||
- 错误处理、重试逻辑、使用量追踪
|
||||
|
||||
#### OAuth (`../src/services/oauth/`)
|
||||
|
||||
- **index.ts** — OAuthService 类,实现 OAuth 2.0 Authorization Code Flow + PKCE
|
||||
- **client.ts** — OAuth 客户端操作(构建 auth URL、交换 code 为 token、获取 profile)
|
||||
- **auth-code-listener.ts** — 本地 HTTP 服务器用于自动 OAuth 回调处理
|
||||
- **crypto.ts** — PKCE code verifier/challenge 生成
|
||||
- **codex-client.ts** — Codex 特定 OAuth 客户端
|
||||
|
||||
#### MCP 集成 (`../src/services/mcp/`)
|
||||
|
||||
- **client.ts** — MCP 客户端管理
|
||||
- **types.ts** — MCP 服务器配置的全面类型定义(stdio、SSE、HTTP、WebSocket、SDK、IDE 特定)
|
||||
- **config.ts** — MCP 配置管理
|
||||
- **useManageMCPConnections.ts** — MCP 连接生命周期的 React hook
|
||||
- **channelPermissions.ts** — MCP 频道权限处理(Telegram、iMessage 等)
|
||||
- 支持 XAA(Cross-App Access)、elicitation 处理、资源管理
|
||||
|
||||
#### 分析 (`../src/services/analytics/`) — 已 STUB
|
||||
|
||||
- 所有函数都是空操作(logEvent、attachAnalyticsSink 等)
|
||||
- 作为兼容性边界,使现有调用点保持不变而遥测被禁用
|
||||
- 包括:datadog.ts、firstPartyEventLogger.ts、growthbook.ts、sink.ts
|
||||
|
||||
#### 其他主要服务
|
||||
|
||||
| 服务 | 职责 | 状态 |
|
||||
|---|---|---|
|
||||
| `compact/` | 上下文压缩(auto-compact、micro-compact、reactive compact) | 完整 |
|
||||
| `tools/` | 工具执行编排(StreamingToolExecutor、toolOrchestration) | 完整 |
|
||||
| `plugins/` | 插件操作和安装管理 | 完整 |
|
||||
| `SessionMemory/` | 会话记忆持久化和检索 | 完整 |
|
||||
| `skillSearch/` | 技能搜索,带远程状态管理 | 完整 |
|
||||
| `remoteManagedSettings/` | 远程托管设置同步 | 完整 |
|
||||
| `extractMemories/` | 从对话上下文中提取记忆 | 完整 |
|
||||
| `lsp/` | Language Server Protocol 集成 | 完整 |
|
||||
| `contextCollapse/` | 上下文窗口优化 | 完整 |
|
||||
| `tips/` | 用户提示和指导系统 | 完整 |
|
||||
| `voice.ts` | 语音集成(STT、keyterms、streaming) | 完整 |
|
||||
|
||||
### 6.3 IDE 桥接 (`../src/bridge/`)
|
||||
|
||||
远程控制架构:
|
||||
|
||||
1. 注册环境到后端(机器名、目录、分支、仓库 URL)
|
||||
2. 轮询获取 work items(要执行的会话)
|
||||
3. 为每个会话创建子进程
|
||||
4. 管理会话生命周期(心跳、停止、归档)
|
||||
5. 权限响应回传
|
||||
6. 支持多种生成模式(单会话、worktree、同目录)
|
||||
|
||||
**核心文件:**
|
||||
|
||||
- **bridgeApi.ts** — Bridge API 客户端(环境注册/注销、工作轮询/确认、会话生命周期管理)
|
||||
- **types.ts** — 全面的桥接类型定义(260+ 行)
|
||||
- **bridgeConfig.ts** — 桥接认证/URL 解析
|
||||
- **remoteBridgeCore.ts** — 远程桥接核心逻辑
|
||||
- **replBridgeHandle.ts** — REPL 桥接句柄实现
|
||||
- **replBridgeTransport.ts** — 桥接传输层
|
||||
- **createSession.ts** — 会话创建逻辑
|
||||
- **sessionRunner.ts** — 会话执行运行器
|
||||
- **jwtUtils.ts** — JWT token 工具
|
||||
- **peerSessions.ts** — 对等会话管理
|
||||
- **trustedDevice.ts** — 受信任设备认证
|
||||
|
||||
### 6.4 任务系统 (`../src/tasks/`)
|
||||
|
||||
7 种任务类型:
|
||||
|
||||
| 任务类型 | 用途 |
|
||||
|---|---|
|
||||
| `LocalShellTask` | 后台 Bash 命令执行 |
|
||||
| `LocalAgentTask` | 本地 Agent 子进程(自主工作) |
|
||||
| `RemoteAgentTask` | 远程 CCR Agent(通过 CCR API 执行) |
|
||||
| `InProcessTeammateTask` | 进程内 Teammate(共享主进程) |
|
||||
| `LocalWorkflowTask` | 预定义工作流脚本 |
|
||||
| `MonitorMcpTask` | MCP 资源变更监控 |
|
||||
| `DreamTask` | 异步/Dream 任务 |
|
||||
|
||||
**任务管理特性:**
|
||||
- 任务状态:pending → in_progress → completed(或 deleted)
|
||||
- 后台任务:带 `isBackgrounded` 标志
|
||||
- 任务依赖:`blocks` 和 `blockedBy` 关系
|
||||
- 任务所有权:可分配给特定 Agent
|
||||
- 团队任务列表:团队共享任务列表以协调工作
|
||||
|
||||
### 6.5 工具系统 (`../src/tools/`)
|
||||
|
||||
40+ 工具按功能分类:
|
||||
|
||||
#### 文件操作
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| FileReadTool | 读取文件(支持图像、PDF、Jupyter notebook) |
|
||||
| FileWriteTool | 写入文件(需先读取现有文件) |
|
||||
| FileEditTool | 精确字符串替换(支持 replace_all) |
|
||||
| GlobTool | 快速文件模式匹配 |
|
||||
| GrepTool | 基于 ripgrep 的内容搜索 |
|
||||
| NotebookEditTool | Jupyter notebook 单元格编辑 |
|
||||
| SnipTool | 代码片段管理 |
|
||||
| SendUserFileTool | 向用户发送文件 |
|
||||
|
||||
#### Shell 和系统
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| BashTool | 执行 bash 命令(支持后台执行、超时、沙盒) |
|
||||
| PowerShellTool | 执行 PowerShell 命令(Windows) |
|
||||
| SleepTool | 等待指定时间 |
|
||||
| TerminalCaptureTool | 终端捕获 |
|
||||
|
||||
#### 通信和规划
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| AskUserQuestionTool | 向用户提问(支持多选、预览) |
|
||||
| SendMessageTool | 向其他 Agent 发送消息 |
|
||||
| EnterPlanModeTool | 进入规划模式 |
|
||||
| ExitPlanModeTool | 退出规划模式 |
|
||||
| BriefTool | 向用户发送消息(主通信通道) |
|
||||
|
||||
#### Web 和网络
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| WebFetchTool | 获取 URL 内容(HTML → markdown,15 分钟缓存) |
|
||||
| WebSearchTool | Web 搜索 |
|
||||
| WebBrowserTool | Web 浏览器面板 |
|
||||
| RemoteTriggerTool | 管理远程 Agent 触发器 |
|
||||
|
||||
#### MCP
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| MCPTool | 通用 MCP 工具包装器 |
|
||||
| ListMcpResourcesTool | 列出 MCP 服务器资源 |
|
||||
| ReadMcpResourceTool | 读取 MCP 服务器资源 |
|
||||
| McpAuthTool | MCP 认证处理 |
|
||||
|
||||
#### Agent 和团队
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| AgentTool | 启动专门的 Agent 子进程 |
|
||||
| TeamCreateTool | 创建 Agent 团队 |
|
||||
| TeamDeleteTool | 删除 Agent 团队 |
|
||||
|
||||
#### 任务管理
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| TaskCreateTool | 创建任务列表 |
|
||||
| TaskUpdateTool | 更新任务 |
|
||||
| TaskGetTool | 获取任务详情 |
|
||||
| TaskListTool | 列出所有任务 |
|
||||
| TaskStopTool | 停止后台任务 |
|
||||
| TaskOutputTool | 查看后台任务输出 |
|
||||
|
||||
#### Git
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| EnterWorktreeTool | 创建隔离的 git worktree |
|
||||
| ExitWorktreeTool | 退出 worktree |
|
||||
|
||||
#### 调度
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| CronCreateTool | 创建定时任务 |
|
||||
| CronDeleteTool | 删除定时任务 |
|
||||
| CronListTool | 列出定时任务 |
|
||||
|
||||
#### 配置和发现
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| ConfigTool | 获取/设置配置 |
|
||||
| ToolSearchTool | 获取延迟加载工具的 schema |
|
||||
| DiscoverSkillsTool | 发现可用技能 |
|
||||
| LSPTool | LSP 服务器交互(goToDefinition、findReferences 等) |
|
||||
|
||||
#### 验证
|
||||
|
||||
| 工具 | 功能 |
|
||||
|---|---|
|
||||
| VerifyPlanExecutionTool | 验证计划执行 |
|
||||
|
||||
**工具延迟加载:**
|
||||
- 工具可被延迟加载(不包含在初始 prompt 中)以减少上下文
|
||||
- 延迟加载的工具需要 ToolSearch 来获取 schema
|
||||
- MCP 工具始终延迟加载
|
||||
- 部分工具永不延迟(Agent、Brief when KAIROS enabled)
|
||||
|
||||
### 6.6 命令系统 (`../src/commands/`)
|
||||
|
||||
70+ 斜杠命令:
|
||||
|
||||
#### 配置和设置
|
||||
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `/config` (别名: `/settings`) | 打开配置面板 |
|
||||
| `/keybindings` | 打开/创建快捷键配置文件 |
|
||||
| `/add-dir` | 添加新的工作目录 |
|
||||
| `/memory` | 编辑记忆文件 |
|
||||
| `/sandbox` | 切换沙盒模式 |
|
||||
| `/vim` | 切换 Vim/普通编辑模式 |
|
||||
| `/terminal-setup` | 安装 Shift+Enter 换行绑定 |
|
||||
| `/color` | 设置提示栏颜色 |
|
||||
| `/effort` | 设置模型努力级别 |
|
||||
| `/model` | 更改模型设置 |
|
||||
| `/fast` | 切换快速模式 |
|
||||
| `/theme` | 更改主题 |
|
||||
|
||||
#### 任务和会话管理
|
||||
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `/tasks` (别名: `/bashes`) | 列出和管理后台任务 |
|
||||
| `/plan` | 启用规划模式或查看当前计划 |
|
||||
| `/branch` (别名: `/fork`) | 在当前点创建对话分支 |
|
||||
| `/resume` (别名: `/continue`) | 恢复之前的对话 |
|
||||
| `/session` | 会话管理 |
|
||||
| `/compact` | 清除对话历史但保留摘要 |
|
||||
| `/exit` | 退出 CLI |
|
||||
| `/btw` | 快速旁问(不打断主对话) |
|
||||
|
||||
#### 信息和诊断
|
||||
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `/help` | 显示帮助信息 |
|
||||
| `/context` | 可视化当前上下文使用情况 |
|
||||
| `/cost` | 显示当前会话的总成本和时长 |
|
||||
| `/usage` | 显示使用信息 |
|
||||
| `/stats` | 显示统计信息 |
|
||||
| `/doctor` | 诊断和验证安装 |
|
||||
| `/status` | 显示状态信息 |
|
||||
| `/statusline` | 状态栏管理 |
|
||||
| `/release-notes` | 显示发行说明 |
|
||||
|
||||
#### 功能和集成
|
||||
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `/ide` | 管理 IDE 集成 |
|
||||
| `/agents` | 管理 Agent 配置 |
|
||||
| `/skills` | 技能管理 |
|
||||
| `/plugin` | 插件管理 |
|
||||
| `/mcp` | MCP 管理 |
|
||||
| `/voice` | 语音模式 |
|
||||
| `/remote-control` (别名: `/rc`) | 远程控制会话 |
|
||||
| `/chrome` | Chrome 集成 |
|
||||
|
||||
#### Git 和版本控制
|
||||
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `/commit` | 创建 git 提交(技能) |
|
||||
| `/commit-push-pr` | 提交、推送并创建 PR |
|
||||
| `/diff` | 显示差异 |
|
||||
| `/tag` | 标签管理 |
|
||||
|
||||
#### 账户和认证
|
||||
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `/login` | 登录账户 |
|
||||
| `/logout` | 登出账户 |
|
||||
| `/upgrade` | 升级到 Max |
|
||||
| `/permissions` | 管理权限 |
|
||||
| `/hooks` | Hooks 管理 |
|
||||
| `/init` | 初始化 Claude Code |
|
||||
|
||||
### 6.7 技能系统 (`../src/skills/`)
|
||||
|
||||
13+ 个内置技能:
|
||||
|
||||
| 技能 | 功能 |
|
||||
|---|---|
|
||||
| `/update-config` | 通过 settings.json 配置 hooks 和自动化行为 |
|
||||
| `/simplify` | 审查代码质量(启动 3 个并行 Agent) |
|
||||
| `/loop` | 定时循环执行命令 |
|
||||
| `/verify` | 验证代码变更 |
|
||||
| `/remember` | 审查自动记忆并提议提升到 CLAUDE.md |
|
||||
| `/debug` | 调试技能 |
|
||||
| `/keybindings` | 快捷键管理 |
|
||||
| `/batch` | 批量操作 |
|
||||
| `/stuck` | 获取解困帮助 |
|
||||
| `/skillify` | 创建新技能 |
|
||||
| `/claudeApi` | Claude API 文档和示例 |
|
||||
| `/dream` | Dream 技能 |
|
||||
| `/hunter` | 代码审查工件 |
|
||||
| `/runSkillGenerator` | 运行技能生成器 |
|
||||
|
||||
**技能系统架构:**
|
||||
|
||||
- **注册** — `registerBundledSkill()` 在启动时注册技能
|
||||
- **定义** — 技能包含 name、description、aliases、whenToUse、allowedTools、model、hooks
|
||||
- **引用文件** — 首次调用时提取到磁盘
|
||||
- **发现** — 技能出现在 Skill 工具提示中(bundled 技能永不截断)
|
||||
- **调用** — 通过 Skill 工具或 `/skill-name` 斜杠命令
|
||||
|
||||
### 6.8 插件系统 (`../src/plugins/`)
|
||||
|
||||
- 内置插件格式:`{name}@builtin`
|
||||
- 区别于市场插件:`{name}@{marketplace}`
|
||||
- 可提供 skills、hooks、MCP servers
|
||||
- 通过 `/plugin` UI 管理
|
||||
- 用户设置控制启用状态(默认:`defaultEnabled ?? true`)
|
||||
|
||||
---
|
||||
|
||||
## 七、React Hooks (`../src/hooks/`)
|
||||
|
||||
104 个 React hooks 按类别:
|
||||
|
||||
### 数据获取和状态
|
||||
|
||||
- `useTasksV2`、`useTaskListWatcher` — 任务管理
|
||||
- `useSettings`、`useSettingsChange` — 设置同步
|
||||
- `useApiKeyVerification` — API key 验证
|
||||
- `useInboxPoller` — 消息收件箱轮询
|
||||
- `useDiffData` — Diff 数据计算
|
||||
|
||||
### UI/交互
|
||||
|
||||
- `useTypeahead` — 输入自动完成
|
||||
- `useSearchInput` — 搜索功能
|
||||
- `useArrowKeyHistory` — 命令历史导航
|
||||
- `useVirtualScroll` — 虚拟滚动
|
||||
- `usePasteHandler` — 剪贴板粘贴处理
|
||||
- `useBlink` — 光标闪烁效果
|
||||
|
||||
### IDE 集成
|
||||
|
||||
- `useIdeLogging` — IDE 日志
|
||||
- `useDiffInIDE` — IDE diff 查看
|
||||
- `useIdeAtMentioned` — IDE @mention 处理
|
||||
- `useLspPluginRecommendation` — LSP 插件建议
|
||||
|
||||
### 功能特定
|
||||
|
||||
- `useVoice`、`useVoiceIntegration`、`useVoiceEnabled` — 语音功能
|
||||
- `useDirectConnect` — 直接连接模式
|
||||
- `useSSHSession` — SSH 会话管理
|
||||
- `useBackgroundTaskNavigation` — 后台任务路由
|
||||
- `useSwarmInitialization`、`useSwarmPermissionPoller` — Agent swarm 管理
|
||||
|
||||
### 工具
|
||||
|
||||
- `useTerminalSize` — 终端尺寸
|
||||
- `useMemoryUsage` — 内存监控
|
||||
- `useScheduledTasks` — 任务调度
|
||||
- `useDynamicConfig` — 动态配置加载
|
||||
|
||||
---
|
||||
|
||||
## 八、UI 组件 (`../src/components/`)
|
||||
|
||||
27 个主要组件目录:
|
||||
|
||||
### 核心 UI
|
||||
|
||||
- **ui/** — 基础 UI 组件(按钮、输入框等)
|
||||
- **design-system/** — 设计系统组件
|
||||
|
||||
### 功能组件
|
||||
|
||||
- **tasks/** — 任务列表和管理 UI
|
||||
- **messages/** — 消息显示和渲染
|
||||
- **permissions/** — 权限请求对话框
|
||||
- **mcp/** — MCP 服务器管理 UI
|
||||
- **diff/** — 代码差异查看
|
||||
- **shell/** — Shell/输出显示
|
||||
- **memory/** — 记忆管理 UI
|
||||
- **sandbox/** — 沙盒权限
|
||||
- **agents/** — Agent 创建和管理
|
||||
|
||||
### 设置和配置
|
||||
|
||||
- **Settings/** — 设置面板
|
||||
- **ManagedSettingsSecurityDialog/** — 托管设置安全对话框
|
||||
|
||||
### 集成
|
||||
|
||||
- **groove/** — Grove 集成
|
||||
- **teams/** — 团队协作
|
||||
- **skills/** — 技能管理
|
||||
- **wizard/** — 引导向导
|
||||
- **LspRecommendation/** — LSP 插件建议
|
||||
|
||||
### 专用
|
||||
|
||||
- **TrustDialog/** — 信任/安全对话框
|
||||
- **FeedbackSurvey/** — 用户反馈
|
||||
- **Spinner/** — 加载旋转器
|
||||
- **PromptInput/** — 输入提示组件
|
||||
- **HelpV2/** — 帮助系统
|
||||
- **LogoV2/** — Logo 组件
|
||||
|
||||
---
|
||||
|
||||
## 九、构建与依赖
|
||||
|
||||
### 运行时依赖(关键)
|
||||
|
||||
| 类别 | 包 |
|
||||
|---|---|
|
||||
| **AI SDK** | @anthropic-ai/sdk, @anthropic-ai/bedrock-sdk, @anthropic-ai/vertex-sdk, @anthropic-ai/foundry-sdk |
|
||||
| **Agent SDK** | @anthropic-ai/claude-agent-sdk |
|
||||
| **MCP** | @modelcontextprotocol/sdk, @anthropic-ai/mcpb |
|
||||
| **终端 UI** | react, ink, chalk, cli-highlight |
|
||||
| **搜索** | fuse.js, picomatch |
|
||||
| **协议** | vscode-jsonrpc, vscode-languageserver-protocol |
|
||||
| **可观测性** | @opentelemetry/* (完整 suite,但遥测已 stub) |
|
||||
| **验证** | zod, ajv |
|
||||
| **图片** | sharp |
|
||||
| **网络** | axios, undici, ws, https-proxy-agent |
|
||||
| **CLI** | @commander-js/extra-typings |
|
||||
| **其他** | diff, marked, yaml, lodash-es, lru-cache, semver, chokidar |
|
||||
|
||||
### 开发依赖
|
||||
|
||||
- `@types/bun` — Bun 类型定义
|
||||
- `typescript ^6.0.2` — TypeScript 编译器
|
||||
|
||||
---
|
||||
|
||||
## 十、Git 状态
|
||||
|
||||
当前仓库只有 **1 个 commit**:
|
||||
|
||||
```
|
||||
a4deee0 因无法 fork,手动迁移代码 source: https://github.com/paoloanzn/free-code
|
||||
```
|
||||
|
||||
**76 个未跟踪文件/目录**,包括许多新增的子系统文件(assistant、buddy、fork、workflows、daemon、ssh 等),对应于之前不可编译但已被手动补充代码的 feature flags。
|
||||
|
||||
**3 个已修改文件**:`bun.lock`、`package.json`、`scripts/build.ts`。
|
||||
|
||||
---
|
||||
|
||||
## 十一、架构亮点与设计模式
|
||||
|
||||
1. **编译时 Feature Flag** — `bun:bundle` 的 `feature()` 宏实现真正的死代码消除,非运行时 if/else
|
||||
2. **异步生成器查询管线** — QueryEngine 用 async generator 实现流式 LLM 响应
|
||||
3. **多 Agent 协作** — Agent Swarm 支持 Team 创建、共享任务列表、跨会话消息传递
|
||||
4. **工具延迟加载** — 大型工具集通过 ToolSearch 按需加载 schema,优化 context window
|
||||
5. **多提供商 API 抽象** — 统一接口适配 5 个 API 提供商
|
||||
6. **自定义状态管理** — 轻量 store + useSyncExternalStore,非 Redux
|
||||
7. **IDE 桥接轮询架构** — 远程控制通过环境注册 + work polling 实现
|
||||
8. **上下文压缩** — 多级压缩策略(auto-compact、micro-compact、reactive-compact)
|
||||
9. **MCP 协议完整实现** — 支持 stdio、SSE、HTTP、WebSocket、SDK 等多种传输方式
|
||||
10. **沙盒安全** — 文件/网络访问控制,支持 macOS/Linux 平台
|
||||
|
||||
---
|
||||
|
||||
## 十二、工具函数层 (`../src/utils/`)
|
||||
|
||||
最大的源码目录(577 文件,178,924 行):
|
||||
|
||||
### 模型管理 (`../src/utils/model/`)
|
||||
|
||||
- **model.ts** — 核心模型选择逻辑、别名、规范化
|
||||
- **modelStrings.ts** — 模型名称常量和映射
|
||||
- **providers.ts** — API 提供商检测
|
||||
- **modelCapabilities.ts** — 模型能力标志
|
||||
- **modelAllowlist.ts** — 模型访问控制
|
||||
- **validateModel.ts** — 模型验证
|
||||
- **modelCost.ts** — 模型定价信息
|
||||
- **check1mAccess.ts** — 1M 上下文访问检查
|
||||
|
||||
### 设置管理 (`../src/utils/settings/`)
|
||||
|
||||
- **settings.ts** — 设置加载和持久化
|
||||
- **validation.ts** — 设置验证
|
||||
- **types.ts** — 设置类型定义
|
||||
- **applySettingsChange.ts** — 设置变更应用
|
||||
- **permissionValidation.ts** — 权限验证
|
||||
- **mdm/** — 移动设备管理集成
|
||||
|
||||
### 其他重要工具模块
|
||||
|
||||
- **auth.ts** — 认证和凭证管理(67KB,1800+ 行)
|
||||
- **attachments.ts** — 附件处理(127KB)
|
||||
- **analyzeContext.ts** — 上下文分析(43KB)
|
||||
- **ansiToPng.ts** — ANSI 转 PNG(215KB)
|
||||
- **bash/** — Bash 命令执行
|
||||
- **permissions/** — 权限系统
|
||||
- **config.ts** — 配置管理
|
||||
- **todo/** — TODO 列表管理
|
||||
- **commitAttribution.ts** — Git 提交归属
|
||||
|
||||
---
|
||||
|
||||
*报告生成日期:2026-04-05*
|
||||
43
docs/基础设施设计/reference/原始代码映射-基础设施.md
Normal file
43
docs/基础设施设计/reference/原始代码映射-基础设施.md
Normal file
@ -0,0 +1,43 @@
|
||||
# 基础设施设计 — 原始代码映射
|
||||
|
||||
## 文档元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 原始代码映射
|
||||
- 原始代码来源: `../../../src/` 下相关基础设施子目录
|
||||
- 交叉引用: [基础设施设计总览](../基础设施设计.md)
|
||||
|
||||
## .NET 类型 → 原始 TypeScript 源文件
|
||||
|
||||
| .NET 类型 | 原始 TypeScript 源文件 |
|
||||
|---|---|
|
||||
| IMcpClientManager | `../../../src/services/mcp/` |
|
||||
| MCPServerConnection | `../../../src/services/mcp/` |
|
||||
| ScopedMcpServerConfig | `../../../src/services/mcp/` |
|
||||
| StdioTransport | `../../../src/services/mcp/` |
|
||||
| SseTransport | `../../../src/services/mcp/` |
|
||||
| StreamableHttpTransport | `../../../src/services/mcp/` |
|
||||
| InProcessTransport | `../../../src/services/mcp/` |
|
||||
| McpClient | `../../../src/services/mcp/` |
|
||||
| McpClientManager | `../../../src/services/mcp/` |
|
||||
| McpAuthService | `../../../src/services/mcp/` |
|
||||
| ILspClientManager | `../../../src/services/lsp/` |
|
||||
| LspServerInstance | `../../../src/services/lsp/` |
|
||||
| LspClientManager | `../../../src/services/lsp/` |
|
||||
| LspDiagnosticRegistry | `../../../src/services/lsp/` |
|
||||
| IBridgeService | `../../../src/bridge/` |
|
||||
| BridgeConfig | `../../../src/bridge/` |
|
||||
| SpawnMode | `../../../src/bridge/` |
|
||||
| BridgeService | `../../../src/bridge/` |
|
||||
| SessionSpawner | `../../../src/bridge/` |
|
||||
| IBridgeApiClient | `../../../src/bridge/` |
|
||||
| BackgroundTask | `../../../src/tasks/` |
|
||||
| IBackgroundTaskManager | `../../../src/tasks/` |
|
||||
| BackgroundTaskManager | `../../../src/tasks/` |
|
||||
| AppState | `../../../src/state/` |
|
||||
| McpState | `../../../src/state/` |
|
||||
| PluginState | `../../../src/state/` |
|
||||
| NotificationState | `../../../src/state/` |
|
||||
| SpeculationState | `../../../src/state/` |
|
||||
| IAppStateStore | `../../../src/state/` |
|
||||
| AppStateStore | `../../../src/state/` |
|
||||
| StateSelectors | `../../../src/state/` |
|
||||
203
docs/基础设施设计/基础设施设计-IDE桥接.md
Normal file
203
docs/基础设施设计/基础设施设计-IDE桥接.md
Normal file
@ -0,0 +1,203 @@
|
||||
# 基础设施设计 — IDE 桥接
|
||||
|
||||
## 文档元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 基础设施设计
|
||||
- 原始代码来源: `../../src/bridge/`(32个文件)
|
||||
- 原始设计意图: 将 claude.ai 远程控制、会话启动、工作轮询和 IDE 侧事件流统一到一个可恢复的桥接服务中
|
||||
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-查询引擎](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
|
||||
## 设计目标
|
||||
IDE 桥接层负责把编辑器、运行时与后台工作流连接起来,提供会话启动、消息转发、任务轮询和工作区隔离能力。它既要支撑单会话模型,也要允许 worktree 隔离与环境注册/注销。
|
||||
|
||||
## 12.1 IBridgeService 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// IDE 桥接服务 — 实现 claude.ai 远程控制功能
|
||||
/// 对应原始 bridgeMain.ts / replBridge.ts
|
||||
/// </summary>
|
||||
public interface IBridgeService
|
||||
{
|
||||
/// <summary>注册环境到 claude.ai</summary>
|
||||
Task<BridgeEnvironment> RegisterEnvironmentAsync();
|
||||
|
||||
/// <summary>轮询获取工作项</summary>
|
||||
Task<WorkItem?> PollForWorkAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>启动会话</summary>
|
||||
Task<SessionHandle> SpawnSessionAsync(SessionSpawnOptions options);
|
||||
|
||||
/// <summary>确认工作项</summary>
|
||||
Task AcknowledgeWorkAsync(string workId, string sessionToken);
|
||||
|
||||
/// <summary>发送权限响应</summary>
|
||||
Task SendPermissionResponseAsync(string sessionId, PermissionResponse response);
|
||||
|
||||
/// <summary>心跳(延长工作租约)</summary>
|
||||
Task HeartbeatAsync(string workId, string sessionToken);
|
||||
|
||||
/// <summary>停止工作</summary>
|
||||
Task StopWorkAsync(string workId);
|
||||
|
||||
/// <summary>注销环境</summary>
|
||||
Task DeregisterEnvironmentAsync();
|
||||
|
||||
/// <summary>桥接状态</summary>
|
||||
BridgeStatus Status { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## 12.2 BridgeConfig 与 SpawnMode
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 桥接配置 — 对应原始 BridgeConfig
|
||||
/// </summary>
|
||||
public record BridgeConfig
|
||||
{
|
||||
public required string Directory { get; init; }
|
||||
public required string MachineName { get; init; }
|
||||
public required string Branch { get; init; }
|
||||
public string? GitRepoUrl { get; init; }
|
||||
public int MaxSessions { get; init; } = 1;
|
||||
public SpawnMode SpawnMode { get; init; } = SpawnMode.Worktree;
|
||||
public bool Verbose { get; init; }
|
||||
public bool Sandbox { get; init; }
|
||||
public required Guid BridgeId { get; init; }
|
||||
public required string WorkerType { get; init; } // "claude_code" | "claude_code_assistant"
|
||||
public required Guid EnvironmentId { get; init; }
|
||||
public string? ReuseEnvironmentId { get; init; }
|
||||
public required string ApiBaseUrl { get; init; }
|
||||
public required string SessionIngressUrl { get; init; }
|
||||
public int SessionTimeoutMs { get; init; } = 24 * 60 * 60 * 1000; // 24h
|
||||
}
|
||||
|
||||
/// <summary>会话生成模式</summary>
|
||||
public enum SpawnMode
|
||||
{
|
||||
SingleSession, // 单次会话, 结束即销毁
|
||||
Worktree, // 持久, 每会话隔离 git worktree
|
||||
SameDir // 持久, 共享工作目录
|
||||
}
|
||||
```
|
||||
|
||||
## 12.3 BridgeService 实现
|
||||
|
||||
```csharp
|
||||
public sealed class BridgeService : IBridgeService, IAsyncDisposable
|
||||
{
|
||||
private readonly IBridgeApiClient _apiClient;
|
||||
private readonly ISessionSpawner _sessionSpawner;
|
||||
private readonly IBridgeLogger _logger;
|
||||
private readonly BridgeConfig _config;
|
||||
private readonly ConcurrentDictionary<string, SessionHandle> _activeSessions = new();
|
||||
private string? _environmentId;
|
||||
private string? _environmentSecret;
|
||||
private BridgeStatus _status = BridgeStatus.Idle;
|
||||
|
||||
public async Task<BridgeEnvironment> RegisterEnvironmentAsync()
|
||||
{
|
||||
var result = await _apiClient.RegisterBridgeEnvironment(_config);
|
||||
_environmentId = result.EnvironmentId;
|
||||
_environmentSecret = result.EnvironmentSecret;
|
||||
_status = BridgeStatus.Registered;
|
||||
return new BridgeEnvironment(result.EnvironmentId, result.EnvironmentSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主轮询循环 — 对应原始 bridgeMain.ts 的 pollForWork loop
|
||||
/// </summary>
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
await RegisterEnvironmentAsync();
|
||||
_logger.PrintBanner(_config, _environmentId!);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var work = await PollForWorkAsync(ct);
|
||||
if (work == null) continue;
|
||||
|
||||
_status = BridgeStatus.Attached;
|
||||
_logger.LogSessionStart(work.Id, work.Data.ToString());
|
||||
|
||||
// 解码 work secret → 获取 session token
|
||||
var secret = DecodeWorkSecret(work.Secret);
|
||||
await AcknowledgeWorkAsync(work.Id, secret.SessionIngressToken);
|
||||
|
||||
// 生成会话 (可能使用 worktree 隔离)
|
||||
var sessionDir = await PrepareSessionDirectoryAsync(work);
|
||||
var handle = await _sessionSpawner.SpawnAsync(new SessionSpawnOptions
|
||||
{
|
||||
SessionId = work.Id,
|
||||
SdkUrl = secret.ApiBaseUrl,
|
||||
AccessToken = secret.SessionIngressToken,
|
||||
WorkingDirectory = sessionDir,
|
||||
}, ct);
|
||||
|
||||
_activeSessions[work.Id] = handle;
|
||||
|
||||
// 等待会话完成
|
||||
_ = MonitorSessionAsync(work.Id, handle, ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError($"Poll error: {ex.Message}");
|
||||
await Task.Delay(CalculateBackoff(), ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorSessionAsync(
|
||||
string sessionId, SessionHandle handle, CancellationToken ct)
|
||||
{
|
||||
var doneStatus = await handle.Done;
|
||||
_activeSessions.TryRemove(sessionId, out _);
|
||||
_logger.LogSessionComplete(sessionId, 0);
|
||||
_status = _activeSessions.IsEmpty ? BridgeStatus.Registered : BridgeStatus.Attached;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var handle in _activeSessions.Values)
|
||||
handle.Kill();
|
||||
|
||||
if (_environmentId != null)
|
||||
await DeregisterEnvironmentAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 12.4 IBridgeApiClient
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 桥接 API 客户端 — 对应原始 bridgeApi.ts
|
||||
/// </summary>
|
||||
public interface IBridgeApiClient
|
||||
{
|
||||
Task<(string EnvironmentId, string EnvironmentSecret)> RegisterBridgeEnvironment(BridgeConfig config);
|
||||
Task<WorkResponse?> PollForWork(string envId, string envSecret, CancellationToken ct);
|
||||
Task AcknowledgeWork(string envId, string workId, string sessionToken);
|
||||
Task StopWork(string envId, string workId);
|
||||
Task DeregisterEnvironment(string envId);
|
||||
Task SendPermissionResponseEvent(string sessionId, PermissionResponseEvent evt, string token);
|
||||
Task ArchiveSession(string sessionId);
|
||||
Task HeartbeatWork(string envId, string workId, string sessionToken);
|
||||
}
|
||||
```
|
||||
|
||||
## 12.5 生命周期与传输策略
|
||||
|
||||
- 环境注册先于轮询;只有拿到 `EnvironmentId` / `EnvironmentSecret` 后才开始接收工作项。
|
||||
- `RunAsync` 负责持续轮询,`SpawnSessionAsync` 负责把单个 work item 落到独立会话中。
|
||||
- `Worktree` 模式用于最强隔离,`SameDir` 用于低开销复用,`SingleSession` 用于短生命周期任务。
|
||||
- 传输层上保持 API 侧长轮询/轮询式控制,避免把 IDE 控制流绑死在单一 socket 连接上。
|
||||
- `HeartbeatAsync` 用于延长工作租约,防止长任务被服务端提前回收。
|
||||
|
||||
## 12.6 补充说明
|
||||
|
||||
- 该层的重点不是本地执行,而是把远端工作项和本地会话生命周期做稳定映射。
|
||||
- 所有状态变化最终都应回写到上层状态存储,避免桥接层形成隐式状态孤岛。
|
||||
368
docs/基础设施设计/基础设施设计-LSP集成.md
Normal file
368
docs/基础设施设计/基础设施设计-LSP集成.md
Normal file
@ -0,0 +1,368 @@
|
||||
# 基础设施设计 — LSP 集成
|
||||
|
||||
## 文档元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 基础设施设计
|
||||
- 原始代码来源: `../../src/services/lsp/`(7个文件)
|
||||
- 原始设计意图: 在 .NET 中封装语言服务器生命周期、文件同步与 9 种核心 LSP 操作,并支持按扩展名路由与诊断基线比较
|
||||
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-工具系统](../核心模块设计/核心模块设计-工具系统.md)
|
||||
|
||||
## 设计目标
|
||||
LSP 层用于承载代码补全、诊断、跳转与重命名等 IDE 能力,为编辑体验提供统一协议封装。它既要兼容多语言服务器,又要支持懒启动、文件同步和跨文件诊断聚合。
|
||||
|
||||
## 11.1 ILspClientManager 接口定义
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// LSP 客户端管理器 — 管理 LSP 服务器实例
|
||||
/// 对应原始 LSPServerManager.ts
|
||||
/// </summary>
|
||||
public interface ILspClientManager
|
||||
{
|
||||
/// <summary>初始化 (加载配置, 懒启动)</summary>
|
||||
Task InitializeAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>关闭所有服务器</summary>
|
||||
Task ShutdownAsync();
|
||||
|
||||
/// <summary>获取文件对应的 LSP 服务器</summary>
|
||||
ILspServerInstance? GetServerForFile(string filePath);
|
||||
|
||||
/// <summary>确保文件对应的 LSP 服务器已启动</summary>
|
||||
Task<ILspServerInstance?> EnsureServerStartedAsync(string filePath);
|
||||
|
||||
/// <summary>发送请求到文件对应的 LSP 服务器</summary>
|
||||
Task<T?> SendRequestAsync<T>(string filePath, string method, object? parameters);
|
||||
|
||||
// LSP 操作 (对应原始 LSPTool 的 9 种操作)
|
||||
Task<Location?> GoToDefinitionAsync(string filePath, int line, int character);
|
||||
Task<Location[]> FindReferencesAsync(string filePath, int line, int character);
|
||||
Task<Hover?> HoverAsync(string filePath, int line, int character);
|
||||
Task<Symbol[]> DocumentSymbolsAsync(string filePath);
|
||||
Task<Symbol[]> WorkspaceSymbolsAsync(string query);
|
||||
Task<Diagnostic[]> GetDiagnosticsAsync(string filePath);
|
||||
Task<PrepareRenameResult?> PrepareRenameAsync(string filePath, int line, int character);
|
||||
Task<WorkspaceEdit?> RenameAsync(string filePath, int line, int character, string newName);
|
||||
Task<CodeAction[]> GetCodeActionsAsync(string filePath, int line, int character);
|
||||
|
||||
// 文件同步
|
||||
Task OpenFileAsync(string filePath, string content);
|
||||
Task ChangeFileAsync(string filePath, string content);
|
||||
Task SaveFileAsync(string filePath);
|
||||
Task CloseFileAsync(string filePath);
|
||||
|
||||
/// <summary>所有运行中的服务器</summary>
|
||||
IReadOnlyDictionary<string, ILspServerInstance> GetAllServers();
|
||||
|
||||
/// <summary>是否至少有一个服务器已连接</summary>
|
||||
bool IsConnected { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## 11.2 LspServerInstance 实现
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 单个 LSP 服务器实例 — 管理子进程生命周期
|
||||
/// 对应原始 LSPServerInstance.ts
|
||||
/// </summary>
|
||||
public interface ILspServerInstance
|
||||
{
|
||||
string Name { get; }
|
||||
string Command { get; }
|
||||
IReadOnlyDictionary<string, string> ExtensionToLanguage { get; }
|
||||
LspServerState State { get; }
|
||||
|
||||
Task StartAsync();
|
||||
Task StopAsync();
|
||||
Task<T> SendRequestAsync<T>(string method, object? parameters);
|
||||
Task SendNotificationAsync(string method, object? parameters);
|
||||
void OnRequest(string method, Func<object, object?> handler);
|
||||
}
|
||||
|
||||
public enum LspServerState { Stopped, Starting, Running, Error }
|
||||
|
||||
/// <summary>
|
||||
/// LSP 服务器实例实现
|
||||
/// </summary>
|
||||
public sealed class LspServerInstance : ILspServerInstance, IAsyncDisposable
|
||||
{
|
||||
private Process? _process;
|
||||
private JsonRpc? _rpc; // 使用 StreamJsonRpc 库
|
||||
private readonly string _name;
|
||||
private readonly ScopedLspServerConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Dictionary<string, Func<object, object?>> _requestHandlers = new();
|
||||
|
||||
public LspServerState State { get; private set; } = LspServerState.Stopped;
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (State == LspServerState.Running) return;
|
||||
State = LspServerState.Starting;
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = _config.Command,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
foreach (var arg in _config.Args)
|
||||
psi.ArgumentList.Add(arg);
|
||||
if (_config.Env != null)
|
||||
foreach (var (k, v) in _config.Env)
|
||||
psi.Environment[k] = v;
|
||||
|
||||
_process = Process.Start(psi)
|
||||
?? throw new InvalidOperationException($"Failed to start LSP server {_name}");
|
||||
|
||||
// 使用 StreamJsonRpc 建立 LSP 连接
|
||||
_rpc = JsonRpc.Attach(_process.StandardInput.BaseStream, _process.StandardOutput.BaseStream);
|
||||
_rpc.Disconnected += (_, _) => State = LspServerState.Stopped;
|
||||
|
||||
// 发送 initialize 请求
|
||||
var initResult = await _rpc.InvokeAsync<LspInitializeResult>("initialize", new
|
||||
{
|
||||
processId = Environment.ProcessId,
|
||||
rootUri = PathToFileUri(Directory.GetCurrentDirectory()),
|
||||
capabilities = new
|
||||
{
|
||||
textDocument = new
|
||||
{
|
||||
definition = new { dynamicRegistration = false },
|
||||
references = new { dynamicRegistration = false },
|
||||
hover = new { dynamicRegistration = false, contentFormat = new[] { "markdown", "plaintext" } },
|
||||
publishDiagnostics = new { relatedInformation = true },
|
||||
rename = new { prepareSupport = true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 发送 initialized 通知
|
||||
await _rpc.NotifyAsync("initialized", new { });
|
||||
State = LspServerState.Running;
|
||||
}
|
||||
|
||||
public Task<T> SendRequestAsync<T>(string method, object? parameters)
|
||||
=> _rpc!.InvokeAsync<T>(method, parameters);
|
||||
|
||||
public Task SendNotificationAsync(string method, object? parameters)
|
||||
=> _rpc!.NotifyAsync(method, parameters);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_rpc != null)
|
||||
{
|
||||
try { await _rpc.NotifyAsync("shutdown", null); } catch { }
|
||||
try { await _rpc.NotifyAsync("exit", null); } catch { }
|
||||
}
|
||||
if (_process?.HasExited == false)
|
||||
{
|
||||
_process.WaitForExit(5000);
|
||||
if (!_process.HasExited) _process.Kill(entireProcessTree: true);
|
||||
}
|
||||
State = LspServerState.Stopped;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.3 LspClientManager 实现
|
||||
|
||||
```csharp
|
||||
public sealed class LspClientManager : ILspClientManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ILspServerInstance> _servers = new();
|
||||
private readonly Dictionary<string, List<string>> _extensionMap = new(); // ext → [serverNames]
|
||||
private readonly HashSet<string> _openedFiles = new(); // URI 跟踪
|
||||
private readonly ILogger<LspClientManager> _logger;
|
||||
|
||||
public bool IsConnected => _servers.Values.Any(s => s.State == LspServerState.Running);
|
||||
|
||||
/// <summary>
|
||||
/// 初始化: 加载 LSP 配置,构建扩展映射,不启动服务器(懒启动)
|
||||
/// 对应原始 initializeLspServerManager()
|
||||
/// </summary>
|
||||
public async Task InitializeAsync(CancellationToken ct = default)
|
||||
{
|
||||
var configs = await LoadAllLspServerConfigsAsync();
|
||||
|
||||
foreach (var (name, config) in configs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config.Command))
|
||||
{
|
||||
_logger.LogWarning("LSP server {Name} missing command, skipping", name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建扩展 → 服务器映射
|
||||
foreach (var ext in config.ExtensionToLanguage.Keys)
|
||||
{
|
||||
var normalized = ext.ToLowerInvariant();
|
||||
if (!_extensionMap.ContainsKey(normalized))
|
||||
_extensionMap[normalized] = new();
|
||||
_extensionMap[normalized].Add(name);
|
||||
}
|
||||
|
||||
_servers[name] = new LspServerInstance(name, config, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
public ILspServerInstance? GetServerForFile(string filePath)
|
||||
{
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
var serverNames = _extensionMap.GetValueOrDefault(ext);
|
||||
return serverNames?.Count > 0 ? _servers.GetValueOrDefault(serverNames[0]) : null;
|
||||
}
|
||||
|
||||
public async Task<ILspServerInstance?> EnsureServerStartedAsync(string filePath)
|
||||
{
|
||||
var server = GetServerForFile(filePath);
|
||||
if (server == null) return null;
|
||||
|
||||
if (server.State is LspServerState.Stopped or LspServerState.Error)
|
||||
await server.StartAsync();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
public async Task<T?> SendRequestAsync<T>(string filePath, string method, object? parameters)
|
||||
{
|
||||
var server = await EnsureServerStartedAsync(filePath);
|
||||
if (server == null) return default;
|
||||
return await server.SendRequestAsync<T>(method, parameters);
|
||||
}
|
||||
|
||||
// === LSP 操作实现 ===
|
||||
|
||||
public async Task<Location?> GoToDefinitionAsync(string filePath, int line, int character)
|
||||
=> await SendRequestAsync<Location?>(filePath, "textDocument/definition",
|
||||
new { textDocument = new { uri = PathToFileUri(filePath) },
|
||||
position = new { line = line - 1, character } });
|
||||
|
||||
public async Task<Location[]> FindReferencesAsync(string filePath, int line, int character)
|
||||
=> await SendRequestAsync<Location[]>(filePath, "textDocument/references",
|
||||
new { textDocument = new { uri = PathToFileUri(filePath) },
|
||||
position = new { line = line - 1, character },
|
||||
context = new { includeDeclaration = true } }) ?? [];
|
||||
|
||||
public async Task<Hover?> HoverAsync(string filePath, int line, int character)
|
||||
=> await SendRequestAsync<Hover?>(filePath, "textDocument/hover",
|
||||
new { textDocument = new { uri = PathToFileUri(filePath) },
|
||||
position = new { line = line - 1, character } });
|
||||
|
||||
public async Task<Symbol[]> DocumentSymbolsAsync(string filePath)
|
||||
=> await SendRequestAsync<Symbol[]>(filePath, "textDocument/documentSymbol",
|
||||
new { textDocument = new { uri = PathToFileUri(filePath) } }) ?? [];
|
||||
|
||||
public async Task<Symbol[]> WorkspaceSymbolsAsync(string query)
|
||||
{
|
||||
// 遍历所有已启动的服务器
|
||||
var results = new List<Symbol>();
|
||||
foreach (var server in _servers.Values.Where(s => s.State == LspServerState.Running))
|
||||
{
|
||||
try
|
||||
{
|
||||
var symbols = await server.SendRequestAsync<Symbol[]>(
|
||||
"workspace/symbol", new { query });
|
||||
if (symbols != null) results.AddRange(symbols);
|
||||
}
|
||||
catch { /* 继续尝试其他服务器 */ }
|
||||
}
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
// === 文件同步 ===
|
||||
|
||||
public async Task OpenFileAsync(string filePath, string content)
|
||||
{
|
||||
var server = await EnsureServerStartedAsync(filePath);
|
||||
if (server == null) return;
|
||||
|
||||
var uri = PathToFileUri(filePath);
|
||||
if (_openedFiles.Contains(uri)) return; // 已打开
|
||||
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
var languageId = server.ExtensionToLanguage.GetValueOrDefault(ext) ?? "plaintext";
|
||||
|
||||
await server.SendNotificationAsync("textDocument/didOpen", new
|
||||
{
|
||||
textDocument = new { uri, languageId, version = 1, text = content }
|
||||
});
|
||||
_openedFiles.Add(uri);
|
||||
}
|
||||
|
||||
public async Task ChangeFileAsync(string filePath, string content)
|
||||
{
|
||||
var server = GetServerForFile(filePath);
|
||||
if (server?.State != LspServerState.Running)
|
||||
return; // 先 OpenFile
|
||||
|
||||
var uri = PathToFileUri(filePath);
|
||||
if (!_openedFiles.Contains(uri))
|
||||
return; // 先 OpenFile
|
||||
|
||||
await server.SendNotificationAsync("textDocument/didChange", new
|
||||
{
|
||||
textDocument = new { uri, version = 1 },
|
||||
contentChanges = new[] { new { text = content } }
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
var tasks = _servers.Values
|
||||
.Where(s => s.State is LspServerState.Running or LspServerState.Error)
|
||||
.Select(s => s.DisposeAsync().AsTask());
|
||||
await Task.WhenAll(tasks);
|
||||
_servers.Clear();
|
||||
_extensionMap.Clear();
|
||||
_openedFiles.Clear();
|
||||
}
|
||||
|
||||
private static string PathToFileUri(string path) =>
|
||||
new Uri(Path.GetFullPath(path)).AbsoluteUri;
|
||||
}
|
||||
```
|
||||
|
||||
## 11.4 LspDiagnosticRegistry 基线比较
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// LSP 诊断收集与基线对比
|
||||
/// 对应原始 LSPDiagnosticRegistry.ts
|
||||
/// </summary>
|
||||
public sealed class LspDiagnosticRegistry
|
||||
{
|
||||
private readonly Dictionary<string, List<Diagnostic>> _baseline = new();
|
||||
private readonly Dictionary<string, List<Diagnostic>> _current = new();
|
||||
|
||||
/// <summary>保存当前诊断为基线(编辑前快照)</summary>
|
||||
public void SaveBaseline(string filePath)
|
||||
{
|
||||
if (_current.TryGetValue(filePath, out var diagnostics))
|
||||
_baseline[filePath] = new(diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>更新诊断(来自 textDocument/publishDiagnostics 通知)</summary>
|
||||
public void UpdateDiagnostics(string filePath, IReadOnlyList<Diagnostic> diagnostics)
|
||||
{
|
||||
_current[filePath] = diagnostics.ToList();
|
||||
}
|
||||
|
||||
/// <summary>获取新增的诊断(相对于基线)</summary>
|
||||
public IReadOnlyList<Diagnostic> GetNewDiagnostics(string filePath)
|
||||
{
|
||||
var baseline = _baseline.GetValueOrDefault(filePath) ?? [];
|
||||
var current = _current.GetValueOrDefault(filePath) ?? [];
|
||||
var baselineSet = new HashSet<Diagnostic>(baseline);
|
||||
return current.Where(d => !baselineSet.Contains(d)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>清除文件基线</summary>
|
||||
public void ClearBaseline(string filePath) => _baseline.Remove(filePath);
|
||||
}
|
||||
```
|
||||
802
docs/基础设施设计/基础设施设计-MCP协议集成.md
Normal file
802
docs/基础设施设计/基础设施设计-MCP协议集成.md
Normal file
@ -0,0 +1,802 @@
|
||||
# 基础设施设计 — MCP 协议集成
|
||||
|
||||
## 文档元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 基础设施设计
|
||||
- 原始代码来源: `../../src/services/mcp/`(22个文件)
|
||||
- 原始设计意图: 将 MCP 服务器、工具、资源与认证统一抽象为可管理的 .NET 协议层,并支持多传输、多作用域与自动适配
|
||||
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-工具系统](../核心模块设计/核心模块设计-工具系统.md) | [原始代码映射](reference/原始代码映射-基础设施.md)
|
||||
|
||||
## 设计目标
|
||||
MCP 协议层负责把外部工具、远程服务、认证流程与本地执行环境统一起来,向上提供稳定的 .NET 抽象。该层不仅要兼容多种 transport,还要把 MCP 工具/命令/资源适配为上层工具系统可消费的接口。
|
||||
|
||||
## 10.1 IMcpClientManager 接口定义
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// MCP 客户端管理器 — 管理多个 MCP 服务器连接
|
||||
/// 对应原始 useManageMCPConnections.ts
|
||||
/// </summary>
|
||||
public interface IMcpClientManager
|
||||
{
|
||||
/// <summary>连接所有配置的 MCP 服务器</summary>
|
||||
Task ConnectServersAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>获取所有已连接服务器的工具(适配为 ITool)</summary>
|
||||
Task<IReadOnlyList<ITool>> GetToolsAsync();
|
||||
|
||||
/// <summary>获取所有已连接服务器的命令(适配为 ICommand)</summary>
|
||||
Task<IReadOnlyList<ICommand>> GetCommandsAsync();
|
||||
|
||||
/// <summary>列出指定服务器的资源</summary>
|
||||
Task<IReadOnlyList<ServerResource>> ListResourcesAsync(
|
||||
string? serverName = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>读取指定资源</summary>
|
||||
Task<ResourceContent> ReadResourceAsync(
|
||||
string serverName, string resourceUri, CancellationToken ct = default);
|
||||
|
||||
/// <summary>断开指定服务器</summary>
|
||||
Task DisconnectServerAsync(string serverName);
|
||||
|
||||
/// <summary>重连指定服务器(用于断线恢复)</summary>
|
||||
Task ReconnectServerAsync(string serverName);
|
||||
|
||||
/// <summary>获取所有服务器连接状态</summary>
|
||||
IReadOnlyList<MCPServerConnection> GetConnections();
|
||||
|
||||
/// <summary>触发认证流程(OAuth)</summary>
|
||||
Task AuthenticateServerAsync(string serverName);
|
||||
|
||||
/// <summary>重新加载所有配置并重连</summary>
|
||||
Task ReloadAsync();
|
||||
}
|
||||
```
|
||||
|
||||
## 10.2 MCPServerConnection 抽象 record
|
||||
|
||||
对应原始 `types.ts` 中的 union type。
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// MCP 服务器连接状态 — 替代原始 TypeScript 联合类型
|
||||
/// 原始: ConnectedMCPServer | FailedMCPServer | NeedsAuthMCPServer | PendingMCPServer | DisabledMCPServer
|
||||
/// </summary>
|
||||
public abstract record MCPServerConnection
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public string ConnectionType { get; init; }
|
||||
public ScopedMcpServerConfig Config { get; init; }
|
||||
|
||||
// 类型判别(替代 TypeScript 可辨识联合)
|
||||
public bool IsConnected => this is Connected;
|
||||
public bool IsFailed => this is Failed;
|
||||
public bool NeedsAuth => this is NeedsAuthentication;
|
||||
public bool IsPending => this is Pending;
|
||||
public bool IsDisabled => this is Disabled;
|
||||
|
||||
public sealed record Connected(
|
||||
string Name,
|
||||
ScopedMcpServerConfig Config,
|
||||
McpClient Client,
|
||||
ServerCapabilities Capabilities,
|
||||
ServerInfo? ServerInfo,
|
||||
string? Instructions,
|
||||
Func<Task> Cleanup
|
||||
) : MCPServerConnection { ConnectionType = "connected"; }
|
||||
|
||||
public sealed record Failed(
|
||||
string Name,
|
||||
ScopedMcpServerConfig Config,
|
||||
string? Error
|
||||
) : MCPServerConnection { ConnectionType = "failed"; }
|
||||
|
||||
public sealed record NeedsAuthentication(
|
||||
string Name,
|
||||
ScopedMcpServerConfig Config
|
||||
) : MCPServerConnection { ConnectionType = "needs-auth"; }
|
||||
|
||||
public sealed record Pending(
|
||||
string Name,
|
||||
ScopedMcpServerConfig Config,
|
||||
int? ReconnectAttempt = null,
|
||||
int? MaxReconnectAttempts = null
|
||||
) : MCPServerConnection { ConnectionType = "pending"; }
|
||||
|
||||
public sealed record Disabled(
|
||||
string Name,
|
||||
ScopedMcpServerConfig Config
|
||||
) : MCPServerConnection { ConnectionType = "disabled"; }
|
||||
}
|
||||
```
|
||||
|
||||
## 10.3 ScopedMcpServerConfig 配置层级
|
||||
|
||||
对应原始 `types.ts` 中的 Zod schema 定义。
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// MCP 服务器配置 — 替代原始 8 种 Zod schema
|
||||
/// 使用 FluentValidation 进行校验
|
||||
/// </summary>
|
||||
public abstract record ScopedMcpServerConfig
|
||||
{
|
||||
public required ConfigScope Scope { get; init; }
|
||||
public string? PluginSource { get; init; }
|
||||
}
|
||||
|
||||
public record StdioServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public string Command { get; init; } = "";
|
||||
public IReadOnlyList<string> Args { get; init; } = [];
|
||||
public IReadOnlyDictionary<string, string>? Env { get; init; }
|
||||
}
|
||||
|
||||
public record SseServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; init; }
|
||||
public string? HeadersHelper { get; init; }
|
||||
public McpOAuthConfig? OAuth { get; init; }
|
||||
}
|
||||
|
||||
public record SseIdeServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public required string IdeName { get; init; }
|
||||
public bool IdeRunningInWindows { get; init; }
|
||||
}
|
||||
|
||||
public record WsIdeServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public required string IdeName { get; init; }
|
||||
public string? AuthToken { get; init; }
|
||||
public bool IdeRunningInWindows { get; init; }
|
||||
}
|
||||
|
||||
public record HttpServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; init; }
|
||||
public string? HeadersHelper { get; init; }
|
||||
public McpOAuthConfig? OAuth { get; init; }
|
||||
}
|
||||
|
||||
public record WebSocketServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; init; }
|
||||
public string? HeadersHelper { get; init; }
|
||||
}
|
||||
|
||||
public record SdkServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string ServerName { get; init; }
|
||||
}
|
||||
|
||||
public record ClaudeAiProxyServerConfig : ScopedMcpServerConfig
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public required string Id { get; init; }
|
||||
}
|
||||
|
||||
public enum ConfigScope
|
||||
{
|
||||
Local, User, Project, Dynamic, Enterprise, ClaudeAi, Managed
|
||||
}
|
||||
|
||||
public record McpOAuthConfig
|
||||
{
|
||||
public string? ClientId { get; init; }
|
||||
public int? CallbackPort { get; init; }
|
||||
public string? AuthServerMetadataUrl { get; init; }
|
||||
public bool Xaa { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
## 10.4 传输层实现
|
||||
|
||||
对应原始 `@modelcontextprotocol/sdk` 中的多种传输 + 自定义 WebSocketTransport。
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// MCP 传输层抽象 — JSON-RPC 2.0 over various transports
|
||||
/// </summary>
|
||||
public interface IMcpTransport : IAsyncDisposable
|
||||
{
|
||||
Task ConnectAsync(CancellationToken ct = default);
|
||||
Task SendAsync(JsonRpcMessage message, CancellationToken ct = default);
|
||||
IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default);
|
||||
Task CloseAsync();
|
||||
bool IsConnected { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stdio 传输 — 子进程 stdin/stdout
|
||||
/// 对应原始 StdioClientTransport
|
||||
/// </summary>
|
||||
public sealed class StdioTransport : IMcpTransport
|
||||
{
|
||||
private readonly Process _process;
|
||||
private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
|
||||
public StdioTransport(StdioServerConfig config, string workingDirectory)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = config.Command,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = workingDirectory,
|
||||
};
|
||||
|
||||
foreach (var arg in config.Args)
|
||||
psi.ArgumentList.Add(arg);
|
||||
|
||||
// 合并环境变量
|
||||
if (config.Env != null)
|
||||
foreach (var (key, value) in config.Env)
|
||||
psi.Environment[key] = value;
|
||||
|
||||
_process = new Process { StartInfo = psi };
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
_process.Start();
|
||||
|
||||
// 后台读取 stdout → 解析 JSON-RPC 消息 → 写入 channel
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using var reader = new StreamReader(_process.StandardOutput.BaseStream,
|
||||
System.Text.Encoding.UTF8);
|
||||
while (!reader.EndOfStream && !ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct);
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
var message = JsonSerializer.Deserialize<JsonRpcMessage>(line);
|
||||
if (message != null)
|
||||
await _incoming.Writer.WriteAsync(message, ct);
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public async Task SendAsync(JsonRpcMessage message, CancellationToken ct = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(message);
|
||||
await _process.StandardInput.WriteLineAsync(json.AsMemory(), ct);
|
||||
await _process.StandardInput.FlushAsync();
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
|
||||
=> _incoming.Reader.ReadAllAsync(ct);
|
||||
|
||||
public bool IsConnected => !_process.HasExited;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.StandardInput.Close();
|
||||
if (!_process.WaitForExit(5000))
|
||||
_process.Kill(entireProcessTree: true);
|
||||
}
|
||||
_incoming.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSE 传输 — HTTP Server-Sent Events
|
||||
/// 对应原始 SSEClientTransport
|
||||
/// </summary>
|
||||
public sealed class SseTransport : IMcpTransport
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _url;
|
||||
private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
private string? _messageEndpoint; // 服务器提供的 POST 端点
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
// GET SSE 流 → 解析 endpoint 事件 → 开始监听
|
||||
var response = await _httpClient.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
using var stream = await response.Content.ReadAsStreamAsync(ct);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
while (!reader.EndOfStream && !ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct);
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
if (line.StartsWith("event: endpoint"))
|
||||
{
|
||||
var dataLine = await reader.ReadLineAsync(ct);
|
||||
if (dataLine?.StartsWith("data: ") == true)
|
||||
_messageEndpoint = dataLine[6..];
|
||||
}
|
||||
else if (line.StartsWith("data: "))
|
||||
{
|
||||
var json = line[6..];
|
||||
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json);
|
||||
if (message != null)
|
||||
await _incoming.Writer.WriteAsync(message, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(JsonRpcMessage message, CancellationToken ct = default)
|
||||
{
|
||||
if (_messageEndpoint == null)
|
||||
throw new InvalidOperationException("SSE transport not connected");
|
||||
|
||||
var content = JsonContent.Create(message);
|
||||
await _httpClient.PostAsync(_messageEndpoint, content, ct);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
|
||||
=> _incoming.Reader.ReadAllAsync(ct);
|
||||
|
||||
public bool IsConnected => _messageEndpoint != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streamable HTTP 传输 — MCP 2025-03-26 规范
|
||||
/// 对应原始 StreamableHTTPClientTransport
|
||||
/// POST 发送消息,响应可能是 JSON 或 SSE
|
||||
/// </summary>
|
||||
public sealed class StreamableHttpTransport : IMcpTransport
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _url;
|
||||
private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
private string? _sessionId; // 服务器分配的 Mcp-Session-Id
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
// 发送 initialize JSON-RPC 请求
|
||||
var initRequest = new JsonRpcRequest
|
||||
{
|
||||
Id = "init-1",
|
||||
Method = "initialize",
|
||||
Params = new { protocolVersion = "2025-03-26", capabilities = new { }, clientInfo = new { name = "free-code", version = "1.0" } }
|
||||
};
|
||||
|
||||
var response = await SendInternalAsync(initRequest, ct);
|
||||
// 从响应头提取 session ID
|
||||
if (response.Headers.Contains("Mcp-Session-Id"))
|
||||
_sessionId = response.Headers.GetValues("Mcp-Session-Id").First();
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendInternalAsync(
|
||||
JsonRpcRequest request, CancellationToken ct)
|
||||
{
|
||||
using var content = JsonContent.Create(request);
|
||||
var httpReq = new HttpRequestMessage(HttpMethod.Post, _url) { Content = content };
|
||||
|
||||
// MCP Streamable HTTP 规范: Accept 必须包含 application/json 和 text/event-stream
|
||||
httpReq.Headers.Accept.Add(new("application/json"));
|
||||
httpReq.Headers.Accept.Add(new("text/event-stream"));
|
||||
|
||||
if (_sessionId != null)
|
||||
httpReq.Headers.Add("Mcp-Session-Id", _sessionId);
|
||||
|
||||
return await _httpClient.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
}
|
||||
|
||||
// ... SendAsync 和 ListenAsync 实现类似 SSE 但处理 JSON/SSE 双模式响应
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket 传输 — 对应原始 WebSocketTransport (自定义实现)
|
||||
/// </summary>
|
||||
public sealed class WebSocketTransport : IMcpTransport
|
||||
{
|
||||
private readonly ClientWebSocket _webSocket;
|
||||
private readonly string _url;
|
||||
private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _webSocket.ConnectAsync(new Uri(_url), ct);
|
||||
_ = ReceiveLoopAsync(ct);
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(CancellationToken ct)
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
while (_webSocket.State == WebSocketState.Open && !ct.IsCancellationRequested)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await _webSocket.ReceiveAsync(buffer, ct);
|
||||
await ms.WriteAsync(buffer, 0, result.Count, ct);
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
var json = System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json);
|
||||
if (message != null)
|
||||
await _incoming.Writer.WriteAsync(message, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(JsonRpcMessage message, CancellationToken ct = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(message);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
await _webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
|
||||
=> _incoming.Reader.ReadAllAsync(ct);
|
||||
|
||||
public bool IsConnected => _webSocket.State == WebSocketState.Open;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进程内传输 — 对应原始 InProcessTransport (linked transport pair)
|
||||
/// 用于 Chrome/Computer Use MCP 服务器在同一进程运行
|
||||
/// </summary>
|
||||
public sealed class InProcessTransport : IMcpTransport
|
||||
{
|
||||
private readonly Channel<JsonRpcMessage> _serverToClient = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
private readonly Channel<JsonRpcMessage> _clientToServer = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
|
||||
// 创建一对连接的传输(客户端 + 服务器各一个)
|
||||
public static (InProcessTransport client, InProcessTransport server) CreateLinkedPair()
|
||||
{
|
||||
var client = new InProcessTransport();
|
||||
var server = new InProcessTransport();
|
||||
// 交叉连接 channel: client writes → server reads, vice versa
|
||||
client._outgoing = server._serverToClient.Writer;
|
||||
server._outgoing = client._clientToServer.Writer;
|
||||
return (client, server);
|
||||
}
|
||||
|
||||
private ChannelWriter<JsonRpcMessage>? _outgoing;
|
||||
public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
|
||||
=> _serverToClient.Reader.ReadAllAsync(ct);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 10.5 McpClient 核心
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 MCP 客户端
|
||||
/// 对应原始 @modelcontextprotocol/sdk Client
|
||||
/// </summary>
|
||||
public sealed class McpClient
|
||||
{
|
||||
private readonly IMcpTransport _transport;
|
||||
private int _requestId;
|
||||
private readonly ConcurrentDictionary<string, TaskCompletionSource<JsonRpcResponse>> _pending = new();
|
||||
|
||||
public ServerCapabilities? Capabilities { get; private set; }
|
||||
public ServerInfo? ServerInfo { get; private set; }
|
||||
public string? Instructions { get; private set; }
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _transport.ConnectAsync(ct);
|
||||
|
||||
// 发送 initialize → 接收 response
|
||||
var response = await SendRequestAsync("initialize", new
|
||||
{
|
||||
protocolVersion = "2025-03-26",
|
||||
capabilities = new { roots = new { }, elicitation = new { } },
|
||||
clientInfo = new { name = "free-code", version = "1.0" }
|
||||
}, ct);
|
||||
|
||||
Capabilities = DeserializeCapabilities(response);
|
||||
ServerInfo = DeserializeServerInfo(response);
|
||||
Instructions = DeserializeInstructions(response);
|
||||
|
||||
// 发送 initialized 通知
|
||||
await SendNotificationAsync("notifications/initialized", null, ct);
|
||||
}
|
||||
|
||||
public async Task<ListToolsResult> ListToolsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var response = await SendRequestAsync("tools/list", new { }, ct);
|
||||
return Deserialize<ListToolsResult>(response);
|
||||
}
|
||||
|
||||
public async Task<CallToolResult> CallToolAsync(
|
||||
string toolName, JsonElement? arguments = null, CancellationToken ct = default)
|
||||
{
|
||||
var response = await SendRequestAsync("tools/call", new
|
||||
{
|
||||
name = toolName,
|
||||
arguments
|
||||
}, ct);
|
||||
return Deserialize<CallToolResult>(response);
|
||||
}
|
||||
|
||||
public async Task<ListResourcesResult> ListResourcesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var response = await SendRequestAsync("resources/list", new { }, ct);
|
||||
return Deserialize<ListResourcesResult>(response);
|
||||
}
|
||||
|
||||
public async Task<ReadResourceResult> ReadResourceAsync(
|
||||
string uri, CancellationToken ct = default)
|
||||
{
|
||||
var response = await SendRequestAsync("resources/read", new { uri }, ct);
|
||||
return Deserialize<ReadResourceResult>(response);
|
||||
}
|
||||
|
||||
public async Task<ListPromptsResult> ListPromptsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var response = await SendRequestAsync("prompts/list", new { }, ct);
|
||||
return Deserialize<ListPromptsResult>(response);
|
||||
}
|
||||
|
||||
private async Task<JsonElement> SendRequestAsync(
|
||||
string method, object? @params, CancellationToken ct)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _requestId).ToString();
|
||||
var tcs = new TaskCompletionSource<JsonRpcResponse>();
|
||||
_pending[id] = tcs;
|
||||
|
||||
var message = new JsonRpcRequest { Id = id, Method = method, Params = @params };
|
||||
await _transport.SendAsync(message, ct);
|
||||
|
||||
using var reg = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||
var response = await tcs.Task;
|
||||
return response.Result;
|
||||
}
|
||||
|
||||
private async Task SendNotificationAsync(
|
||||
string method, object? @params, CancellationToken ct)
|
||||
{
|
||||
var message = new JsonRpcNotification { Method = method, Params = @params };
|
||||
await _transport.SendAsync(message, ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10.6 McpClientManager 实现
|
||||
|
||||
```csharp
|
||||
public class McpClientManager : IMcpClientManager
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IFeatureFlagService _features;
|
||||
private readonly IAppStateStore _stateStore;
|
||||
private readonly ILogger<McpClientManager> _logger;
|
||||
private readonly ConcurrentDictionary<string, MCPServerConnection> _connections = new();
|
||||
|
||||
/// <summary>
|
||||
/// 连接所有配置的 MCP 服务器
|
||||
/// 对应原始 useManageMCPConnections.ts 的 effect
|
||||
/// </summary>
|
||||
public async Task ConnectServersAsync(CancellationToken ct = default)
|
||||
{
|
||||
var configs = await LoadAllServerConfigsAsync();
|
||||
var localServers = configs.Where(c => IsLocalServer(c.Value)).ToList();
|
||||
var remoteServers = configs.Where(c => !IsLocalServer(c.Value)).ToList();
|
||||
|
||||
// 本地服务器: 批量连接 (默认 concurrency: 3)
|
||||
var localBatchSize = GetLocalBatchSize(); // MCP_SERVER_CONNECTION_BATCH_SIZE
|
||||
await Parallel.ForEachAsync(localServers,
|
||||
new ParallelOptions { MaxDegreeOfParallelism = localBatchSize, CancellationToken = ct },
|
||||
async (kvp, _) =>
|
||||
{
|
||||
var connection = await ConnectToServerAsync(kvp.Key, kvp.Value, ct);
|
||||
_connections[kvp.Key] = connection;
|
||||
UpdateState();
|
||||
});
|
||||
|
||||
// 远程服务器: 批量连接 (默认 concurrency: 20)
|
||||
var remoteBatchSize = GetRemoteBatchSize(); // MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE
|
||||
await Parallel.ForEachAsync(remoteServers,
|
||||
new ParallelOptions { MaxDegreeOfParallelism = remoteBatchSize, CancellationToken = ct },
|
||||
async (kvp, _) =>
|
||||
{
|
||||
var connection = await ConnectToServerAsync(kvp.Key, kvp.Value, ct);
|
||||
_connections[kvp.Key] = connection;
|
||||
UpdateState();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<MCPServerConnection> ConnectToServerAsync(
|
||||
string name, ScopedMcpServerConfig config, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 创建传输层
|
||||
var transport = CreateTransport(name, config);
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
var client = new McpClient(transport);
|
||||
var timeout = GetConnectionTimeout(); // MCP_TIMEOUT 环境变量, 默认 30s
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(timeout);
|
||||
|
||||
await client.ConnectAsync(timeoutCts.Token);
|
||||
|
||||
// 3. 获取能力、工具、资源
|
||||
var tools = await client.ListToolsAsync(ct);
|
||||
var capabilities = client.Capabilities!;
|
||||
|
||||
// 4. 截断过长的 instructions (MAX_MCP_DESCRIPTION_LENGTH = 2048)
|
||||
var instructions = client.Instructions;
|
||||
if (instructions?.Length > 2048)
|
||||
instructions = instructions[..2048] + "… [truncated]";
|
||||
|
||||
return new MCPServerConnection.Connected(name, config, client,
|
||||
capabilities, client.ServerInfo, instructions,
|
||||
async () => await transport.CloseAsync());
|
||||
}
|
||||
catch (UnauthorizedException)
|
||||
{
|
||||
// SSE/HTTP/claudeai-proxy: 需要认证
|
||||
return new MCPServerConnection.NeedsAuthentication(name, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "MCP server {Name} connection failed", name);
|
||||
return new MCPServerConnection.Failed(name, config, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private IMcpTransport CreateTransport(string name, ScopedMcpServerConfig config)
|
||||
{
|
||||
return config switch
|
||||
{
|
||||
StdioServerConfig stdio => new StdioTransport(stdio,
|
||||
Directory.GetCurrentDirectory()),
|
||||
SseServerConfig sse => new SseTransport(
|
||||
CreateHttpClient(sse.Headers), sse.Url),
|
||||
SseIdeServerConfig sseIde => new SseTransport(
|
||||
new HttpClient(), sseIde.Url),
|
||||
HttpServerConfig http => new StreamableHttpTransport(
|
||||
CreateHttpClient(http.Headers), http.Url),
|
||||
WsIdeServerConfig wsIde => new WebSocketTransport(wsIde.Url, wsIde.AuthToken),
|
||||
WebSocketServerConfig ws => new WebSocketTransport(ws.Url, null, ws.Headers),
|
||||
ClaudeAiProxyServerConfig proxy => new StreamableHttpTransport(
|
||||
CreateProxyHttpClient(), proxy.Url),
|
||||
_ => throw new InvalidOperationException($"Unsupported server type: {config}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有工具并适配为 ITool 接口
|
||||
/// 对应原始 MCPTool 适配器
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<ITool>> GetToolsAsync()
|
||||
{
|
||||
var tools = new List<ITool>();
|
||||
foreach (var connection in _connections.Values)
|
||||
{
|
||||
if (connection is MCPServerConnection.Connected connected)
|
||||
{
|
||||
var mcpTools = await connected.Client.ListToolsAsync();
|
||||
foreach (var tool in mcpTools.Tools)
|
||||
{
|
||||
tools.Add(new McpToolWrapper(connected.Name, tool, connected.Client));
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
/// <summary>更新 AppState 中的 MCP 状态</summary>
|
||||
private void UpdateState()
|
||||
{
|
||||
_stateStore.Update(state => state with
|
||||
{
|
||||
Mcp = state.Mcp with
|
||||
{
|
||||
Clients = _connections.Values.ToList(),
|
||||
Tools = GetToolsFromConnections(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static bool IsLocalServer(ScopedMcpServerConfig config) =>
|
||||
config is StdioServerConfig or SdkServerConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MCP 工具包装器 — 将 MCP tool 适配为 ITool 接口
|
||||
/// 对应原始 MCPTool.ts
|
||||
/// </summary>
|
||||
public sealed class McpToolWrapper : ITool<JsonElement, JsonElement>
|
||||
{
|
||||
private readonly string _serverName;
|
||||
private readonly McpToolDefinition _definition;
|
||||
private readonly McpClient _client;
|
||||
|
||||
public string Name => $"mcp__{_serverName}__{_definition.Name}";
|
||||
public ToolCategory Category => ToolCategory.Mcp;
|
||||
|
||||
public async Task<ToolResult<JsonElement>> ExecuteAsync(
|
||||
JsonElement input, ToolExecutionContext context, CancellationToken ct)
|
||||
{
|
||||
var timeout = GetMcpToolTimeout(); // MCP_TOOL_TIMEOUT, 默认 ~27.8h
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(timeout);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _client.CallToolAsync(_definition.Name, input, timeoutCts.Token);
|
||||
|
||||
// 处理二进制内容 → 持久化到文件
|
||||
if (result.HasBinaryContent)
|
||||
{
|
||||
var persisted = await PersistBinaryContentAsync(result);
|
||||
return new ToolResult<JsonElement>(persisted);
|
||||
}
|
||||
|
||||
return new ToolResult<JsonElement>(result.Content);
|
||||
}
|
||||
catch (McpAuthException)
|
||||
{
|
||||
// 标记服务器需要认证
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public JsonElement GetInputSchema() => _definition.InputSchema;
|
||||
public bool IsReadOnly(JsonElement input) => !_definition.HasDestructiveBehavior;
|
||||
public bool IsConcurrencySafe(JsonElement input) => false;
|
||||
}
|
||||
```
|
||||
|
||||
## 10.7 McpAuthService
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// MCP OAuth 认证 — 对应原始 auth.ts 中的 ClaudeAuthProvider
|
||||
/// </summary>
|
||||
public sealed class McpAuthService
|
||||
{
|
||||
private readonly IdentityModel.OidcClient _oidcClient;
|
||||
private readonly ISecureTokenStorage _tokenStorage; // macOS Keychain / credential manager
|
||||
|
||||
/// <summary>
|
||||
/// 执行 OAuth 授权流程: 发现 → 浏览器授权 → code 交换 → token 存储
|
||||
/// </summary>
|
||||
public async Task<McpOAuthTokens> AuthorizeAsync(
|
||||
string serverName, McpOAuthConfig config, CancellationToken ct)
|
||||
{
|
||||
// 1. 检查缓存的 token
|
||||
var cached = await _tokenStorage.GetAsync($"mcp-{serverName}");
|
||||
if (cached != null && !cached.IsExpired)
|
||||
return cached;
|
||||
|
||||
// 2. 发现授权服务器元数据
|
||||
var metadata = await DiscoverAuthorizationServerAsync(config.AuthServerMetadataUrl!, ct);
|
||||
|
||||
// 3. 启动本地 HTTP 监听器
|
||||
var callbackPort = config.CallbackPort ?? GetAvailablePort();
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add($"http://localhost:{callbackPort}/");
|
||||
listener.Start();
|
||||
|
||||
// 4. 构建授权 URL → 打开浏览器
|
||||
var authUrl = BuildAuthorizationUrl(metadata, callbackPort);
|
||||
OpenBrowser(authUrl);
|
||||
|
||||
// 5. 等待回调 → 提取 code
|
||||
var context = await listener.GetContextAsync();
|
||||
var code = context.Request.QueryString["code"];
|
||||
|
||||
// 6. 交换 token
|
||||
var tokens = await ExchangeCodeAsync(metadata, code, callbackPort, ct);
|
||||
await _tokenStorage.SetAsync($"mcp-{serverName}", tokens);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
```
|
||||
321
docs/基础设施设计/基础设施设计-后台任务管理.md
Normal file
321
docs/基础设施设计/基础设施设计-后台任务管理.md
Normal file
@ -0,0 +1,321 @@
|
||||
# 基础设施设计 — 后台任务管理
|
||||
|
||||
## 文档元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 基础设施设计
|
||||
- 原始代码来源: `../../src/tasks/`(10个文件)
|
||||
- 原始设计意图: 将 Shell、Agent、远程会话、工作流、监控与记忆合并任务统一纳入后台调度和状态同步框架
|
||||
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [服务子系统设计-会话记忆](../服务子系统设计/服务子系统设计-会话记忆与上下文.md)
|
||||
|
||||
## 设计目标
|
||||
后台任务管理层统一承载本地 shell、agent、远程 agent、监控与工作流型任务,并保证可调度、可观测与可取消。它必须和全局状态存储、查询引擎、桥接层协同工作,且在宿主关闭时能优雅终止。
|
||||
|
||||
## 13.1 BackgroundTask 任务层次
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 后台任务基类 — 所有任务类型的公共抽象
|
||||
/// 对应原始 types.ts 中的 TaskState 联合类型
|
||||
/// </summary>
|
||||
public abstract record BackgroundTask
|
||||
{
|
||||
public required string TaskId { get; init; }
|
||||
public abstract BackgroundTaskType TaskType { get; }
|
||||
public TaskStatus Status { get; set; } = TaskStatus.Pending;
|
||||
public DateTime? StartedAt { get; set; }
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public bool IsBackgrounded { get; set; } = true;
|
||||
}
|
||||
|
||||
public enum BackgroundTaskType
|
||||
{
|
||||
LocalShell,
|
||||
LocalAgent,
|
||||
RemoteAgent,
|
||||
InProcessTeammate,
|
||||
LocalWorkflow,
|
||||
MonitorMcp,
|
||||
Dream
|
||||
}
|
||||
|
||||
public enum TaskStatus { Pending, Running, Completed, Failed, Stopped }
|
||||
|
||||
// === 具体任务类型 ===
|
||||
|
||||
/// <summary>
|
||||
/// 本地 Shell 任务 — 后台 bash 命令
|
||||
/// 对应原始 LocalShellTask
|
||||
/// </summary>
|
||||
public sealed record LocalShellTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.LocalShell;
|
||||
public required string Command { get; init; }
|
||||
public ProcessStartInfo? ProcessStartInfo { get; init; }
|
||||
public string? Stdout { get; set; }
|
||||
public string? Stderr { get; set; }
|
||||
public int? ExitCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地 Agent 任务 — 子代理(forked process 或 worktree 隔离)
|
||||
/// 对应原始 LocalAgentTask
|
||||
/// </summary>
|
||||
public sealed record LocalAgentTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.LocalAgent;
|
||||
public required string Prompt { get; init; }
|
||||
public string? Model { get; init; }
|
||||
public string? AgentType { get; init; } // explore, librarian, oracle, metis, momus
|
||||
public string? WorkingDirectory { get; init; }
|
||||
public List<Message> Messages { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程 Agent 任务 — claude.ai 上的远程会话
|
||||
/// 对应原始 RemoteAgentTask
|
||||
/// </summary>
|
||||
public sealed record RemoteAgentTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.RemoteAgent;
|
||||
public required string SessionUrl { get; init; }
|
||||
public string? Plan { get; set; }
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进程内 Teammate 任务 — 协作代理
|
||||
/// 对应原始 InProcessTeammateTask
|
||||
/// </summary>
|
||||
public sealed record InProcessTeammateTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.InProcessTeammate;
|
||||
public required string AgentName { get; init; }
|
||||
public string? AgentType { get; init; }
|
||||
public string? Color { get; init; }
|
||||
public required string WorkingDirectory { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地工作流任务 — 多步骤工作流
|
||||
/// 对应原始 LocalWorkflowTask
|
||||
/// </summary>
|
||||
public sealed record LocalWorkflowTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.LocalWorkflow;
|
||||
public required string WorkflowName { get; init; }
|
||||
public required List<WorkflowStep> Steps { get; init; }
|
||||
public int CurrentStepIndex { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MCP SSE 监控任务 — 监控 MCP 服务器连接状态
|
||||
/// 对应原始 MonitorMcpTask
|
||||
/// </summary>
|
||||
public sealed record MonitorMcpTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.MonitorMcp;
|
||||
public required string ServerName { get; init; }
|
||||
public int ReconnectAttempt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dream 任务 — 后台记忆合并
|
||||
/// 对应原始 DreamTask
|
||||
/// </summary>
|
||||
public sealed record DreamTask : BackgroundTask
|
||||
{
|
||||
public override BackgroundTaskType TaskType => BackgroundTaskType.Dream;
|
||||
public required string TriggerReason { get; init; } // time | session_count
|
||||
}
|
||||
```
|
||||
|
||||
## 13.2 IBackgroundTaskManager 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 后台任务管理器 — Channel-based 任务调度
|
||||
/// 对应原始 task 工具 + 后台任务 UI 指示器
|
||||
/// </summary>
|
||||
public interface IBackgroundTaskManager
|
||||
{
|
||||
/// <summary>创建 Shell 任务</summary>
|
||||
Task<LocalShellTask> CreateShellTaskAsync(string command, ProcessStartInfo psi);
|
||||
|
||||
/// <summary>创建 Agent 任务</summary>
|
||||
Task<LocalAgentTask> CreateAgentTaskAsync(string prompt, string? agentType, string? model);
|
||||
|
||||
/// <summary>创建远程 Agent 任务</summary>
|
||||
Task<RemoteAgentTask> CreateRemoteAgentTaskAsync(string sessionUrl);
|
||||
|
||||
/// <summary>创建 Dream 任务</summary>
|
||||
Task<DreamTask> CreateDreamTaskAsync(string triggerReason);
|
||||
|
||||
/// <summary>停止任务</summary>
|
||||
Task StopTaskAsync(string taskId);
|
||||
|
||||
/// <summary>获取任务输出</summary>
|
||||
Task<string?> GetTaskOutputAsync(string taskId);
|
||||
|
||||
/// <summary>列出所有任务</summary>
|
||||
IReadOnlyList<BackgroundTask> ListTasks();
|
||||
|
||||
/// <summary>获取指定任务</summary>
|
||||
BackgroundTask? GetTask(string taskId);
|
||||
|
||||
/// <summary>任务状态变更事件</summary>
|
||||
event EventHandler<TaskStateChangedEventArgs>? TaskStateChanged;
|
||||
}
|
||||
```
|
||||
|
||||
## 13.3 BackgroundTaskManager 实现
|
||||
|
||||
```csharp
|
||||
public class BackgroundTaskManager : IBackgroundTaskManager, IHostedService
|
||||
{
|
||||
private readonly Channel<BackgroundTask> _taskChannel = Channel.CreateUnbounded<BackgroundTask>();
|
||||
private readonly ConcurrentDictionary<string, BackgroundTask> _tasks = new();
|
||||
private readonly IAppStateStore _stateStore;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<BackgroundTaskManager> _logger;
|
||||
private readonly CancellationTokenSource _shutdownCts = new();
|
||||
|
||||
public event EventHandler<TaskStateChangedEventArgs>? TaskStateChanged;
|
||||
|
||||
public async Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
// 启动任务调度循环
|
||||
_ = Task.Run(() => DispatchLoopAsync(_shutdownCts.Token), _shutdownCts.Token);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务调度循环 — 从 channel 读取任务并执行
|
||||
/// </summary>
|
||||
private async Task DispatchLoopAsync(CancellationToken ct)
|
||||
{
|
||||
await foreach (var task in _taskChannel.Reader.ReadAllAsync(ct))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
task.Status = TaskStatus.Running;
|
||||
task.StartedAt = DateTime.UtcNow;
|
||||
UpdateState();
|
||||
TaskStateChanged?.Invoke(this, new(task.TaskId, task.Status));
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteTaskAsync(task, ct);
|
||||
task.Status = TaskStatus.Completed;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
task.Status = TaskStatus.Stopped;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
task.Status = TaskStatus.Failed;
|
||||
task.ErrorMessage = ex.Message;
|
||||
_logger.LogError(ex, "Task {TaskId} failed", task.TaskId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
task.CompletedAt = DateTime.UtcNow;
|
||||
UpdateState();
|
||||
TaskStateChanged?.Invoke(this, new(task.TaskId, task.Status));
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private Task ExecuteTaskAsync(BackgroundTask task, CancellationToken ct) => task switch
|
||||
{
|
||||
LocalShellTask shell => ExecuteShellTaskAsync(shell, ct),
|
||||
LocalAgentTask agent => ExecuteAgentTaskAsync(agent, ct),
|
||||
RemoteAgentTask remote => ExecuteRemoteAgentTaskAsync(remote, ct),
|
||||
DreamTask dream => ExecuteDreamTaskAsync(dream, ct),
|
||||
MonitorMcpTask monitor => ExecuteMonitorTaskAsync(monitor, ct),
|
||||
LocalWorkflowTask workflow => ExecuteWorkflowTaskAsync(workflow, ct),
|
||||
InProcessTeammateTask teammate => ExecuteTeammateTaskAsync(teammate, ct),
|
||||
_ => Task.CompletedTask
|
||||
};
|
||||
|
||||
private async Task ExecuteShellTaskAsync(LocalShellTask task, CancellationToken ct)
|
||||
{
|
||||
using var process = new Process { StartInfo = task.ProcessStartInfo! };
|
||||
process.Start();
|
||||
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(ct);
|
||||
await process.WaitForExitAsync(ct);
|
||||
|
||||
task.Stdout = stdout;
|
||||
task.Stderr = stderr;
|
||||
task.ExitCode = process.ExitCode;
|
||||
}
|
||||
|
||||
private async Task ExecuteAgentTaskAsync(LocalAgentTask task, CancellationToken ct)
|
||||
{
|
||||
// 使用 QueryEngine 在隔离 context 中执行子代理查询
|
||||
var engine = _services.GetRequiredService<IQueryEngine>();
|
||||
await foreach (var msg in engine.SubmitMessageAsync(task.Prompt,
|
||||
new(Model: task.Model), ct))
|
||||
{
|
||||
// 收集消息到 task.Messages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteDreamTaskAsync(DreamTask task, CancellationToken ct)
|
||||
{
|
||||
var dreamService = _services.GetRequiredService<IAutoDreamService>();
|
||||
await dreamService.RunDreamCycleAsync(ct);
|
||||
}
|
||||
|
||||
public Task<LocalShellTask> CreateShellTaskAsync(string command, ProcessStartInfo psi)
|
||||
{
|
||||
var task = new LocalShellTask
|
||||
{
|
||||
TaskId = Guid.NewGuid().ToString("N")[..8],
|
||||
Command = command,
|
||||
ProcessStartInfo = psi,
|
||||
};
|
||||
_tasks[task.TaskId] = task;
|
||||
_taskChannel.Writer.TryWrite(task);
|
||||
UpdateState();
|
||||
return Task.FromResult(task);
|
||||
}
|
||||
|
||||
public Task StopTaskAsync(string taskId)
|
||||
{
|
||||
if (_tasks.TryGetValue(taskId, out var task) && task.Status == TaskStatus.Running)
|
||||
{
|
||||
task.Status = TaskStatus.Stopped;
|
||||
// 进程级取消由具体执行器处理
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BackgroundTask> ListTasks() => _tasks.Values.ToList();
|
||||
public BackgroundTask? GetTask(string taskId) => _tasks.GetValueOrDefault(taskId);
|
||||
|
||||
/// <summary>更新 AppState 中的任务状态</summary>
|
||||
private void UpdateState()
|
||||
{
|
||||
_stateStore.Update(state =>
|
||||
{
|
||||
var tasks = state.Tasks.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
foreach (var task in _tasks.Values)
|
||||
tasks[task.TaskId] = task;
|
||||
return state with { Tasks = tasks };
|
||||
});
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken ct)
|
||||
{
|
||||
_shutdownCts.Cancel();
|
||||
_taskChannel.Writer.TryComplete();
|
||||
foreach (var task in _tasks.Values.Where(t => t.Status == TaskStatus.Running))
|
||||
await StopTaskAsync(task.TaskId);
|
||||
}
|
||||
}
|
||||
```
|
||||
253
docs/基础设施设计/基础设施设计-状态管理.md
Normal file
253
docs/基础设施设计/基础设施设计-状态管理.md
Normal file
@ -0,0 +1,253 @@
|
||||
# 基础设施设计 — 状态管理
|
||||
|
||||
## 文档元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 基础设施设计
|
||||
- 原始代码来源: `../../src/state/`(5个文件)
|
||||
- 原始设计意图: 用不可变全局状态串联 MCP、插件、通知、后台任务、桥接与推理状态,并通过事件驱动方式向 UI 广播更新
|
||||
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-查询引擎](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
|
||||
## 设计目标
|
||||
状态管理层负责承载应用运行态、MCP 状态、插件状态、通知状态与推理/猜测状态,并通过不可变更新维持可预测性。它是基础设施层的共享事实来源,供 UI、协议层和后台任务共同读取。
|
||||
|
||||
## 14.1 AppState 不可变 Record
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 应用全局状态 — 不可变 record
|
||||
/// 对应原始 AppState 类型 (AppStateStore.ts 中 ~450 行类型定义)
|
||||
/// </summary>
|
||||
public sealed record AppState
|
||||
{
|
||||
// === 核心状态 ===
|
||||
public SettingsJson Settings { get; init; } = new();
|
||||
public bool Verbose { get; init; }
|
||||
public ModelSetting MainLoopModel { get; init; }
|
||||
public ModelSetting MainLoopModelForSession { get; init; }
|
||||
public string? StatusLineText { get; init; }
|
||||
public ExpandedView ExpandedView { get; init; } = ExpandedView.None;
|
||||
public bool IsBriefOnly { get; init; }
|
||||
|
||||
// === 权限 ===
|
||||
public ToolPermissionContext ToolPermissionContext { get; init; } = new();
|
||||
public string? Agent { get; init; }
|
||||
public bool KairosEnabled { get; init; }
|
||||
|
||||
// === UI 状态 ===
|
||||
public int SelectedIPAgentIndex { get; init; } = -1;
|
||||
public int CoordinatorTaskIndex { get; init; } = -1;
|
||||
public ViewSelectionMode ViewSelectionMode { get; init; } = ViewSelectionMode.None;
|
||||
public FooterItem? FooterSelection { get; init; }
|
||||
|
||||
// === 后台任务 ===
|
||||
public IReadOnlyDictionary<string, BackgroundTask> Tasks { get; init; } = new Dictionary<string, BackgroundTask>();
|
||||
public string? ForegroundedTaskId { get; init; }
|
||||
public string? ViewingAgentTaskId { get; init; }
|
||||
public IReadOnlyDictionary<string, AgentId> AgentNameRegistry { get; init; } = new Dictionary<string, AgentId>();
|
||||
|
||||
// === MCP ===
|
||||
public McpState Mcp { get; init; } = new();
|
||||
|
||||
// === 插件 ===
|
||||
public PluginState Plugins { get; init; } = new();
|
||||
|
||||
// === 远程/桥接 ===
|
||||
public string? RemoteSessionUrl { get; init; }
|
||||
public RemoteConnectionStatus RemoteConnectionStatus { get; init; } = RemoteConnectionStatus.Connecting;
|
||||
public int RemoteBackgroundTaskCount { get; init; }
|
||||
|
||||
// === 桥接 ===
|
||||
public bool ReplBridgeEnabled { get; init; }
|
||||
public bool ReplBridgeExplicit { get; init; }
|
||||
public bool ReplBridgeOutboundOnly { get; init; }
|
||||
public bool ReplBridgeConnected { get; init; }
|
||||
public bool ReplBridgeSessionActive { get; init; }
|
||||
public bool ReplBridgeReconnecting { get; init; }
|
||||
public string? ReplBridgeConnectUrl { get; init; }
|
||||
public string? ReplBridgeSessionUrl { get; init; }
|
||||
public string? ReplBridgeEnvironmentId { get; init; }
|
||||
public string? ReplBridgeSessionId { get; init; }
|
||||
public string? ReplBridgeError { get; init; }
|
||||
public string? ReplBridgeInitialName { get; init; }
|
||||
public bool ShowRemoteCallout { get; init; }
|
||||
|
||||
// === 同伴 ===
|
||||
public string? CompanionReaction { get; init; }
|
||||
public long? CompanionPetAt { get; init; }
|
||||
|
||||
// === 通知 ===
|
||||
public NotificationState Notifications { get; init; } = new();
|
||||
|
||||
// === 记忆 ===
|
||||
public AgentDefinitionsResult AgentDefinitions { get; init; } = new();
|
||||
|
||||
// === 文件历史 ===
|
||||
public FileHistoryState FileHistory { get; init; } = new();
|
||||
|
||||
// === 归属 ===
|
||||
public AttributionState Attribution { get; init; } = new();
|
||||
|
||||
// === TODO ===
|
||||
public IReadOnlyDictionary<string, TodoList> Todos { get; init; } = new Dictionary<string, TodoList>();
|
||||
|
||||
// === 推测 ===
|
||||
public SpeculationState Speculation { get; init; } = SpeculationState.Idle;
|
||||
public long SpeculationSessionTimeSavedMs { get; init; }
|
||||
|
||||
// === 其他 ===
|
||||
public bool? ThinkingEnabled { get; init; }
|
||||
public bool PromptSuggestionEnabled { get; init; }
|
||||
public EffortValue? EffortValue { get; init; }
|
||||
public bool FastMode { get; init; }
|
||||
public int AuthVersion { get; init; }
|
||||
public IReadOnlySet<string> ActiveOverlays { get; init; } = new HashSet<string>();
|
||||
}
|
||||
|
||||
// === 嵌套状态类型 ===
|
||||
|
||||
public sealed record McpState
|
||||
{
|
||||
public IReadOnlyList<MCPServerConnection> Clients { get; init; } = [];
|
||||
public IReadOnlyList<ITool> Tools { get; init; } = [];
|
||||
public IReadOnlyList<ICommand> Commands { get; init; } = [];
|
||||
public IReadOnlyDictionary<string, List<ServerResource>> Resources { get; init; } = new Dictionary<string, List<ServerResource>>();
|
||||
public int PluginReconnectKey { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PluginState
|
||||
{
|
||||
public IReadOnlyList<LoadedPlugin> Enabled { get; init; } = [];
|
||||
public IReadOnlyList<LoadedPlugin> Disabled { get; init; } = [];
|
||||
public IReadOnlyList<ICommand> Commands { get; init; } = [];
|
||||
public IReadOnlyList<PluginError> Errors { get; init; } = [];
|
||||
public PluginInstallationStatus InstallationStatus { get; init; } = new();
|
||||
public bool NeedsRefresh { get; init; }
|
||||
}
|
||||
|
||||
public sealed record NotificationState
|
||||
{
|
||||
public Notification? Current { get; init; }
|
||||
public IReadOnlyList<Notification> Queue { get; init; } = [];
|
||||
}
|
||||
|
||||
public record SpeculationState
|
||||
{
|
||||
public static readonly SpeculationState Idle = new() { Status = "idle" };
|
||||
public string Status { get; init; } = "idle";
|
||||
}
|
||||
```
|
||||
|
||||
## 14.2 IAppStateStore 接口与 AppStateStore 实现
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 应用状态存储 — 事件驱动不可变状态管理
|
||||
/// 对应原始 Store<T> + AppStateStore
|
||||
/// 模式: Redux/Elm 风格 (updater 函数 + 变更通知)
|
||||
/// </summary>
|
||||
public interface IAppStateStore
|
||||
{
|
||||
/// <summary>获取当前状态(不可变快照)</summary>
|
||||
AppState GetState();
|
||||
|
||||
/// <summary>更新状态(通过 updater 函数)</summary>
|
||||
void Update(Func<AppState, AppState> updater);
|
||||
|
||||
/// <summary>订阅状态变更</summary>
|
||||
IDisposable Subscribe(Action<AppState> listener);
|
||||
|
||||
/// <summary>状态变更事件 (C# event 模式)</summary>
|
||||
event EventHandler<StateChangedEventArgs>? StateChanged;
|
||||
}
|
||||
|
||||
public sealed class AppStateStore : IAppStateStore
|
||||
{
|
||||
private AppState _state;
|
||||
private readonly object _lock = new();
|
||||
private readonly List<Action<AppState>> _listeners = new();
|
||||
private readonly Action<AppState, AppState>? _onChangeCallback;
|
||||
|
||||
public event EventHandler<StateChangedEventArgs>? StateChanged;
|
||||
|
||||
public AppStateStore()
|
||||
{
|
||||
_state = CreateDefaultState();
|
||||
}
|
||||
|
||||
public AppState GetState()
|
||||
{
|
||||
lock (_lock) return _state;
|
||||
}
|
||||
|
||||
public void Update(Func<AppState, AppState> updater)
|
||||
{
|
||||
AppState prev, next;
|
||||
lock (_lock)
|
||||
{
|
||||
prev = _state;
|
||||
next = updater(prev);
|
||||
if (ReferenceEquals(next, prev)) return; // 无变更
|
||||
_state = next;
|
||||
}
|
||||
|
||||
// 通知变更
|
||||
_onChangeCallback?.Invoke(next, prev);
|
||||
StateChanged?.Invoke(this, new StateChangedEventArgs(prev, next));
|
||||
|
||||
// 通知订阅者
|
||||
foreach (var listener in _listeners.ToArray())
|
||||
listener(next);
|
||||
}
|
||||
|
||||
public IDisposable Subscribe(Action<AppState> listener)
|
||||
{
|
||||
lock (_lock) _listeners.Add(listener);
|
||||
return new Subscription(() =>
|
||||
{
|
||||
lock (_lock) _listeners.Remove(listener);
|
||||
});
|
||||
}
|
||||
|
||||
private static AppState CreateDefaultState() => new AppState();
|
||||
|
||||
private sealed class Subscription : IDisposable
|
||||
{
|
||||
private readonly Action _unsubscribe;
|
||||
public Subscription(Action unsubscribe) => _unsubscribe = unsubscribe;
|
||||
public void Dispose() => _unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record StateChangedEventArgs(AppState OldState, AppState NewState);
|
||||
```
|
||||
|
||||
## 14.3 StateSelectors
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 状态选择器 — 对应原始 selectors.ts
|
||||
/// 从 AppState 中派生计算值,避免在组件中重复逻辑
|
||||
/// </summary>
|
||||
public static class StateSelectors
|
||||
{
|
||||
public static bool IsBridgeReady(this AppState state) =>
|
||||
state.ReplBridgeEnabled && state.ReplBridgeConnected;
|
||||
|
||||
public static bool IsBridgeConnected(this AppState state) =>
|
||||
state.ReplBridgeEnabled && state.ReplBridgeSessionActive;
|
||||
|
||||
public static IReadOnlyList<BackgroundTask> GetBackgroundTasks(this AppState state) =>
|
||||
state.Tasks.Values.Where(t => t.Status is TaskStatus.Running or TaskStatus.Pending)
|
||||
.Where(t => t.IsBackgrounded)
|
||||
.ToList();
|
||||
|
||||
public static bool HasActiveMcpServers(this AppState state) =>
|
||||
state.Mcp.Clients.Any(c => c.IsConnected);
|
||||
|
||||
public static int GetActiveSessionCount(this AppState state) =>
|
||||
state.Tasks.Values.Count(t => t.Status == TaskStatus.Running);
|
||||
|
||||
public static string? GetActiveModel(this AppState state) =>
|
||||
state.MainLoopModelForSession ?? state.MainLoopModel;
|
||||
}
|
||||
```
|
||||
83
docs/基础设施设计/基础设施设计.md
Normal file
83
docs/基础设施设计/基础设施设计.md
Normal file
@ -0,0 +1,83 @@
|
||||
# 基础设施设计
|
||||
|
||||
> **所属项目**: free-code .NET 10 重写
|
||||
> **文档类型**: 模块索引
|
||||
> **对应源码**: `../../src/services/mcp/`, `../../src/services/lsp/`, `../../src/bridge/`, `../../src/tasks/`, `../../src/state/`
|
||||
> **配套文档**: [总体概述](../总体概述与技术选型/总体概述与技术选型.md) | [核心模块设计](../核心模块设计/核心模块设计.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
基础设施层负责支撑整个 CLI 运行时的底层能力,包括外部协议对接、IDE 通信、任务调度和全局状态管理。这一层不直接实现业务逻辑,而是为核心模块提供可靠的基础服务。
|
||||
|
||||
原始 TypeScript 实现分散在多个目录中,总计约 76 个文件。.NET 10 重写将其整理为五个职责清晰的子模块,每个子模块通过接口向上层暴露能力,内部实现细节完全封装。
|
||||
|
||||
---
|
||||
|
||||
## 子模块列表
|
||||
|
||||
### [MCP 协议集成](基础设施设计-MCP协议集成.md)
|
||||
|
||||
管理多个 MCP 服务器连接,实现 JSON-RPC 2.0 传输层(Stdio、SSE、Streamable HTTP、WebSocket、进程内),并将 MCP 工具适配为统一的 `ITool` 接口供工具系统使用。
|
||||
|
||||
- 原始源码: `../../src/services/mcp/`(22 个文件)
|
||||
- 核心类型: `IMcpClientManager`、`MCPServerConnection`、`McpClient`、`McpAuthService`
|
||||
|
||||
---
|
||||
|
||||
### [LSP 集成](基础设施设计-LSP集成.md)
|
||||
|
||||
管理语言服务器协议(LSP)子进程的生命周期,提供跳转定义、查找引用、悬停提示、重命名、诊断等 9 种标准 LSP 操作,以及诊断基线对比能力。
|
||||
|
||||
- 原始源码: `../../src/services/lsp/`(7 个文件)
|
||||
- 核心类型: `ILspClientManager`、`LspServerInstance`、`LspDiagnosticRegistry`
|
||||
|
||||
---
|
||||
|
||||
### [IDE 桥接](基础设施设计-IDE桥接.md)
|
||||
|
||||
实现 claude.ai 远程控制能力,通过轮询 API 获取工作项、生成隔离的会话目录(git worktree),并管理会话生命周期。支持 VS Code、JetBrains 等 IDE 的 MCP over SSE/WebSocket 接入。
|
||||
|
||||
- 原始源码: `../../src/bridge/`(32 个文件)
|
||||
- 核心类型: `IBridgeService`、`BridgeConfig`、`SpawnMode`、`IBridgeApiClient`
|
||||
|
||||
---
|
||||
|
||||
### [后台任务管理](基础设施设计-后台任务管理.md)
|
||||
|
||||
基于 `Channel<T>` 的任务调度系统,支持 7 种任务类型(Shell、本地代理、远程代理、进程内协作代理、工作流、MCP 监控、Dream 记忆合并),作为 `IHostedService` 随应用生命周期启停。
|
||||
|
||||
- 原始源码: `../../src/tasks/`(10 个文件)
|
||||
- 核心类型: `BackgroundTask`、`IBackgroundTaskManager`、`BackgroundTaskManager`
|
||||
|
||||
---
|
||||
|
||||
### [状态管理](基础设施设计-状态管理.md)
|
||||
|
||||
应用全局不可变状态存储,采用 Redux/Elm 风格的 updater 函数模式。`AppState` 是一个深层嵌套的不可变 record,`AppStateStore` 通过加锁保证线程安全,并在每次更新后触发订阅通知。
|
||||
|
||||
- 原始源码: `../../src/state/`(5 个文件)
|
||||
- 核心类型: `AppState`、`IAppStateStore`、`AppStateStore`、`StateSelectors`
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
核心模块(工具系统、查询引擎)
|
||||
↓ 使用
|
||||
基础设施层
|
||||
├── MCP 协议集成 ──→ 状态管理(更新 McpState)
|
||||
├── LSP 集成 ──→ 状态管理(无直接依赖,通过工具层间接访问)
|
||||
├── IDE 桥接 ──→ 状态管理(更新桥接状态字段)
|
||||
└── 后台任务管理 ──→ 状态管理(更新 Tasks 字典)
|
||||
```
|
||||
|
||||
所有子模块都通过依赖注入接收 `IAppStateStore`,并在状态变更时调用 `Update()` 方法。UI 层订阅 `StateChanged` 事件实现响应式刷新。
|
||||
|
||||
---
|
||||
|
||||
## 原始代码映射
|
||||
|
||||
完整的 .NET 类型与原始 TypeScript 文件对应关系,参见 [reference/原始代码映射-基础设施.md](reference/原始代码映射-基础设施.md)。
|
||||
328
docs/总体概述与技术选型/reference/.NET-10-平台介绍.md
Normal file
328
docs/总体概述与技术选型/reference/.NET-10-平台介绍.md
Normal file
@ -0,0 +1,328 @@
|
||||
# .NET 10 平台介绍
|
||||
|
||||
> **文档版本**: 1.0
|
||||
> **日期**: 2026-04-05
|
||||
> **所属部分**: 第一部分 — 参考文档
|
||||
> **单一职责**: 说明 .NET 10 平台中与本项目直接相关的特性,不涉及具体映射细节
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. AOT 编译与单文件发布](#1-aot-编译与单文件发布)
|
||||
- [2. C# 13 语言特性](#2-c-13-语言特性)
|
||||
- [3. System.Text.Json 与 AOT 友好序列化](#3-systemtextjson-与-aot-友好序列化)
|
||||
- [4. Microsoft.Extensions.Hosting](#4-microsoftextensionshosting)
|
||||
- [5. AssemblyLoadContext 插件隔离](#5-assemblyloadcontext-插件隔离)
|
||||
- [6. System.Threading.Channels 异步任务管理](#6-systemthreadingchannels-异步任务管理)
|
||||
- [7. 关键 NuGet 包清单](#7-关键-nuget-包清单)
|
||||
|
||||
---
|
||||
|
||||
## 1. AOT 编译与单文件发布
|
||||
|
||||
### 设计意图
|
||||
|
||||
原始项目通过 Bun 将所有 TypeScript 打包为单一可执行二进制,做到零依赖安装。.NET 10 的 Native AOT(Ahead-of-Time)编译提供了对等的能力,并在启动速度和内存占用上更有优势。
|
||||
|
||||
### PublishSingleFile
|
||||
|
||||
`PublishSingleFile` 将运行时和应用代码打包为一个文件,发布时无需 .NET 运行时预装。
|
||||
|
||||
```xml
|
||||
<!-- 项目文件中的 AOT + 单文件配置 -->
|
||||
<PropertyGroup>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<AotPublish>true</AotPublish>
|
||||
<RuntimeIdentifiers>osx-arm64;osx-x64;linux-x64;linux-arm64;win-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Native AOT 的约束
|
||||
|
||||
AOT 编译会在发布时静态分析所有代码路径,任何运行时反射都可能被裁剪(trim)。这意味着:
|
||||
|
||||
- `System.Text.Json` 必须使用 Source Generator 模式,不能依赖运行时反射序列化
|
||||
- 插件系统无法直接 AOT,主程序集使用 AOT,插件程序集在 `AssemblyLoadContext` 中通过 JIT 加载
|
||||
- 需要为 `[DynamicallyAccessedMembers]` 标注所有通过反射访问的类型
|
||||
|
||||
### 启动性能对比
|
||||
|
||||
| 方式 | 冷启动时间(参考值) |
|
||||
|------|-----------------|
|
||||
| Bun 打包 JS | ~80ms |
|
||||
| .NET 10 AOT | ~15-30ms |
|
||||
| .NET 10 JIT(无 AOT) | ~150-300ms |
|
||||
|
||||
---
|
||||
|
||||
## 2. C# 13 语言特性
|
||||
|
||||
### 设计意图
|
||||
|
||||
TypeScript 的类型系统依赖接口、联合类型和 Zod 运行时校验。C# 13 提供了更强的编译时保证,用 record 类型替代不可变 DTO,用模式匹配替代联合类型的分支处理。
|
||||
|
||||
### Record 类型
|
||||
|
||||
Record 类型天然不可变,自动生成 `Equals`、`GetHashCode`、`ToString` 和解构支持,适合表达状态快照和配置对象。
|
||||
|
||||
```csharp
|
||||
// 不可变配置模型
|
||||
public record AppSettings(
|
||||
string Model,
|
||||
string Theme,
|
||||
bool VerboseMode,
|
||||
IReadOnlyList<string> AllowedDirectories
|
||||
);
|
||||
|
||||
// 带 required 修饰符的 init-only 属性
|
||||
public record FileReadInput
|
||||
{
|
||||
public required string FilePath { get; init; }
|
||||
public int? Offset { get; init; }
|
||||
public int? Limit { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
### 模式匹配
|
||||
|
||||
C# 13 的模式匹配可以替代 TypeScript 中对联合类型的 `switch` + `instanceof` 处理。
|
||||
|
||||
```csharp
|
||||
// 替代 TS 的 if (msg.type === 'tool_use') { ... }
|
||||
var result = message switch
|
||||
{
|
||||
ToolUseMessage toolUse => HandleToolUse(toolUse),
|
||||
AssistantMessage assistant => HandleAssistant(assistant),
|
||||
ErrorMessage { Code: >= 500 } serverError => HandleServerError(serverError),
|
||||
_ => HandleUnknown(message)
|
||||
};
|
||||
```
|
||||
|
||||
### required init 属性
|
||||
|
||||
`required` 关键字在编译时强制调用方必须初始化某个属性,比构造函数参数更灵活,比 Nullable 警告更明确。
|
||||
|
||||
```csharp
|
||||
public class ToolExecutionContext
|
||||
{
|
||||
public required string SessionId { get; init; }
|
||||
public required IPermissionEngine PermissionEngine { get; init; }
|
||||
public CancellationToken CancellationToken { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. System.Text.Json 与 AOT 友好序列化
|
||||
|
||||
### 设计意图
|
||||
|
||||
原始项目直接使用 `JSON.parse`/`JSON.stringify`,无类型安全。`System.Text.Json` 提供高性能序列化,配合 Source Generator 完全绕过运行时反射,AOT 环境下也可正常工作。
|
||||
|
||||
### Source Generator 模式
|
||||
|
||||
通过 `[JsonSerializable]` 特性,编译器在构建时生成所有序列化代码,不依赖运行时反射。
|
||||
|
||||
```csharp
|
||||
// 声明需要序列化的类型
|
||||
[JsonSerializable(typeof(ApiRequest))]
|
||||
[JsonSerializable(typeof(ApiResponse))]
|
||||
[JsonSerializable(typeof(McpToolCall))]
|
||||
[JsonSerializable(typeof(List<ToolDefinition>))]
|
||||
internal partial class AppJsonContext : JsonSerializerContext { }
|
||||
|
||||
// 使用时传入 context
|
||||
var json = JsonSerializer.Serialize(request, AppJsonContext.Default.ApiRequest);
|
||||
var response = JsonSerializer.Deserialize(json, AppJsonContext.Default.ApiResponse);
|
||||
```
|
||||
|
||||
### 流式 JSON 处理
|
||||
|
||||
MCP 和 API 响应都基于流式 JSON,使用 `Utf8JsonReader` 可以零拷贝解析,不需要先将整个响应体读入内存。
|
||||
|
||||
```csharp
|
||||
// SSE 流式解析示例
|
||||
await foreach (var line in sseReader.ReadLinesAsync(ct))
|
||||
{
|
||||
if (!line.StartsWith("data: ")) continue;
|
||||
var data = line["data: ".Length..];
|
||||
if (data == "[DONE]") break;
|
||||
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(data));
|
||||
var chunk = JsonSerializer.Deserialize(ref reader, AppJsonContext.Default.StreamChunk);
|
||||
yield return chunk;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Microsoft.Extensions.Hosting
|
||||
|
||||
### 设计意图
|
||||
|
||||
原始项目没有显式的 DI 容器,各模块通过模块级变量和闭包共享状态。.NET 的 `Microsoft.Extensions.Hosting` 提供了标准化的应用生命周期管理,包括 DI 注册、配置系统和 `BackgroundService`。
|
||||
|
||||
### Host 构建
|
||||
|
||||
```csharp
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((ctx, cfg) =>
|
||||
{
|
||||
cfg.AddJsonFile("appsettings.json", optional: true);
|
||||
cfg.AddEnvironmentVariables("FREECODE_");
|
||||
cfg.AddCommandLine(args);
|
||||
})
|
||||
.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
services.AddCoreServices()
|
||||
.AddEngine()
|
||||
.AddTools()
|
||||
.AddCommands()
|
||||
.AddApiProviders()
|
||||
.AddMcp()
|
||||
.AddLsp();
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.AddConsole();
|
||||
logging.SetMinimumLevel(LogLevel.Warning); // 终端应用默认不打印 Info 日志
|
||||
})
|
||||
.Build();
|
||||
```
|
||||
|
||||
### BackgroundService
|
||||
|
||||
`BackgroundService` 是 .NET 中运行后台循环任务的标准模式,替代原始项目中的自定义异步循环。
|
||||
|
||||
```csharp
|
||||
public class TaskSchedulerService : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await foreach (var task in _taskChannel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
_ = Task.Run(() => ExecuteTaskAsync(task, stoppingToken), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. AssemblyLoadContext 插件隔离
|
||||
|
||||
### 设计意图
|
||||
|
||||
原始项目使用 `动态 import()` 加载插件,JavaScript 的模块系统天然支持按需加载。.NET 中通过 `AssemblyLoadContext` 实现对等的运行时程序集隔离,并支持卸载(collectible context)。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```csharp
|
||||
public class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
|
||||
public PluginLoadContext(string pluginPath)
|
||||
: base(isCollectible: true) // 可卸载
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
// 优先从插件目录解析,避免与主程序集冲突
|
||||
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载流程
|
||||
|
||||
```csharp
|
||||
// 加载插件
|
||||
var context = new PluginLoadContext(pluginPath);
|
||||
var assembly = context.LoadFromAssemblyPath(pluginPath);
|
||||
var plugin = (IPlugin)Activator.CreateInstance(assembly.GetType("MyPlugin")!)!;
|
||||
|
||||
// 卸载插件(释放所有强引用后 GC 自动回收)
|
||||
context.Unload();
|
||||
```
|
||||
|
||||
### AOT 注意事项
|
||||
|
||||
主程序集使用 AOT 编译,但插件程序集在运行时通过 `AssemblyLoadContext` 以 JIT 模式加载。这是混合部署模式:主入口 AOT 启动快、内存小,插件保持 JIT 的灵活性。
|
||||
|
||||
---
|
||||
|
||||
## 6. System.Threading.Channels 异步任务管理
|
||||
|
||||
### 设计意图
|
||||
|
||||
原始项目的后台任务通过自定义队列和 Promise 链管理,存在竞态风险。`System.Threading.Channels` 是 .NET 内建的高性能生产者-消费者通道,线程安全,背压(backpressure)支持完善。
|
||||
|
||||
### Channel 基本模式
|
||||
|
||||
```csharp
|
||||
// 创建无界通道
|
||||
var channel = Channel.CreateUnbounded<BackgroundTask>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleWriter = false,
|
||||
SingleReader = true // 单消费者简化并发模型
|
||||
});
|
||||
|
||||
// 生产者(任意线程)
|
||||
await channel.Writer.WriteAsync(new LocalShellTask { Command = "npm test" }, ct);
|
||||
|
||||
// 消费者(BackgroundService 中)
|
||||
await foreach (var task in channel.Reader.ReadAllAsync(ct))
|
||||
{
|
||||
await ProcessTaskAsync(task, ct);
|
||||
}
|
||||
```
|
||||
|
||||
### 有界通道与背压
|
||||
|
||||
```csharp
|
||||
// 有界通道:满了就等待(背压)
|
||||
var bounded = Channel.CreateBounded<ApiRequest>(new BoundedChannelOptions(capacity: 100)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键 NuGet 包清单
|
||||
|
||||
以下是本项目直接依赖的 NuGet 包,版本号为设计时目标版本。
|
||||
|
||||
| 包名 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| `Terminal.Gui` | 2.* | 终端 TUI 框架,替代 React+Ink |
|
||||
| `Spectre.Console` | 0.* | 富文本输出:表格、进度条、面板 |
|
||||
| `System.CommandLine` | 2.* | CLI 参数解析,替代 Commander.js |
|
||||
| `FluentValidation` | 12.* | POCO 对象校验,替代 Zod |
|
||||
| `JsonSchema.Net` | 7.* | JSON Schema 生成与校验 |
|
||||
| `Refit` | 8.* | 强类型 HTTP 客户端 |
|
||||
| `Polly` | 8.* | 重试、熔断、超时策略 |
|
||||
| `StreamJsonRpc` | 2.* | JSON-RPC 2.0 实现,用于 MCP 协议 |
|
||||
| `OmniSharp.Extensions.LanguageServer.Client` | 0.* | LSP 客户端 |
|
||||
| `Grpc.Net.Client` | 2.* | gRPC 客户端(部分 API 提供商) |
|
||||
| `Microsoft.Identity.Client` (MSAL) | 4.* | OAuth/OIDC 认证 |
|
||||
| `IdentityModel.OidcClient` | 6.* | OIDC 客户端流程 |
|
||||
| `AWSSDK.BedrockRuntime` | 4.* | AWS Bedrock 流式 API |
|
||||
| `Google.Apis.Auth` | 1.* | Google Cloud 认证(Vertex AI) |
|
||||
| `Microsoft.AspNetCore.SignalR.Client` | 10.* | WebSocket 高层抽象 |
|
||||
| `Markdig` | 0.* | Markdown 渲染 |
|
||||
| `DiffPlex` | 1.* | diff 计算与展示 |
|
||||
| `SixLabors.ImageSharp` | 3.* | 图片处理 |
|
||||
| `PdfPig` | 0.* | PDF 文本提取 |
|
||||
| `YamlDotNet` | 16.* | YAML 解析 |
|
||||
| `Microsoft.Extensions.Hosting` | 10.* | DI + 生命周期管理 |
|
||||
| `System.Threading.Channels` | 10.* | 生产者-消费者通道 |
|
||||
| `xunit` | 2.* | 单元测试框架 |
|
||||
| `NSubstitute` | 5.* | Mock 框架 |
|
||||
| `FluentAssertions` | 7.* | 断言库 |
|
||||
346
docs/总体概述与技术选型/reference/mcp-sdk-implement.md
Normal file
346
docs/总体概述与技术选型/reference/mcp-sdk-implement.md
Normal file
@ -0,0 +1,346 @@
|
||||
# MCP SDK 自研实现方案
|
||||
|
||||
> 关联文档:[总体概述](../总体概述与技术选型.md) | [MCP协议集成](../../基础设施设计/基础设施设计-MCP协议集成.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么自研
|
||||
|
||||
社区目前没有成熟的 .NET MCP SDK。检索 NuGet.org 和相关 GitHub 仓库后,现状如下:
|
||||
|
||||
- 微软官方尚未发布 `Microsoft.Extensions.Mcp` 正式包(截至 2026 年初仍在 preview 阶段,API 不稳定)
|
||||
- 社区的几个实验性实现覆盖场景有限,停留在 MCP 协议的早期版本,不支持 Streamable HTTP Transport
|
||||
- 没有任何包支持原生 AOT 编译,大量依赖运行时反射
|
||||
|
||||
自研的核心收益:
|
||||
1. **协议版本精确对齐**,与 TypeScript SDK 的行为保持一致,减少互操作问题
|
||||
2. **传输层可插拔**,在测试环境使用 InProcessTransport,生产环境使用 StdioTransport 或 SseTransport
|
||||
3. **AOT 友好**,所有序列化走 `JsonSerializerContext`,无运行时反射
|
||||
4. **依赖最小化**,不引入不受控的第三方包,安全审查更简单
|
||||
|
||||
---
|
||||
|
||||
## 2. JSON-RPC 2.0 协议基础
|
||||
|
||||
MCP 基于 JSON-RPC 2.0 协议。三种消息类型构成所有交互:
|
||||
|
||||
**请求 (Request)**
|
||||
|
||||
客户端发起,期待服务端响应。必须携带 `id`,方法名决定语义:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "req-001",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "read_file",
|
||||
"arguments": { "path": "/tmp/foo.txt" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应 (Response)**
|
||||
|
||||
服务端对请求的回复,`id` 必须与请求一致。成功用 `result`,失败用 `error`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "req-001",
|
||||
"result": {
|
||||
"content": [{ "type": "text", "text": "file contents here" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**通知 (Notification)**
|
||||
|
||||
单向消息,不携带 `id`,无需响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/tools/list_changed"
|
||||
}
|
||||
```
|
||||
|
||||
对应的 C# 模型:
|
||||
|
||||
```csharp
|
||||
public record JsonRpcRequest(
|
||||
string JsonRpc,
|
||||
string Id,
|
||||
string Method,
|
||||
JsonElement? Params
|
||||
);
|
||||
|
||||
public record JsonRpcResponse(
|
||||
string JsonRpc,
|
||||
string Id,
|
||||
JsonElement? Result,
|
||||
JsonRpcError? Error
|
||||
);
|
||||
|
||||
public record JsonRpcNotification(
|
||||
string JsonRpc,
|
||||
string Method,
|
||||
JsonElement? Params
|
||||
);
|
||||
|
||||
public record JsonRpcError(int Code, string Message, JsonElement? Data);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 传输层实现
|
||||
|
||||
传输层负责消息的发送和接收,与协议逻辑解耦。所有传输层实现同一接口:
|
||||
|
||||
```csharp
|
||||
public interface ITransport : IAsyncDisposable
|
||||
{
|
||||
Task<JsonRpcMessage> ReadMessageAsync(CancellationToken ct);
|
||||
Task SendMessageAsync(JsonRpcMessage message, CancellationToken ct);
|
||||
Task ConnectAsync(CancellationToken ct);
|
||||
Task CloseAsync(CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
### StdioTransport
|
||||
|
||||
通过子进程的 stdin/stdout 通信,是 MCP 最常见的本地工具接入方式:
|
||||
|
||||
```csharp
|
||||
public sealed class StdioTransport : ITransport
|
||||
{
|
||||
private readonly Process _process;
|
||||
private readonly Channel<JsonRpcMessage> _inbound =
|
||||
Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
_process.Start();
|
||||
// 启动后台读取循环,将 stdout 行解析为 JsonRpcMessage 写入 Channel
|
||||
_ = Task.Run(() => ReadLoopAsync(ct), ct);
|
||||
}
|
||||
|
||||
public async Task<JsonRpcMessage> ReadMessageAsync(CancellationToken ct)
|
||||
=> await _inbound.Reader.ReadAsync(ct);
|
||||
|
||||
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken ct)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(message, FreeCodeJsonContext.Default.JsonRpcMessage);
|
||||
await _process.StandardInput.WriteLineAsync(json.AsMemory(), ct);
|
||||
await _process.StandardInput.FlushAsync(ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每行 JSON 对应一条消息,严格遵循 MCP 规范的 newline-delimited JSON 格式。
|
||||
|
||||
### SseTransport
|
||||
|
||||
服务端通过 HTTP Server-Sent Events 推送消息,客户端通过 HTTP POST 发送:
|
||||
|
||||
```csharp
|
||||
public sealed class SseTransport : ITransport
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly Uri _sseEndpoint;
|
||||
private readonly Uri _postEndpoint;
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
var response = await _http.GetAsync(
|
||||
_sseEndpoint,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
ct);
|
||||
// 以流方式持续读取 SSE 事件行
|
||||
_ = Task.Run(() => ReadSseLoopAsync(response, ct), ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### StreamableHttpTransport
|
||||
|
||||
MCP 协议 2025-03-26 版本引入的新传输方式,支持单次 HTTP 请求内的双向流式通信:
|
||||
|
||||
```csharp
|
||||
public sealed class StreamableHttpTransport : ITransport
|
||||
{
|
||||
// 使用 HttpClient 发送请求,响应体作为 NDJSON 流持续读取
|
||||
// 请求体也可以是 NDJSON 流(客户端到服务端)
|
||||
// 通过 Content-Type: application/jsonl 区分
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocketTransport
|
||||
|
||||
适用于需要长连接和低延迟的场景:
|
||||
|
||||
```csharp
|
||||
public sealed class WebSocketTransport : ITransport
|
||||
{
|
||||
private readonly ClientWebSocket _ws = new();
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
await _ws.ConnectAsync(_uri, ct);
|
||||
_ = Task.Run(() => ReceiveLoopAsync(ct), ct);
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken ct)
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
message, FreeCodeJsonContext.Default.JsonRpcMessage);
|
||||
await _ws.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### InProcessTransport
|
||||
|
||||
测试和嵌入场景专用,通过一对 `Channel<JsonRpcMessage>` 模拟双向通信,无网络开销:
|
||||
|
||||
```csharp
|
||||
public sealed class InProcessTransport : ITransport
|
||||
{
|
||||
private readonly Channel<JsonRpcMessage> _clientToServer;
|
||||
private readonly Channel<JsonRpcMessage> _serverToClient;
|
||||
|
||||
// 客户端和服务端各持有一端的 Reader/Writer
|
||||
// 完全同步,deterministic,适合单元测试
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 客户端生命周期
|
||||
|
||||
MCP 客户端从连接到关闭经历五个阶段:
|
||||
|
||||
```
|
||||
connect → initialize → initialized → [tool calls / resource reads] → shutdown
|
||||
```
|
||||
|
||||
**阶段一:connect**
|
||||
|
||||
传输层建立物理连接(进程启动、HTTP 连接、WebSocket 握手)。
|
||||
|
||||
**阶段二:initialize**
|
||||
|
||||
客户端发送 `initialize` 请求,声明协议版本和能力:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "init-1",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-03-26",
|
||||
"clientInfo": { "name": "FreeCode", "version": "0.1.0" },
|
||||
"capabilities": { "roots": { "listChanged": true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**阶段三:initialized**
|
||||
|
||||
服务端响应 `initialize`,返回其支持的能力和协议版本后,客户端发送 `notifications/initialized` 通知,握手完成。
|
||||
|
||||
**阶段四:工具调用**
|
||||
|
||||
握手完成后,客户端可以发起任意 `tools/call`、`resources/read`、`prompts/get` 等请求。`McpClient` 内部维护一个 pending request 字典,通过 `id` 匹配请求和响应:
|
||||
|
||||
```csharp
|
||||
public async Task<CallToolResult> CallToolAsync(
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, JsonElement> arguments,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
var tcs = new TaskCompletionSource<JsonRpcResponse>();
|
||||
_pending[id] = tcs;
|
||||
|
||||
await _transport.SendMessageAsync(new JsonRpcRequest(
|
||||
"2.0", id, "tools/call",
|
||||
BuildParams(toolName, arguments)), ct);
|
||||
|
||||
var response = await tcs.Task.WaitAsync(ct);
|
||||
return ParseCallToolResult(response);
|
||||
}
|
||||
```
|
||||
|
||||
**阶段五:shutdown**
|
||||
|
||||
客户端发送 `shutdown` 请求,等待服务端 `null` 响应后发送 `exit` 通知,最后关闭传输层。
|
||||
|
||||
---
|
||||
|
||||
## 5. MCP 工具适配为 ITool 接口
|
||||
|
||||
McpToolWrapper 将 MCP 服务端暴露的工具包装为 FreeCode 的 `ITool` 接口,使得 MCP 工具和本地工具在 Engine 层完全透明:
|
||||
|
||||
```csharp
|
||||
public sealed class McpToolWrapper : ITool
|
||||
{
|
||||
private readonly McpClient _client;
|
||||
private readonly McpToolDefinition _definition;
|
||||
|
||||
public string Name => $"mcp_{_client.ServerId}_{_definition.Name}";
|
||||
public string Description => _definition.Description ?? string.Empty;
|
||||
|
||||
public JsonElement InputSchema => _definition.InputSchema;
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var args = ParseArguments(input);
|
||||
var result = await _client.CallToolAsync(_definition.Name, args, ct);
|
||||
|
||||
return new ToolResult(
|
||||
Success: !result.IsError,
|
||||
Content: result.Content.Select(MapContent).ToList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
McpManager 在启动时连接所有配置的 MCP 服务器,拉取工具列表,为每个工具创建 McpToolWrapper 并注册到 ToolRegistry。Engine 层通过 ToolRegistry 统一调度,无需感知来源。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 TypeScript SDK 对比
|
||||
|
||||
| 特性 | @modelcontextprotocol/sdk (TS) | FreeCode.Mcp (C# 自研) |
|
||||
|------|-------------------------------|----------------------|
|
||||
| 传输层 | Stdio、SSE、StreamableHttp | Stdio、SSE、StreamableHttp、WebSocket、InProcess |
|
||||
| 序列化 | Zod 运行时验证 | System.Text.Json + Source Generator |
|
||||
| AOT 支持 | 不适用 | 完全支持 |
|
||||
| 异步模型 | Promise / async-await | ValueTask / Channel\<T\> |
|
||||
| 协议版本 | 跟随官方 | 对齐 2025-03-26 |
|
||||
| 测试支持 | Jest mock | InProcessTransport |
|
||||
| 服务端实现 | 完整 | 计划中(v0.3) |
|
||||
|
||||
TypeScript SDK 使用 Zod schema 自动验证所有传入和传出消息。C# 自研版本通过 `JsonSerializerContext` 反序列化时的类型约束和 FluentValidation 检查实现等效的安全性,且无运行时反射开销。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键设计决策
|
||||
|
||||
**Channel\<T\> 用于异步消息传递**
|
||||
|
||||
传输层的读取循环与业务调用层之间通过 `Channel<JsonRpcMessage>` 解耦。读取循环是一个长期运行的后台任务,不阻塞调用线程。`Channel<T>` 支持背压(有界通道)和取消,比 `BlockingCollection<T>` 更适合全异步场景。
|
||||
|
||||
**不可变 Record 表示协议状态**
|
||||
|
||||
所有 JSON-RPC 消息和 MCP 协议模型都定义为 C# `record`,天然不可变。消息一旦构建就不会被修改,消除了多线程读写竞争的可能,也使测试断言更简单。
|
||||
|
||||
**id 用字符串而不是整数**
|
||||
|
||||
JSON-RPC 2.0 允许 id 为字符串或整数。FreeCode.Mcp 统一使用 `Guid.NewGuid().ToString("N")` 生成字符串 id,避免高并发下的整数碰撞,也方便日志追踪。
|
||||
|
||||
**传输层不感知协议**
|
||||
|
||||
`ITransport` 只负责原始 `JsonRpcMessage` 的收发,不理解 `initialize`、`tools/call` 等语义。协议逻辑完全在 `McpClient` 和 `McpServer` 中,传输层可以随时替换,不影响上层逻辑。这个边界在集成测试中尤其有价值:用 InProcessTransport 替换 StdioTransport,测试速度从秒级降到毫秒级。
|
||||
151
docs/总体概述与技术选型/reference/技术栈映射说明.md
Normal file
151
docs/总体概述与技术选型/reference/技术栈映射说明.md
Normal file
@ -0,0 +1,151 @@
|
||||
# 技术栈映射说明
|
||||
|
||||
> 关联文档:[总体概述](../总体概述与技术选型.md)
|
||||
|
||||
本文档说明 FreeCode .NET 版在技术选型上的完整映射逻辑,以及每个替代方案的选择依据。
|
||||
|
||||
---
|
||||
|
||||
## 1. 映射原则
|
||||
|
||||
在将 TypeScript/Bun 技术栈迁移到 .NET 10 的过程中,我们遵循三条核心原则,优先级由高到低:
|
||||
|
||||
**成熟库优先**
|
||||
选用社区已经过生产环境验证的库,而不是最新但尚不稳定的方案。工具越成熟,坑越少,维护成本越低。
|
||||
|
||||
**官方优先**
|
||||
微软官方或 .NET 基金会维护的包拥有更长的生命周期承诺和更一致的 API 风格。第三方包在官方方案缺失或明显不足时才纳入考虑。
|
||||
|
||||
**AOT 兼容优先**
|
||||
FreeCode 最终需要编译为原生 AOT 二进制,以实现秒级启动和最小依赖。凡是依赖运行时反射、动态代码生成的库都要审慎对待,并在必要时通过 Source Generator 绕过。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈映射总表
|
||||
|
||||
| 层级 | 原始 (TS/Bun) | .NET 10 替代 | 版本 | 理由 |
|
||||
|------|--------------|-------------|------|------|
|
||||
| 运行时 | Bun 1.x | .NET 10 AOT | 10.0 | 原生 AOT 编译,秒级启动,单文件分发,内存占用更低 |
|
||||
| 语言 | TypeScript 6 ESM | C# 13 | 13.0 | 静态类型、模式匹配、records、primary constructors,表达力与 TS 相当 |
|
||||
| 终端 UI | React 19 + Ink 6 | Terminal.Gui v2 | 2.x | 唯一成熟的跨平台 .NET TUI 框架,响应式布局,AOT 友好 |
|
||||
| 富文本输出 | Ink built-in | Spectre.Console | 0.49.x | 表格、进度条、标记语言,API 优雅,零反射,AOT 完全兼容 |
|
||||
| CLI 解析 | Commander.js | System.CommandLine v2 | 2.0.0-beta | 微软官方,支持 AOT,子命令/选项/参数体系完整 |
|
||||
| Schema 验证 | Zod v4 | FluentValidation + JsonSchema.Net | 11.x / 7.x | FluentValidation 负责业务规则验证,JsonSchema.Net 负责 JSON Schema 生成与校验 |
|
||||
| 代码搜索 | ripgrep (内置) | ripgrep via Process | — | ripgrep 本身是独立二进制,通过 `System.Diagnostics.Process` 调用,行为完全一致 |
|
||||
| LSP 客户端 | vscode-languageserver-protocol | OmniSharp.Extensions.LanguageServer | 0.19.x | OmniSharp 团队出品,协议覆盖完整,支持双向消息和自定义扩展 |
|
||||
| MCP 协议 | @modelcontextprotocol/sdk | 自研 MCP SDK | — | 社区尚无成熟 .NET MCP SDK,自研可精确控制传输层和协议版本,详见 [MCP SDK 自研实现方案](mcp-sdk-implement.md) |
|
||||
| HTTP 客户端 | Bun fetch | HttpClient + Refit | — / 7.x | HttpClient 是 .NET 标准组件,Refit 提供声明式接口减少样板代码,支持 AOT |
|
||||
| OAuth / OIDC | 自研 | MSAL.NET + IdentityModel.OidcClient | 4.x / 5.x | MSAL.NET 覆盖 Azure/GitHub/Google,OidcClient 处理标准 OIDC 设备码流 |
|
||||
| WebSocket | ws | System.Net.WebSockets + SignalR | — / 8.x | 标准库 WebSockets 用于原始帧处理,SignalR 用于需要自动重连的场景 |
|
||||
| 子进程 | Bun.spawn | System.Diagnostics.Process | — | BCL 标准组件,行为跨平台一致,支持异步读写流 |
|
||||
| JSON 序列化 | JSON.parse/stringify | System.Text.Json | — | BCL 标准组件,Source Generator 模式下零反射,AOT 完全兼容 |
|
||||
| 后台任务 | 自研 TaskQueue | BackgroundService + Channel\<T\> | — | BackgroundService 是 .NET 托管服务标准抽象,Channel\<T\> 提供高性能无锁消息传递 |
|
||||
| 插件系统 | dynamic import() | AssemblyLoadContext | — | .NET 官方插件隔离机制,支持热加载和独立卸载 |
|
||||
| 依赖注入 | 无 | Microsoft.Extensions.DependencyInjection | 10.0 | .NET 官方 DI 容器,轻量、AOT 友好,通过 Source Generator 消除反射 |
|
||||
| 日志 | console.log | Microsoft.Extensions.Logging | 10.0 | 官方日志抽象,支持结构化日志,后端可插拔(Console/File/OTLP) |
|
||||
|
||||
---
|
||||
|
||||
## 3. NuGet 包完整清单
|
||||
|
||||
```xml
|
||||
<!-- FreeCode.csproj 及子项目汇总包引用 -->
|
||||
|
||||
<!-- 终端 UI -->
|
||||
<PackageReference Include="Terminal.Gui" Version="2.*" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.*" />
|
||||
|
||||
<!-- CLI 解析 -->
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.*" />
|
||||
|
||||
<!-- 验证 -->
|
||||
<PackageReference Include="FluentValidation" Version="11.*" />
|
||||
<PackageReference Include="JsonSchema.Net" Version="7.*" />
|
||||
|
||||
<!-- HTTP / REST -->
|
||||
<PackageReference Include="Refit" Version="7.*" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="7.*" />
|
||||
|
||||
<!-- OAuth / OIDC -->
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" Version="5.*" />
|
||||
|
||||
<!-- LSP -->
|
||||
<PackageReference Include="OmniSharp.Extensions.LanguageServer.Client" Version="0.19.*" />
|
||||
<PackageReference Include="OmniSharp.Extensions.LanguageProtocol" Version="0.19.*" />
|
||||
|
||||
<!-- JSON -->
|
||||
<!-- System.Text.Json 随 .NET 10 SDK 内置,无需额外引用 -->
|
||||
|
||||
<!-- DI / 日志 / 托管 -->
|
||||
<!-- Microsoft.Extensions.* 随 .NET 10 SDK 内置 -->
|
||||
|
||||
<!-- 后台任务 -->
|
||||
<!-- System.Threading.Channels 随 .NET 10 SDK 内置 -->
|
||||
|
||||
<!-- AST 搜索(ast-grep 绑定) -->
|
||||
<PackageReference Include="AstGrep.Bindings" Version="0.30.*" />
|
||||
|
||||
<!-- 序列化辅助 -->
|
||||
<PackageReference Include="System.Text.Json.SourceGeneration" Version="10.0.*" />
|
||||
|
||||
<!-- 测试 -->
|
||||
<PackageReference Include="xunit" Version="2.*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
|
||||
<PackageReference Include="Moq" Version="4.*" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.*" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. AOT 兼容性说明
|
||||
|
||||
原生 AOT 编译是本项目的核心约束,它要求在编译期完成所有类型分析,运行时不允许动态反射、动态代码生成和未知类型的延迟绑定。
|
||||
|
||||
**Source Generator 策略**
|
||||
|
||||
System.Text.Json 的 AOT 支持通过 `JsonSerializerContext` 实现。每个需要序列化的模型都注册到一个 partial context 类,编译器在构建期生成序列化代码,完全绕开运行时反射:
|
||||
|
||||
```csharp
|
||||
[JsonSerializable(typeof(ChatRequest))]
|
||||
[JsonSerializable(typeof(ChatResponse))]
|
||||
internal partial class FreeCodeJsonContext : JsonSerializerContext { }
|
||||
```
|
||||
|
||||
`System.CommandLine` v2 的 AOT 支持已在 beta 阶段引入,通过 Source Generator 生成命令绑定代码,替代早期版本的反射绑定。
|
||||
|
||||
Microsoft.Extensions.DependencyInjection 从 .NET 8 起提供 `ServiceProviderOptions.ValidateScopes` 的 AOT 路径,通过 `[ServiceDescriptor]` 属性消除反射注册扫描。
|
||||
|
||||
**裁剪 (Trimming) 配置**
|
||||
|
||||
```xml
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimmerRootDescriptor>TrimmerRoots.xml</TrimmerRootDescriptor>
|
||||
```
|
||||
|
||||
Terminal.Gui v2 和 Spectre.Console 均已标注 `[RequiresUnreferencedCode]` 警告可抑制区域,实际裁剪测试证明两者在启用 `TrimmerRootDescriptor` 白名单后正常工作。
|
||||
|
||||
**不兼容项处理**
|
||||
|
||||
AssemblyLoadContext 用于插件加载,插件程序集本身不参与 AOT 编译,以动态加载的 JIT 模式运行。主程序 AOT 编译不受影响,两者通过接口合约隔离。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键选型设计说明
|
||||
|
||||
**Terminal.Gui v2 vs. 其他 TUI 方案**
|
||||
|
||||
Spectre.Console 的 Live 组件可以渲染动态布局,但它本质上是顺序输出,不支持真正的交互式控件(文本框、列表框、焦点切换)。FreeCode 的 REPL 界面需要多面板、快捷键绑定和滚动视图,Terminal.Gui v2 是目前唯一满足条件的成熟方案。
|
||||
|
||||
**FluentValidation + JsonSchema.Net vs. 单一方案**
|
||||
|
||||
Zod 同时承担运行时验证和 Schema 生成两个职责。.NET 生态里没有一个库同时做好两件事,因此分开处理:FluentValidation 的 Fluent API 在业务规则验证上表达力强,JsonSchema.Net 处理 JSON Schema 的生成和校验(主要用于工具参数描述)。
|
||||
|
||||
**Refit vs. 纯 HttpClient**
|
||||
|
||||
Refit 将 HTTP 接口声明为 C# interface,通过 Source Generator(v7+)生成实现代码,不依赖运行时代理,AOT 兼容。对于结构化的 LLM API 调用来说,减少样板代码的收益明显。
|
||||
|
||||
**Channel\<T\> vs. BlockingCollection/Queue**
|
||||
|
||||
`Channel<T>` 是 .NET 的高性能异步消息原语,支持有界/无界背压,完全基于 `ValueTask` 避免内存分配,与 `BackgroundService` 的异步生命周期天然契合。`BlockingCollection` 是同步阻塞模型,在异步场景下会占用线程。
|
||||
199
docs/总体概述与技术选型/reference/解决方案结构说明.md
Normal file
199
docs/总体概述与技术选型/reference/解决方案结构说明.md
Normal file
@ -0,0 +1,199 @@
|
||||
# 解决方案结构说明
|
||||
|
||||
> 关联文档:[总体概述](../总体概述与技术选型.md)
|
||||
|
||||
本文档描述 FreeCode .NET 解决方案的项目划分方式、各项目职责和相互依赖关系,帮助开发者快速定位代码归属和贡献边界。
|
||||
|
||||
---
|
||||
|
||||
## 1. FreeCode.sln 概览
|
||||
|
||||
解决方案包含 **17 个源码项目** 和 **9 个测试项目**,合计 26 个项目。整体采用垂直切分策略:每个项目对应一个关注点,通过接口而非直接引用来解耦。
|
||||
|
||||
```
|
||||
FreeCode.sln
|
||||
├── 源码项目 (17)
|
||||
│ ├── FreeCode # 主可执行入口
|
||||
│ ├── FreeCode.Core # 核心接口与模型
|
||||
│ ├── FreeCode.Engine # 查询引擎
|
||||
│ ├── FreeCode.Tools # 工具实现
|
||||
│ ├── FreeCode.Commands # 命令实现
|
||||
│ ├── FreeCode.ApiProviders # LLM API 适配
|
||||
│ ├── FreeCode.Mcp # MCP 协议
|
||||
│ ├── FreeCode.Lsp # LSP 集成
|
||||
│ ├── FreeCode.Bridge # IDE 桥接
|
||||
│ ├── FreeCode.Services # 业务服务
|
||||
│ ├── FreeCode.Tasks # 后台任务
|
||||
│ ├── FreeCode.Skills # 技能系统
|
||||
│ ├── FreeCode.Plugins # 插件系统
|
||||
│ ├── FreeCode.Features # 特性开关
|
||||
│ ├── FreeCode.State # 状态管理
|
||||
│ └── FreeCode.TerminalUI # 终端 UI 组件
|
||||
└── 测试项目 (9)
|
||||
├── FreeCode.Core.Tests
|
||||
├── FreeCode.Engine.Tests
|
||||
├── FreeCode.Tools.Tests
|
||||
├── FreeCode.Commands.Tests
|
||||
├── FreeCode.ApiProviders.Tests
|
||||
├── FreeCode.Mcp.Tests
|
||||
├── FreeCode.Services.Tests
|
||||
├── FreeCode.Tasks.Tests
|
||||
└── FreeCode.Integration.Tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 各项目职责说明
|
||||
|
||||
| 项目名 | 职责 | 关键类型 | 依赖项目 |
|
||||
|--------|------|----------|----------|
|
||||
| **FreeCode** | 主可执行 Console App,入口点、DI 容器组装、命令行解析 | `Program`, `RootCommand`, `Startup` | 全部项目 |
|
||||
| **FreeCode.Core** | 核心接口与模型,25+ 接口、20+ 模型,无外部依赖,是整个解决方案的契约层 | `ITool`, `ICommand`, `IApiProvider`, `ChatMessage`, `ToolResult`, `AppConfig` | 无 |
|
||||
| **FreeCode.Engine** | QueryEngine 协调消息流与工具调用,PromptBuilder 构建系统提示词 | `QueryEngine`, `PromptBuilder`, `ToolDispatcher`, `ConversationHistory` | Core |
|
||||
| **FreeCode.Tools** | 50+ 工具实现,按子目录分组:FileSystem、Shell、Agent、Web、Mcp、UserInteraction、PlanMode、Swarms、Worktree | `BashTool`, `ReadFileTool`, `GlobTool`, `GrepTool`, `AgentTool`, `BrowserTool`, `McpTool`, `PlanModeTool`, `WorktreeTool` | Core, Services, Mcp |
|
||||
| **FreeCode.Commands** | 70+ 斜杠命令实现,每个命令对应一个类 | `ClearCommand`, `CompactCommand`, `ReviewWorkCommand`, `InitDeepCommand`, `RalphLoopCommand` | Core, Engine, State, Skills |
|
||||
| **FreeCode.ApiProviders** | 5 个 LLM 提供商适配:Anthropic、OpenAI、Gemini、GitHub Copilot、Bedrock | `ClaudeProvider`, `OpenAiProvider`, `GeminiProvider`, `CopilotProvider`, `BedrockProvider` | Core |
|
||||
| **FreeCode.Mcp** | MCP 协议客户端与服务端,传输层实现,工具适配器 | `McpClient`, `McpServer`, `StdioTransport`, `SseTransport`, `McpToolWrapper` | Core |
|
||||
| **FreeCode.Lsp** | LSP 客户端集成,支持 goto-definition、find-references、diagnostics、rename | `LspClient`, `LspDiagnosticsProvider`, `LspWorkspaceManager` | Core |
|
||||
| **FreeCode.Bridge** | IDE 桥接层,与 VS Code / JetBrains 等 IDE 扩展通信 | `BridgeServer`, `BridgeMessage`, `IdeEventHandler` | Core, Services |
|
||||
| **FreeCode.Services** | 业务服务层:文件系统抽象、配置管理、权限管理、搜索服务 | `FileService`, `ConfigService`, `PermissionService`, `RipgrepService`, `AstGrepService` | Core |
|
||||
| **FreeCode.Tasks** | 后台任务管理:任务队列、任务状态跟踪、MCP 监控任务、Workflow 任务 | `BackgroundTaskManager`, `TaskQueue`, `MonitorMcpTask`, `LocalWorkflowTask` | Core, Services |
|
||||
| **FreeCode.Skills** | 技能系统:技能加载、索引、搜索和执行 | `SkillLoader`, `SkillIndex`, `SkillSearchService`, `SkillExecutor` | Core, Services |
|
||||
| **FreeCode.Plugins** | 插件系统:通过 AssemblyLoadContext 动态加载插件,插件发现和生命周期管理 | `PluginLoader`, `PluginHost`, `PluginRegistry` | Core |
|
||||
| **FreeCode.Features** | 特性开关系统,控制实验性功能的启用状态,支持编译期和运行期两种模式 | `FeatureFlags`, `FeatureRegistry`, `IFeatureFlag` | Core |
|
||||
| **FreeCode.State** | 应用状态管理,线程安全的状态读写,状态订阅 | `AppStateStore`, `ConversationState`, `SessionState` | Core |
|
||||
| **FreeCode.TerminalUI** | Terminal.Gui v2 组件封装,REPL 界面、输出渲染、进度显示 | `ReplView`, `OutputPane`, `StatusBar`, `ThemeManager` | Core, State |
|
||||
|
||||
---
|
||||
|
||||
## 3. 项目依赖关系图
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ FreeCode.Core │
|
||||
│ (接口 + 模型) │
|
||||
└────────┬────────┘
|
||||
│ 被所有项目引用
|
||||
┌─────────────────────┼──────────────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌─────────▼─────────┐ ┌──────────▼────────┐
|
||||
│FreeCode. │ │FreeCode.ApiProviders│ │FreeCode.Services │
|
||||
│Features │ │(LLM 适配层) │ │(文件/搜索/配置) │
|
||||
└──────┬──────┘ └─────────┬─────────┘ └──────────┬────────┘
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌─────────▼─────────┐ ┌──────────▼────────┐
|
||||
│FreeCode. │ │ FreeCode.Engine │ │ FreeCode.Mcp │
|
||||
│State │ │ (QueryEngine) │ │ (MCP 协议) │
|
||||
└──────┬──────┘ └─────────┬─────────┘ └──────────┬────────┘
|
||||
│ │ │
|
||||
┌──────▼─────────────────────▼──────────────────────────▼────────┐
|
||||
│ FreeCode.Tools │
|
||||
│ (50+ 工具, 依赖 Services + Mcp) │
|
||||
└──────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌────────▼───────┐ ┌───────▼────────┐
|
||||
│FreeCode. │ │FreeCode.Skills │ │FreeCode.Tasks │
|
||||
│Commands │ │(技能系统) │ │(后台任务) │
|
||||
└──────┬──────┘ └────────┬───────┘ └───────┬────────┘
|
||||
│ │ │
|
||||
└────────────────────┼────────────────────┘
|
||||
│
|
||||
┌──────────▼─────────┐
|
||||
│ FreeCode.TerminalUI│
|
||||
│ (Terminal.Gui 组件)│
|
||||
└──────────┬─────────┘
|
||||
│
|
||||
┌───────────────▼────────────────┐
|
||||
│ FreeCode (主入口) │
|
||||
│ DI 组装 + CLI + 程序生命周期 │
|
||||
└────────────────────────────────┘
|
||||
|
||||
独立模块 (通过接口与主流程交互):
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│FreeCode.Lsp │ │FreeCode.Bridge│ │FreeCode.Plugins│
|
||||
│(LSP 客户端) │ │(IDE 桥接) │ │(动态插件) │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Directory.Build.props 共享配置
|
||||
|
||||
所有项目共享一个根级别的 `Directory.Build.props`,统一配置编译选项,避免在每个 `.csproj` 中重复:
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- 语言版本 -->
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!-- 目标框架 -->
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<!-- AOT 编译 (仅主项目覆盖为 true) -->
|
||||
<PublishAot>false</PublishAot>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
|
||||
<!-- 警告即错误 -->
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<NoWarn>CS1591</NoWarn>
|
||||
|
||||
<!-- 分析器 -->
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
||||
|
||||
<!-- 版本管理 -->
|
||||
<Version>0.1.0</Version>
|
||||
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- 测试项目特定设置 -->
|
||||
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))">
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
主项目 `FreeCode.csproj` 在此基础上覆盖 AOT 相关设置:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试项目结构
|
||||
|
||||
9 个测试项目覆盖核心业务逻辑,遵循一对一映射原则(一个源码项目对应一个测试项目),加一个集成测试项目。
|
||||
|
||||
| 测试项目 | 对应源码项目 | 测试重点 |
|
||||
|----------|-------------|----------|
|
||||
| FreeCode.Core.Tests | FreeCode.Core | 模型验证、接口约定、序列化正确性 |
|
||||
| FreeCode.Engine.Tests | FreeCode.Engine | QueryEngine 消息流、工具调度逻辑、PromptBuilder 输出 |
|
||||
| FreeCode.Tools.Tests | FreeCode.Tools | 各工具的单元行为,沙箱文件系统,进程调用 mock |
|
||||
| FreeCode.Commands.Tests | FreeCode.Commands | 命令解析、执行逻辑、输出格式 |
|
||||
| FreeCode.ApiProviders.Tests | FreeCode.ApiProviders | 请求构建、响应解析、错误处理、流式解析 |
|
||||
| FreeCode.Mcp.Tests | FreeCode.Mcp | JSON-RPC 消息编解码、传输层、客户端生命周期 |
|
||||
| FreeCode.Services.Tests | FreeCode.Services | 文件服务、配置加载、权限检查、搜索结果解析 |
|
||||
| FreeCode.Tasks.Tests | FreeCode.Tasks | 任务队列背压、取消、状态转换 |
|
||||
| FreeCode.Integration.Tests | 全部项目 | 端到端命令执行,真实 API(可选,通过环境变量控制) |
|
||||
|
||||
测试框架统一使用 **xUnit 2.x** + **FluentAssertions 6.x** + **Moq 4.x**。集成测试通过 `[Trait("Category", "Integration")]` 标注,默认 CI 跳过,本地按需运行:
|
||||
|
||||
```bash
|
||||
dotnet test --filter "Category!=Integration"
|
||||
dotnet test --filter "Category=Integration" # 需要设置 ANTHROPIC_API_KEY
|
||||
```
|
||||
252
docs/总体概述与技术选型/总体概述与技术选型.md
Normal file
252
docs/总体概述与技术选型/总体概述与技术选型.md
Normal file
@ -0,0 +1,252 @@
|
||||
# 总体概述与技术选型
|
||||
|
||||
> **文档版本**: 1.0
|
||||
> **日期**: 2026-04-05
|
||||
> **所属部分**: 第一部分
|
||||
> **原始设计来源**: `DESIGN-NET10.md` 第一部分(第 1-4 节及附录)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 项目概述](#1-项目概述)
|
||||
- [2. 技术栈映射](#2-技术栈映射)
|
||||
- [3. 顶层架构设计](#3-顶层架构设计)
|
||||
- [4. 解决方案结构](#4-解决方案结构)
|
||||
- [5. 关键设计决策](#5-关键设计决策)
|
||||
- [模块覆盖对照表](#模块覆盖对照表)
|
||||
- [参考文档](#参考文档)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
free-code 是 Anthropic Claude Code CLI 的社区 fork,一个终端原生 AI 编码代理。本次重写目标是将整个项目从 TypeScript/Bun/React+Ink 技术栈完整迁移至 .NET 10,同时保持 100% 功能对等。
|
||||
|
||||
**原始版本规模**: v2.1.87,包含 200+ 个 TS/TSX 源文件、50+ 个 Agent 工具、70+ 个 Slash 命令、5 个 LLM 提供商接入。
|
||||
|
||||
### 核心功能清单
|
||||
|
||||
- **交互式 REPL**: 终端 AI 对话界面,流式响应、工具调用可视化、权限审批
|
||||
- **50+ Agent 工具**: 文件读写编辑、Bash 执行、代码搜索、Web 访问、子代理生成
|
||||
- **70+ Slash 命令**: 配置、会话管理、OAuth、插件、主题等
|
||||
- **5 个 LLM 提供商**: Anthropic、OpenAI Codex、AWS Bedrock、Google Vertex AI、Anthropic Foundry
|
||||
- **MCP/LSP 协议**: 外部工具集成 + 语言服务器
|
||||
- **IDE 桥接**: VS Code/JetBrains 远程会话控制
|
||||
- **技能/插件系统**: 可扩展 prompt 模板 + 功能包
|
||||
- **后台任务、会话记忆、语音输入、远程会话、同伴系统**
|
||||
|
||||
### 重写目标
|
||||
|
||||
| 目标 | 描述 |
|
||||
|------|------|
|
||||
| 功能对等 | 100% 覆盖所有功能 |
|
||||
| 性能提升 | .NET 10 AOT 编译,启动速度与内存双优化 |
|
||||
| 类型安全 | C# 13 强类型替代 TypeScript 运行时校验 |
|
||||
| 原生集成 | 更深度的 OS 集成(进程、文件系统、沙箱) |
|
||||
| 可维护性 | 清晰模块边界、DI、接口驱动设计 |
|
||||
|
||||
> .NET 10 平台特性详见: [./reference/.NET-10-平台介绍.md](reference/.NET-10-平台介绍.md)
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈映射
|
||||
|
||||
将 TypeScript 生态的每个技术点一一对应至 .NET 10 生态,选型原则是成熟度优先、官方方案优先、AOT 兼容性优先。
|
||||
|
||||
| 层级 | 原始 (TS/Bun) | .NET 10 替代 | 理由 |
|
||||
|------|-------------|-------------|------|
|
||||
| 运行时 | Bun >= 1.3.11 | .NET 10 + AOT | 单一二进制,跨平台 |
|
||||
| 语言 | TypeScript 6 ESM | C# 13 | record types, pattern matching |
|
||||
| 终端 UI | React 19 + Ink 6 | **Terminal.Gui v2** | 唯一成熟 .NET TUI,组件模型接近 React |
|
||||
| 输出渲染 | Ink 内置 | **Spectre.Console** | 表格/面板/进度条/富文本 |
|
||||
| CLI 解析 | Commander.js | **System.CommandLine v2** | 官方框架,子命令/选项/补全 |
|
||||
| Schema 校验 | Zod v4 | **FluentValidation** + **JsonSchema.Net** | POCO 校验 + JSON Schema |
|
||||
| 代码搜索 | ripgrep (bundled) | ripgrep via Process wrapper | 保持 ripgrep 性能 |
|
||||
| LSP 客户端 | vscode-languageserver-protocol | **OmniSharp.Extensions.LanguageServer** | 最成熟 .NET LSP 库 |
|
||||
| MCP 协议 | @modelcontextprotocol/sdk | **自研 MCP SDK** | 社区无成熟方案 |
|
||||
| HTTP | Bun fetch | **HttpClient + Refit** | 强类型 REST,流式支持 |
|
||||
| OAuth | 自定义 client | **MSAL.NET + IdentityModel.OidcClient** | 成熟 OAuth/OIDC |
|
||||
| WebSocket | ws (npm) | **System.Net.WebSockets + SignalR** | 内建 + 高层抽象 |
|
||||
| 进程管理 | Bun spawn | **System.Diagnostics.Process** | 内建跨平台 |
|
||||
| JSON | JSON.parse/stringify | **System.Text.Json** | 高性能 AOT 友好 |
|
||||
| 后台任务 | 自定义 | **BackgroundService + Channels** | .NET 标准模式 |
|
||||
| 插件加载 | 动态 import() | **AssemblyLoadContext** | 运行时程序集隔离 |
|
||||
| DI | 无 | **Microsoft.Extensions.DependencyInjection** | 标准 DI |
|
||||
| 日志 | console | **Microsoft.Extensions.Logging** | 结构化日志 |
|
||||
|
||||
> 每个映射条目的详细说明,以及完整 NuGet 包版本清单,见: [./reference/技术栈映射说明.md](reference/技术栈映射说明.md)
|
||||
|
||||
---
|
||||
|
||||
## 3. 顶层架构设计
|
||||
|
||||
系统分五层,从下往上依次为基础层、基础设施层、核心服务层、应用层、表现层。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
│ Terminal.Gui v2 (REPL) │ System.CommandLine (CLI) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Application Layer │
|
||||
│ REPL Controller │ Command Dispatcher │ QueryEngine │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Core Services Layer │
|
||||
│ ToolSystem(50+) │ PermissionEngine │ FeatureFlags │
|
||||
│ AgentSpawner │ Memory/Context │ PromptBuilder │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer │
|
||||
│ ApiProviderRouter │ McpClientManager │ LspClientManager │
|
||||
│ OAuthService │ BackgroundTasks │ IDE Bridge │
|
||||
│ SkillLoader │ PluginLoader │ RemoteSessions │
|
||||
│ RateLimitService │ NotificationSvc │ VoiceService │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Foundation Layer │
|
||||
│ AppState Store │ Config System │ Logging │ DI Container │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**核心数据流**: 用户输入 → CLI/REPL → QueryEngine → PromptBuilder → ApiProvider → LLM 响应 → ToolUse 循环 → 渲染输出
|
||||
|
||||
每一层仅依赖其下层,不允许跨层向上依赖。基础设施层中的 MCP SDK 是自研实现,因为社区目前没有成熟的 .NET 方案,详见: [./reference/mcp-sdk-implement.md](reference/mcp-sdk-implement.md)
|
||||
|
||||
---
|
||||
|
||||
## 4. 解决方案结构
|
||||
|
||||
整个项目组织为一个 .sln 文件管理 17 个 C# 项目,加上 9 个测试项目。
|
||||
|
||||
```
|
||||
FreeCode.sln
|
||||
├── src/
|
||||
│ ├── FreeCode/ # 主可执行 (Console App, AOT)
|
||||
│ ├── FreeCode.Core/ # 核心接口 + 模型 (25+ 接口, 20+ 模型)
|
||||
│ ├── FreeCode.Engine/ # QueryEngine + PromptBuilder
|
||||
│ ├── FreeCode.Tools/ # 50+ Agent 工具
|
||||
│ │ ├── FileSystem/ (Read, Edit, Write, Glob, Grep, NotebookEdit)
|
||||
│ │ ├── Shell/ (Bash, PowerShell)
|
||||
│ │ ├── Agent/ (Agent, Skill, Task* tools)
|
||||
│ │ ├── Web/ (WebFetch, WebSearch)
|
||||
│ │ ├── Mcp/ (ListMcpResources, ReadMcpResource, ToolSearch)
|
||||
│ │ ├── UserInteraction/ (AskUserQuestion, Brief)
|
||||
│ │ ├── PlanMode/ (EnterPlan, ExitPlan)
|
||||
│ │ ├── AgentSwarms/ (SendMessage, TeamCreate, TeamDelete)
|
||||
│ │ └── Worktree/ (EnterWorktree, ExitWorktree)
|
||||
│ ├── FreeCode.Commands/ # 70+ Slash 命令
|
||||
│ ├── FreeCode.ApiProviders/ # 5 LLM 提供商
|
||||
│ ├── FreeCode.Mcp/ # MCP 协议 (Transport + Protocol + ToolWrapper)
|
||||
│ ├── FreeCode.Lsp/ # LSP 集成
|
||||
│ ├── FreeCode.Bridge/ # IDE 桥接
|
||||
│ ├── FreeCode.Services/ # 业务服务 (Auth/Memory/Voice/Remote/Notification/...)
|
||||
│ ├── FreeCode.Tasks/ # 后台任务管理
|
||||
│ ├── FreeCode.Skills/ # 技能系统
|
||||
│ ├── FreeCode.Plugins/ # 插件系统 (AssemblyLoadContext)
|
||||
│ ├── FreeCode.Features/ # 特性开关
|
||||
│ ├── FreeCode.State/ # 状态管理 (AppState record)
|
||||
│ └── FreeCode.TerminalUI/ # Terminal.Gui 组件 (REPL/Components/Theme/Keybindings)
|
||||
├── tests/ # 9 个测试项目
|
||||
└── scripts/ # build.ps1, build.sh, publish.sh
|
||||
```
|
||||
|
||||
> 每个项目的职责边界、依赖关系图和 `Directory.Build.props` 共享配置,详见: [./reference/解决方案结构说明.md](reference/解决方案结构说明.md)
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键设计决策
|
||||
|
||||
这些决策在整个设计方案中一以贯之,影响所有模块的实现方式。
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 终端 UI 框架 | Terminal.Gui v2 | .NET 生态内唯一成熟的 TUI 方案 |
|
||||
| CLI 解析框架 | System.CommandLine v2 | 官方库,v2 已稳定,子命令/选项/自动补全齐备 |
|
||||
| MCP 协议客户端 | 自研 SDK | 社区目前没有成熟的 .NET 实现 |
|
||||
| 插件隔离方式 | AssemblyLoadContext | .NET 原生支持,程序集可卸载 |
|
||||
| 状态管理模式 | Record + Event | 不可变状态 + 事件驱动,类似 Redux/Elm |
|
||||
| AOT 兼容策略 | Source Generator 优先 | 尽量避免反射,确保 AOT 编译不裁剪 |
|
||||
| 代码搜索 | ripgrep 进程包装 | 保持原有 ripgrep 的搜索性能 |
|
||||
|
||||
---
|
||||
|
||||
## 模块覆盖对照表
|
||||
|
||||
> 说明:
|
||||
> - ✅ 已覆盖:已有独立设计文档
|
||||
> - ↗️ 归属:作为其他文档的小节或附属内容覆盖
|
||||
> - 📋 空目录/stub:当前快照为空目录、占位文件或仅剩 stub,不单独出文档
|
||||
|
||||
| 原始 ../../src/ 目录 | 文件数 | docs/ 覆盖文档 | 状态 |
|
||||
|------|------:|------|------|
|
||||
| `entrypoints` | 11 | [核心模块设计 — CLI启动与解析](../核心模块设计/核心模块设计-CLI启动与解析.md) | ✅ 已覆盖 |
|
||||
| `screens` | 3 | [核心模块设计 — CLI启动与解析](../核心模块设计/核心模块设计-CLI启动与解析.md) | ↗️ 归属 |
|
||||
| `QueryEngine.ts` | 1 | [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md) | ✅ 已覆盖 |
|
||||
| `tools.ts`、`tools/` | 210 | [核心模块设计 — 工具系统](../核心模块设计/核心模块设计-工具系统.md) | ✅ 已覆盖 |
|
||||
| `commands.ts`、`commands/` | 218 | [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md) | ✅ 已覆盖 |
|
||||
| `services/api/` | 39 | [核心模块设计 — API 多提供商路由](../核心模块设计/核心模块设计-API提供商路由.md) | ✅ 已覆盖 |
|
||||
| `services/lsp/` | 7 | [基础设施设计 — LSP集成](../基础设施设计/基础设施设计-LSP集成.md) | ✅ 已覆盖 |
|
||||
| `services/mcp/` | 22 | [基础设施设计 — MCP协议集成](../基础设施设计/基础设施设计-MCP协议集成.md) | ✅ 已覆盖 |
|
||||
| `services/oauth/` | 7 | [服务子系统设计 — 认证与OAuth](../服务子系统设计/服务子系统设计-认证与OAuth.md) | ✅ 已覆盖 |
|
||||
| `services/SessionMemory/` | 3 | [服务子系统设计 — 会话记忆与上下文](../服务子系统设计/服务子系统设计-会话记忆与上下文.md) | ✅ 已覆盖 |
|
||||
| `services/contextCollapse/` | 3 | [服务子系统设计 — 会话记忆与上下文](../服务子系统设计/服务子系统设计-会话记忆与上下文.md) | ↗️ 归属 |
|
||||
| `services/extractMemories/` | 2 | [服务子系统设计 — 会话记忆与上下文](../服务子系统设计/服务子系统设计-会话记忆与上下文.md) | ↗️ 归属 |
|
||||
| `services/teamMemorySync/` | 5 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/autoDream/` | 4 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/plugins/` | 3 | [UI与扩展设计 — 插件系统](../UI与扩展设计/UI与扩展设计-插件系统.md) | ✅ 已覆盖 |
|
||||
| `services/PromptSuggestion/` | 2 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/policyLimits/` | 2 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/tips/` | 3 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/analytics/` | 8 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/voice*` | 3 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/settingsSync/` | 2 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `services/bootstrap/` | 1 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `bridge/` | 32 | [基础设施设计 — IDE桥接](../基础设施设计/基础设施设计-IDE桥接.md) | ✅ 已覆盖 |
|
||||
| `tasks/` | 14 | [基础设施设计 — 后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md) | ✅ 已覆盖 |
|
||||
| `state/` | 6 | [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md) | ✅ 已覆盖 |
|
||||
| `skills/` | 50 | [UI与扩展设计 — 技能系统](../UI与扩展设计/UI与扩展设计-技能系统.md) | ✅ 已覆盖 |
|
||||
| `plugins/` | 2 | [UI与扩展设计 — 插件系统](../UI与扩展设计/UI与扩展设计-插件系统.md) | ✅ 已覆盖 |
|
||||
| `coordinator/` | 2 | [核心模块设计 — 多代理协调器 (Coordinator)](../核心模块设计/核心模块设计-多代理协调.md) | ✅ 已覆盖 |
|
||||
| `assistant/` | 5 | [核心模块设计 — 多代理协调器 (Coordinator)](../核心模块设计/核心模块设计-多代理协调.md) | ↗️ 归属 |
|
||||
| `context/` | 9 | [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md) | ↗️ 归属 |
|
||||
| `hooks/` | 104 | [UI与扩展设计 — Terminal-Gui 终端UI](../UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md) | ↗️ 归属 |
|
||||
| `components/` | 400 | [UI与扩展设计 — Terminal-Gui 终端UI](../UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md) | ↗️ 归属 |
|
||||
| `ink/` | 98 | [UI与扩展设计 — Terminal-Gui 终端UI](../UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md) | ↗️ 归属 |
|
||||
| `ink.ts` | 1 | [UI与扩展设计 — Terminal-Gui 终端UI](../UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md) | ↗️ 归属 |
|
||||
| `keybindings/` | 14 | [UI与扩展设计 — Terminal-Gui 终端UI](../UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md) | ↗️ 归属 |
|
||||
| `constants/` | 22 | [总体概述与技术选型](总体概述与技术选型.md) / 各子模块引用 | ↗️ 归属 |
|
||||
| `utils/` | 577 | 各模块与基础设施文档的公共支撑 | ↗️ 归属 |
|
||||
| `types/` | 12 | 各模块与基础设施文档的公共支撑 | ↗️ 归属 |
|
||||
| `remote/` | 4 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `daemon/` | 2 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `proactive/` | 2 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | ↗️ 归属 |
|
||||
| `server/` | 11 | [基础设施设计 — 其他基础设施子系统](reference/解决方案结构说明.md) | ↗️ 归属 |
|
||||
| `vim/` | 5 | [UI与扩展设计 — Terminal-Gui 终端UI](../UI与扩展设计/UI与扩展设计-Terminal-Gui终端UI.md) | ↗️ 归属 |
|
||||
| `memdir/` | 9 | [服务子系统设计 — 会话记忆与上下文](../服务子系统设计/服务子系统设计-会话记忆与上下文.md) | ↗️ 归属 |
|
||||
| `migrations/` | 11 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `bootstrap/` | 1 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `buddy/` | 6 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `cli/` | 21 | [核心模块设计 — CLI启动与解析](../核心模块设计/核心模块设计-CLI启动与解析.md) | ↗️ 归属 |
|
||||
| `query/` | 4 | [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md) | ↗️ 归属 |
|
||||
| `self-hosted-runner/` | 1 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `ssh/` | 1 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `native-ts/` | 4 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `upstreamproxy/` | 2 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `environment-runner/` | 1 | [服务子系统设计 — 其他服务子系统](../服务子系统设计/服务子系统设计-其他服务子系统.md) | 📋 空目录/stub |
|
||||
| `jobs/` | 1 | [基础设施设计 — 后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md) | ↗️ 归属 |
|
||||
| `moreright/` | 1 | [UI与扩展设计 — 插件系统](../UI与扩展设计/UI与扩展设计-插件系统.md) | 📋 空目录/stub |
|
||||
|
||||
### 备注
|
||||
|
||||
- `services/*` 采用“核心子系统 + 其他服务子系统”组合覆盖,避免为每个轻量子目录单独出文档。
|
||||
- `constants/`、`types/`、`utils/` 属于横切支撑层,通常作为被引用内容而非独立模块文档。
|
||||
- `migrations/`、`bootstrap/`、`buddy/`、`daemon/`、`proactive/`、`remote/` 等目录在当前重写文档体系中主要作为其他模块的实现细节归属,不单独展开。
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [.NET 10 平台介绍](reference/.NET-10-平台介绍.md) | AOT、C# 13、核心 API 特性 |
|
||||
| [技术栈映射说明](reference/技术栈映射说明.md) | TS → .NET 完整映射 + NuGet 清单 |
|
||||
| [解决方案结构说明](reference/解决方案结构说明.md) | 17 个项目职责与依赖关系 |
|
||||
| [MCP SDK 实现](reference/mcp-sdk-implement.md) | 自研 MCP SDK 协议与传输层设计 |
|
||||
22
docs/服务子系统设计/reference/原始代码映射-服务子系统.md
Normal file
22
docs/服务子系统设计/reference/原始代码映射-服务子系统.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 服务子系统设计 — 原始代码映射
|
||||
|
||||
## 元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 代码映射
|
||||
- 原始代码来源: `../../../src/services/` 与 `../../../src/utils/memory/`
|
||||
- 关联总览: [服务子系统设计总览](../服务子系统设计.md)
|
||||
|
||||
## 映射表
|
||||
|
||||
| 文档模块 | 原始代码路径 | 说明 |
|
||||
|---|---|---|
|
||||
| 会话记忆与上下文 | `../../../src/utils/memory/` | 会话记忆、梦境摘要、团队同步逻辑 |
|
||||
| 远程会话管理 | `../../../src/services/remote/` | 远程连接、事件流、状态机 |
|
||||
| 语音输入 | `../../../src/services/voice/` | 语音采集、识别与授权门控 |
|
||||
| 通知服务 | `../../../src/services/notification/` | 终端通知与平台探测 |
|
||||
| 限流服务 | `../../../src/services/rate-limit/` | 头部解析、重试策略、退避控制 |
|
||||
| 伙伴系统 | `../../../src/services/companion/` | 确定性角色生成与外观枚举 |
|
||||
|
||||
## 说明
|
||||
- 该映射仅记录服务子系统相关文件夹,不展开到单个实现文件,便于与设计文档保持一一对应。
|
||||
- 所有条目遵循 .NET 命名风格,接口以 `I` 开头,类型使用 PascalCase。
|
||||
170
docs/服务子系统设计/服务子系统设计-会话记忆与上下文.md
Normal file
170
docs/服务子系统设计/服务子系统设计-会话记忆与上下文.md
Normal file
@ -0,0 +1,170 @@
|
||||
# 服务子系统设计 — 会话记忆与上下文
|
||||
|
||||
## 元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 服务子系统设计
|
||||
- 原始代码来源: `../../src/utils/memory/`
|
||||
- 关联总览: [服务子系统设计总览](服务子系统设计.md)
|
||||
- 原始映射: [原始代码映射](reference/原始代码映射-服务子系统.md)
|
||||
- 相关模块: [核心模块设计 — 查询引擎](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- 相关模块: [基础设施设计 — 后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md)
|
||||
- 相关模块: [基础设施设计 — 特性开关](../UI与扩展设计/UI与扩展设计-特性开关系统.md)
|
||||
|
||||
## 1. 设计目标
|
||||
会话记忆与上下文子系统负责把长会话中的关键内容压缩、沉淀、同步,并在需要时为查询引擎提供可控、低成本的上下文补充。该子系统强调阈值触发、轻量模型处理、以及在写入团队知识库前的安全校验。
|
||||
|
||||
## 2. 核心数据结构
|
||||
|
||||
```csharp
|
||||
public enum MemoryType
|
||||
{
|
||||
Session,
|
||||
Dream,
|
||||
Team
|
||||
}
|
||||
|
||||
public sealed record MemoryEntry(
|
||||
string Id,
|
||||
MemoryType Type,
|
||||
string SessionId,
|
||||
string Content,
|
||||
DateTimeOffset CreatedAt,
|
||||
IReadOnlyList<string> Tags);
|
||||
```
|
||||
|
||||
`MemoryType` 用于区分会话记忆、自动梦境摘要与团队共享记忆。`MemoryEntry` 记录统一承载记忆内容、来源会话、创建时间和标签,便于检索与同步。
|
||||
|
||||
## 3. 会话记忆服务
|
||||
|
||||
```csharp
|
||||
public interface ISessionMemoryService
|
||||
{
|
||||
Task<MemoryEntry?> ExtractAsync(
|
||||
string sessionId,
|
||||
int tokenCount,
|
||||
int toolCallCount,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveAsync(MemoryEntry entry, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<MemoryEntry>> GetRecentAsync(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
`ISessionMemoryService` 使用阈值触发提取:当 token 数或 tool call 数超过阈值时,触发一次记忆抽取,生成摘要型 `MemoryEntry`。
|
||||
|
||||
```csharp
|
||||
public sealed class SessionMemoryService : ISessionMemoryService
|
||||
{
|
||||
private readonly int _tokenThreshold = 12000;
|
||||
private readonly int _toolCallThreshold = 24;
|
||||
|
||||
public async Task<MemoryEntry?> ExtractAsync(
|
||||
string sessionId,
|
||||
int tokenCount,
|
||||
int toolCallCount,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var shouldExtract = tokenCount >= _tokenThreshold || toolCallCount >= _toolCallThreshold;
|
||||
if (!shouldExtract)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Task.FromResult(new MemoryEntry(
|
||||
Id: Guid.NewGuid().ToString("N"),
|
||||
Type: MemoryType.Session,
|
||||
SessionId: sessionId,
|
||||
Content: "基于会话上下文生成的压缩记忆。",
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
Tags: new[] { "session", "summary" }));
|
||||
}
|
||||
|
||||
public Task SaveAsync(MemoryEntry entry, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
public Task<IReadOnlyList<MemoryEntry>> GetRecentAsync(string sessionId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<MemoryEntry>>(Array.Empty<MemoryEntry>());
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 自动梦境服务
|
||||
|
||||
```csharp
|
||||
public interface IAutoDreamService
|
||||
{
|
||||
Task<MemoryEntry?> GenerateAsync(
|
||||
string sessionId,
|
||||
TimeSpan idleDuration,
|
||||
int sessionCount,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
`IAutoDreamService` 在两类条件触发:会话空闲超过 24 小时,或会话累计次数达到设定阈值。它用于生成更高层级的长期偏好、主题趋势和待办线索。
|
||||
|
||||
```csharp
|
||||
public sealed class AutoDreamService : IAutoDreamService
|
||||
{
|
||||
private readonly TimeSpan _idleThreshold = TimeSpan.FromHours(24);
|
||||
private readonly int _sessionCountThreshold = 10;
|
||||
|
||||
public async Task<MemoryEntry?> GenerateAsync(
|
||||
string sessionId,
|
||||
TimeSpan idleDuration,
|
||||
int sessionCount,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (idleDuration < _idleThreshold && sessionCount < _sessionCountThreshold)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Task.FromResult(new MemoryEntry(
|
||||
Id: Guid.NewGuid().ToString("N"),
|
||||
Type: MemoryType.Dream,
|
||||
SessionId: sessionId,
|
||||
Content: "基于空闲周期与会话积累生成的自动梦境摘要。",
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
Tags: new[] { "dream", "long-term" }));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 团队记忆同步服务
|
||||
|
||||
```csharp
|
||||
public interface ITeamMemorySyncService
|
||||
{
|
||||
Task PushAsync(MemoryEntry entry, CancellationToken cancellationToken = default);
|
||||
Task<MemoryEntry?> PullAsync(string memoryId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
`ITeamMemorySyncService` 支持推送与拉取团队记忆。推送前必须执行 secret scanning,避免把敏感信息写入共享知识库。
|
||||
|
||||
```csharp
|
||||
public sealed class TeamMemorySyncService : ITeamMemorySyncService
|
||||
{
|
||||
public Task PushAsync(MemoryEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hasSecret = false;
|
||||
if (hasSecret)
|
||||
{
|
||||
throw new InvalidOperationException("检测到敏感信息,禁止推送团队记忆。");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<MemoryEntry?> PullAsync(string memoryId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<MemoryEntry?>(null);
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 设计说明
|
||||
- 记忆相关操作使用轻量模型 `claude-haiku-4-5`,优先保证低延迟与低成本。
|
||||
- 记忆抽取优先走阈值触发,避免每轮上下文都进行重处理。
|
||||
- 团队记忆同步前必须进行 secret scanning,防止把密钥、令牌或隐私数据外发。
|
||||
- 该子系统与查询引擎联动,用于补充局部上下文,而不是替代主对话历史。
|
||||
166
docs/服务子系统设计/服务子系统设计-其他服务子系统.md
Normal file
166
docs/服务子系统设计/服务子系统设计-其他服务子系统.md
Normal file
@ -0,0 +1,166 @@
|
||||
# 服务子系统设计 — 其他服务子系统
|
||||
|
||||
## 元数据
|
||||
- 项目名称: free-code
|
||||
- 文档类型: 服务子系统设计
|
||||
- 原始代码来源: `../../src/services/`
|
||||
- 关联总览: [服务子系统设计总览](服务子系统设计.md)
|
||||
- 原始映射: [原始代码映射](reference/原始代码映射-服务子系统.md)
|
||||
- 相关模块: [基础设施设计 — 后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md)
|
||||
- 相关模块: [UI与扩展设计 — 特性开关系统](../UI与扩展设计/UI与扩展设计-特性开关系统.md)
|
||||
|
||||
## 21.1 远程会话
|
||||
|
||||
```csharp
|
||||
public interface IRemoteSessionManager
|
||||
{
|
||||
Task ConnectAsync(string endpoint, CancellationToken cancellationToken = default);
|
||||
Task DisconnectAsync(CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<RemoteEvent> ReadEventsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public abstract record RemoteEvent(DateTimeOffset Timestamp);
|
||||
public sealed record RemoteConnectedEvent(DateTimeOffset Timestamp, string SessionId) : RemoteEvent(Timestamp);
|
||||
public sealed record RemoteDisconnectedEvent(DateTimeOffset Timestamp, string Reason) : RemoteEvent(Timestamp);
|
||||
public sealed record RemoteMessageEvent(DateTimeOffset Timestamp, string Message) : RemoteEvent(Timestamp);
|
||||
|
||||
public enum RemoteConnectionStatus
|
||||
{
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting,
|
||||
Failed
|
||||
}
|
||||
```
|
||||
|
||||
远程会话管理用于把本地交互扩展到远端主机或托管环境,并通过事件流表达连接状态、消息和断连原因。
|
||||
|
||||
## 21.2 语音输入
|
||||
|
||||
```csharp
|
||||
public interface IVoiceService
|
||||
{
|
||||
Task StartAsync(CancellationToken cancellationToken = default);
|
||||
Task StopAsync(CancellationToken cancellationToken = default);
|
||||
Task<string?> RecognizeAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class VoiceService : IVoiceService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<string?> RecognizeAsync(CancellationToken cancellationToken = default) => Task.FromResult<string?>(null);
|
||||
}
|
||||
```
|
||||
|
||||
语音输入采用特性开关与 OAuth 门控:只有启用对应功能且完成授权后,语音能力才会暴露给会话流程。
|
||||
|
||||
## 21.3 通知服务
|
||||
|
||||
```csharp
|
||||
public interface INotificationService
|
||||
{
|
||||
Task NotifyAsync(string title, string message, CancellationToken cancellationToken = default);
|
||||
bool IsSupportedTerminal();
|
||||
}
|
||||
|
||||
public sealed class NotificationService : INotificationService
|
||||
{
|
||||
public Task NotifyAsync(string title, string message, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public bool IsSupportedTerminal()
|
||||
{
|
||||
var terminal = Environment.GetEnvironmentVariable("TERM_PROGRAM") ?? string.Empty;
|
||||
return terminal is "iTerm2" or "Kitty" or "Ghostty";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通知服务依赖终端类型检测,在 iTerm2、Kitty、Ghostty 等环境中可提供更稳定的本地通知体验。
|
||||
|
||||
## 21.4 限流服务
|
||||
|
||||
```csharp
|
||||
public interface IRateLimitService
|
||||
{
|
||||
bool CanProceed(IDictionary<string, string> headers);
|
||||
TimeSpan? GetRetryAfter(IDictionary<string, string> headers);
|
||||
}
|
||||
|
||||
public sealed class RateLimitService : IRateLimitService
|
||||
{
|
||||
public bool CanProceed(IDictionary<string, string> headers)
|
||||
=> !GetRetryAfter(headers).HasValue;
|
||||
|
||||
public TimeSpan? GetRetryAfter(IDictionary<string, string> headers)
|
||||
{
|
||||
foreach (var pair in headers)
|
||||
{
|
||||
if (pair.Key.StartsWith("anthropic-ratelimit-tokens-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return TimeSpan.FromSeconds(30);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
限流服务通过解析 `anthropic-ratelimit-tokens-*` 头部推断令牌配额状态,并用于动态退避与重试控制。
|
||||
|
||||
## 21.5 伙伴系统
|
||||
|
||||
```csharp
|
||||
public interface ICompanionService
|
||||
{
|
||||
Companion Create(string seed);
|
||||
}
|
||||
|
||||
public sealed class CompanionService : ICompanionService
|
||||
{
|
||||
public Companion Create(string seed)
|
||||
{
|
||||
var prng = new Mulberry32(seed.GetHashCode());
|
||||
return new Companion(
|
||||
Species: Species.Cat,
|
||||
Eye: Eye.Blue,
|
||||
Hat: Hat.Bowler,
|
||||
Rarity: Rarity.Rare,
|
||||
Name: $"Companion-{prng.NextInt(1000)}");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Mulberry32
|
||||
{
|
||||
private uint _state;
|
||||
public Mulberry32(int seed) => _state = (uint)seed;
|
||||
public int NextInt(int max) => (int)(NextUInt() % (uint)max);
|
||||
private uint NextUInt()
|
||||
{
|
||||
_state += 0x6D2B79F5;
|
||||
var t = _state;
|
||||
t = (t ^ (t >> 15)) * (t | 1);
|
||||
t ^= t + (t ^ (t >> 7)) * (t | 61);
|
||||
return t ^ (t >> 14);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Companion(Species Species, Eye Eye, Hat Hat, Rarity Rarity, string Name);
|
||||
|
||||
public enum Species { Cat, Dog, Fox, Owl }
|
||||
public enum Eye { Blue, Green, Gold, Red }
|
||||
public enum Hat { None, Bowler, Cap, Crown }
|
||||
public enum Rarity { Common, Uncommon, Rare, Epic, Legendary }
|
||||
```
|
||||
|
||||
伙伴系统使用确定性生成逻辑:相同种子输入会产生相同角色结果,便于同步、复现和测试。
|
||||
|
||||
## 21.6 交叉引用
|
||||
- [服务子系统设计总览](服务子系统设计.md)
|
||||
- [原始代码映射](reference/原始代码映射-服务子系统.md)
|
||||
- [核心模块设计 — 查询引擎](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [基础设施设计 — 后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md)
|
||||
- [UI与扩展设计 — 特性开关系统](../UI与扩展设计/UI与扩展设计-特性开关系统.md)
|
||||
304
docs/服务子系统设计/服务子系统设计-认证与OAuth.md
Normal file
304
docs/服务子系统设计/服务子系统设计-认证与OAuth.md
Normal file
@ -0,0 +1,304 @@
|
||||
# 服务子系统设计 — 认证与 OAuth
|
||||
|
||||
> **所属项目**: free-code .NET 10 重写
|
||||
> **文档类型**: 子模块设计
|
||||
> **原始源码**: `../../src/services/oauth/`
|
||||
> **配套文档**: [服务子系统设计总览](服务子系统设计.md) | [参考映射](reference/原始代码映射-服务子系统.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
认证模块负责管理用户身份凭证的完整生命周期,涵盖 OAuth 授权流程启动、回调处理、token 交换、安全存储和登出。原始 TypeScript 实现包含两条独立的 OAuth 流程:Anthropic 控制台 OAuth 和 OpenAI Codex OAuth,均通过本地 HTTP 监听器完成浏览器回调拦截。
|
||||
|
||||
.NET 10 重写将认证能力抽象为 `IAuthService` 接口,安全存储单独抽象为 `ISecureTokenStorage`,允许在不同平台(macOS Keychain、Windows DPAPI、Linux Secret Service)下替换存储后端。
|
||||
|
||||
---
|
||||
|
||||
## 19.1 IAuthService 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 认证服务接口 — 管理 OAuth 登录、登出和 token 获取
|
||||
/// 对应原始 ../../src/services/oauth/ 目录的整体能力
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>当前是否已通过任意提供商完成认证</summary>
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
/// <summary>是否为 Claude.ai 用户(通过 claudeai_token 判断)</summary>
|
||||
bool IsClaudeAiUser { get; }
|
||||
|
||||
/// <summary>是否为 Anthropic 内部用户</summary>
|
||||
bool IsInternalUser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动 OAuth 授权流程
|
||||
/// </summary>
|
||||
/// <param name="provider">提供商名称,支持 "anthropic" 和 "codex"</param>
|
||||
Task LoginAsync(string provider = "anthropic");
|
||||
|
||||
/// <summary>清除所有本地存储的 token,退出登录</summary>
|
||||
Task LogoutAsync();
|
||||
|
||||
/// <summary>获取当前有效的 OAuth access token,未登录时返回 null</summary>
|
||||
Task<string?> GetOAuthTokenAsync();
|
||||
|
||||
/// <summary>认证状态发生变化时触发(登录或登出后)</summary>
|
||||
event EventHandler? AuthStateChanged;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19.2 AuthService 实现
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 认证服务实现 — Anthropic OAuth + Codex OAuth 双流程
|
||||
/// 对应原始 ../../src/services/oauth/anthropic.ts 和 openai.ts
|
||||
/// </summary>
|
||||
public sealed class AuthService : IAuthService
|
||||
{
|
||||
private readonly ISecureTokenStorage _tokenStorage;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
|
||||
public bool IsAuthenticated => _tokenStorage.Get("oauth_token") != null;
|
||||
public bool IsClaudeAiUser => _tokenStorage.Get("claudeai_token") != null;
|
||||
public bool IsInternalUser => false; // Anthropic 内部用户标记,外部构建始终为 false
|
||||
|
||||
public event EventHandler? AuthStateChanged;
|
||||
|
||||
public AuthService(ISecureTokenStorage tokenStorage, ILogger<AuthService> logger)
|
||||
{
|
||||
_tokenStorage = tokenStorage;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anthropic OAuth 流程
|
||||
/// 1. 启动本地 HTTP 监听器(拦截浏览器回调)
|
||||
/// 2. 构建授权 URL 并打开浏览器
|
||||
/// 3. 等待回调获取 authorization code
|
||||
/// 4. 用 code 交换 access token + refresh token
|
||||
/// 5. 写入安全存储
|
||||
/// </summary>
|
||||
public async Task LoginAsync(string provider = "anthropic")
|
||||
{
|
||||
if (provider == "anthropic")
|
||||
{
|
||||
// 1. 启动本地 HTTP 监听器(获取 OAuth 回调)
|
||||
var callbackPort = GetAvailablePort();
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add($"http://localhost:{callbackPort}/");
|
||||
listener.Start();
|
||||
|
||||
// 2. 构建 OAuth 授权 URL
|
||||
var authUrl = $"https://console.anthropic.com/oauth/authorize"
|
||||
+ $"?client_id=free-code-cli"
|
||||
+ $"&redirect_uri=http://localhost:{callbackPort}/"
|
||||
+ $"&response_type=code"
|
||||
+ $"&scope=openid profile email";
|
||||
|
||||
// 3. 打开浏览器
|
||||
OpenBrowser(authUrl);
|
||||
|
||||
// 4. 等待回调 → 获取 code
|
||||
var context = await listener.GetContextAsync();
|
||||
var code = context.Request.QueryString["code"];
|
||||
|
||||
// 5. 交换 token
|
||||
using var httpClient = new HttpClient();
|
||||
var tokenResponse = await httpClient.PostAsync(
|
||||
"https://console.anthropic.com/oauth/token",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "authorization_code",
|
||||
["code"] = code ?? "",
|
||||
["redirect_uri"] = $"http://localhost:{callbackPort}/",
|
||||
["client_id"] = "free-code-cli",
|
||||
}));
|
||||
|
||||
var tokens = await tokenResponse.Content.ReadFromJsonAsync<OAuthTokens>();
|
||||
_tokenStorage.Set("oauth_token", tokens?.AccessToken);
|
||||
_tokenStorage.Set("oauth_refresh_token", tokens?.RefreshToken);
|
||||
}
|
||||
else if (provider == "codex")
|
||||
{
|
||||
// OpenAI Codex OAuth — 不同端点,流程相同
|
||||
var authUrl = "https://auth.openai.com/authorize"
|
||||
+ "?client_id=codex-cli"
|
||||
+ "&response_type=code"
|
||||
+ "&scope=openid profile email";
|
||||
// 后续 code exchange 流程与 Anthropic 流程对称
|
||||
}
|
||||
|
||||
AuthStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public Task LogoutAsync()
|
||||
{
|
||||
_tokenStorage.Remove("oauth_token");
|
||||
_tokenStorage.Remove("oauth_refresh_token");
|
||||
_tokenStorage.Remove("claudeai_token");
|
||||
AuthStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string?> GetOAuthTokenAsync() =>
|
||||
Task.FromResult(_tokenStorage.Get("oauth_token"));
|
||||
|
||||
private static int GetAvailablePort()
|
||||
{
|
||||
using var socket = new System.Net.Sockets.TcpListener(
|
||||
System.Net.IPAddress.Loopback, 0);
|
||||
socket.Start();
|
||||
var port = ((System.Net.IPEndPoint)socket.LocalEndpoint).Port;
|
||||
socket.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
private static void OpenBrowser(string url)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
Process.Start("open", url);
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
Process.Start("xdg-open", url);
|
||||
else
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OAuth token 响应的反序列化模型
|
||||
/// </summary>
|
||||
internal sealed record OAuthTokens
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string? AccessToken { get; init; }
|
||||
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string? RefreshToken { get; init; }
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int ExpiresIn { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19.3 ISecureTokenStorage 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 安全 token 存储接口 — 平台无关
|
||||
/// macOS: Keychain / Windows: DPAPI / Linux: Secret Service
|
||||
/// </summary>
|
||||
public interface ISecureTokenStorage
|
||||
{
|
||||
/// <summary>读取指定键的 token,不存在时返回 null</summary>
|
||||
string? Get(string key);
|
||||
|
||||
/// <summary>写入指定键的 token,value 为 null 时等同于 Remove</summary>
|
||||
void Set(string key, string? value);
|
||||
|
||||
/// <summary>删除指定键的 token</summary>
|
||||
void Remove(string key);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19.4 KeychainTokenStorage — macOS Keychain 集成
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// macOS Keychain 安全存储实现
|
||||
/// 对应原始代码中对系统 Keychain 的直接访问
|
||||
/// 通过 security CLI 工具与 macOS Keychain Services 通信
|
||||
/// </summary>
|
||||
public sealed class KeychainTokenStorage : ISecureTokenStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// 读取 Keychain 中的 token
|
||||
/// 等效命令: security find-generic-password -s free-code-{key} -w
|
||||
/// </summary>
|
||||
public string? Get(string key)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "security",
|
||||
Arguments = $"find-generic-password -s free-code-{key} -w",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
using var process = Process.Start(psi);
|
||||
var output = process?.StandardOutput.ReadToEnd().Trim();
|
||||
return string.IsNullOrEmpty(output) ? null : output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入 token 到 Keychain(-U 标志: 不存在则创建,存在则更新)
|
||||
/// 等效命令: security add-generic-password -U -s free-code-{key} -p {value}
|
||||
/// </summary>
|
||||
public void Set(string key, string? value)
|
||||
{
|
||||
if (value == null) { Remove(key); return; }
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "security",
|
||||
Arguments = $"add-generic-password -U -s free-code-{key} -p {value}",
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
Process.Start(psi)?.WaitForExit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Keychain 删除 token
|
||||
/// 等效命令: security delete-generic-password -s free-code-{key}
|
||||
/// </summary>
|
||||
public void Remove(string key)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "security",
|
||||
Arguments = $"delete-generic-password -s free-code-{key}",
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
Process.Start(psi)?.WaitForExit();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计说明
|
||||
|
||||
**本地 HTTP 监听器模式**
|
||||
|
||||
OAuth 授权码流程要求一个回调 URI。命令行工具无法注册自定义 URL Scheme(不同于桌面应用),因此采用本地 HTTP 监听器方案:随机选取可用端口,启动监听后打开浏览器,用户授权后浏览器重定向到 `localhost:{port}/?code=...`,CLI 截获 code 完成交换。这是命令行 OAuth 的通行做法,与 GitHub CLI、Azure CLI 的实现模式一致。
|
||||
|
||||
**Keychain 使用 CLI 而非 P/Invoke**
|
||||
|
||||
原始 TypeScript 代码通过 Node.js 调用 `security` 命令行工具,.NET 重写保持相同策略。使用 `security` CLI 而非直接调用 Keychain Services C API,避免了 macOS 10.15+ 的 hardened runtime 代码签名限制,也无需 entitlements 配置。代价是每次读写有子进程启动开销,但认证操作频率极低,可以接受。
|
||||
|
||||
**跨平台存储抽象**
|
||||
|
||||
`ISecureTokenStorage` 接口的引入使 DI 容器可以按运行平台注入不同实现。注册时可这样处理:
|
||||
|
||||
```csharp
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
services.AddSingleton<ISecureTokenStorage, KeychainTokenStorage>();
|
||||
else
|
||||
services.AddSingleton<ISecureTokenStorage, EncryptedFileTokenStorage>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [服务子系统设计总览](服务子系统设计.md)
|
||||
- [原始代码映射 — 服务子系统](reference/原始代码映射-服务子系统.md)
|
||||
- [核心模块设计 — API 提供商路由](../核心模块设计/核心模块设计-API提供商路由.md)
|
||||
93
docs/服务子系统设计/服务子系统设计.md
Normal file
93
docs/服务子系统设计/服务子系统设计.md
Normal file
@ -0,0 +1,93 @@
|
||||
# 服务子系统设计 — 总览
|
||||
|
||||
> **所属项目**: free-code .NET 10 重写
|
||||
> **文档类型**: 模块索引
|
||||
> **对应源码**: `../../src/services/oauth/`, `../../src/utils/memory/`, `../../src/voice/`
|
||||
> **配套文档**: [总体概述](../总体概述与技术选型/总体概述与技术选型.md) | [核心模块设计](../核心模块设计/核心模块设计.md) | [基础设施设计](../基础设施设计/基础设施设计.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
服务子系统层封装了 free-code 运行时所需的各类横切关注点,包括身份认证、会话记忆、语音输入、通知推送、速率限制跟踪和同伴系统。这一层不直接参与 LLM 查询主路径,而是为核心模块和 UI 层提供基础能力。
|
||||
|
||||
原始 TypeScript 实现中,这些服务分散于 `../../src/services/oauth/`、`../../src/utils/memory/`、`../../src/voice/`、`../../src/buddy/` 等多个目录。.NET 10 重写将其整理为三个职责清晰的文档,每个服务通过接口向上层暴露,内部实现完全封装。
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
服务子系统
|
||||
│
|
||||
├── IAuthService 认证与 OAuth(Anthropic + Codex)
|
||||
│ └── ISecureTokenStorage Keychain / 跨平台安全存储
|
||||
│
|
||||
├── ISessionMemoryService 会话记忆提取(阈值触发)
|
||||
│ └── IAutoDreamService 后台记忆合并(24h / 会话数触发)
|
||||
│ └── ITeamMemorySyncService 团队记忆 Git 同步
|
||||
│
|
||||
└── 其他服务
|
||||
├── IRemoteSessionManager 远程会话管理
|
||||
├── IVoiceService 语音输入(push-to-talk)
|
||||
├── INotificationService 终端通知(iTerm2 / Kitty / Ghostty)
|
||||
├── IRateLimitService API 速率限制跟踪
|
||||
└── ICompanionService 同伴系统(确定性 ASCII 宠物)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 子模块列表
|
||||
|
||||
### [认证与 OAuth](服务子系统设计-认证与OAuth.md)
|
||||
|
||||
覆盖 `IAuthService` 接口、`AuthService` 实现(Anthropic 和 Codex OAuth 流程),以及 `ISecureTokenStorage` 与 macOS Keychain 的集成。
|
||||
|
||||
- 原始源码: `../../src/services/oauth/`
|
||||
- 核心类型: `IAuthService`、`AuthService`、`ISecureTokenStorage`、`KeychainTokenStorage`
|
||||
|
||||
---
|
||||
|
||||
### [会话记忆与上下文](服务子系统设计-会话记忆与上下文.md)
|
||||
|
||||
覆盖 `ISessionMemoryService` 的阈值触发提取机制、`IAutoDreamService` 的后台记忆合并循环,以及 `ITeamMemorySyncService` 的 Git 推拉与秘密扫描流程。
|
||||
|
||||
- 原始源码: `../../src/utils/memory/`
|
||||
- 核心类型: `ISessionMemoryService`、`SessionMemoryService`、`IAutoDreamService`、`AutoDreamService`、`ITeamMemorySyncService`、`TeamMemorySyncService`
|
||||
|
||||
---
|
||||
|
||||
### [其他服务子系统](服务子系统设计-其他服务子系统.md)
|
||||
|
||||
覆盖远程会话、语音输入、终端通知、API 速率限制跟踪和同伴(Buddy)系统的完整接口与实现设计。
|
||||
|
||||
- 原始源码: `../../src/voice/`、`../../src/buddy/`、notification 系统、rate limit 头解析
|
||||
- 核心类型: `IRemoteSessionManager`、`IVoiceService`、`INotificationService`、`IRateLimitService`、`ICompanionService`
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
**接口驱动的安全存储**
|
||||
|
||||
原始 TypeScript 代码直接调用 Keychain 命令行工具,与平台深度耦合。.NET 重写通过 `ISecureTokenStorage` 接口隔离平台细节,macOS 使用 `KeychainTokenStorage`,其他平台可替换为 DPAPI 或 Secret Service 实现,不影响上层逻辑。
|
||||
|
||||
**记忆提取采用轻量模型**
|
||||
|
||||
`SessionMemoryService` 和 `AutoDreamService` 均使用 `claude-haiku-4-5` 执行提取和合并,而非主会话使用的 Opus/Sonnet 模型。这样既控制了成本,又不阻塞主查询路径(记忆操作在后台异步完成)。
|
||||
|
||||
**团队记忆的秘密扫描门控**
|
||||
|
||||
`TeamMemorySyncService.PushAsync` 在提交到 Git 之前强制扫描变更内容,任何检测到 API key 或密码模式的内容都会中止推送并抛出异常,防止意外泄露敏感信息。
|
||||
|
||||
**同伴生成的确定性保证**
|
||||
|
||||
`CompanionService` 使用 `HashString(userId + "friend-2026-401")` 作为种子,通过 Mulberry32 PRNG 生成同伴属性。相同的 userId 永远产生相同的同伴,确保用户在不同设备或重装后看到同一只宠物。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [原始代码映射 — 服务子系统](reference/原始代码映射-服务子系统.md)
|
||||
- [核心模块设计 — 查询引擎](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [基础设施设计 — 后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md)
|
||||
115
docs/核心模块设计/reference/原始代码映射-核心模块.md
Normal file
115
docs/核心模块设计/reference/原始代码映射-核心模块.md
Normal file
@ -0,0 +1,115 @@
|
||||
# 原始代码映射 — 核心模块
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 文档类型: 参考映射表
|
||||
> 上级文档: [核心模块设计总览](../核心模块设计.md)
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
|
||||
本文档建立 .NET 10 类型与原始 TypeScript 源文件之间的映射关系。每一行说明对应 .NET 类型重写了哪个 TS 文件的哪部分逻辑,以及重写时做出的关键设计决策。
|
||||
|
||||
---
|
||||
|
||||
## 核心模块映射表
|
||||
|
||||
### 启动与 CLI 解析
|
||||
|
||||
| .NET 类型 | 原始 TS 文件 | 对应逻辑 | 设计变化 |
|
||||
|-----------|-------------|---------|---------|
|
||||
| `Program` | `../../../src/entrypoints/cli.tsx` | 模块顶层的入口逻辑和条件分支 | 拆分为四阶段,引入 `IHostBuilder` 管理生命周期 |
|
||||
| `QuickPathHandler` | `../../../src/entrypoints/cli.tsx` | 文件顶部散布的 `process.argv.includes()` 快速路径检查 | 集中到静态类,Phase 1 执行,无 DI 开销 |
|
||||
| `CliCommandBuilder` | `../../../src/entrypoints/cli.tsx` | Commander.js 的 `program.option(...).action(...)` 配置 | 映射到 `System.CommandLine` 的 `RootCommand` + `Option<T>` |
|
||||
| `OneShotMode` | `../../../src/entrypoints/cli.tsx` | `-p` 参数触发的非交互式执行路径 | 独立类,与 REPL 模式分离 |
|
||||
| `REPLMode` | `../../../src/screens/REPL.tsx` | React/Ink 交互式 UI 主循环 | 替换为基于 Spectre.Console 或原生终端 API 的 .NET UI |
|
||||
|
||||
---
|
||||
|
||||
### 查询引擎
|
||||
|
||||
| .NET 类型 | 原始 TS 文件 | 对应逻辑 | 设计变化 |
|
||||
|-----------|-------------|---------|---------|
|
||||
| `IQueryEngine` | `../../../src/QueryEngine.ts` | 隐式的模块导出函数签名 | 显式接口,便于测试替换 |
|
||||
| `QueryEngine` | `../../../src/QueryEngine.ts` | 核心消息循环(约 800 行)| 提取 System Prompt 构建为独立类;`IAsyncEnumerable` 替换回调式流 |
|
||||
| `SubmitMessageOptions` | `../../../src/QueryEngine.ts` | `query()` 函数的参数对象 | `record` 类型,不可变,编译期类型安全 |
|
||||
| `SystemPromptBuilder` | `../../../src/QueryEngine.ts` | 内联的 `getSystemPrompt()` 函数(约 200 行)| 提取为独立接口 `IPromptBuilder`,支持 DI 替换 |
|
||||
| `SDKMessage`(联合类型)| `../../../src/types/message.ts` | `SDKMessage` TypeScript 联合类型 | C# discriminated union 通过继承体系实现 |
|
||||
| `TokenUsage` | `../../../src/QueryEngine.ts` | 内联的 token 统计逻辑 | 独立 record,从 API 响应头提取 |
|
||||
|
||||
---
|
||||
|
||||
### 工具系统
|
||||
|
||||
| .NET 类型 | 原始 TS 文件 | 对应逻辑 | 设计变化 |
|
||||
|-----------|-------------|---------|---------|
|
||||
| `ITool` | `../../../src/Tool.ts` | 工具基础接口定义(约 40 行)| 拆分为非泛型 `ITool` 和泛型 `ITool<TInput,TOutput>` 两层 |
|
||||
| `ITool<TInput,TOutput>` | `../../../src/Tool.ts` | 工具执行方法签名 | 泛型化,输入/输出类型在编译期检查 |
|
||||
| `ToolBase<TInput,TOutput>` | `../../../src/Tool.ts` | 工具基类的默认实现 | 抽象类,集成 FluentValidation 验证点 |
|
||||
| `ToolExecutionContext` | `../../../src/Tool.ts` | 工具执行时接收的上下文对象 | `record` 类型,不可变,避免工具意外修改上下文 |
|
||||
| `ToolCategory` | `../../../src/Tool.ts` | 工具分类枚举 | 直接映射,枚举成员一一对应 |
|
||||
| `BashTool` | `../../../src/tools/BashTool.tsx` | Bash 工具完整实现 | `child_process.spawn` → `Process`;`Promise.race` → `.WaitAsync(timeout)` |
|
||||
| `BashToolInput` | `../../../src/tools/BashTool.tsx` | Bash 工具输入类型 | TypeScript 接口 → C# `record`,不可变 |
|
||||
| `BashToolOutput` | `../../../src/tools/BashTool.tsx` | Bash 工具输出类型 | 同上 |
|
||||
| `ToolRegistry` | `../../../src/tools.ts` | `getTools()` 函数和工具数组 | 注册表类,懒加载缓存,稳定排序保证 prompt cache |
|
||||
| `CommandClassifier` | `../../../src/tools/BashTool.tsx` | 命令只读性判断逻辑 | 提取为独立工具类 |
|
||||
|
||||
---
|
||||
|
||||
### 命令系统
|
||||
|
||||
| .NET 类型 | 原始 TS 文件 | 对应逻辑 | 设计变化 |
|
||||
|-----------|-------------|---------|---------|
|
||||
| `ICommand` | `../../../src/commands.ts` | 命令对象的隐式接口约定 | 显式接口,`CommandCategory` 和 `CommandAvailability` 枚举化 |
|
||||
| `CommandContext` | `../../../src/commands.ts` | 命令执行时传入的上下文 | 独立 record,包含会话状态和 UI 接口 |
|
||||
| `CommandResult` | `../../../src/commands.ts` | 命令执行返回值 | `record(bool Success, string? Output)` |
|
||||
| `CommandRegistry` | `../../../src/commands.ts` | `COMMAND_LIST` 数组及导出函数 | 注册表类,按认证状态和 feature flag 动态过滤 |
|
||||
| `CommandCategory` | `../../../src/commands.ts` | `type: 'prompt' \| 'local' \| 'local-jsx'` | C# `enum`,JSX 对话框模式映射为 `LocalDialog` |
|
||||
| `CommandAvailability` | `../../../src/commands.ts` | `availability: string` 字段 | C# `enum`,强类型替换字符串约定 |
|
||||
| 各具体命令类 | `../../../src/commands/` | 各 `*Command.ts` 文件 | 一一对应,每个文件对应一个实现类 |
|
||||
|
||||
---
|
||||
|
||||
### API 多提供商路由
|
||||
|
||||
| .NET 类型 | 原始 TS 文件 | 对应逻辑 | 设计变化 |
|
||||
|-----------|-------------|---------|---------|
|
||||
| `IApiProvider` | `../../../src/services/api/` | 各提供商模块的导出函数签名 | 统一抽象接口,所有提供商实现同一接口 |
|
||||
| `ApiProviderRouter` | `../../../src/services/api/` | `process.env` 条件判断选择客户端 | 路由器类,构造时确定提供商,运行时不再判断 |
|
||||
| `ApiProviderType` | `../../../src/services/api/` | 字符串常量区分提供商 | C# `enum`,编译期安全 |
|
||||
| `ApiRequest` | `../../../src/services/api/` | API 调用参数对象 | `record`,不可变 |
|
||||
| `AnthropicProvider` | `../../../src/services/api/claude.ts` | Anthropic SDK 流式调用(约 2000 行)| 移除 SDK 依赖,直接 `HttpClient` + 手动 SSE 解析 |
|
||||
| `CodexProvider` | `../../../src/services/api/` | OpenAI Codex 客户端 | 对应原始 Codex fetch adapter |
|
||||
| `BedrockProvider` | `../../../src/services/api/` | AWS Bedrock 客户端 | 使用 AWS SDK for .NET |
|
||||
| `VertexProvider` | `../../../src/services/api/` | Google Vertex 客户端 | 使用 Google Cloud SDK for .NET |
|
||||
| `FoundryProvider` | `../../../src/services/api/` | Anthropic Foundry 客户端 | 基于 `AnthropicProvider` 适配自定义端点 |
|
||||
|
||||
---
|
||||
|
||||
## 未直接映射的原始文件
|
||||
|
||||
以下 `../../../src/` 文件的逻辑分散到了基础设施设计或服务子系统文档中:
|
||||
|
||||
| 原始文件 | 重写位置 |
|
||||
|----------|---------|
|
||||
| `../../../src/state/` | [基础设施设计 — 状态管理](../../基础设施设计/基础设施设计-状态管理.md) |
|
||||
| `../../../src/services/mcp*` | [基础设施设计 — MCP 协议集成](../../基础设施设计/基础设施设计-MCP协议集成.md) |
|
||||
| `../../../src/services/oauth/` | [服务子系统设计 — 认证](../../服务子系统设计/) |
|
||||
| `../../../src/skills/` | [服务子系统设计 — 技能系统](../../服务子系统设计/) |
|
||||
| `../../../src/plugins/` | [服务子系统设计 — 插件系统](../../服务子系统设计/) |
|
||||
| `../../../src/bridge/` | [服务子系统设计 — IDE 桥接](../../服务子系统设计/) |
|
||||
| `../../../src/tasks/` | [服务子系统设计 — 后台任务](../../服务子系统设计/) |
|
||||
| `../../../src/voice/` | [服务子系统设计 — 语音输入](../../服务子系统设计/) |
|
||||
| `../../../src/components/` | [UI 与扩展设计](../../UI与扩展设计/) |
|
||||
| `../../../src/hooks/` | [UI 与扩展设计](../../UI与扩展设计/) |
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](../核心模块设计.md)
|
||||
- [CLI 启动与解析](../核心模块设计-CLI启动与解析.md)
|
||||
- [查询引擎 (QueryEngine)](../核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [工具系统](../核心模块设计-工具系统.md)
|
||||
- [命令系统](../核心模块设计-命令系统.md)
|
||||
- [API 多提供商路由](../核心模块设计-API提供商路由.md)
|
||||
200
docs/核心模块设计/核心模块设计-API提供商路由.md
Normal file
200
docs/核心模块设计/核心模块设计-API提供商路由.md
Normal file
@ -0,0 +1,200 @@
|
||||
# 核心模块设计 — API 多提供商路由
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 原始代码来源: `../../src/services/api/`
|
||||
> 原始设计意图: 支持五种 API 提供商(Anthropic、OpenAI Codex、AWS Bedrock、Google Vertex、Anthropic Foundry)的统一路由,通过环境变量自动检测活跃提供商,并为每个提供商实现 SSE 流式解析
|
||||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
API 多提供商路由模块是 free-code 多后端能力的核心。它将具体 API 调用细节从 `QueryEngine` 中解耦出来,使查询引擎无需关心当前使用的是哪家服务商的 API。
|
||||
|
||||
原始 TypeScript 实现通过 `../../src/services/api/` 目录下的多个文件分别实现各提供商的客户端。.NET 重写引入 `IApiProvider` 接口统一抽象,通过 `ApiProviderRouter` 在运行时选择具体实现。
|
||||
|
||||
---
|
||||
|
||||
## 9.1 提供商枚举
|
||||
|
||||
```csharp
|
||||
public enum ApiProviderType
|
||||
{
|
||||
Anthropic, // 默认,直连 Anthropic API
|
||||
OpenAICodex, // CLAUDE_CODE_USE_OPENAI=1
|
||||
AwsBedrock, // CLAUDE_CODE_USE_BEDROCK=1
|
||||
GoogleVertex, // CLAUDE_CODE_USE_VERTEX=1
|
||||
AnthropicFoundry // CLAUDE_CODE_USE_FOUNDRY=1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9.2 IApiProvider 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>API提供商抽象接口</summary>
|
||||
public interface IApiProvider
|
||||
{
|
||||
/// <summary>发起流式 API 请求,返回 SSE 消息流</summary>
|
||||
IAsyncEnumerable<SDKMessage> StreamAsync(
|
||||
ApiRequest request,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public record ApiRequest(
|
||||
string SystemPrompt,
|
||||
IReadOnlyList<ApiMessage> Messages,
|
||||
IReadOnlyList<ITool> Tools,
|
||||
string? Model = null
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9.3 ApiProviderRouter — 路由器
|
||||
|
||||
`ApiProviderRouter` 在构造时检测环境变量,确定活跃提供商,后续每次调用 `GetActiveProvider()` 都返回对应的实例。
|
||||
|
||||
**原始设计意图:** 原始代码通过 `process.env` 检查选择不同的 API 客户端模块。.NET 版本将这一逻辑集中在路由器的构造函数中,避免散布在代码各处的条件判断。
|
||||
|
||||
```csharp
|
||||
public class ApiProviderRouter : IApiProviderRouter
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private ApiProviderType _activeProvider;
|
||||
|
||||
public ApiProviderRouter(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
_activeProvider = DetectProvider(); // 从环境变量自动检测
|
||||
}
|
||||
|
||||
private static ApiProviderType DetectProvider()
|
||||
{
|
||||
if (Env("CLAUDE_CODE_USE_OPENAI") == "1") return ApiProviderType.OpenAICodex;
|
||||
if (Env("CLAUDE_CODE_USE_BEDROCK") == "1") return ApiProviderType.AwsBedrock;
|
||||
if (Env("CLAUDE_CODE_USE_VERTEX") == "1") return ApiProviderType.GoogleVertex;
|
||||
if (Env("CLAUDE_CODE_USE_FOUNDRY") == "1") return ApiProviderType.AnthropicFoundry;
|
||||
return ApiProviderType.Anthropic;
|
||||
}
|
||||
|
||||
public IApiProvider GetActiveProvider() => _activeProvider switch
|
||||
{
|
||||
ApiProviderType.Anthropic => _services.GetRequiredService<AnthropicProvider>(),
|
||||
ApiProviderType.OpenAICodex => _services.GetRequiredService<CodexProvider>(),
|
||||
ApiProviderType.AwsBedrock => _services.GetRequiredService<BedrockProvider>(),
|
||||
ApiProviderType.GoogleVertex => _services.GetRequiredService<VertexProvider>(),
|
||||
ApiProviderType.AnthropicFoundry => _services.GetRequiredService<FoundryProvider>(),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
private static string? Env(string name) => Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
```
|
||||
|
||||
检测优先级为:OpenAI > Bedrock > Vertex > Foundry > Anthropic(默认)。若同时设置多个环境变量,优先级靠前的生效。
|
||||
|
||||
---
|
||||
|
||||
## 9.4 AnthropicProvider — SSE 流式解析
|
||||
|
||||
`AnthropicProvider` 实现直连 Anthropic Messages API 的 SSE 流式解析,是五个提供商中最核心的实现。
|
||||
|
||||
**原始设计意图:** 原始 `claude.ts` 使用 Anthropic TypeScript SDK 处理流式响应。.NET 版本直接使用 `HttpClient` 发送 HTTP 请求,手动解析 SSE 事件行,避免引入额外 SDK 依赖。
|
||||
|
||||
```csharp
|
||||
public class AnthropicProvider : IApiProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> StreamAsync(
|
||||
ApiRequest request, [EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var payload = new {
|
||||
model = request.Model ?? "claude-sonnet-4-6",
|
||||
max_tokens = 16384,
|
||||
stream = true,
|
||||
system = request.SystemPrompt,
|
||||
messages = request.Messages,
|
||||
tools = request.Tools
|
||||
};
|
||||
|
||||
var httpReq = new HttpRequestMessage(HttpMethod.Post,
|
||||
$"{GetBaseUrl()}/v1/messages")
|
||||
{
|
||||
Content = JsonContent.Create(payload)
|
||||
};
|
||||
httpReq.Headers.Add("x-api-key", GetApiKey());
|
||||
httpReq.Headers.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
using var response = await _httpClient.SendAsync(
|
||||
httpReq, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
// SSE 流式解析
|
||||
using var stream = await response.Content.ReadAsStreamAsync(ct);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
while (!reader.EndOfStream && !ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct);
|
||||
if (string.IsNullOrEmpty(line) || !line.StartsWith("data: ")) continue;
|
||||
|
||||
var json = line[6..];
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var type = doc.RootElement.GetProperty("type").GetString();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "content_block_delta":
|
||||
yield return new SDKMessage.StreamingDelta(
|
||||
doc.RootElement.GetProperty("delta")
|
||||
.GetProperty("text").GetString()!);
|
||||
break;
|
||||
case "content_block_start":
|
||||
var block = doc.RootElement.GetProperty("content_block");
|
||||
if (block.GetProperty("type").GetString() == "tool_use")
|
||||
yield return new SDKMessage.ToolUseStart(
|
||||
block.GetProperty("id").GetString()!,
|
||||
block.GetProperty("name").GetString()!,
|
||||
block.GetProperty("input"));
|
||||
break;
|
||||
case "message_stop":
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSE 事件类型映射
|
||||
|
||||
| SSE `type` | 映射到 `SDKMessage` 子类 | 说明 |
|
||||
|------------|--------------------------|------|
|
||||
| `content_block_delta` | `SDKMessage.StreamingDelta` | 文本流式增量 |
|
||||
| `content_block_start` (tool_use) | `SDKMessage.ToolUseStart` | 工具调用开始 |
|
||||
| `message_stop` | `yield break` | 流结束 |
|
||||
| 其他 | 忽略 | `ping`、`message_start` 等元数据事件 |
|
||||
|
||||
`HttpCompletionOption.ResponseHeadersRead` 确保在响应头返回后立即开始读取流,而不是等待整个响应体下载完成,这是 SSE 流式解析的关键设置。
|
||||
|
||||
---
|
||||
|
||||
## 9.5 提供商配置汇总
|
||||
|
||||
| 提供商 | 环境变量 | 认证方式 | 端点 |
|
||||
|--------|----------|----------|------|
|
||||
| Anthropic | 默认 | `ANTHROPIC_API_KEY` 或 OAuth | `https://api.anthropic.com` |
|
||||
| OpenAI Codex | `CLAUDE_CODE_USE_OPENAI=1` | OAuth via OpenAI | `https://api.openai.com` |
|
||||
| AWS Bedrock | `CLAUDE_CODE_USE_BEDROCK=1` | AWS 标准凭证链 | `AWS_REGION` 对应端点 |
|
||||
| Google Vertex | `CLAUDE_CODE_USE_VERTEX=1` | GCP ADC | GCP 项目对应端点 |
|
||||
| Anthropic Foundry | `CLAUDE_CODE_USE_FOUNDRY=1` | `ANTHROPIC_FOUNDRY_API_KEY` | 自定义部署端点 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](核心模块设计.md)
|
||||
- [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||||
245
docs/核心模块设计/核心模块设计-CLI启动与解析.md
Normal file
245
docs/核心模块设计/核心模块设计-CLI启动与解析.md
Normal file
@ -0,0 +1,245 @@
|
||||
# 核心模块设计 — CLI 启动与解析
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 原始代码来源: `../../src/entrypoints/cli.tsx`, `../../src/screens/REPL.tsx`
|
||||
> 原始设计意图: 解析命令行参数,区分交互式 REPL 模式和一次性 prompt 模式,并处理 daemon/bridge 等特殊运行模式
|
||||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||||
> 交叉参考: [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
CLI 启动模块是整个应用的入口,对应原始 TypeScript 项目的 `cli.tsx`。它的核心职责是在加载任何业务逻辑之前,先判断这次调用属于哪种运行模式,并将控制权交给对应的处理器。
|
||||
|
||||
.NET 重写将原始代码中混合在一个文件里的逻辑拆分为三个独立的类:
|
||||
|
||||
- `Program` — 四阶段启动协调器
|
||||
- `QuickPathHandler` — 无需 DI 容器的快速路径处理
|
||||
- `CliCommandBuilder` — 基于 `System.CommandLine` 的命令行定义
|
||||
|
||||
---
|
||||
|
||||
## 5.1 入口点 Program.cs
|
||||
|
||||
`Program.cs` 组织为四个顺序执行的阶段,每一阶段都有明确的失败边界。
|
||||
|
||||
**原始设计意图:** 原始 `cli.tsx` 在模块顶层直接调用 Commander.js 解析参数,然后根据 flag 条件决定走哪条路径。.NET 版本将这一隐式流程显式化为四个阶段,并统一使用 `Microsoft.Extensions.Hosting` 管理生命周期。
|
||||
|
||||
```csharp
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Phase 1: 快速路径(不加载DI容器,零开销)
|
||||
if (QuickPathHandler.TryHandle(args, out var exitCode))
|
||||
return exitCode;
|
||||
|
||||
// Phase 2: 构建Host
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(cfg => cfg
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddJsonFile(Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".free-code", "settings.json"), optional: true, reloadOnChange: true))
|
||||
.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
services.AddCoreServices();
|
||||
services.AddEngine();
|
||||
services.AddTools();
|
||||
services.AddCommands();
|
||||
services.AddApiProviders();
|
||||
services.AddMcp();
|
||||
services.AddLsp();
|
||||
services.AddBridge();
|
||||
services.AddBusinessServices();
|
||||
services.AddTasks();
|
||||
services.AddSkills();
|
||||
services.AddPlugins();
|
||||
services.AddFeatures();
|
||||
services.AddState();
|
||||
services.AddTerminalUI();
|
||||
})
|
||||
.ConfigureLogging(logging => logging
|
||||
.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Information))
|
||||
.Build();
|
||||
|
||||
// Phase 3: 初始化核心服务
|
||||
await host.Services.GetRequiredService<IAppInitializer>().InitializeAsync();
|
||||
|
||||
// Phase 4: 启动REPL或执行一次性命令
|
||||
var runner = host.Services.GetRequiredService<IAppRunner>();
|
||||
return await runner.RunAsync(args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 四阶段说明
|
||||
|
||||
| 阶段 | 职责 | 失败处理 |
|
||||
|------|------|----------|
|
||||
| Phase 1 | 快速路径检测,无 DI 开销 | 匹配则直接返回,不进入后续阶段 |
|
||||
| Phase 2 | 构建 Host,注册所有服务 | `HostBuilder` 抛出则进程退出 |
|
||||
| Phase 3 | 异步初始化(OAuth 刷新、MCP 连接等) | `InitializeAsync` 可抛出 `AppInitializationException` |
|
||||
| Phase 4 | 启动 REPL 或执行一次性 prompt | `RunAsync` 返回退出码 |
|
||||
|
||||
`reloadOnChange: true` 设置在 `~/.free-code/settings.json` 上,允许用户在 REPL 运行期间修改配置后立即生效,无需重启。
|
||||
|
||||
---
|
||||
|
||||
## 5.2 快速路径处理器
|
||||
|
||||
`QuickPathHandler` 对应原始 `cli.tsx` 中散布在文件顶部的 fast-path 条件判断。
|
||||
|
||||
**原始设计意图:** 原始代码在 Commander.js 解析之前,用一系列 `process.argv.includes()` 检查来处理特殊 flag,避免触发 React/Ink 的初始化开销。.NET 版本将这些检查集中到一个静态类,并且同样在 DI 容器构建之前执行。
|
||||
|
||||
```csharp
|
||||
public static class QuickPathHandler
|
||||
{
|
||||
public static bool TryHandle(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = 0;
|
||||
|
||||
// --version
|
||||
if (args.Contains("--version"))
|
||||
{
|
||||
var ver = Assembly.GetEntryAssembly()!
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
|
||||
.InformationalVersion;
|
||||
Console.WriteLine($"free-code {ver}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// --dump-system-prompt
|
||||
if (args.Contains("--dump-system-prompt"))
|
||||
{
|
||||
var builder = new SystemPromptBuilder(/* minimal config */);
|
||||
Console.WriteLine(builder.BuildDefaultPrompt());
|
||||
return true;
|
||||
}
|
||||
|
||||
// MCP daemon mode
|
||||
if (args.Contains("--mcp-daemon"))
|
||||
{
|
||||
exitCode = McpDaemon.Run(args).GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bridge mode
|
||||
if (args.Contains("--bridge"))
|
||||
{
|
||||
exitCode = BridgeMain.Run(args).GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Background session mode
|
||||
if (args.Contains("--background-session"))
|
||||
{
|
||||
exitCode = BackgroundSessionRunner.Run(args).GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Template mode
|
||||
if (args.Contains("--template"))
|
||||
{
|
||||
exitCode = TemplateRunner.Run(args).GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 快速路径 flag 列表
|
||||
|
||||
| Flag | 处理器 | 说明 |
|
||||
|------|--------|------|
|
||||
| `--version` | 直接打印 | 从程序集元数据读取版本号 |
|
||||
| `--dump-system-prompt` | `SystemPromptBuilder` | 打印默认 System Prompt,用于调试 |
|
||||
| `--mcp-daemon` | `McpDaemon.Run` | 以 MCP 服务器模式运行,供 IDE 插件调用 |
|
||||
| `--bridge` | `BridgeMain.Run` | IDE 远程控制桥接模式 |
|
||||
| `--background-session` | `BackgroundSessionRunner.Run` | 后台会话模式,供 Agent 工具启动子进程 |
|
||||
| `--template` | `TemplateRunner.Run` | 模板执行模式 |
|
||||
|
||||
注意 `--mcp-daemon` 和 `--bridge` 的处理器使用 `.GetAwaiter().GetResult()` 同步等待,因为此时还没有 `async` 上下文可用(`Main` 的 `await` 路径已被 Phase 1 短路)。
|
||||
|
||||
---
|
||||
|
||||
## 5.3 CLI 命令定义 (CliCommandBuilder)
|
||||
|
||||
`CliCommandBuilder` 对应原始 `cli.tsx` 中的 Commander.js 配置,负责定义全局选项和根命令的处理逻辑。
|
||||
|
||||
**原始设计意图:** Commander.js 的 `program.option(...).action(...)` 链式 API 被映射到 `System.CommandLine` 的 `RootCommand` + `Option<T>` 模式。两者在概念上完全对应,只是 API 风格不同。
|
||||
|
||||
```csharp
|
||||
public class CliCommandBuilder
|
||||
{
|
||||
public RootCommand Build()
|
||||
{
|
||||
var root = new RootCommand("free-code - The free build of Claude Code");
|
||||
|
||||
// 全局选项
|
||||
var modelOption = new Option<string?>("--model", "Override default model");
|
||||
var verboseOption = new Option<bool>("--verbose", "Verbose output");
|
||||
var resumeOption = new Option<string?>("--resume", "Resume session ID");
|
||||
root.AddGlobalOption(modelOption);
|
||||
root.AddGlobalOption(verboseOption);
|
||||
root.AddGlobalOption(resumeOption);
|
||||
|
||||
// 一次性 prompt 模式 (-p)
|
||||
var promptOption = new Option<string?>("-p", "One-shot prompt (non-interactive)");
|
||||
root.AddOption(promptOption);
|
||||
|
||||
root.SetHandler(async (string? prompt, string? model, bool verbose, string? resume) =>
|
||||
{
|
||||
if (prompt != null)
|
||||
return await OneShotMode.ExecuteAsync(prompt, model);
|
||||
return await REPLMode.StartAsync(model, verbose, resume);
|
||||
}, promptOption, modelOption, verboseOption, resumeOption);
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 选项说明
|
||||
|
||||
| 选项 | 类型 | 作用域 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--model` | `string?` | 全局 | 覆盖配置文件中的默认模型 |
|
||||
| `--verbose` | `bool` | 全局 | 开启详细日志输出 |
|
||||
| `--resume` | `string?` | 全局 | 恢复指定 session ID 的对话历史 |
|
||||
| `-p` | `string?` | 根命令 | 非交互式一次性 prompt 模式 |
|
||||
|
||||
`--model`、`--verbose`、`--resume` 作为全局选项注册,子命令也可以访问。`-p` 仅在根命令级别有效,因为子命令有各自的参数结构。
|
||||
|
||||
当 `-p` 存在时,走 `OneShotMode.ExecuteAsync` 路径(非交互式,执行完毕后退出)。否则走 `REPLMode.StartAsync` 启动交互式终端 UI。
|
||||
|
||||
---
|
||||
|
||||
## 模块间依赖
|
||||
|
||||
```
|
||||
Program.cs
|
||||
├── QuickPathHandler (静态,无依赖)
|
||||
│ ├── SystemPromptBuilder (仅 --dump-system-prompt 路径)
|
||||
│ ├── McpDaemon
|
||||
│ ├── BridgeMain
|
||||
│ ├── BackgroundSessionRunner
|
||||
│ └── TemplateRunner
|
||||
│
|
||||
└── CliCommandBuilder (由 IAppRunner 使用)
|
||||
├── OneShotMode
|
||||
└── REPLMode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](核心模块设计.md)
|
||||
- [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
191
docs/核心模块设计/核心模块设计-命令系统.md
Normal file
191
docs/核心模块设计/核心模块设计-命令系统.md
Normal file
@ -0,0 +1,191 @@
|
||||
# 核心模块设计 — 命令系统
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 原始代码来源: `../../src/commands.ts`, `../../src/commands/`
|
||||
> 原始设计意图: 定义并注册所有斜杠命令(/command),提供会话管理、配置变更、认证操作等用户可交互的命令集合
|
||||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
命令系统管理用户在 REPL 中输入的斜杠命令(如 `/clear`、`/model`、`/help`)。它与工具系统平行,但面向的是用户直接操作而非 Agent 的自主调用。
|
||||
|
||||
原始 TypeScript 实现在 `commands.ts` 中以数组形式注册约 80 个命令对象,每个对象实现固定的接口字段。.NET 重写引入 `ICommand` 接口和 `CommandRegistry` 注册表,通过 DI 容器管理命令生命周期,并支持基于认证状态和 feature flag 的动态可用性控制。
|
||||
|
||||
---
|
||||
|
||||
## 8.1 ICommand 接口
|
||||
|
||||
```csharp
|
||||
public interface ICommand
|
||||
{
|
||||
string Name { get; }
|
||||
string[]? Aliases { get; }
|
||||
string Description { get; }
|
||||
CommandCategory Category { get; } // Prompt, Local, LocalDialog
|
||||
CommandAvailability Availability { get; } // Always, RequiresAuth, ClaudeAiOnly, InternalOnly
|
||||
bool IsEnabled();
|
||||
Task<CommandResult> ExecuteAsync(
|
||||
CommandContext context, string? args = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public enum CommandCategory { Prompt, Local, LocalDialog }
|
||||
public enum CommandAvailability { Always, RequiresAuth, ClaudeAiOnly, InternalOnly }
|
||||
public record CommandResult(bool Success, string? Output = null);
|
||||
```
|
||||
|
||||
### 命令分类说明
|
||||
|
||||
`CommandCategory` 决定命令的执行方式:
|
||||
|
||||
| 类别 | 说明 |
|
||||
|------|------|
|
||||
| `Prompt` | 命令内容作为 prompt 发送给 LLM(如 `/commit` 生成提交信息) |
|
||||
| `Local` | 在本地直接执行,不经过 LLM(如 `/clear` 清空历史) |
|
||||
| `LocalDialog` | 在本地执行并打开对话框(如 `/config` 弹出配置界面) |
|
||||
|
||||
`CommandAvailability` 控制命令的可见性:
|
||||
|
||||
| 可用性 | 显示条件 |
|
||||
|--------|---------|
|
||||
| `Always` | 始终可用 |
|
||||
| `RequiresAuth` | 需要已登录任意账号 |
|
||||
| `ClaudeAiOnly` | 仅 Claude.ai 账号用户可见 |
|
||||
| `InternalOnly` | 仅 Anthropic 内部用户可见 |
|
||||
|
||||
---
|
||||
|
||||
## 8.2 CommandRegistry 命令注册表
|
||||
|
||||
`CommandRegistry` 完整注册 70+ 个命令,并提供基于认证状态的过滤能力。
|
||||
|
||||
```csharp
|
||||
public class CommandRegistry : ICommandRegistry
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IFeatureFlagService _features;
|
||||
private readonly IAuthService _authService;
|
||||
private List<ICommand>? _cachedCommands;
|
||||
|
||||
public async Task<IReadOnlyList<ICommand>> GetCommandsAsync()
|
||||
{
|
||||
if (_cachedCommands != null) return _cachedCommands;
|
||||
_cachedCommands = new List<ICommand>();
|
||||
|
||||
// === 核心命令 (~65个) ===
|
||||
// 会话管理
|
||||
Add<SessionCommand>(); Add<ResumeCommand>(); Add<RenameCommand>();
|
||||
Add<ClearCommand>(); Add<ExportCommand>();
|
||||
// 配置
|
||||
Add<ConfigCommand>(); Add<ModelCommand>(); Add<ThemeCommand>();
|
||||
Add<ColorCommand>(); Add<OutputStyleCommand>(); Add<KeybindingsCommand>();
|
||||
// 认证
|
||||
Add<LoginCommand>(); Add<LogoutCommand>();
|
||||
// 状态与统计
|
||||
Add<StatusCommand>(); Add<CostCommand>(); Add<StatsCommand>(); Add<ExtraUsageCommand>();
|
||||
// 工具与诊断
|
||||
Add<DiffCommand>(); Add<CopyCommand>(); Add<DoctorCommand>();
|
||||
Add<MemoryCommand>(); Add<AgentsCommand>();
|
||||
// Git 集成
|
||||
Add<CommitCommand>(); Add<CommitPushPrCommand>(); Add<BranchCommand>();
|
||||
// MCP / LSP / Hooks
|
||||
Add<McpCommand>(); Add<HooksCommand>(); Add<FilesCommand>();
|
||||
// 扩展系统
|
||||
Add<SkillsCommand>(); Add<PluginCommand>();
|
||||
// 杂项
|
||||
Add<HelpCommand>(); Add<ExitCommand>(); Add<VersionCommand>();
|
||||
Add<AddDirCommand>(); Add<VimCommand>(); Add<FastCommand>();
|
||||
Add<UpgradeCommand>(); Add<FeedbackCommand>(); Add<TagCommand>();
|
||||
Add<CompactCommand>(); Add<ContextCommand>();
|
||||
Add<PermissionsCommand>(); Add<EffortCommand>();
|
||||
Add<TasksCommand>(); Add<TeleportCommand>(); Add<RewindCommand>();
|
||||
Add<TerminalSetupCommand>(); Add<DesktopCommand>(); Add<MobileCommand>();
|
||||
Add<ChromeCommand>(); Add<PrivacySettingsCommand>();
|
||||
Add<AssistantCommand>(); Add<SandboxToggleCommand>();
|
||||
Add<RemoteEnvCommand>(); Add<RateLimitOptionsCommand>();
|
||||
Add<PassesCommand>(); Add<BreakCacheCommand>(); Add<SummaryCommand>();
|
||||
Add<ShareCommand>(); Add<ReleaseNotesCommand>(); Add<StatusLineCommand>();
|
||||
Add<PrCommentsCommand>(); Add<ReviewCommand>(); Add<UltrareviewCommand>();
|
||||
Add<InstallGitHubAppCommand>(); Add<InstallSlackAppCommand>();
|
||||
|
||||
// === 条件命令 ===
|
||||
if (_features.IsEnabled(FeatureFlags.Buddy))
|
||||
Add<BtwCommand>();
|
||||
if (_features.IsEnabled(FeatureFlags.Ultraplan))
|
||||
Add<UltraplanCommand>();
|
||||
Add<ThinkbackCommand>(); Add<ThinkbackPlayCommand>();
|
||||
|
||||
// === 内部命令 (~15个) ===
|
||||
Add<HeapdumpCommand>(); Add<MockLimitsCommand>();
|
||||
Add<BridgeKickCommand>(); Add<AntTraceCommand>();
|
||||
Add<PerfIssueCommand>(); Add<DebugToolCallCommand>();
|
||||
Add<OnboardingCommand>(); Add<BughunterCommand>();
|
||||
Add<GoodClaudeCommand>(); Add<IssueCommand>();
|
||||
Add<AdvisorCommand>(); Add<InsightsCommand>();
|
||||
Add<ResetLimitsCommand>(); Add<CtxVizCommand>();
|
||||
Add<OauthRefreshCommand>();
|
||||
|
||||
return _cachedCommands;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ICommand>> GetEnabledCommandsAsync()
|
||||
{
|
||||
var all = await GetCommandsAsync();
|
||||
return all.Where(c => c.IsEnabled() && MeetsAvailability(c)).ToList();
|
||||
}
|
||||
|
||||
private bool MeetsAvailability(ICommand cmd) => cmd.Availability switch
|
||||
{
|
||||
CommandAvailability.Always => true,
|
||||
CommandAvailability.RequiresAuth => _authService.IsAuthenticated,
|
||||
CommandAvailability.ClaudeAiOnly => _authService.IsClaudeAiUser,
|
||||
CommandAvailability.InternalOnly => _authService.IsInternalUser,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private void Add<T>() where T : ICommand =>
|
||||
_cachedCommands!.Add(_services.GetRequiredService<T>());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8.3 命令分组统计
|
||||
|
||||
| 分组 | 命令数量 | 说明 |
|
||||
|------|----------|------|
|
||||
| 会话管理 | 5 | `session`、`resume`、`rename`、`clear`、`export` |
|
||||
| 配置 | 6 | `config`、`model`、`theme`、`color`、`output-style`、`keybindings` |
|
||||
| 认证 | 2 | `login`、`logout` |
|
||||
| 状态与统计 | 4 | `status`、`cost`、`stats`、`extra-usage` |
|
||||
| 工具与诊断 | 5 | `diff`、`copy`、`doctor`、`memory`、`agents` |
|
||||
| Git 集成 | 3 | `commit`、`commit-push-pr`、`branch` |
|
||||
| MCP/LSP/Hooks | 3 | `mcp`、`hooks`、`files` |
|
||||
| 扩展系统 | 2 | `skills`、`plugin` |
|
||||
| 杂项(核心) | ~30 | `help`、`exit`、`version`、`add-dir` 等 |
|
||||
| 条件命令 | 2-4 | `btw`(BUDDY)、`ultraplan`(ULTRAPLAN)、`thinkback` |
|
||||
| 内部命令 | 15 | `heapdump`、`mock-limits`、`bughunter` 等 |
|
||||
|
||||
---
|
||||
|
||||
## 8.4 与工具系统的区别
|
||||
|
||||
命令系统和工具系统都是 free-code 的扩展点,但职责不同:
|
||||
|
||||
| 维度 | 命令系统 | 工具系统 |
|
||||
|------|----------|----------|
|
||||
| 触发方 | 用户(输入 `/command`) | Agent 自主决策 |
|
||||
| 输入格式 | 斜杠 + 可选文本参数 | 结构化 JSON(JSON Schema 验证) |
|
||||
| 出现在 System Prompt | 是(命令描述段) | 是(工具描述段) |
|
||||
| 权限控制粒度 | `CommandAvailability` 枚举 | `PermissionEngine` + `ToolPermissionContext` |
|
||||
| 执行上下文 | `CommandContext` | `ToolExecutionContext` |
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](核心模块设计.md)
|
||||
- [工具系统](核心模块设计-工具系统.md)
|
||||
- [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||||
301
docs/核心模块设计/核心模块设计-多代理协调.md
Normal file
301
docs/核心模块设计/核心模块设计-多代理协调.md
Normal file
@ -0,0 +1,301 @@
|
||||
# 核心模块设计 — 多代理协调器 (Coordinator)
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 文档类型: 核心模块设计
|
||||
> 原始代码来源: `../../src/coordinator/`、`../../src/assistant/`
|
||||
> 原始设计意图: 将单轮 LLM 交互扩展为多代理编排,负责 worker 生成、消息路由、团队管理与协调模式切换
|
||||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||||
> 交叉参考: [工具系统](核心模块设计-工具系统.md) | [后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
多代理协调器是 free-code 中用于“把工作拆给多个 worker”的编排层。它不是简单的工具调用器,而是面向复杂任务的调度中枢:当用户问题可被拆分为研究、实现、验证等多个并行分支时,协调器负责创建 worker、下发上下文、接收回传结果,并在需要时继续同一 worker 的上下文。
|
||||
|
||||
原始源码中,`../../src/coordinator/coordinatorMode.ts` 承担了本模块的核心语义:定义 coordinator mode 的开关、worker 可用工具集合、系统提示词、session mode 匹配逻辑,以及 worker 结果的消息协议。`../../src/assistant/` 则提供与会话历史、会话筛选和辅助选择相关的配套能力,可视为协调器在“恢复旧会话”和“读取历史事件”场景下的支撑模块。
|
||||
|
||||
在 .NET 10 重写中,本模块应被抽象为独立服务层,避免与 QueryEngine、工具系统、后台任务管理直接耦合,同时保留原有行为:
|
||||
|
||||
- 允许 coordinator 生成 worker 并并发分发任务
|
||||
- 允许 worker 通过消息队列继续执行
|
||||
- 允许在会话恢复时自动匹配 coordinator/normal 模式
|
||||
- 允许读取会话历史以重建协作上下文
|
||||
|
||||
---
|
||||
|
||||
## 设计职责
|
||||
|
||||
### 1. 多代理任务编排
|
||||
|
||||
协调器负责将一个高层目标拆解为多个子任务,并为每个子任务创建独立 worker。worker 适合执行以下类型工作:
|
||||
|
||||
- 代码库研究
|
||||
- 定向实现
|
||||
- 验证与测试
|
||||
- 失败修复
|
||||
|
||||
### 2. worker 生命周期管理
|
||||
|
||||
协调器需掌握 worker 的创建、继续、停止与结果归档。worker 不是一次性调用;其价值在于保留已加载上下文后继续推进。
|
||||
|
||||
### 3. 代理间消息路由
|
||||
|
||||
worker 的结果以结构化任务通知返回,协调器再据此向用户总结,或把后续指令发回同一个 worker。
|
||||
|
||||
### 4. 团队协作管理
|
||||
|
||||
协调器支持 team create / delete / send-message 这类“代理团队”操作,用于将多个 worker 视为一个协作单元管理。
|
||||
|
||||
### 5. 会话模式同步
|
||||
|
||||
当用户恢复旧会话时,协调器需要根据保存的 session mode 自动切换当前环境变量,确保继续运行时处于正确的 coordinator/normal 模式。
|
||||
|
||||
---
|
||||
|
||||
## 原始实现要点
|
||||
|
||||
### `../../src/coordinator/coordinatorMode.ts`
|
||||
|
||||
该文件是本模块的语义核心,包含以下能力:
|
||||
|
||||
#### coordinator 模式开关
|
||||
|
||||
`isCoordinatorMode()` 通过 feature gate `COORDINATOR_MODE` 和环境变量 `CLAUDE_CODE_COORDINATOR_MODE` 判断当前是否进入协调模式。
|
||||
|
||||
#### 会话模式匹配
|
||||
|
||||
`matchSessionMode(sessionMode)` 会比较当前模式与已恢复会话记录的模式:
|
||||
|
||||
- 若一致,直接返回
|
||||
- 若不一致,自动翻转环境变量
|
||||
- 同时记录 analytics 事件
|
||||
|
||||
这说明 coordinator 是“会话状态敏感”的,而不是一个纯工具注册器。
|
||||
|
||||
#### worker 工具上下文
|
||||
|
||||
`getCoordinatorUserContext(mcpClients, scratchpadDir)` 会为 worker 注入可用工具说明:
|
||||
|
||||
- 简化模式下仅暴露 Bash / Read / Edit
|
||||
- 常规模式下暴露 async agent 允许工具集
|
||||
- 追加 MCP 服务名
|
||||
- 在 scratchpad gate 开启时暴露 scratchpad 目录
|
||||
|
||||
#### coordinator 系统提示词
|
||||
|
||||
`getCoordinatorSystemPrompt()` 描述了 coordinator 的核心职责:
|
||||
|
||||
- 直接回答能答的问题
|
||||
- 用 worker 处理适合并行化的工作
|
||||
- 只能把新信息总结给用户,不能把 worker 当作对话对象
|
||||
- 通过 `Agent`、`SendMessage`、`TaskStop` 管理 worker 生命周期
|
||||
|
||||
同时它还明确了 worker 结果的 `<task-notification>` 协议格式。
|
||||
|
||||
### `../../src/coordinator/workerAgent.ts`
|
||||
|
||||
当前快照中该文件为占位实现,但从模块命名可知,它原本承载 worker agent 的启动入口或 worker 代理适配层。在 .NET 重写里,这一职责应由 worker 启动器与任务执行器拆分承接。
|
||||
|
||||
### `../../src/assistant/*`
|
||||
|
||||
`../../src/assistant/sessionHistory.ts` 提供了会话事件分页读取:
|
||||
|
||||
- 先构造 OAuth 请求上下文
|
||||
- 再按 `anchor_to_latest` / `before_id` 拉取分页事件
|
||||
- 返回历史事件、游标与是否还有更旧内容
|
||||
|
||||
`sessionDiscovery.ts`、`gate.ts`、`index.ts`、`AssistantSessionChooser.tsx` 在本快照中多为占位或轻量 UI 适配,但它们体现出 assistant 模块的定位:用于发现、筛选、恢复与展示历史会话。
|
||||
|
||||
---
|
||||
|
||||
## .NET 10 接口设计
|
||||
|
||||
### ICoordinatorService
|
||||
|
||||
```csharp
|
||||
public interface ICoordinatorService
|
||||
{
|
||||
bool IsCoordinatorMode { get; }
|
||||
|
||||
string? MatchSessionMode(SessionMode? sessionMode);
|
||||
|
||||
string BuildCoordinatorSystemPrompt(CoordinatorPromptContext context);
|
||||
|
||||
CoordinatorUserContext BuildWorkerContext(
|
||||
IReadOnlyList<McpClientDescriptor> mcpClients,
|
||||
string? scratchpadDirectory = null);
|
||||
|
||||
Task<WorkerHandle> SpawnWorkerAsync(
|
||||
SpawnWorkerRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task SendMessageAsync(
|
||||
string workerId,
|
||||
string message,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task StopWorkerAsync(
|
||||
string workerId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WorkerResult?> GetWorkerResultAsync(string workerId);
|
||||
|
||||
Task<TeamHandle> CreateTeamAsync(CreateTeamRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeleteTeamAsync(string teamId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task SendTeamMessageAsync(string teamId, string message,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
### IAgentWorker
|
||||
|
||||
```csharp
|
||||
public interface IAgentWorker
|
||||
{
|
||||
string WorkerId { get; }
|
||||
string Description { get; }
|
||||
WorkerStatus Status { get; }
|
||||
|
||||
Task StartAsync(CancellationToken cancellationToken);
|
||||
Task ContinueAsync(string message, CancellationToken cancellationToken);
|
||||
Task StopAsync(CancellationToken cancellationToken);
|
||||
Task<WorkerSnapshot> CaptureSnapshotAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### 支撑模型
|
||||
|
||||
```csharp
|
||||
public enum SessionMode { Normal, Coordinator }
|
||||
|
||||
public sealed record CoordinatorPromptContext(
|
||||
string WorkingDirectory,
|
||||
string? ScratchpadDirectory,
|
||||
IReadOnlyList<string> AllowedTools,
|
||||
IReadOnlyList<string> McpServerNames);
|
||||
|
||||
public sealed record CoordinatorUserContext(
|
||||
string WorkerToolsContext,
|
||||
string SystemPrompt);
|
||||
|
||||
public sealed record SpawnWorkerRequest(
|
||||
string Description,
|
||||
string Prompt,
|
||||
string WorkerType,
|
||||
string? Model = null,
|
||||
string? TeamId = null);
|
||||
```
|
||||
|
||||
这些类型遵循 .NET 命名习惯,使用 PascalCase,并把“提示词构建”和“worker 运行实例”分离,避免协调器直接依赖底层 LLM 调用细节。
|
||||
|
||||
---
|
||||
|
||||
## Worker 生成与生命周期
|
||||
|
||||
### 生成流程
|
||||
|
||||
1. 协调器接收高层目标
|
||||
2. 判断是否适合并行拆分
|
||||
3. 为每个分支构造自包含 prompt
|
||||
4. 通过 worker 工厂创建 `IAgentWorker`
|
||||
5. 记录 worker 到会话/任务存储
|
||||
|
||||
### 生命周期阶段
|
||||
|
||||
| 阶段 | 说明 |
|
||||
|------|------|
|
||||
| Created | worker 已创建但未启动 |
|
||||
| Running | worker 正在执行 |
|
||||
| Waiting | worker 暂停等待继续指令 |
|
||||
| Completed | worker 已完成任务 |
|
||||
| Failed | worker 执行失败 |
|
||||
| Stopped | worker 被协调器主动停止 |
|
||||
|
||||
### 生命周期原则
|
||||
|
||||
- worker 应保留自己的上下文快照
|
||||
- 继续 worker 时优先复用旧上下文,而不是重新启动新 worker
|
||||
- 停止 worker 应保留最后状态,供后续诊断或继续执行
|
||||
|
||||
---
|
||||
|
||||
## 消息路由
|
||||
|
||||
### 结果回传协议
|
||||
|
||||
原始实现定义了 `<task-notification>` XML 作为 worker 结果消息格式。.NET 重写可保留相同语义,但建议使用结构化模型解析后再渲染给终端:
|
||||
|
||||
```csharp
|
||||
public sealed record WorkerNotification(
|
||||
string TaskId,
|
||||
string Status,
|
||||
string Summary,
|
||||
string? Result,
|
||||
WorkerUsage? Usage);
|
||||
```
|
||||
|
||||
### 路由规则
|
||||
|
||||
- worker → coordinator:上报任务完成、失败、暂停、进度
|
||||
- coordinator → worker:发送续作指令、修正指令、停止指令
|
||||
- coordinator → 用户:仅输出总结,不暴露内部通知原文
|
||||
|
||||
### 设计约束
|
||||
|
||||
- 不能用一个 worker 检查另一个 worker
|
||||
- 不能把低价值文件内容回读任务交给 worker
|
||||
- 继续 worker 时必须使用其原始 task id
|
||||
|
||||
---
|
||||
|
||||
## Agent Team 管理
|
||||
|
||||
team 机制用于把多个 worker 归入一个协作单元,常见场景是“研究组 + 实现组 + 验证组”。
|
||||
|
||||
### TeamCreate
|
||||
|
||||
创建 team 时应生成团队 ID,并维护成员 worker、目标、状态与消息历史。
|
||||
|
||||
### TeamDelete
|
||||
|
||||
删除 team 应清理团队元数据,但不能无条件销毁已完成 worker 的结果记录。
|
||||
|
||||
### SendMessage
|
||||
|
||||
team message 应广播到 team 成员,或按策略路由给主 worker。 .NET 设计应把广播规则显式化,避免隐式分支。
|
||||
|
||||
---
|
||||
|
||||
## 关联 `../../src/assistant/` 的 .NET 设计
|
||||
|
||||
`../../src/assistant/` 更适合映射为“会话访问与恢复辅助服务”,建议拆为:
|
||||
|
||||
- `IAssistantSessionHistoryService`:读取会话事件分页
|
||||
- `IAssistantSessionDiscoveryService`:发现可恢复会话
|
||||
- `IAssistantSessionChooser`:会话选择 UI 适配层
|
||||
|
||||
其中 `sessionHistory.ts` 的分页逻辑适合直接迁移为可测试的基础设施服务,而非放进协调器主服务中。
|
||||
|
||||
---
|
||||
|
||||
## 设计要点
|
||||
|
||||
- coordinator 是编排层,不是 worker 执行层
|
||||
- worker 适合做局部自治,coordinator 负责全局收敛
|
||||
- 会话模式必须可恢复、可切换、可记录
|
||||
- worker prompt 必须自包含,不能依赖隐式上下文
|
||||
- 协调器输出面向用户,内部通知面向系统
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](核心模块设计.md)
|
||||
- [工具系统](核心模块设计-工具系统.md)
|
||||
- [后台任务管理](../基础设施设计/基础设施设计-后台任务管理.md)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
359
docs/核心模块设计/核心模块设计-工具系统.md
Normal file
359
docs/核心模块设计/核心模块设计-工具系统.md
Normal file
@ -0,0 +1,359 @@
|
||||
# 核心模块设计 — 工具系统
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 原始代码来源: `../../src/tools.ts`, `../../src/tools/`
|
||||
> 原始设计意图: 定义 Agent 可调用工具的接口体系,提供工具基类复用逻辑,并统一管理内置工具与 MCP 工具的组装与去重
|
||||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||||
> 交叉参考: [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md) | [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
工具系统是 Agent 行为能力的载体。每一个工具对应 Agent 可以执行的一类操作,从执行 shell 命令(`BashTool`)到读写文件、搜索代码、访问网络,再到调用外部 MCP 服务。
|
||||
|
||||
原始 TypeScript 实现在 `tools.ts` 注册工具,在 `../../src/tools/` 目录下实现每个工具,工具本身是对象字面量或类实例。.NET 重写引入了完整的接口层次和抽象基类,使工具实现更标准化,并通过 DI 容器统一管理生命周期。
|
||||
|
||||
---
|
||||
|
||||
## 7.1 核心接口
|
||||
|
||||
### ITool — 工具基础接口
|
||||
|
||||
```csharp
|
||||
/// <summary>Agent工具基础接口</summary>
|
||||
public interface ITool
|
||||
{
|
||||
string Name { get; }
|
||||
string[]? Aliases { get; }
|
||||
string? SearchHint { get; }
|
||||
ToolCategory Category { get; }
|
||||
bool IsEnabled();
|
||||
JsonElement GetInputSchema();
|
||||
Task<string> GetDescriptionAsync(object? input = null);
|
||||
bool IsConcurrencySafe(object input);
|
||||
bool IsReadOnly(object input);
|
||||
}
|
||||
```
|
||||
|
||||
### ITool\<TInput, TOutput\> — 泛型工具接口
|
||||
|
||||
```csharp
|
||||
/// <summary>泛型工具接口</summary>
|
||||
public interface ITool<TInput, TOutput> : ITool where TInput : class
|
||||
{
|
||||
Task<ToolResult<TOutput>> ExecuteAsync(
|
||||
TInput input, ToolExecutionContext context, CancellationToken ct = default);
|
||||
Task<ValidationResult> ValidateInputAsync(TInput input);
|
||||
Task<PermissionResult> CheckPermissionAsync(TInput input, ToolExecutionContext context);
|
||||
}
|
||||
```
|
||||
|
||||
### 支撑类型
|
||||
|
||||
```csharp
|
||||
public record ToolResult<T>(
|
||||
T Data,
|
||||
bool IsError = false,
|
||||
string? ErrorMessage = null,
|
||||
List<Message>? SideMessages = null
|
||||
);
|
||||
|
||||
public record ToolExecutionContext(
|
||||
string WorkingDirectory,
|
||||
PermissionMode PermissionMode,
|
||||
IReadOnlyList<AdditionalWorkingDirectory> AdditionalWorkingDirectories,
|
||||
IPermissionEngine PermissionEngine,
|
||||
ILspClientManager LspManager,
|
||||
IBackgroundTaskManager TaskManager,
|
||||
IServiceProvider Services
|
||||
);
|
||||
|
||||
public enum ToolCategory
|
||||
{
|
||||
FileSystem, Shell, Agent, Web, Lsp, Mcp,
|
||||
UserInteraction, Todo, Task, PlanMode,
|
||||
AgentSwarm, Worktree, Config
|
||||
}
|
||||
```
|
||||
|
||||
`ToolExecutionContext` 是执行时上下文,包含工具执行所需的所有环境信息。工具实现通过 `context` 参数访问当前工作目录、权限引擎、LSP 客户端等,而不是直接依赖全局状态。
|
||||
|
||||
---
|
||||
|
||||
## 7.2 工具基类 ToolBase\<TInput, TOutput\>
|
||||
|
||||
`ToolBase` 为工具实现提供默认行为,减少每个具体工具需要编写的样板代码。
|
||||
|
||||
**原始设计意图:** 原始 TypeScript 工具通过对象字面量共享一些约定,但没有强制继承关系。.NET 的抽象基类在编译期保证所有工具遵循统一接口,并为验证逻辑提供可选的 FluentValidation 集成点。
|
||||
|
||||
```csharp
|
||||
public abstract class ToolBase<TInput, TOutput> : ITool<TInput, TOutput>
|
||||
where TInput : class
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public virtual string[]? Aliases => null;
|
||||
public virtual string? SearchHint => null;
|
||||
public abstract ToolCategory Category { get; }
|
||||
public virtual bool IsEnabled() => true;
|
||||
public abstract JsonElement GetInputSchema();
|
||||
|
||||
public virtual Task<string> GetDescriptionAsync(object? input = null)
|
||||
=> Task.FromResult($"Execute {Name}");
|
||||
|
||||
public abstract bool IsConcurrencySafe(TInput input);
|
||||
public abstract bool IsReadOnly(TInput input);
|
||||
public abstract Task<ToolResult<TOutput>> ExecuteAsync(
|
||||
TInput input, ToolExecutionContext context, CancellationToken ct);
|
||||
|
||||
public virtual Task<ValidationResult> ValidateInputAsync(TInput input)
|
||||
{
|
||||
var validator = GetValidator();
|
||||
if (validator != null)
|
||||
{
|
||||
var result = validator.Validate(input);
|
||||
return Task.FromResult(result.IsValid
|
||||
? ValidationResult.Success()
|
||||
: ValidationResult.Failure(result.Errors.Select(e => e.ErrorMessage)));
|
||||
}
|
||||
return Task.FromResult(ValidationResult.Success());
|
||||
}
|
||||
|
||||
public virtual Task<PermissionResult> CheckPermissionAsync(
|
||||
TInput input, ToolExecutionContext context)
|
||||
=> Task.FromResult(PermissionResult.Allowed());
|
||||
|
||||
protected virtual IValidator<TInput>? GetValidator() => null;
|
||||
}
|
||||
```
|
||||
|
||||
子类只需实现 `Name`、`Category`、`GetInputSchema`、`IsConcurrencySafe`、`IsReadOnly` 和 `ExecuteAsync`。验证逻辑通过重写 `GetValidator()` 返回一个 FluentValidation 的 `IValidator<TInput>` 实例来接入。
|
||||
|
||||
---
|
||||
|
||||
## 7.3 BashTool 完整实现
|
||||
|
||||
`BashTool` 是最核心也最复杂的工具,展示了工具系统的完整实现模式。
|
||||
|
||||
**原始设计意图:** 原始 `BashTool.tsx` 使用 Node.js 的 `child_process.spawn` 执行命令,通过 Promise 管理超时,并通过 `IBackgroundTaskManager` 支持后台执行。.NET 版本使用 `Process` 和 `WaitForExitAsync` + `.WaitAsync(timeout)` 实现相同语义。
|
||||
|
||||
```csharp
|
||||
public class BashTool : ToolBase<BashToolInput, BashToolOutput>
|
||||
{
|
||||
public override string Name => "Bash";
|
||||
public override ToolCategory Category => ToolCategory.Shell;
|
||||
|
||||
private readonly IBackgroundTaskManager _taskManager;
|
||||
private readonly IFeatureFlagService _features;
|
||||
|
||||
public override async Task<ToolResult<BashToolOutput>> ExecuteAsync(
|
||||
BashToolInput input, ToolExecutionContext context, CancellationToken ct)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
Arguments = $"-c \"{input.Command.Replace("\"", "\\\"")}\"",
|
||||
WorkingDirectory = context.WorkingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
// 沙箱
|
||||
if (!input.DangerouslyDisableSandbox)
|
||||
ApplySandboxRestrictions(psi, context);
|
||||
|
||||
// 后台执行
|
||||
if (input.RunInBackground)
|
||||
return await RunInBackgroundAsync(input, psi, ct);
|
||||
|
||||
// 前台执行
|
||||
using var process = new Process { StartInfo = psi };
|
||||
process.Start();
|
||||
|
||||
var timeoutMs = input.Timeout ?? 120_000;
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(ct)
|
||||
.WaitAsync(TimeSpan.FromMilliseconds(timeoutMs), ct);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
return new ToolResult<BashToolOutput>(new BashToolOutput
|
||||
{
|
||||
Stdout = await process.StandardOutput.ReadToEndAsync(ct),
|
||||
Stderr = await process.StandardError.ReadToEndAsync(ct),
|
||||
ExitCode = -1,
|
||||
Interrupted = true
|
||||
});
|
||||
}
|
||||
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(ct);
|
||||
|
||||
return new ToolResult<BashToolOutput>(new BashToolOutput
|
||||
{
|
||||
Stdout = stdout, Stderr = stderr,
|
||||
ExitCode = process.ExitCode, Interrupted = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<ToolResult<BashToolOutput>> RunInBackgroundAsync(
|
||||
BashToolInput input, ProcessStartInfo psi, CancellationToken ct)
|
||||
{
|
||||
var task = await _taskManager.CreateShellTaskAsync(input.Command, psi);
|
||||
return new ToolResult<BashToolOutput>(new BashToolOutput
|
||||
{
|
||||
Stdout = $"Background task started: {task.TaskId}",
|
||||
BackgroundTaskId = task.TaskId
|
||||
});
|
||||
}
|
||||
|
||||
public override bool IsConcurrencySafe(BashToolInput input) => false;
|
||||
public override bool IsReadOnly(BashToolInput input) =>
|
||||
CommandClassifier.IsReadCommand(input.Command);
|
||||
}
|
||||
```
|
||||
|
||||
### BashTool 输入/输出类型
|
||||
|
||||
```csharp
|
||||
public record BashToolInput
|
||||
{
|
||||
public string Command { get; init; } = "";
|
||||
public int? Timeout { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public bool RunInBackground { get; init; }
|
||||
public bool DangerouslyDisableSandbox { get; init; }
|
||||
}
|
||||
|
||||
public record BashToolOutput
|
||||
{
|
||||
public string Stdout { get; init; } = "";
|
||||
public string Stderr { get; init; } = "";
|
||||
public int ExitCode { get; init; }
|
||||
public bool Interrupted { get; init; }
|
||||
public string? BackgroundTaskId { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
`IsConcurrencySafe` 返回 `false` 表示 Bash 命令不能并发执行(防止状态污染)。`IsReadOnly` 委托给 `CommandClassifier.IsReadCommand`,该工具通过命令前缀分析判断是否为只读操作(如 `ls`、`cat`、`grep` 等),只读命令在某些权限模式下可以跳过确认。
|
||||
|
||||
---
|
||||
|
||||
## 7.4 ToolRegistry — 工具池组装与 MCP 去重
|
||||
|
||||
`ToolRegistry` 负责将内置工具和 MCP 工具组装为统一的工具池,并应用去重和权限过滤规则。
|
||||
|
||||
```csharp
|
||||
public class ToolRegistry : IToolRegistry
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IFeatureFlagService _features;
|
||||
private readonly IMcpClientManager _mcpManager;
|
||||
private List<ITool>? _cachedBaseTools;
|
||||
|
||||
public async Task<IReadOnlyList<ITool>> GetToolsAsync(
|
||||
ToolPermissionContext? permissionContext = null)
|
||||
{
|
||||
var baseTools = GetBaseTools();
|
||||
var mcpTools = await _mcpManager.GetToolsAsync();
|
||||
var pool = AssembleToolPool(baseTools, mcpTools);
|
||||
|
||||
if (permissionContext != null)
|
||||
pool = FilterByDenyRules(pool, permissionContext);
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
private List<ITool> GetBaseTools()
|
||||
{
|
||||
if (_cachedBaseTools != null) return _cachedBaseTools;
|
||||
_cachedBaseTools = new List<ITool>();
|
||||
|
||||
// 核心20个工具 (始终可用)
|
||||
_cachedBaseTools.AddRange(new ITool[] {
|
||||
Get<FileReadTool>(), Get<FileEditTool>(), Get<FileWriteTool>(),
|
||||
Get<GlobTool>(), Get<GrepTool>(), Get<BashTool>(),
|
||||
Get<AgentTool>(), Get<SkillTool>(), Get<TaskOutputTool>(),
|
||||
Get<WebFetchTool>(), Get<WebSearchTool>(), Get<LspTool>(),
|
||||
Get<TodoWriteTool>(), Get<AskUserQuestionTool>(), Get<BriefTool>(),
|
||||
Get<ListMcpResourcesTool>(), Get<ReadMcpResourceTool>(),
|
||||
Get<ToolSearchTool>(), Get<ConfigTool>(), Get<NotebookEditTool>(),
|
||||
});
|
||||
|
||||
// 条件工具 (feature-flagged)
|
||||
if (_features.IsEnabled(FeatureFlags.AgentTriggers))
|
||||
{
|
||||
_cachedBaseTools.Add(Get<CronTool>());
|
||||
_cachedBaseTools.Add(Get<MonitorTool>());
|
||||
}
|
||||
|
||||
if (_features.IsEnabled(FeatureFlags.V2Todo))
|
||||
{
|
||||
_cachedBaseTools.AddRange(new ITool[] {
|
||||
Get<TaskCreateTool>(), Get<TaskGetTool>(), Get<TaskUpdateTool>(),
|
||||
Get<TaskListTool>(), Get<TaskStopTool>(),
|
||||
});
|
||||
}
|
||||
|
||||
// Swarm + PlanMode + Worktree (始终注册)
|
||||
_cachedBaseTools.AddRange(new ITool[] {
|
||||
Get<SendMessageTool>(), Get<TeamCreateTool>(), Get<TeamDeleteTool>(),
|
||||
Get<EnterPlanModeTool>(), Get<ExitPlanModeTool>(),
|
||||
Get<EnterWorktreeTool>(), Get<ExitWorktreeTool>(),
|
||||
});
|
||||
|
||||
// 稳定排序 (prompt cache一致性)
|
||||
_cachedBaseTools.Sort((a, b) =>
|
||||
string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
return _cachedBaseTools;
|
||||
}
|
||||
|
||||
/// <summary>组装工具池: 内置优先, MCP去重</summary>
|
||||
private List<ITool> AssembleToolPool(
|
||||
List<ITool> baseTools, IReadOnlyList<ITool> mcpTools)
|
||||
{
|
||||
var pool = new List<ITool>(baseTools);
|
||||
var baseNames = baseTools.Select(t => t.Name).ToHashSet();
|
||||
|
||||
foreach (var mcpTool in mcpTools)
|
||||
{
|
||||
if (!baseNames.Contains(mcpTool.Name))
|
||||
pool.Add(mcpTool);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
private T Get<T>() where T : ITool => _services.GetRequiredService<T>();
|
||||
}
|
||||
```
|
||||
|
||||
### 工具分类
|
||||
|
||||
| 类别 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| 核心工具 | 20 个 | 始终可用,不受 feature flag 影响 |
|
||||
| 条件工具 | 7 个 | 由 `AgentTriggers`、`V2Todo` 等 feature flag 控制 |
|
||||
| Swarm/Plan/Worktree | 7 个 | 始终注册,但某些工具在特定模式外会返回错误 |
|
||||
| MCP 工具 | 动态 | 从已连接的 MCP 服务器获取,同名工具被内置工具覆盖 |
|
||||
|
||||
### 去重策略说明
|
||||
|
||||
内置工具名称集合构建为 `HashSet<string>` 后,MCP 工具逐一检查。若名称已存在于集合中,则该 MCP 工具被静默跳过。这一策略确保:
|
||||
|
||||
1. 外部 MCP 服务器无法意外替换 `Bash`、`FileRead` 等核心工具
|
||||
2. 用户可以通过 MCP 扩展新工具,但不能降低现有工具的可靠性
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](核心模块设计.md)
|
||||
- [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
|
||||
- [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md)
|
||||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||||
301
docs/核心模块设计/核心模块设计-查询引擎-QueryEngine.md
Normal file
301
docs/核心模块设计/核心模块设计-查询引擎-QueryEngine.md
Normal file
@ -0,0 +1,301 @@
|
||||
# 核心模块设计 — 查询引擎 (QueryEngine)
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 原始代码来源: `../../src/QueryEngine.ts`
|
||||
> 原始设计意图: 协调 LLM 消息流转、工具调用循环和 System Prompt 构建,是整个 Agent 行为的核心管道
|
||||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||||
> 交叉参考: [CLI 启动与解析](核心模块设计-CLI启动与解析.md) | [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
`QueryEngine` 是 free-code 的核心消息处理管道。每当用户提交一条消息,`IQueryEngine.SubmitMessageAsync` 便接管控制权,驱动一个"助手回复 → 工具调用 → 执行结果 → 继续"的循环,直到助手不再请求工具调用或用户取消。
|
||||
|
||||
原始 `QueryEngine.ts` 是一个约 1200 行的 TypeScript 文件,包含消息历史管理、System Prompt 构建、工具执行调度、上下文压缩判断、记忆提取等所有逻辑。.NET 重写将其中的 System Prompt 构建拆分为独立的 `SystemPromptBuilder`(实现 `IPromptBuilder`),其余核心循环保留在 `QueryEngine` 中。
|
||||
|
||||
---
|
||||
|
||||
## 6.1 IQueryEngine 接口
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// LLM查询引擎 - 核心消息处理管道
|
||||
/// 对应原始 ../../src/QueryEngine.ts
|
||||
/// </summary>
|
||||
public interface IQueryEngine
|
||||
{
|
||||
/// <summary>提交用户消息并返回流式响应</summary>
|
||||
IAsyncEnumerable<SDKMessage> SubmitMessageAsync(
|
||||
string content,
|
||||
SubmitMessageOptions? options = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>取消当前查询</summary>
|
||||
Task CancelAsync();
|
||||
|
||||
/// <summary>获取消息历史</summary>
|
||||
IReadOnlyList<Message> GetMessages();
|
||||
|
||||
/// <summary>获取当前token使用量</summary>
|
||||
TokenUsage GetCurrentUsage();
|
||||
}
|
||||
|
||||
public record SubmitMessageOptions(
|
||||
string? Model = null,
|
||||
ToolPermissionContext? PermissionContext = null,
|
||||
string? QuerySource = null,
|
||||
bool IsSpeculation = false
|
||||
);
|
||||
```
|
||||
|
||||
`IAsyncEnumerable<SDKMessage>` 返回类型让调用方(终端 UI 层)可以在消息流抵达时即时渲染,无需等待整个响应完成。`SDKMessage` 是一个联合类型(discriminated union),涵盖流式文本增量、工具调用开始、工具执行结果等所有消息类型。
|
||||
|
||||
---
|
||||
|
||||
## 6.2 QueryEngine 实现
|
||||
|
||||
### 依赖关系
|
||||
|
||||
`QueryEngine` 通过构造函数注入以下依赖,每一项都有对应的原始 TypeScript 概念:
|
||||
|
||||
| .NET 依赖 | 对应原始概念 |
|
||||
|-----------|-------------|
|
||||
| `IApiProviderRouter` | `claude.ts` 的 API 调用函数 |
|
||||
| `IToolRegistry` | `tools.ts` 的 `getTools()` |
|
||||
| `IPermissionEngine` | `permission.ts` 的权限检查逻辑 |
|
||||
| `IPromptBuilder` | `getSystemPrompt()` 内联函数 |
|
||||
| `ISessionMemoryService` | `memory.ts` 相关逻辑 |
|
||||
| `IFeatureFlagService` | `isFeatureEnabled()` |
|
||||
|
||||
### 流式循环实现
|
||||
|
||||
```csharp
|
||||
public class QueryEngine : IQueryEngine
|
||||
{
|
||||
private readonly IApiProviderRouter _providerRouter;
|
||||
private readonly IToolRegistry _toolRegistry;
|
||||
private readonly IPermissionEngine _permissionEngine;
|
||||
private readonly IPromptBuilder _promptBuilder;
|
||||
private readonly ISessionMemoryService _memoryService;
|
||||
private readonly IFeatureFlagService _featureFlags;
|
||||
private readonly ILogger<QueryEngine> _logger;
|
||||
private readonly List<Message> _messages = new();
|
||||
private CancellationTokenSource? _activeCts;
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> SubmitMessageAsync(
|
||||
string content,
|
||||
SubmitMessageOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
_activeCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
options ??= new SubmitMessageOptions();
|
||||
|
||||
// 1. 构建用户消息
|
||||
var userMessage = new Message
|
||||
{
|
||||
Role = MessageRole.User,
|
||||
Content = new UserContent(content),
|
||||
Timestamp = DateTime.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString()
|
||||
};
|
||||
_messages.Add(userMessage);
|
||||
yield return new SDKMessage.UserMessage(userMessage);
|
||||
|
||||
// 2. 查询循环 (Assistant → ToolUse → Execute → Continue)
|
||||
var shouldContinue = true;
|
||||
while (shouldContinue && !_activeCts.Token.IsCancellationRequested)
|
||||
{
|
||||
// 2a. 构建System Prompt
|
||||
var systemPrompt = await _promptBuilder.BuildAsync(
|
||||
_messages, options.PermissionContext, options);
|
||||
|
||||
// 2b. 获取可用工具
|
||||
var tools = await _toolRegistry.GetToolsAsync(options.PermissionContext);
|
||||
|
||||
// 2c. 调用API Provider
|
||||
var provider = _providerRouter.GetActiveProvider();
|
||||
var apiRequest = new ApiRequest
|
||||
{
|
||||
SystemPrompt = systemPrompt,
|
||||
Messages = BuildApiMessages(_messages),
|
||||
Tools = tools,
|
||||
Model = options.Model
|
||||
};
|
||||
|
||||
await foreach (var response in provider.StreamAsync(
|
||||
apiRequest, _activeCts.Token))
|
||||
{
|
||||
switch (response)
|
||||
{
|
||||
case SDKMessage.AssistantMessage am:
|
||||
_messages.Add(am.ToMessage());
|
||||
yield return am;
|
||||
break;
|
||||
|
||||
case SDKMessage.StreamingDelta sd:
|
||||
yield return sd;
|
||||
break;
|
||||
|
||||
case SDKMessage.ToolUseStart tus:
|
||||
yield return tus;
|
||||
break;
|
||||
|
||||
case SDKMessage.ToolUseResult tur:
|
||||
shouldContinue = tur.ShouldContinue;
|
||||
_messages.Add(tur.ToMessage());
|
||||
yield return tur;
|
||||
break;
|
||||
|
||||
case SDKMessage.CompactBoundary cb:
|
||||
yield return cb;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 后处理
|
||||
await PostQueryProcessingAsync(options);
|
||||
}
|
||||
|
||||
private async Task PostQueryProcessingAsync(SubmitMessageOptions options)
|
||||
{
|
||||
// 记忆提取 (EXTRACT_MEMORIES flag)
|
||||
if (_featureFlags.IsEnabled(FeatureFlags.ExtractMemories))
|
||||
{
|
||||
_ = _memoryService.TryExtractAsync(_messages); // fire-and-forget
|
||||
}
|
||||
|
||||
// Prompt建议
|
||||
// 上下文压缩检查
|
||||
// 统计更新
|
||||
}
|
||||
|
||||
public Task CancelAsync()
|
||||
{
|
||||
_activeCts?.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Message> GetMessages() => _messages.AsReadOnly();
|
||||
public TokenUsage GetCurrentUsage() => /* 从最近API响应头提取 */;
|
||||
}
|
||||
```
|
||||
|
||||
### 循环流程图
|
||||
|
||||
```
|
||||
用户提交消息
|
||||
│
|
||||
▼
|
||||
构建 System Prompt ◄────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
获取工具列表 │
|
||||
│ │
|
||||
▼ │
|
||||
调用 API Provider (流式) │
|
||||
│ │
|
||||
├── AssistantMessage → 存入历史 │
|
||||
├── StreamingDelta → 直接 yield │
|
||||
├── ToolUseStart → 直接 yield │
|
||||
└── ToolUseResult ────────────────────┤
|
||||
│ ShouldContinue = true │
|
||||
└────────────────────────────►┘
|
||||
│ ShouldContinue = false
|
||||
▼
|
||||
后处理(记忆提取等)
|
||||
│
|
||||
▼
|
||||
结束
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.3 System Prompt Builder
|
||||
|
||||
`SystemPromptBuilder` 对应原始 `QueryEngine.ts` 中内联的 `getSystemPrompt()` 函数,负责将多个信息源组装为最终发送给 API 的 System Prompt。
|
||||
|
||||
**原始设计意图:** 原始代码将系统指令、工具描述、命令描述、记忆、上下文信息拼接在一个函数中,每次查询都重新计算。.NET 重写将其提取为独立接口 `IPromptBuilder`,便于测试和替换。
|
||||
|
||||
```csharp
|
||||
public class SystemPromptBuilder : IPromptBuilder
|
||||
{
|
||||
private readonly IToolRegistry _toolRegistry;
|
||||
private readonly ICommandRegistry _commandRegistry;
|
||||
private readonly ISessionMemoryService _memoryService;
|
||||
private readonly IFeatureFlagService _features;
|
||||
private readonly ICompanionService? _companionService;
|
||||
|
||||
public async Task<string> BuildAsync(
|
||||
IReadOnlyList<Message> messages,
|
||||
ToolPermissionContext? permissionContext,
|
||||
SubmitMessageOptions options)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// 1. 基础系统指令 (CLAUDE.md 等价物)
|
||||
sb.AppendLine(await BuildBaseInstructionsAsync());
|
||||
|
||||
// 2. 工具描述 (JSON Schema)
|
||||
var tools = await _toolRegistry.GetToolsAsync(permissionContext);
|
||||
sb.AppendLine(BuildToolDescriptions(tools));
|
||||
|
||||
// 3. 命令描述
|
||||
var commands = await _commandRegistry.GetEnabledCommandsAsync();
|
||||
sb.AppendLine(BuildCommandDescriptions(commands));
|
||||
|
||||
// 4. 会话记忆
|
||||
var memory = await _memoryService.GetCurrentMemoryAsync();
|
||||
if (memory != null)
|
||||
sb.AppendLine($"<session_memory>\n{memory.Content}\n</session_memory>");
|
||||
|
||||
// 5. 上下文信息 (工作目录、Git状态、环境)
|
||||
sb.AppendLine(await BuildContextInfoAsync());
|
||||
|
||||
// 6. 同伴介绍 (BUDDY flag)
|
||||
if (_features.IsEnabled(FeatureFlags.Buddy) && _companionService != null)
|
||||
{
|
||||
var companion = await _companionService.GetCompanionAsync();
|
||||
if (companion != null)
|
||||
sb.AppendLine(CompanionPromptBuilder.BuildIntro(companion));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 六段构建逻辑说明
|
||||
|
||||
| 段落 | 内容来源 | 条件 |
|
||||
|------|----------|------|
|
||||
| 1. 基础指令 | `BuildBaseInstructionsAsync()` — 读取内置指令文本 | 始终包含 |
|
||||
| 2. 工具描述 | `IToolRegistry.GetToolsAsync()` — 当前可用工具的 JSON Schema | 始终包含 |
|
||||
| 3. 命令描述 | `ICommandRegistry.GetEnabledCommandsAsync()` — 斜杠命令列表 | 始终包含 |
|
||||
| 4. 会话记忆 | `ISessionMemoryService.GetCurrentMemoryAsync()` | 存在记忆时包含 |
|
||||
| 5. 上下文信息 | 工作目录、Git 状态、环境变量摘要 | 始终包含 |
|
||||
| 6. 同伴介绍 | `ICompanionService.GetCompanionAsync()` | `BUDDY` feature flag 开启时包含 |
|
||||
|
||||
工具描述(段落 2)的稳定性直接影响 Anthropic 的 prompt cache 命中率。`ToolRegistry` 对工具列表进行字典序排序(见[工具系统](核心模块设计-工具系统.md))正是为了保证这一段的内容在相同工具集下保持不变。
|
||||
|
||||
---
|
||||
|
||||
## 取消机制
|
||||
|
||||
`CancelAsync()` 通过 `CancellationTokenSource.Cancel()` 触发取消。`SubmitMessageAsync` 内的 `[EnumeratorCancellation]` 特性确保当外部 `CancellationToken` 被取消时,异步枚举自动停止。
|
||||
|
||||
两个取消来源通过 `CreateLinkedTokenSource` 合并:
|
||||
|
||||
- 用户调用 `CancelAsync()`(例如按 Ctrl+C)
|
||||
- 外部传入的 `ct` 参数(例如超时或应用关闭)
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [核心模块设计总览](核心模块设计.md)
|
||||
- [CLI 启动与解析](核心模块设计-CLI启动与解析.md)
|
||||
- [工具系统](核心模块设计-工具系统.md)
|
||||
- [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md)
|
||||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||||
118
docs/核心模块设计/核心模块设计.md
Normal file
118
docs/核心模块设计/核心模块设计.md
Normal file
@ -0,0 +1,118 @@
|
||||
# 核心模块设计 — 总览
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 配套文档: [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md) | [基础设施设计](../基础设施设计/) | [服务子系统设计](../服务子系统设计/) | [参考映射](reference/原始代码映射-核心模块.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
核心模块是整个 free-code .NET 10 重写的骨架,直接对应原始 TypeScript 项目中最关键的几个文件。本组文档覆盖从进程启动到 LLM 调用的完整主路径,包含五个相互协作的子系统。
|
||||
|
||||
原始 TypeScript 实现将大量逻辑集中在少数几个大文件里(`cli.tsx`、`QueryEngine.ts`、`tools.ts`、`commands.ts`)。.NET 重写将这些职责拆分为更清晰的接口、抽象类和注册表,并通过依赖注入在运行时组装。
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
Program.cs (入口)
|
||||
│
|
||||
├── QuickPathHandler 快速路径:零 DI 开销,直接处理 flag 参数
|
||||
│
|
||||
└── IAppRunner (REPL/OnShot)
|
||||
│
|
||||
└── IQueryEngine 消息处理主管道
|
||||
│
|
||||
├── IApiProviderRouter → 路由到 Anthropic / Bedrock / Vertex / Codex / Foundry
|
||||
├── IToolRegistry → 核心工具 + MCP 工具去重组装
|
||||
├── ICommandRegistry → 70+ 斜杠命令
|
||||
└── IPromptBuilder → 6 段式 System Prompt 构建
|
||||
```
|
||||
|
||||
一次用户提交的消息从 `IQueryEngine.SubmitMessageAsync` 进入,经历以下流程:
|
||||
|
||||
1. 构建 System Prompt(`SystemPromptBuilder`)
|
||||
2. 获取可用工具列表(`ToolRegistry`)
|
||||
3. 通过 `ApiProviderRouter` 选择活跃的 API 提供商并发起流式请求
|
||||
4. 在 `while` 循环中处理响应流,直到模型不再触发工具调用
|
||||
5. 后处理:记忆提取、上下文压缩检查、统计更新
|
||||
|
||||
---
|
||||
|
||||
## 子模块列表
|
||||
|
||||
### [CLI 启动与解析](核心模块设计-CLI启动与解析.md)
|
||||
|
||||
覆盖 `Program.cs` 的四阶段启动流程、`QuickPathHandler` 快速路径处理,以及基于 `System.CommandLine` 的 CLI 命令定义。
|
||||
|
||||
原始来源: `../../src/entrypoints/cli.tsx`, `../../src/screens/REPL.tsx`
|
||||
|
||||
---
|
||||
|
||||
### [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
|
||||
|
||||
覆盖 `IQueryEngine` 接口、`QueryEngine` 流式循环实现,以及 `SystemPromptBuilder` 的六段构建逻辑。
|
||||
|
||||
原始来源: `../../src/QueryEngine.ts`
|
||||
|
||||
---
|
||||
|
||||
### [工具系统](核心模块设计-工具系统.md)
|
||||
|
||||
覆盖 `ITool` / `ITool<TInput,TOutput>` 接口体系、`ToolBase<TInput,TOutput>` 抽象基类、`BashTool` 完整实现,以及 `ToolRegistry` 组装与 MCP 去重策略。
|
||||
|
||||
原始来源: `../../src/tools.ts`, `../../src/tools/`
|
||||
|
||||
---
|
||||
|
||||
### [多代理协调器 (Coordinator)](核心模块设计-多代理协调.md)
|
||||
|
||||
覆盖 coordinator mode 的模式切换、worker 启停与继续、团队消息路由、会话恢复匹配,以及 `../../src/assistant/` 的历史会话读取配套能力。
|
||||
|
||||
原始来源: `../../src/coordinator/`, `../../src/assistant/`
|
||||
|
||||
---
|
||||
|
||||
### [命令系统](核心模块设计-命令系统.md)
|
||||
|
||||
覆盖 `ICommand` 接口、`CommandRegistry` 注册 70+ 斜杠命令,以及命令分类与可用性控制机制。
|
||||
|
||||
原始来源: `../../src/commands.ts`, `../../src/commands/`
|
||||
|
||||
---
|
||||
|
||||
### [API 多提供商路由](核心模块设计-API提供商路由.md)
|
||||
|
||||
覆盖 `ApiProviderRouter` 的环境变量自动检测、`AnthropicProvider` SSE 流式解析实现,以及全部提供商枚举。
|
||||
|
||||
原始来源: `../../src/services/api/`
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
**依赖注入替代全局单例**
|
||||
|
||||
原始 TypeScript 代码广泛使用模块级全局变量共享状态。.NET 重写通过 `IServiceProvider` 和构造函数注入统一管理所有依赖,生命周期由 `AddCoreServices()` / `AddEngine()` 等扩展方法集中配置。
|
||||
|
||||
**快速路径零开销**
|
||||
|
||||
`QuickPathHandler.TryHandle` 在 DI 容器构建之前执行,确保 `--version`、`--mcp-daemon` 等场景不承担任何初始化开销。
|
||||
|
||||
**工具池稳定排序**
|
||||
|
||||
`ToolRegistry` 对基础工具列表按名称字典序排序,保证在相同 feature flag 组合下 System Prompt 中的工具顺序不变,从而最大化 Anthropic prompt cache 命中率。
|
||||
|
||||
**MCP 工具去重策略**
|
||||
|
||||
内置工具的名称集合作为 `HashSet` 优先占位,MCP 同名工具被静默丢弃,防止外部 MCP 服务器意外覆盖核心工具行为。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
- [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md)
|
||||
- [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md)
|
||||
116
docs/测试与构建/reference/原始代码映射-测试与构建.md
Normal file
116
docs/测试与构建/reference/原始代码映射-测试与构建.md
Normal file
@ -0,0 +1,116 @@
|
||||
# 原始代码映射 — 测试与构建
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 文档类型: 参考映射表
|
||||
> 上级文档: [测试与构建总览](../测试与构建.md)
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
|
||||
本文档建立 .NET 10 测试类型和构建配置与原始 TypeScript 源文件之间的映射关系。测试层是 .NET 重写新增的内容(原始项目无系统化测试),因此大部分映射说明的是"覆盖哪个原始模块的逻辑"而不是"重写了哪个测试文件"。构建部分则记录了从 Bun 脚本到 MSBuild/dotnet publish 的对应关系。
|
||||
|
||||
---
|
||||
|
||||
## 测试覆盖映射
|
||||
|
||||
### 单元测试 — FreeCode.Tests.Unit
|
||||
|
||||
| 测试类 | 测试项目路径 | 覆盖原始 TS 文件 | 测试重点 |
|
||||
|--------|------------|----------------|---------|
|
||||
| `BashToolTests` | `Tests/Tools/BashToolTests.cs` | `../../../src/tools/BashTool.tsx` | 命令执行、超时中断、后台任务 ID、只读分类 |
|
||||
| `FileReadToolTests` | `Tests/Tools/FileReadToolTests.cs` | `../../../src/tools/FileReadTool.tsx` | 路径验证、编码处理、行范围切片 |
|
||||
| `FileEditToolTests` | `Tests/Tools/FileEditToolTests.cs` | `../../../src/tools/FileEditTool.tsx` | 字符串替换、多匹配报错、文件不存在报错 |
|
||||
| `FileWriteToolTests` | `Tests/Tools/FileWriteToolTests.cs` | `../../../src/tools/FileWriteTool.tsx` | 新建文件、覆盖已有文件、父目录不存在 |
|
||||
| `GlobToolTests` | `Tests/Tools/GlobToolTests.cs` | `../../../src/tools/GlobTool.tsx` | 模式匹配、路径限制、结果排序 |
|
||||
| `GrepToolTests` | `Tests/Tools/GrepToolTests.cs` | `../../../src/tools/GrepTool.tsx` | 正则搜索、include 过滤、上下文行 |
|
||||
| `AgentToolTests` | `Tests/Tools/AgentToolTests.cs` | `../../../src/tools/AgentTool.tsx` | 子代理启动、结果收集、嵌套深度限制 |
|
||||
| `ToolRegistryTests` | `Tests/Tools/ToolRegistryTests.cs` | `../../../src/tools.ts` | 基础工具注册、MCP 工具去重、稳定排序 |
|
||||
| `QueryEngineTests` | `Tests/Engine/QueryEngineTests.cs` | `../../../src/QueryEngine.ts` | 消息循环、工具调用轮次、流式输出 |
|
||||
| `SystemPromptBuilderTests` | `Tests/Engine/SystemPromptBuilderTests.cs` | `../../../src/QueryEngine.ts`(内联 `getSystemPrompt`)| 六段构建顺序、feature flag 条件段 |
|
||||
| `CommandRegistryTests` | `Tests/Commands/CommandRegistryTests.cs` | `../../../src/commands.ts` | 命令注册、可用性过滤、认证状态过滤 |
|
||||
| `AnthropicProviderTests` | `Tests/ApiProviders/AnthropicProviderTests.cs` | `../../../src/services/api/claude.ts` | SSE 解析、事件类型映射、错误处理 |
|
||||
| `CodexProviderTests` | `Tests/ApiProviders/CodexProviderTests.cs` | `../../../src/services/api/`(Codex adapter)| 消息格式转换、流式响应 |
|
||||
| `ProviderRouterTests` | `Tests/ApiProviders/ProviderRouterTests.cs` | `../../../src/services/api/`(env 判断逻辑)| 环境变量路由、默认提供商 |
|
||||
| `McpClientTests` | `Tests/Mcp/McpClientTests.cs` | `../../../src/services/mcp/` | JSON-RPC 请求/响应、工具调用 |
|
||||
| `McpClientManagerTests` | `Tests/Mcp/McpClientManagerTests.cs` | `../../../src/services/mcp/` | 多服务器管理、断线重连 |
|
||||
| `TransportTests` | `Tests/Mcp/TransportTests.cs` | `../../../src/services/mcp/`(传输层)| stdio 传输、SSE 传输 |
|
||||
| `LspClientManagerTests` | `Tests/Lsp/LspClientManagerTests.cs` | `../../../src/services/lsp/` | 服务器实例管理、LSP 协议握手 |
|
||||
| `AuthServiceTests` | `Tests/Services/AuthServiceTests.cs` | `../../../src/services/oauth/` | Token 缓存、刷新逻辑 |
|
||||
| `RateLimitServiceTests` | `Tests/Services/RateLimitServiceTests.cs` | `../../../src/services/` | 令牌桶算法、429 处理 |
|
||||
| `SessionMemoryServiceTests` | `Tests/Services/SessionMemoryServiceTests.cs` | `../../../src/services/sessionMemory/` | 记忆读写、过期淘汰 |
|
||||
| `CompanionServiceTests` | `Tests/Services/CompanionServiceTests.cs` | 无直接对应(新增 Companion 功能)| 骰子确定性、用户隔离 |
|
||||
| `NotificationServiceTests` | `Tests/Services/NotificationServiceTests.cs` | `../../../src/services/` | 通知订阅、事件分发 |
|
||||
| `AppStateStoreTests` | `Tests/State/AppStateStoreTests.cs` | `../../../src/state/` | 状态不可变性、订阅通知、并发安全 |
|
||||
|
||||
---
|
||||
|
||||
### 集成测试 — FreeCode.Tests.Integration
|
||||
|
||||
| 测试类 | 测试项目路径 | 覆盖场景 | 涉及原始模块 |
|
||||
|--------|------------|---------|------------|
|
||||
| `QueryPipelineTests` | `QueryPipelineTests.cs` | 完整查询管道:QueryEngine + Provider + ToolRegistry | `../../../src/QueryEngine.ts` + `../../../src/tools.ts` |
|
||||
| `McpIntegrationTests` | `McpIntegrationTests.cs` | 真实 MCP stdio 服务器连接和工具调用 | `../../../src/services/mcp/` |
|
||||
| `LspIntegrationTests` | `LspIntegrationTests.cs` | LSP 服务器启动、诊断获取 | `../../../src/services/lsp/` |
|
||||
| `BridgeIntegrationTests` | `BridgeIntegrationTests.cs` | IDE Bridge 握手和命令分发 | `../../../src/bridge/` |
|
||||
| `TaskManagerTests` | `TaskManagerTests.cs` | 后台任务创建、状态更新、取消 | `../../../src/tasks/` |
|
||||
| `PluginLoadingTests` | `PluginLoadingTests.cs` | AssemblyLoadContext 加载卸载插件 | `../../../src/plugins/` |
|
||||
|
||||
---
|
||||
|
||||
### E2E 测试 — FreeCode.Tests.E2E
|
||||
|
||||
| 测试类 | 测试项目路径 | 覆盖场景 | 涉及原始模块 |
|
||||
|--------|------------|---------|------------|
|
||||
| `CliTests` | `CliTests.cs` | `--version`、`--help` 标志输出 | `../../../src/entrypoints/cli.tsx` |
|
||||
| `OneShotModeTests` | `OneShotModeTests.cs` | `-p "prompt"` 一次性模式完整流程 | `../../../src/entrypoints/cli.tsx` |
|
||||
| `InteractiveReplTests` | `InteractiveReplTests.cs` | 伪终端输入输出验证 | `../../../src/screens/REPL.tsx` |
|
||||
|
||||
---
|
||||
|
||||
## 构建映射
|
||||
|
||||
### 构建脚本对应
|
||||
|
||||
| .NET 构建文件 | 原始 TS 文件 | 功能对应 | 关键差异 |
|
||||
|--------------|------------|---------|---------|
|
||||
| `Directory.Build.props` | 无直接对应 | 全局项目属性 | MSBuild 中心化配置,Bun 项目无此概念 |
|
||||
| `build.sh` | `scripts/build.ts` | 多平台批量构建 | Bun 打包 JS bundle;`dotnet publish` 输出 AOT 原生二进制 |
|
||||
| `install.sh` | `install.sh`(原始)| 一键安装 | 检测逻辑相同;目标文件从 Bun bundle 改为 AOT 二进制 |
|
||||
| `FreeCode.csproj` | `package.json` | 项目依赖和元数据 | 明确引用各子项目,取代 npm 依赖树 |
|
||||
|
||||
---
|
||||
|
||||
### 发布目标对应
|
||||
|
||||
| .NET RID | 原始 Bun 平台 | 说明 |
|
||||
|----------|-------------|------|
|
||||
| `osx-arm64` | Darwin arm64 | Apple Silicon Mac,与原始 `install.sh` 的 `Darwin-arm64` 分支对应 |
|
||||
| `osx-x64` | Darwin x86_64 | Intel Mac,与原始 `Darwin-x86_64` 分支对应 |
|
||||
| `linux-x64` | Linux x86_64 | 主流 Linux 服务器,与原始 `Linux-x86_64` 分支对应 |
|
||||
| `linux-arm64` | Linux aarch64 | 与原始 `Linux-aarch64` 分支对应 |
|
||||
| `win-x64` | 无(原始不支持 Windows)| .NET 重写新增的目标平台 |
|
||||
|
||||
---
|
||||
|
||||
## 未直接映射的原始文件
|
||||
|
||||
以下原始文件无对应测试文件(原始项目没有测试),相关逻辑的验证由集成测试和 E2E 测试覆盖:
|
||||
|
||||
| 原始文件 | 验证方式 |
|
||||
|----------|---------|
|
||||
| `../../../src/entrypoints/cli.tsx` | `CliTests.cs`、`OneShotModeTests.cs` E2E 验证 |
|
||||
| `../../../src/screens/REPL.tsx` | `InteractiveReplTests.cs` E2E 验证 |
|
||||
| `../../../src/components/` | 无自动化 UI 测试(Terminal.Gui 视图层手工验证)|
|
||||
| `../../../src/hooks/` | 通过 `QueryEngineTests` 和 `AppStateStoreTests` 间接覆盖 |
|
||||
| `../../../src/state/` | `AppStateStoreTests.cs` 单元测试 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [测试与构建总览](../测试与构建.md)
|
||||
- [测试方案设计](../测试与构建-测试方案设计.md)
|
||||
- [构建与部署](../测试与构建-构建与部署.md)
|
||||
- [迁移路线图](../测试与构建-迁移路线图.md)
|
||||
- [原始代码映射 — 核心模块](../../核心模块设计/reference/原始代码映射-核心模块.md)
|
||||
235
docs/测试与构建/测试与构建-构建与部署.md
Normal file
235
docs/测试与构建/测试与构建-构建与部署.md
Normal file
@ -0,0 +1,235 @@
|
||||
# 构建与部署
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 文档类型: 构建设计
|
||||
> 来源章节: DESIGN-NET10-PART3.md § 23
|
||||
> 上级文档: [测试与构建总览](测试与构建.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
构建目标是跨 5 个平台的 AOT 原生二进制,输出单文件、无运行时依赖的可执行文件。MSBuild 通过 `Directory.Build.props` 统一管理全局属性,`build.sh` 封装多平台批量发布,`install.sh` 提供一键下载安装体验。
|
||||
|
||||
原始 TypeScript 项目使用 Bun 打包为单一 JavaScript bundle。.NET 重写的 AOT 路径在启动时间和内存占用上接近原生,同时消除了 Bun 运行时依赖。
|
||||
|
||||
---
|
||||
|
||||
## 23.1 MSBuild 配置
|
||||
|
||||
### Directory.Build.props(全局属性)
|
||||
|
||||
所有子项目自动继承这些属性,无需在每个 `.csproj` 中重复声明。
|
||||
|
||||
```xml
|
||||
<!-- Directory.Build.props -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AotPublish>true</AotPublish>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>partial</TrimMode>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<EnableAotAnalyzer>true</EnableAotAnalyzer>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyName>free-code</AssemblyName>
|
||||
<RootNamespace>FreeCode</RootNamespace>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
关键属性说明:
|
||||
|
||||
| 属性 | 值 | 作用 |
|
||||
|------|----|------|
|
||||
| `AotPublish` | `true` | 启用 AOT 原生编译 |
|
||||
| `PublishSingleFile` | `true` | 输出单一可执行文件 |
|
||||
| `PublishTrimmed` | `true` | 裁剪未使用的程序集 |
|
||||
| `TrimMode` | `partial` | 部分裁剪,保留反射入口点 |
|
||||
| `JsonSerializerIsReflectionEnabledByDefault` | `false` | 强制使用 Source Generator 序列化 |
|
||||
| `EnableAotAnalyzer` | `true` | 编译期检测 AOT 不兼容代码 |
|
||||
| `LangVersion` | `13.0` | C# 13 特性(primary constructors、集合表达式等)|
|
||||
|
||||
---
|
||||
|
||||
### FreeCode.csproj(主项目)
|
||||
|
||||
```xml
|
||||
<!-- FreeCode.csproj (主项目) -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FreeCode.Core\FreeCode.Core.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Engine\FreeCode.Engine.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Tools\FreeCode.Tools.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Commands\FreeCode.Commands.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.ApiProviders\FreeCode.ApiProviders.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Mcp\FreeCode.Mcp.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Lsp\FreeCode.Lsp.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Bridge\FreeCode.Bridge.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Services\FreeCode.Services.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Tasks\FreeCode.Tasks.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Skills\FreeCode.Skills.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Plugins\FreeCode.Plugins.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Features\FreeCode.Features.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.State\FreeCode.State.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.TerminalUI\FreeCode.TerminalUI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
主项目引用全部 15 个子项目,自身仅包含 `Program.cs` 入口,所有业务逻辑分布在各子项目库中。
|
||||
|
||||
---
|
||||
|
||||
## 23.2 构建脚本
|
||||
|
||||
### build.sh(多平台批量构建)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# build.sh — 多平台构建
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-$(git describe --tags --always)}"
|
||||
OUTPUT_DIR="./dist"
|
||||
|
||||
# 构建目标平台
|
||||
PLATFORMS=(
|
||||
"osx-arm64"
|
||||
"osx-x64"
|
||||
"linux-x64"
|
||||
"linux-arm64"
|
||||
"win-x64"
|
||||
)
|
||||
|
||||
for PLATFORM in "${PLATFORMS[@]}"; do
|
||||
echo "Building for $PLATFORM..."
|
||||
RID="${PLATFORM}"
|
||||
|
||||
dotnet publish src/FreeCode/FreeCode.csproj \
|
||||
-c Release \
|
||||
-r "$RID" \
|
||||
-o "$OUTPUT_DIR/$PLATFORM" \
|
||||
/p:VersionPrefix="$VERSION" \
|
||||
/p:AotPublish=true \
|
||||
/p:PublishSingleFile=true \
|
||||
/p:PublishTrimmed=true
|
||||
|
||||
# 重命名二进制
|
||||
if [[ "$PLATFORM" == win-* ]]; then
|
||||
mv "$OUTPUT_DIR/$PLATFORM/free-code.exe" \
|
||||
"$OUTPUT_DIR/$PLATFORM/free-code-$VERSION-$PLATFORM.exe"
|
||||
else
|
||||
chmod +x "$OUTPUT_DIR/$PLATFORM/free-code"
|
||||
mv "$OUTPUT_DIR/$PLATFORM/free-code" \
|
||||
"$OUTPUT_DIR/$PLATFORM/free-code-$VERSION-$PLATFORM"
|
||||
fi
|
||||
|
||||
echo "✓ $PLATFORM done"
|
||||
done
|
||||
|
||||
echo "All builds complete in $OUTPUT_DIR/"
|
||||
```
|
||||
|
||||
脚本逻辑说明:
|
||||
|
||||
1. 版本号优先读取命令行参数,fallback 到 `git describe --tags --always`
|
||||
2. 遍历 5 个 RID,每个平台独立调用 `dotnet publish`
|
||||
3. 输出产物按 `free-code-{VERSION}-{PLATFORM}` 格式命名
|
||||
4. Windows 平台保留 `.exe` 后缀,其余平台添加可执行权限
|
||||
|
||||
---
|
||||
|
||||
## 23.3 安装脚本
|
||||
|
||||
### install.sh(一键安装)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# install.sh — 一键安装
|
||||
set -euo pipefail
|
||||
|
||||
# 检测平台
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
case "$OS-$ARCH" in
|
||||
Darwin-arm64) PLATFORM="osx-arm64" ;;
|
||||
Darwin-x86_64) PLATFORM="osx-x64" ;;
|
||||
Linux-x86_64) PLATFORM="linux-x64" ;;
|
||||
Linux-aarch64) PLATFORM="linux-arm64" ;;
|
||||
*) echo "Unsupported platform: $OS-$ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
INSTALL_DIR="$HOME/.free-code/bin"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# 下载
|
||||
BINARY_URL="https://github.com/paoloanzn/free-code/releases/latest/download/free-code-latest-$PLATFORM"
|
||||
echo "Downloading free-code for $PLATFORM..."
|
||||
curl -fsSL "$BINARY_URL" -o "$INSTALL_DIR/free-code"
|
||||
chmod +x "$INSTALL_DIR/free-code"
|
||||
|
||||
# 添加到 PATH
|
||||
if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
|
||||
SHELL_RC="$HOME/.zshrc"
|
||||
[ -f "$HOME/.bashrc" ] && SHELL_RC="$HOME/.bashrc"
|
||||
echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$SHELL_RC"
|
||||
echo "Added $INSTALL_DIR to PATH in $SHELL_RC"
|
||||
fi
|
||||
|
||||
echo "✓ free-code installed to $INSTALL_DIR/free-code"
|
||||
echo " Run 'free-code' to start"
|
||||
```
|
||||
|
||||
安装流程:
|
||||
|
||||
1. 通过 `uname -s` 和 `uname -m` 检测当前平台
|
||||
2. 从 GitHub Releases 下载对应 RID 的预编译二进制
|
||||
3. 安装到 `~/.free-code/bin/free-code`
|
||||
4. 自动检测 shell 配置文件(优先 `.zshrc`,fallback `.bashrc`)并追加 PATH
|
||||
|
||||
---
|
||||
|
||||
## 23.4 目标平台矩阵
|
||||
|
||||
| RID | 操作系统 | 架构 | 说明 |
|
||||
|-----|---------|------|------|
|
||||
| `osx-arm64` | macOS | Apple Silicon | M1/M2/M3 Mac |
|
||||
| `osx-x64` | macOS | Intel x64 | Intel Mac |
|
||||
| `linux-x64` | Linux | x86-64 | 主流 Linux 服务器 |
|
||||
| `linux-arm64` | Linux | AArch64 | Raspberry Pi、AWS Graviton 等 |
|
||||
| `win-x64` | Windows | x64 | Windows 10/11 |
|
||||
|
||||
---
|
||||
|
||||
## 23.5 发布参数说明
|
||||
|
||||
| dotnet publish 参数 | 含义 |
|
||||
|---------------------|------|
|
||||
| `-c Release` | Release 配置,开启优化 |
|
||||
| `-r {RID}` | 目标运行时,指定后产物为自包含 |
|
||||
| `/p:AotPublish=true` | AOT 原生编译,消除 JIT 开销 |
|
||||
| `/p:PublishSingleFile=true` | 将所有资源打包为单一可执行文件 |
|
||||
| `/p:PublishTrimmed=true` | 裁剪未引用的程序集,减小体积 |
|
||||
| `/p:VersionPrefix={VERSION}` | 注入版本号到程序集元数据 |
|
||||
|
||||
AOT 编译将 IL 代码提前编译为目标平台原生机器码,启动时间从 JIT 模式的数百毫秒降至个位数毫秒,与原始 Bun 构建产物的冷启动时间相当。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [测试与构建总览](测试与构建.md)
|
||||
- [原始代码映射 — 测试与构建](reference/原始代码映射-测试与构建.md)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
- [迁移路线图](测试与构建-迁移路线图.md)
|
||||
369
docs/测试与构建/测试与构建-测试方案设计.md
Normal file
369
docs/测试与构建/测试与构建-测试方案设计.md
Normal file
@ -0,0 +1,369 @@
|
||||
# 测试方案设计
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 文档类型: 测试设计
|
||||
> 来源章节: DESIGN-NET10-PART3.md § 22
|
||||
> 上级文档: [测试与构建总览](测试与构建.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
原始 TypeScript 项目没有系统化的测试覆盖。.NET 重写从零建立分层测试体系,目标是核心路径 80%+ 覆盖率。测试框架组合为 **xUnit 2 + NSubstitute 5 + FluentAssertions 7**,三个独立测试项目分别承载单元、集成和 E2E 三个层次。
|
||||
|
||||
---
|
||||
|
||||
## 22.1 测试项目结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── FreeCode.Tests.Unit/ # 120+ 单元测试
|
||||
│ ├── Engine/
|
||||
│ │ ├── QueryEngineTests.cs
|
||||
│ │ └── SystemPromptBuilderTests.cs
|
||||
│ ├── Tools/
|
||||
│ │ ├── BashToolTests.cs
|
||||
│ │ ├── FileEditToolTests.cs
|
||||
│ │ ├── FileReadToolTests.cs
|
||||
│ │ ├── FileWriteToolTests.cs
|
||||
│ │ ├── GlobToolTests.cs
|
||||
│ │ ├── GrepToolTests.cs
|
||||
│ │ ├── AgentToolTests.cs
|
||||
│ │ └── ToolRegistryTests.cs
|
||||
│ ├── Commands/
|
||||
│ │ └── CommandRegistryTests.cs
|
||||
│ ├── ApiProviders/
|
||||
│ │ ├── AnthropicProviderTests.cs
|
||||
│ │ ├── CodexProviderTests.cs
|
||||
│ │ └── ProviderRouterTests.cs
|
||||
│ ├── Mcp/
|
||||
│ │ ├── McpClientTests.cs
|
||||
│ │ ├── McpClientManagerTests.cs
|
||||
│ │ └── TransportTests.cs
|
||||
│ ├── Lsp/
|
||||
│ │ └── LspClientManagerTests.cs
|
||||
│ ├── Services/
|
||||
│ │ ├── AuthServiceTests.cs
|
||||
│ │ ├── RateLimitServiceTests.cs
|
||||
│ │ ├── SessionMemoryServiceTests.cs
|
||||
│ │ ├── CompanionServiceTests.cs
|
||||
│ │ └── NotificationServiceTests.cs
|
||||
│ └── State/
|
||||
│ └── AppStateStoreTests.cs
|
||||
├── FreeCode.Tests.Integration/ # 60 集成测试
|
||||
│ ├── QueryPipelineTests.cs
|
||||
│ ├── McpIntegrationTests.cs
|
||||
│ ├── LspIntegrationTests.cs
|
||||
│ ├── BridgeIntegrationTests.cs
|
||||
│ ├── TaskManagerTests.cs
|
||||
│ └── PluginLoadingTests.cs
|
||||
└── FreeCode.Tests.E2E/ # 20 端到端测试
|
||||
├── CliTests.cs
|
||||
├── OneShotModeTests.cs
|
||||
└── InteractiveReplTests.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22.2 单元测试示例
|
||||
|
||||
### BashToolTests.cs
|
||||
|
||||
测试覆盖: 命令执行、超时处理、后台任务、只读分类。
|
||||
原始来源: `../../src/tools/BashTool.tsx`
|
||||
|
||||
```csharp
|
||||
// === BashToolTests.cs ===
|
||||
public class BashToolTests
|
||||
{
|
||||
private readonly BashTool _tool = new(
|
||||
Substitute.For<IBackgroundTaskManager>(),
|
||||
Substitute.For<IFeatureFlagService>());
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SimpleCommand_ReturnsOutput()
|
||||
{
|
||||
// Arrange
|
||||
var input = new BashToolInput { Command = "echo hello" };
|
||||
var context = new ToolExecutionContext(
|
||||
WorkingDirectory: Path.GetTempPath(),
|
||||
PermissionMode: PermissionMode.Default,
|
||||
AdditionalWorkingDirectories: [],
|
||||
PermissionEngine: Substitute.For<IPermissionEngine>(),
|
||||
LspManager: Substitute.For<ILspClientManager>(),
|
||||
TaskManager: Substitute.For<IBackgroundTaskManager>(),
|
||||
Services: Substitute.For<IServiceProvider>());
|
||||
|
||||
// Act
|
||||
var result = await _tool.ExecuteAsync(input, context);
|
||||
|
||||
// Assert
|
||||
result.IsError.Should().BeFalse();
|
||||
result.Data.Stdout.Trim().Should().Be("hello");
|
||||
result.Data.ExitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Timeout_KillsProcess()
|
||||
{
|
||||
var input = new BashToolInput { Command = "sleep 10", Timeout = 500 };
|
||||
var context = CreateContext();
|
||||
|
||||
var result = await _tool.ExecuteAsync(input, context);
|
||||
|
||||
result.Data.Interrupted.Should().BeTrue();
|
||||
result.Data.ExitCode.Should().Be(-1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Background_ReturnsTaskId()
|
||||
{
|
||||
var taskManager = Substitute.For<IBackgroundTaskManager>();
|
||||
taskManager.CreateShellTaskAsync(Arg.Any<string>(), Arg.Any<ProcessStartInfo>())
|
||||
.Returns(new LocalShellTask { TaskId = "bg-123", Command = "sleep 10" });
|
||||
|
||||
var tool = new BashTool(taskManager, Substitute.For<IFeatureFlagService>());
|
||||
var input = new BashToolInput { Command = "sleep 10", RunInBackground = true };
|
||||
var result = await tool.ExecuteAsync(input, CreateContext());
|
||||
|
||||
result.Data.BackgroundTaskId.Should().Be("bg-123");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ls", true)]
|
||||
[InlineData("rm -rf /", false)]
|
||||
[InlineData("cat file.txt", true)]
|
||||
[InlineData("echo test > file", false)]
|
||||
public void IsReadOnly_ClassifiesCorrectly(string command, bool expected)
|
||||
{
|
||||
var input = new BashToolInput { Command = command };
|
||||
_tool.IsReadOnly(input).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ToolRegistryTests.cs
|
||||
|
||||
测试覆盖: 基础工具注册、MCP 工具去重策略。
|
||||
原始来源: `../../src/tools.ts`
|
||||
|
||||
```csharp
|
||||
// === ToolRegistryTests.cs ===
|
||||
public class ToolRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetToolsAsync_ReturnsBaseTools()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
var tools = await registry.GetToolsAsync();
|
||||
|
||||
tools.Should().Contain(t => t.Name == "Read");
|
||||
tools.Should().Contain(t => t.Name == "Edit");
|
||||
tools.Should().Contain(t => t.Name == "Bash");
|
||||
tools.Should().Contain(t => t.Name == "Agent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AssembleToolPool_DeduplicatesMcpTools()
|
||||
{
|
||||
// 内置 "Bash" 工具 + MCP 也提供 "Bash" → 只保留内置
|
||||
var registry = CreateRegistry();
|
||||
var mcpManager = Substitute.For<IMcpClientManager>();
|
||||
mcpManager.GetToolsAsync().Returns(new List<ITool>
|
||||
{
|
||||
CreateTool("Bash"), // 与内置重名,应被丢弃
|
||||
CreateTool("SlackSearch"), // 新工具,应被添加
|
||||
});
|
||||
|
||||
// 验证: Bash 只有一个 (内置), SlackSearch 被添加
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### AnthropicProviderTests.cs
|
||||
|
||||
测试覆盖: SSE 流式事件解析。
|
||||
原始来源: `../../src/services/api/claude.ts`
|
||||
|
||||
```csharp
|
||||
// === AnthropicProviderTests.cs ===
|
||||
public class AnthropicProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StreamAsync_ParsesSSECorrectly()
|
||||
{
|
||||
var sseResponse = "event: message_start\ndata: {\"type\":\"message_start\"}\n\n"
|
||||
+ "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"Hello\"}}\n\n"
|
||||
+ "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
|
||||
|
||||
var handler = new MockHttpHandler(sseResponse);
|
||||
var provider = new AnthropicProvider(new HttpClient(handler));
|
||||
|
||||
var messages = new List<SDKMessage>();
|
||||
await foreach (var msg in provider.StreamAsync(new ApiRequest()))
|
||||
messages.Add(msg);
|
||||
|
||||
messages.Should().ContainSingle(m => m is SDKMessage.StreamingDelta sd && sd.Text == "Hello");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CompanionServiceTests.cs
|
||||
|
||||
测试覆盖: 骰子结果确定性、不同用户产生不同结果。
|
||||
原始来源: 无直接对应(新增 Companion 功能)
|
||||
|
||||
```csharp
|
||||
// === CompanionServiceTests.cs ===
|
||||
public class CompanionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void RollBones_IsDeterministic()
|
||||
{
|
||||
// 相同 userId → 相同骨骼
|
||||
var service = new CompanionService(Substitute.For<IGlobalConfig>());
|
||||
var c1 = service.RollBones("user-123");
|
||||
var c2 = service.RollBones("user-123");
|
||||
c1.Should().BeEquivalentTo(c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollBones_DifferentUsers_DifferentResults()
|
||||
{
|
||||
var service = new CompanionService(Substitute.For<IGlobalConfig>());
|
||||
var c1 = service.RollBones("user-A");
|
||||
var c2 = service.RollBones("user-B");
|
||||
// 极大概率不同 (18 种族, 6 眼形, 5 稀有度)
|
||||
(c1.Species != c2.Species || c1.Eye != c2.Eye).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22.3 集成测试示例
|
||||
|
||||
### McpIntegrationTests.cs
|
||||
|
||||
测试覆盖: stdio MCP 服务器连接建立。
|
||||
原始来源: `../../src/services/mcp/`
|
||||
|
||||
```csharp
|
||||
// === McpIntegrationTests.cs ===
|
||||
public class McpIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private McpClientManager _manager = null!;
|
||||
private string _tempDir = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"mcp-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
|
||||
// 创建一个测试用 MCP stdio 服务器脚本
|
||||
var serverScript = Path.Combine(_tempDir, "test-server.sh");
|
||||
await File.WriteAllTextAsync(serverScript, @"#!/bin/bash
|
||||
echo '{""jsonrpc"":""2.0"",""id"":1,""result"":{""protocolVersion"":""2025-03-26"",""capabilities"":{""tools"":{}}}}'
|
||||
");
|
||||
Process.Start("chmod", $"+x {serverScript}")?.WaitForExit();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectServersAsync_ConnectsToStdioServer()
|
||||
{
|
||||
// 配置一个 stdio MCP 服务器
|
||||
var config = new StdioServerConfig { Command = "echo", Scope = ConfigScope.Local };
|
||||
// ... 验证连接成功
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22.4 E2E 测试示例
|
||||
|
||||
### CliTests.cs
|
||||
|
||||
测试覆盖: 版本标志输出、一次性模式响应。
|
||||
原始来源: `../../src/entrypoints/cli.tsx`
|
||||
|
||||
```csharp
|
||||
// === CliTests.cs ===
|
||||
public class CliTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VersionFlag_PrintsVersion()
|
||||
{
|
||||
var result = await RunCliAsync("--version");
|
||||
result.ExitCode.Should().Be(0);
|
||||
result.Output.Should().StartWith("free-code ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OneShotMode_ReturnsResponse()
|
||||
{
|
||||
// 需要 mock API server
|
||||
var result = await RunCliAsync("-p", "what is 2+2?");
|
||||
result.ExitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
private static async Task<(int ExitCode, string Output)> RunCliAsync(params string[] args)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"run --project src/FreeCode {string.Join(" ", args)}",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
return (process.ExitCode, output);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22.5 测试金字塔
|
||||
|
||||
| 层次 | 项目 | 数量 | 执行时间目标 |
|
||||
|------|------|------|------------|
|
||||
| 单元测试 | `FreeCode.Tests.Unit` | 120+ | < 30 秒 |
|
||||
| 集成测试 | `FreeCode.Tests.Integration` | 60 | < 3 分钟 |
|
||||
| E2E 测试 | `FreeCode.Tests.E2E` | 20 | < 10 分钟 |
|
||||
| **合计** | | **200** | |
|
||||
|
||||
单元测试覆盖所有工具、引擎、命令、API 提供商、MCP/LSP 客户端、服务层和状态管理。集成测试验证跨组件交互(查询管道、MCP 连接、LSP 通信、Bridge 握手、任务管理器、插件加载)。E2E 测试启动完整进程,验证 CLI 行为对外部用户可见的部分。
|
||||
|
||||
---
|
||||
|
||||
## 22.6 测试框架版本
|
||||
|
||||
| 包 | 版本 | 用途 |
|
||||
|----|------|------|
|
||||
| `xunit` | 2.x | 测试运行器和断言基础 |
|
||||
| `NSubstitute` | 5.x | 接口 mock / stub / spy |
|
||||
| `FluentAssertions` | 7.x | 可读的断言链 |
|
||||
| `Microsoft.NET.Test.Sdk` | 最新 | dotnet test 集成 |
|
||||
| `coverlet.collector` | 最新 | 代码覆盖率收集 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [测试与构建总览](测试与构建.md)
|
||||
- [原始代码映射 — 测试与构建](reference/原始代码映射-测试与构建.md)
|
||||
- [核心模块设计 — 工具系统](../核心模块设计/核心模块设计-工具系统.md)
|
||||
- [基础设施设计](../基础设施设计/基础设施设计.md)
|
||||
234
docs/测试与构建/测试与构建-迁移路线图.md
Normal file
234
docs/测试与构建/测试与构建-迁移路线图.md
Normal file
@ -0,0 +1,234 @@
|
||||
# 迁移路线图
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 文档类型: 规划设计
|
||||
> 来源章节: DESIGN-NET10-PART3.md § 24
|
||||
> 上级文档: [测试与构建总览](测试与构建.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
从 TypeScript/Bun 到 .NET 10 的完整重写分 12 个阶段、35 个工作周推进,设置 8 个里程碑节点作为质量检查点。第 36-37 周为缓冲期和文档收尾,实际总工期约 37 周。
|
||||
|
||||
路线图遵循"先主路径、后完整功能、最后质量和发布"的顺序,确保每个里程碑都有可运行的产物,而不是等到全部完成才能验证。
|
||||
|
||||
---
|
||||
|
||||
## 24.1 35 周详细规划
|
||||
|
||||
### Phase 1: 基础框架(Week 1-4)
|
||||
|
||||
```
|
||||
Phase 1: 基础框架 (Week 1-4)
|
||||
├── Week 1: 解决方案结构 + Directory.Build.props + 所有 17 个项目创建
|
||||
├── Week 2: DI 容器 + 核心接口 (ITool, ICommand, IApiProvider, IQueryEngine)
|
||||
├── Week 3: System.CommandLine CLI 解析 + QuickPathHandler
|
||||
├── Week 4: AppState record + AppStateStore + FeatureFlags
|
||||
└── 里程碑 M1: dotnet run 输出版本号
|
||||
```
|
||||
|
||||
**目标**: 建立可编译的解决方案骨架,所有接口定义就位,最基础的 CLI 入口可以运行。
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 核心引擎(Week 5-8)
|
||||
|
||||
```
|
||||
Phase 2: 核心引擎 (Week 5-8)
|
||||
├── Week 5: QueryEngine 流式循环 + Message 类型
|
||||
├── Week 6: SystemPromptBuilder (6段构建)
|
||||
├── Week 7: ToolBase<TInput,TOutput> + FluentValidation
|
||||
├── Week 8: ToolRegistry + CommandRegistry 注册
|
||||
└── 里程碑 M2: 一次性 prompt 模式工作 (-p)
|
||||
```
|
||||
|
||||
**目标**: 一次性模式(`-p "prompt"`)可以向 API 发送请求并接收流式响应,不需要完整 UI。
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: API 提供商(Week 9-11)
|
||||
|
||||
```
|
||||
Phase 3: API 提供商 (Week 9-11)
|
||||
├── Week 9: AnthropicProvider SSE 解析
|
||||
├── Week 10: CodexProvider 消息格式适配
|
||||
├── Week 11: BedrockProvider + VertexProvider + FoundryProvider
|
||||
└── 里程碑: 所有 5 个提供商路由工作
|
||||
```
|
||||
|
||||
**目标**: 环境变量切换任意一个提供商都能正常处理流式响应。
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 核心工具(Week 12-15)
|
||||
|
||||
```
|
||||
Phase 4: 核心工具 (Week 12-15)
|
||||
├── Week 12: FileReadTool + FileEditTool + FileWriteTool
|
||||
├── Week 13: BashTool + GlobTool + GrepTool
|
||||
├── Week 14: AgentTool + SkillTool
|
||||
├── Week 15: WebFetch + WebSearch + TodoWrite + AskUser + 其他
|
||||
└── 里程碑 M3: Bash/Read/Edit 工具链可用
|
||||
```
|
||||
|
||||
**目标**: 模型可以调用文件操作和 Shell 命令工具,完成基本编码任务。
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 命令实现(Week 16-18)
|
||||
|
||||
```
|
||||
Phase 5: 命令实现 (Week 16-18)
|
||||
├── Week 16: 核心 30 个命令 (help, exit, clear, config, model, login...)
|
||||
├── Week 17: 功能命令 25 个 (mcp, plugins, skills, tasks...)
|
||||
├── Week 18: 条件 + 内部命令 15 个
|
||||
└── 里程碑: 所有 70+ 命令注册
|
||||
```
|
||||
|
||||
**目标**: 全部斜杠命令可以被解析和分发,核心命令(`/help`、`/model`、`/login`)有完整实现。
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: 终端 UI(Week 19-22)
|
||||
|
||||
```
|
||||
Phase 6: 终端 UI (Week 19-22)
|
||||
├── Week 19: Terminal.Gui App + REPLScreen 基本布局
|
||||
├── Week 20: PromptInput + MessageList + 流式渲染
|
||||
├── Week 21: PermissionDialog + ToolUseDisplay + StatusBar
|
||||
├── Week 22: Theme + Keybindings + CompanionSprite
|
||||
└── 里程碑 M4: 交互式 REPL 可用
|
||||
```
|
||||
|
||||
**目标**: 用户可以在终端中交互式输入 prompt 并看到流式输出,工具调用有权限确认界面。
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: 基础设施(Week 23-25)
|
||||
|
||||
```
|
||||
Phase 7: 基础设施 (Week 23-25)
|
||||
├── Week 23: MCP SDK (传输层 + 客户端 + McpClientManager)
|
||||
├── Week 24: LSP (LspClientManager + LspServerInstance)
|
||||
├── Week 25: OAuth + Bridge + BackgroundTasks + RateLimit
|
||||
└── 里程碑 M5: 功能对等原始项目
|
||||
```
|
||||
|
||||
**目标**: MCP 服务器可以连接并提供工具,LSP 集成工作,Bridge 模式可以与 IDE 通信,这是与原始 TypeScript 项目功能对等的节点。
|
||||
|
||||
---
|
||||
|
||||
### Phase 8: 高级服务(Week 26-28)
|
||||
|
||||
```
|
||||
Phase 8: 高级服务 (Week 26-28)
|
||||
├── Week 26: SessionMemory + AutoDream
|
||||
├── Week 27: TeamMemorySync + RemoteSessions
|
||||
├── Week 28: Voice + Notifications + Companion
|
||||
└── 里程碑: 所有高级功能工作
|
||||
```
|
||||
|
||||
**目标**: 会话记忆、团队记忆同步、语音输入、Companion 精灵等扩展功能全部就位。
|
||||
|
||||
---
|
||||
|
||||
### Phase 9: 扩展系统(Week 29-30)
|
||||
|
||||
```
|
||||
Phase 9: 扩展系统 (Week 29-30)
|
||||
├── Week 29: SkillLoader + frontmatter 解析
|
||||
├── Week 30: PluginManager + AssemblyLoadContext + Marketplace
|
||||
└── 里程碑 M6: 可加载外部技能和插件
|
||||
```
|
||||
|
||||
**目标**: 外部 `.md` 技能文件和 `.dll` 插件可以在运行时动态加载,插件市场接口就位。
|
||||
|
||||
---
|
||||
|
||||
### Phase 10: 测试(Week 31-33)
|
||||
|
||||
```
|
||||
Phase 10: 测试 (Week 31-33)
|
||||
├── Week 31: 120 单元测试 (工具/引擎/提供商/服务)
|
||||
├── Week 32: 60 集成测试 (MCP/LSP/Bridge/管道)
|
||||
├── Week 33: 20 E2E 测试 + 性能基准
|
||||
└── 里程碑 M7: 核心路径 80%+ 覆盖率
|
||||
```
|
||||
|
||||
**目标**: 三层测试套件全部通过,覆盖率报告显示核心路径 80% 以上,性能基准记录在案。
|
||||
|
||||
---
|
||||
|
||||
### Phase 11: 构建发布(Week 34-35)
|
||||
|
||||
```
|
||||
Phase 11: 构建发布 (Week 34-35)
|
||||
├── Week 34: AOT 优化 + Source Generator + 5 平台构建
|
||||
├── Week 35: 安装脚本 + CI/CD + 文档
|
||||
└── 里程碑 M8: 一键安装脚本工作
|
||||
```
|
||||
|
||||
**目标**: `install.sh` 可以在 macOS 和 Linux 上一键安装,CI/CD 管道自动构建和发布。
|
||||
|
||||
---
|
||||
|
||||
### Phase 12: 文档(Week 36-37,缓冲期)
|
||||
|
||||
```
|
||||
Phase 12: 文档 (Week 36-37, buffer)
|
||||
├── README + 架构文档 + API 文档
|
||||
└── 最终验收
|
||||
```
|
||||
|
||||
**目标**: 文档完整,所有设计文档更新为最终实现,项目可以公开发布。
|
||||
|
||||
---
|
||||
|
||||
## 24.2 里程碑表
|
||||
|
||||
| 里程碑 | 完成周 | 验收标准 | 依赖 |
|
||||
|--------|--------|---------|------|
|
||||
| M1 | Week 4 | `dotnet run` 输出版本号,无运行时错误 | Phase 1 完成 |
|
||||
| M2 | Week 8 | `-p "hello"` 返回 API 响应,流式输出到终端 | Phase 2 完成 |
|
||||
| M3 | Week 15 | 模型成功调用 Bash/Read/Edit 工具完成简单任务 | Phase 4 完成 |
|
||||
| M4 | Week 22 | 交互式 REPL 可用,权限确认 UI 正常 | Phase 6 完成 |
|
||||
| M5 | Week 25 | 功能对等原始 TypeScript 项目,MCP/LSP/Bridge 工作 | Phase 7 完成 |
|
||||
| M6 | Week 30 | 可加载外部技能文件和插件 dll | Phase 9 完成 |
|
||||
| M7 | Week 33 | 测试覆盖率 >= 80%,全部测试通过 | Phase 10 完成 |
|
||||
| M8 | Week 35 | 一键安装脚本在 macOS/Linux 工作,CI 绿灯 | Phase 11 完成 |
|
||||
|
||||
---
|
||||
|
||||
## 24.3 风险评估
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解策略 |
|
||||
|------|------|------|---------|
|
||||
| MCP SDK 无成熟 .NET 库 | 高 | 高 | Phase 7 前期自研传输层,参考官方 TypeScript SDK 实现 |
|
||||
| Terminal.Gui v2 组件不足 | 中 | 中 | Spectre.Console 补充缺失组件,必要时自定义 View |
|
||||
| AOT 反射限制 | 中 | 高 | Source Generator 生成序列化代码,启用 `EnableAotAnalyzer` 提前暴露问题 |
|
||||
| 插件 ALC 卸载内存泄漏 | 低 | 中 | 定期监控内存,提供强制 GC 入口,文档说明已知限制 |
|
||||
| LSP OmniSharp 库兼容性 | 中 | 中 | StreamJsonRpc 作为备选,最坏情况自实现 JSON-RPC 层 |
|
||||
| StreamJsonRpc AOT 兼容 | 中 | 中 | 评估期间同时准备自定义 JSON-RPC 实现,Phase 7 初期决策 |
|
||||
|
||||
---
|
||||
|
||||
## 24.4 并行开发策略
|
||||
|
||||
各阶段内部的周可以部分并行推进。具体来说:
|
||||
|
||||
- Phase 3 的三家次要提供商(Bedrock、Vertex、Foundry)可以分配给不同开发者同时实现
|
||||
- Phase 4 中文件操作工具(Week 12)和 Shell 工具(Week 13)无依赖关系,可并行
|
||||
- Phase 10 的单元测试可以在 Phase 7-9 期间随功能完成同步编写,不必等到 Phase 10 才开始
|
||||
|
||||
单人开发时按串行顺序执行,团队协作时按上述拆分并行。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [测试与构建总览](测试与构建.md)
|
||||
- [测试方案设计](测试与构建-测试方案设计.md)
|
||||
- [构建与部署](测试与构建-构建与部署.md)
|
||||
- [原始代码映射 — 测试与构建](reference/原始代码映射-测试与构建.md)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
90
docs/测试与构建/测试与构建.md
Normal file
90
docs/测试与构建/测试与构建.md
Normal file
@ -0,0 +1,90 @@
|
||||
# 测试与构建 — 总览
|
||||
|
||||
> 所属项目: free-code .NET 10 重写
|
||||
> 配套文档: [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md) | [核心模块设计](../核心模块设计/核心模块设计.md) | [参考映射](reference/原始代码映射-测试与构建.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本组文档覆盖 free-code .NET 10 重写项目的质量保证、构建发布和迁移规划三个方面。原始 TypeScript 项目没有系统化的测试套件;.NET 重写从零开始建立完整的分层测试体系,并以 AOT 单文件发布为目标设计构建管道。
|
||||
|
||||
测试框架采用 **xUnit 2 + NSubstitute 5 + FluentAssertions 7** 组合。目标覆盖率是核心路径 80%+,由 200 个测试用例(120 单元 + 60 集成 + 20 E2E)构成测试金字塔。
|
||||
|
||||
构建目标是跨 5 个平台(macOS arm64/x64、Linux x64/arm64、Windows x64)的 AOT 原生二进制,通过 `build.sh` 脚本批量发布,配合 `install.sh` 实现一键安装。
|
||||
|
||||
迁移路线图覆盖 35 个工作周,分 12 个阶段,设置 8 个里程碑节点,附带风险评估矩阵。
|
||||
|
||||
---
|
||||
|
||||
## 子模块列表
|
||||
|
||||
### [测试方案设计](测试与构建-测试方案设计.md)
|
||||
|
||||
覆盖三层测试项目结构、单元测试示例(`BashToolTests`、`ToolRegistryTests`、`AnthropicProviderTests`、`CompanionServiceTests`)、集成测试示例(`McpIntegrationTests`)、E2E 测试示例(`CliTests`),以及测试金字塔数量分布。
|
||||
|
||||
原始来源: 无直接对应(原始项目无系统测试)
|
||||
|
||||
---
|
||||
|
||||
### [构建与部署](测试与构建-构建与部署.md)
|
||||
|
||||
覆盖 `Directory.Build.props` 全局 MSBuild 配置、`FreeCode.csproj` 主项目引用、`build.sh` 多平台构建脚本、`install.sh` 一键安装脚本,以及 AOT + 单文件 + Trimmed 发布参数。
|
||||
|
||||
原始来源: `scripts/build.ts`(Bun 构建脚本)、`install.sh`(原始安装脚本)
|
||||
|
||||
---
|
||||
|
||||
### [迁移路线图](测试与构建-迁移路线图.md)
|
||||
|
||||
覆盖 35 周 12 阶段详细规划、M1-M8 里程碑说明表,以及风险评估与缓解策略矩阵。
|
||||
|
||||
原始来源: 无直接对应(新增规划文档)
|
||||
|
||||
---
|
||||
|
||||
## 测试金字塔
|
||||
|
||||
```
|
||||
[E2E: 20]
|
||||
/ CliTests \
|
||||
/ OneShotMode \
|
||||
/ InteractiveRepl \
|
||||
==================
|
||||
[集成: 60]
|
||||
/ QueryPipeline \
|
||||
/ McpIntegration \
|
||||
/ LspIntegration \
|
||||
/ BridgeIntegration \
|
||||
/ TaskManager Plugins \
|
||||
=================================
|
||||
[单元: 120]
|
||||
Engine / Tools / Commands /
|
||||
ApiProviders / Mcp / Lsp /
|
||||
Services / State
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
**测试替身策略**
|
||||
|
||||
所有外部依赖(HTTP 客户端、文件系统、进程管理)通过接口注入,测试中由 NSubstitute 的 `Substitute.For<T>()` 替换,确保单元测试完全隔离,不依赖网络或本地环境。
|
||||
|
||||
**AOT 兼容优先**
|
||||
|
||||
构建配置全面启用 `AotPublish`、`PublishTrimmed`、`EnableAotAnalyzer`,确保代码在编译期就暴露反射问题,而不是在生产运行时崩溃。JSON 序列化通过 Source Generator 生成,完全不依赖运行时反射。
|
||||
|
||||
**平台矩阵覆盖**
|
||||
|
||||
`build.sh` 对 `osx-arm64`、`osx-x64`、`linux-x64`、`linux-arm64`、`win-x64` 五个 RID 各生成独立的原生二进制,与原始 Bun 构建脚本的单平台模式相比,构建产物更小、启动更快。
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [原始代码映射 — 测试与构建](reference/原始代码映射-测试与构建.md)
|
||||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||||
- [核心模块设计 — 工具系统](../核心模块设计/核心模块设计-工具系统.md)
|
||||
- [基础设施设计](../基础设施设计/基础设施设计.md)
|
||||
31
easy-code.slnx
Normal file
31
easy-code.slnx
Normal file
@ -0,0 +1,31 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src\FreeCode\FreeCode.csproj" />
|
||||
<Project Path="src\FreeCode.Core\FreeCode.Core.csproj" />
|
||||
<Project Path="src\FreeCode.Engine\FreeCode.Engine.csproj" />
|
||||
<Project Path="src\FreeCode.Tools\FreeCode.Tools.csproj" />
|
||||
<Project Path="src\FreeCode.Commands\FreeCode.Commands.csproj" />
|
||||
<Project Path="src\FreeCode.ApiProviders\FreeCode.ApiProviders.csproj" />
|
||||
<Project Path="src\FreeCode.Mcp\FreeCode.Mcp.csproj" />
|
||||
<Project Path="src\FreeCode.Lsp\FreeCode.Lsp.csproj" />
|
||||
<Project Path="src\FreeCode.Bridge\FreeCode.Bridge.csproj" />
|
||||
<Project Path="src\FreeCode.Services\FreeCode.Services.csproj" />
|
||||
<Project Path="src\FreeCode.Tasks\FreeCode.Tasks.csproj" />
|
||||
<Project Path="src\FreeCode.Skills\FreeCode.Skills.csproj" />
|
||||
<Project Path="src\FreeCode.Plugins\FreeCode.Plugins.csproj" />
|
||||
<Project Path="src\FreeCode.Features\FreeCode.Features.csproj" />
|
||||
<Project Path="src\FreeCode.State\FreeCode.State.csproj" />
|
||||
<Project Path="src\FreeCode.TerminalUI\FreeCode.TerminalUI.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests\FreeCode.Core.Tests\FreeCode.Core.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Engine.Tests\FreeCode.Engine.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Tools.Tests\FreeCode.Tools.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Commands.Tests\FreeCode.Commands.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.ApiProviders.Tests\FreeCode.ApiProviders.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Mcp.Tests\FreeCode.Mcp.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Services.Tests\FreeCode.Services.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Tasks.Tests\FreeCode.Tasks.Tests.csproj" />
|
||||
<Project Path="tests\FreeCode.Integration.Tests\FreeCode.Integration.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
24
scripts/build.sh
Executable file
24
scripts/build.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOLUTION_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
VERSION="${1:-0.1.0}"
|
||||
|
||||
echo "Building free-code .NET v${VERSION}..."
|
||||
|
||||
RID_TARGETS=("osx-arm64" "osx-x64" "linux-x64" "linux-arm64" "win-x64")
|
||||
|
||||
for RID in "${RID_TARGETS[@]}"; do
|
||||
echo "Publishing for ${RID}..."
|
||||
dotnet publish "${SOLUTION_ROOT}/src/FreeCode/FreeCode.csproj" \
|
||||
-c Release \
|
||||
-r "${RID}" \
|
||||
/p:Version="${VERSION}" \
|
||||
/p:PublishAot=true \
|
||||
/p:PublishSingleFile=true \
|
||||
/p:PublishTrimmed=true \
|
||||
-o "${SOLUTION_ROOT}/dist/${RID}"
|
||||
done
|
||||
|
||||
echo "Build complete. Outputs in ${SOLUTION_ROOT}/dist/"
|
||||
55
scripts/install.sh
Executable file
55
scripts/install.sh
Executable file
@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO="free-code/free-code"
|
||||
INSTALL_DIR="${HOME}/.free-code/bin"
|
||||
|
||||
echo "Installing free-code..."
|
||||
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
case "${OS}" in
|
||||
darwin) OS="osx" ;;
|
||||
linux) OS="linux" ;;
|
||||
*) echo "Unsupported OS: ${OS}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "${ARCH}" in
|
||||
x86_64|amd64) ARCH="x64" ;;
|
||||
arm64|aarch64) ARCH="arm64" ;;
|
||||
*) echo "Unsupported architecture: ${ARCH}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
RID="${OS}-${ARCH}"
|
||||
BINARY_NAME="free-code"
|
||||
|
||||
if [ "${OS}" = "win" ]; then
|
||||
BINARY_NAME="free-code.exe"
|
||||
fi
|
||||
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
echo "Downloading ${RID} binary..."
|
||||
|
||||
echo "Building from source..."
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
dotnet publish "${SCRIPT_DIR}/../src/FreeCode/FreeCode.csproj" \
|
||||
-c Release \
|
||||
-r "${RID}" \
|
||||
/p:PublishAot=true \
|
||||
/p:PublishSingleFile=true \
|
||||
/p:PublishTrimmed=true \
|
||||
-o "${INSTALL_DIR}"
|
||||
|
||||
chmod +x "${INSTALL_DIR}/${BINARY_NAME}" 2>/dev/null || true
|
||||
|
||||
echo "Adding to PATH..."
|
||||
PROFILE_FILES=("${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile")
|
||||
for PROFILE in "${PROFILE_FILES[@]}"; do
|
||||
if [ -f "${PROFILE}" ] && ! grep -q "${INSTALL_DIR}" "${PROFILE}" 2>/dev/null; then
|
||||
printf 'export PATH="%s:$PATH"\n' "${INSTALL_DIR}" >> "${PROFILE}"
|
||||
echo "Added to ${PROFILE}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Installation complete. Run 'source ~/.bashrc' or start a new terminal."
|
||||
187
src/FreeCode.ApiProviders/AnthropicProvider.cs
Normal file
187
src/FreeCode.ApiProviders/AnthropicProvider.cs
Normal file
@ -0,0 +1,187 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
internal sealed record PendingToolUse(string Id, string Name, StringBuilder JsonBuilder);
|
||||
|
||||
public sealed class AnthropicProvider : FreeCode.Core.Interfaces.IApiProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string? _apiKey;
|
||||
private readonly string _model;
|
||||
|
||||
public AnthropicProvider(HttpClient? httpClient = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_baseUrl = Environment.GetEnvironmentVariable("ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com";
|
||||
_apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")
|
||||
?? Environment.GetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN");
|
||||
_model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-sonnet-4-6";
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> StreamAsync(ApiRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(_baseUrl.TrimEnd('/')), "/v1/messages"));
|
||||
httpRequest.Headers.TryAddWithoutValidation("anthropic-version", "2023-06-01");
|
||||
httpRequest.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
if (!string.IsNullOrWhiteSpace(_apiKey))
|
||||
{
|
||||
httpRequest.Headers.TryAddWithoutValidation("x-api-key", _apiKey);
|
||||
httpRequest.Headers.TryAddWithoutValidation("authorization", $"Bearer {_apiKey}");
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model = request.Model ?? _model,
|
||||
system = request.SystemPrompt,
|
||||
messages = request.Messages,
|
||||
tools = request.Tools,
|
||||
stream = true,
|
||||
max_tokens = 4096
|
||||
};
|
||||
|
||||
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var pendingToolUses = new Dictionary<int, PendingToolUse>();
|
||||
|
||||
await foreach (var data in ReadSseDataAsync(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), ct).ConfigureAwait(false))
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var type = typeProperty.GetString();
|
||||
switch (type)
|
||||
{
|
||||
case "content_block_start":
|
||||
if (root.TryGetProperty("content_block", out var contentBlock)
|
||||
&& contentBlock.TryGetProperty("type", out var blockType))
|
||||
{
|
||||
var blockTypeStr = blockType.GetString();
|
||||
var index = root.TryGetProperty("index", out var indexProp) ? indexProp.GetInt32() : -1;
|
||||
|
||||
if (string.Equals(blockTypeStr, "tool_use", StringComparison.OrdinalIgnoreCase) && index >= 0)
|
||||
{
|
||||
pendingToolUses[index] = new PendingToolUse(
|
||||
contentBlock.TryGetProperty("id", out var idProp) ? idProp.GetString() ?? string.Empty : string.Empty,
|
||||
contentBlock.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? string.Empty : string.Empty,
|
||||
new StringBuilder());
|
||||
}
|
||||
else if (string.Equals(blockTypeStr, "text", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (contentBlock.TryGetProperty("text", out var startText))
|
||||
{
|
||||
var startTextValue = startText.GetString();
|
||||
if (!string.IsNullOrEmpty(startTextValue))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(startTextValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "content_block_delta":
|
||||
if (root.TryGetProperty("delta", out var delta))
|
||||
{
|
||||
var deltaIndex = root.TryGetProperty("index", out var di) ? di.GetInt32() : -1;
|
||||
|
||||
if (delta.TryGetProperty("text", out var textDelta))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(textDelta.GetString() ?? string.Empty);
|
||||
}
|
||||
else if (delta.TryGetProperty("partial_json", out var partialJson) && deltaIndex >= 0 && pendingToolUses.TryGetValue(deltaIndex, out var pending))
|
||||
{
|
||||
pending.JsonBuilder.Append(partialJson.GetString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "content_block_stop":
|
||||
var stopIndex = root.TryGetProperty("index", out var si) ? si.GetInt32() : -1;
|
||||
if (stopIndex >= 0 && pendingToolUses.TryGetValue(stopIndex, out var completed))
|
||||
{
|
||||
pendingToolUses.Remove(stopIndex);
|
||||
var rawJson = completed.JsonBuilder.ToString();
|
||||
JsonElement toolInput;
|
||||
if (string.IsNullOrWhiteSpace(rawJson))
|
||||
{
|
||||
toolInput = JsonDocument.Parse("{}").RootElement.Clone();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var inputDoc = JsonDocument.Parse(rawJson);
|
||||
toolInput = inputDoc.RootElement.Clone();
|
||||
}
|
||||
|
||||
yield return new SDKMessage.ToolUseStart(completed.Id, completed.Name, toolInput);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "message_stop":
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> ReadSseDataAsync(Stream stream, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: false);
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (line.Length == 0)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var data = line.AsSpan(5).TrimStart();
|
||||
if (data.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
buffer.Append(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/FreeCode.ApiProviders/ApiProviderRouter.cs
Normal file
37
src/FreeCode.ApiProviders/ApiProviderRouter.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Interfaces;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
public class ApiProviderRouter : IApiProviderRouter
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ApiProviderType _activeProvider;
|
||||
|
||||
public ApiProviderRouter(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
_activeProvider = DetectProvider();
|
||||
}
|
||||
|
||||
private static ApiProviderType DetectProvider()
|
||||
{
|
||||
if (Env("CLAUDE_CODE_USE_OPENAI") == "1") return ApiProviderType.OpenAICodex;
|
||||
if (Env("CLAUDE_CODE_USE_BEDROCK") == "1") return ApiProviderType.AwsBedrock;
|
||||
if (Env("CLAUDE_CODE_USE_VERTEX") == "1") return ApiProviderType.GoogleVertex;
|
||||
if (Env("CLAUDE_CODE_USE_FOUNDRY") == "1") return ApiProviderType.AnthropicFoundry;
|
||||
return ApiProviderType.Anthropic;
|
||||
}
|
||||
|
||||
public IApiProvider GetActiveProvider() => _activeProvider switch
|
||||
{
|
||||
ApiProviderType.Anthropic => (AnthropicProvider)_services.GetService(typeof(AnthropicProvider))!,
|
||||
ApiProviderType.OpenAICodex => (CodexProvider)_services.GetService(typeof(CodexProvider))!,
|
||||
ApiProviderType.AwsBedrock => (BedrockProvider)_services.GetService(typeof(BedrockProvider))!,
|
||||
ApiProviderType.GoogleVertex => (VertexProvider)_services.GetService(typeof(VertexProvider))!,
|
||||
ApiProviderType.AnthropicFoundry => (FoundryProvider)_services.GetService(typeof(FoundryProvider))!,
|
||||
_ => throw new InvalidOperationException($"Unknown provider: {_activeProvider}")
|
||||
};
|
||||
|
||||
private static string? Env(string name) => Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
232
src/FreeCode.ApiProviders/BedrockProvider.cs
Normal file
232
src/FreeCode.ApiProviders/BedrockProvider.cs
Normal file
@ -0,0 +1,232 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FreeCode.Core.Interfaces;
|
||||
using FreeCode.Core.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
public sealed class BedrockProvider : FreeCode.Core.Interfaces.IApiProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _region;
|
||||
private readonly string _modelPrefix;
|
||||
private readonly string? _bearerToken;
|
||||
private readonly string? _apiKey;
|
||||
private readonly bool _skipAuth;
|
||||
private readonly IRateLimitService? _rateLimitService;
|
||||
|
||||
public BedrockProvider(HttpClient? httpClient = null, IConfiguration? configuration = null, IRateLimitService? rateLimitService = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_rateLimitService = rateLimitService;
|
||||
_region = GetSetting(configuration, "AWS_REGION", "Bedrock:Region")
|
||||
?? GetSetting(configuration, "AWS_DEFAULT_REGION", "Bedrock:DefaultRegion")
|
||||
?? "us-east-1";
|
||||
_baseUrl = GetSetting(configuration, "ANTHROPIC_BEDROCK_BASE_URL", "Bedrock:BaseUrl")
|
||||
?? $"https://bedrock-runtime.{_region}.amazonaws.com";
|
||||
_modelPrefix = GetSetting(configuration, "AWS_BEDROCK_MODEL_PREFIX", "Bedrock:ModelPrefix") ?? "us.anthropic";
|
||||
_bearerToken = ResolveBearerToken(configuration);
|
||||
_apiKey = GetSetting(configuration, "ANTHROPIC_BEDROCK_API_KEY", "Bedrock:ApiKey");
|
||||
_skipAuth = string.Equals(GetSetting(configuration, "CLAUDE_CODE_SKIP_BEDROCK_AUTH", "Bedrock:SkipAuth"), "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> StreamAsync(ApiRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var model = request.Model ?? "claude-sonnet-4-6";
|
||||
var modelId = model.Contains(':', StringComparison.Ordinal) ? model : $"{_modelPrefix}.{model}";
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(_baseUrl.TrimEnd('/')), $"/model/{Uri.EscapeDataString(modelId)}/invoke-with-response-stream"));
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
if (!_skipAuth && !string.IsNullOrWhiteSpace(_bearerToken))
|
||||
{
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _bearerToken);
|
||||
}
|
||||
else if (!_skipAuth && !string.IsNullOrWhiteSpace(_apiKey))
|
||||
{
|
||||
httpRequest.Headers.Add("x-api-key", _apiKey);
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
anthropic_version = "2023-06-01",
|
||||
system = request.SystemPrompt,
|
||||
messages = request.Messages,
|
||||
tools = request.Tools,
|
||||
max_tokens = 4096,
|
||||
stream = true
|
||||
};
|
||||
|
||||
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
var responseHeaders = ToHeaderDictionary(response.Headers, response.Content.Headers);
|
||||
if (_rateLimitService?.CanProceed(responseHeaders) == false)
|
||||
{
|
||||
throw CreateRateLimitException(responseHeaders);
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await foreach (var data in ReadSseDataAsync(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), ct).ConfigureAwait(false))
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (typeProperty.GetString())
|
||||
{
|
||||
case "content_block_delta":
|
||||
if (root.TryGetProperty("delta", out var delta))
|
||||
{
|
||||
if (delta.TryGetProperty("text", out var text))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(text.GetString() ?? string.Empty);
|
||||
}
|
||||
else if (delta.TryGetProperty("partial_json", out var partialJson))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(partialJson.GetString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "content_block_start":
|
||||
if (root.TryGetProperty("content_block", out var contentBlock)
|
||||
&& contentBlock.TryGetProperty("type", out var blockType)
|
||||
&& string.Equals(blockType.GetString(), "tool_use", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var input = contentBlock.TryGetProperty("input", out var inputProperty)
|
||||
? inputProperty.Clone()
|
||||
: JsonDocument.Parse("{}").RootElement.Clone();
|
||||
|
||||
yield return new SDKMessage.ToolUseStart(
|
||||
contentBlock.TryGetProperty("id", out var idProperty) ? idProperty.GetString() ?? string.Empty : string.Empty,
|
||||
contentBlock.TryGetProperty("name", out var nameProperty) ? nameProperty.GetString() ?? string.Empty : string.Empty,
|
||||
input);
|
||||
}
|
||||
|
||||
break;
|
||||
case "message_stop":
|
||||
case "response.completed":
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveBearerToken(IConfiguration? configuration)
|
||||
{
|
||||
var credentialsFromEnv = GetSetting(configuration,
|
||||
"AWS_BEARER_TOKEN_BEDROCK",
|
||||
"AWS_SESSION_TOKEN",
|
||||
"AWS_ACCESS_TOKEN",
|
||||
"Bedrock:BearerToken");
|
||||
if (!string.IsNullOrWhiteSpace(credentialsFromEnv))
|
||||
{
|
||||
return credentialsFromEnv;
|
||||
}
|
||||
|
||||
var accessKey = GetSetting(configuration, "AWS_ACCESS_KEY_ID", "Bedrock:AccessKeyId");
|
||||
var secretKey = GetSetting(configuration, "AWS_SECRET_ACCESS_KEY", "Bedrock:SecretAccessKey");
|
||||
if (!string.IsNullOrWhiteSpace(accessKey) && !string.IsNullOrWhiteSpace(secretKey))
|
||||
{
|
||||
var sessionToken = GetSetting(configuration, "AWS_SESSION_TOKEN", "Bedrock:SessionToken");
|
||||
return string.IsNullOrWhiteSpace(sessionToken)
|
||||
? $"{accessKey}:{secretKey}"
|
||||
: $"{accessKey}:{secretKey}:{sessionToken}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetSetting(IConfiguration? configuration, params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(key) ?? configuration?[key];
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ToHeaderDictionary(HttpResponseHeaders responseHeaders, HttpContentHeaders contentHeaders)
|
||||
{
|
||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var header in responseHeaders)
|
||||
{
|
||||
headers[header.Key] = string.Join(",", header.Value);
|
||||
}
|
||||
|
||||
foreach (var header in contentHeaders)
|
||||
{
|
||||
headers[header.Key] = string.Join(",", header.Value);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private Exception CreateRateLimitException(IReadOnlyDictionary<string, string> headers)
|
||||
{
|
||||
var retryAfter = _rateLimitService?.GetRetryAfter(headers as IDictionary<string, string> ?? new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase));
|
||||
return retryAfter is { } delay && delay > TimeSpan.Zero
|
||||
? new HttpRequestException($"Bedrock rate limit exceeded. Retry after {delay.TotalSeconds:F0} seconds.")
|
||||
: new HttpRequestException("Bedrock rate limit exceeded.");
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> ReadSseDataAsync(Stream stream, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: false);
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (line.Length == 0)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var data = line.AsSpan(5).TrimStart();
|
||||
if (data.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
buffer.Append(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/FreeCode.ApiProviders/CodexProvider.cs
Normal file
151
src/FreeCode.ApiProviders/CodexProvider.cs
Normal file
@ -0,0 +1,151 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
public sealed class CodexProvider : FreeCode.Core.Interfaces.IApiProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string? _token;
|
||||
private readonly string _model;
|
||||
|
||||
public CodexProvider(HttpClient? httpClient = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_baseUrl = Environment.GetEnvironmentVariable("OPENAI_BASE_URL") ?? "https://api.openai.com";
|
||||
_token = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
|
||||
?? Environment.GetEnvironmentVariable("OPENAI_BEARER_TOKEN")
|
||||
?? Environment.GetEnvironmentVariable("CLAUDE_CODE_OPENAI_API_KEY");
|
||||
_model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5.3-codex";
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> StreamAsync(ApiRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(_baseUrl.TrimEnd('/')), "/v1/responses"));
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
if (!string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model = request.Model ?? _model,
|
||||
input = request.Messages,
|
||||
instructions = request.SystemPrompt,
|
||||
tools = request.Tools,
|
||||
stream = true
|
||||
};
|
||||
|
||||
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await foreach (var data in ReadSseDataAsync(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), ct).ConfigureAwait(false))
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
var type = root.TryGetProperty("type", out var typeProperty) ? typeProperty.GetString() : null;
|
||||
switch (type)
|
||||
{
|
||||
case "response.output_text.delta":
|
||||
if (root.TryGetProperty("delta", out var delta) || root.TryGetProperty("text", out delta))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(delta.GetString() ?? string.Empty);
|
||||
}
|
||||
|
||||
break;
|
||||
case "response.output_item.added":
|
||||
if (root.TryGetProperty("item", out var item)
|
||||
&& item.TryGetProperty("type", out var itemType)
|
||||
&& (string.Equals(itemType.GetString(), "function_call", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(itemType.GetString(), "tool_call", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var input = item.TryGetProperty("arguments", out var args)
|
||||
? ParseJsonOrEmpty(args.GetString())
|
||||
: JsonDocument.Parse("{}").RootElement.Clone();
|
||||
|
||||
yield return new SDKMessage.ToolUseStart(
|
||||
item.TryGetProperty("id", out var idProperty) ? idProperty.GetString() ?? string.Empty : string.Empty,
|
||||
item.TryGetProperty("name", out var nameProperty) ? nameProperty.GetString() ?? string.Empty : string.Empty,
|
||||
input);
|
||||
}
|
||||
|
||||
break;
|
||||
case "response.completed":
|
||||
case "response.failed":
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonElement ParseJsonOrEmpty(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return JsonDocument.Parse("{}").RootElement.Clone();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonDocument.Parse(json).RootElement.Clone();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return JsonDocument.Parse("{}").RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> ReadSseDataAsync(Stream stream, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: false);
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (line.Length == 0)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var data = line.AsSpan(5).TrimStart();
|
||||
if (data.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
buffer.Append(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/FreeCode.ApiProviders/FoundryProvider.cs
Normal file
141
src/FreeCode.ApiProviders/FoundryProvider.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
public sealed class FoundryProvider : FreeCode.Core.Interfaces.IApiProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string? _apiKey;
|
||||
private readonly string _deployment;
|
||||
|
||||
public FoundryProvider(HttpClient? httpClient = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_baseUrl = Environment.GetEnvironmentVariable("ANTHROPIC_FOUNDRY_BASE_URL")
|
||||
?? Environment.GetEnvironmentVariable("ANTHROPIC_BASE_URL")
|
||||
?? "https://api.anthropic.com";
|
||||
_apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_FOUNDRY_API_KEY")
|
||||
?? Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
|
||||
_deployment = Environment.GetEnvironmentVariable("ANTHROPIC_FOUNDRY_DEPLOYMENT") ?? "default";
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> StreamAsync(ApiRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var model = request.Model ?? _deployment;
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(_baseUrl.TrimEnd('/')), $"/v1/deployments/{Uri.EscapeDataString(model)}/messages"));
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
if (!string.IsNullOrWhiteSpace(_apiKey))
|
||||
{
|
||||
httpRequest.Headers.TryAddWithoutValidation("x-api-key", _apiKey);
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model,
|
||||
system = request.SystemPrompt,
|
||||
messages = request.Messages,
|
||||
tools = request.Tools,
|
||||
stream = true,
|
||||
max_tokens = 4096
|
||||
};
|
||||
|
||||
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await foreach (var data in ReadSseDataAsync(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), ct).ConfigureAwait(false))
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (typeProperty.GetString())
|
||||
{
|
||||
case "content_block_delta":
|
||||
if (root.TryGetProperty("delta", out var delta) && delta.TryGetProperty("text", out var text))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(text.GetString() ?? string.Empty);
|
||||
}
|
||||
|
||||
break;
|
||||
case "content_block_start":
|
||||
if (root.TryGetProperty("content_block", out var contentBlock)
|
||||
&& contentBlock.TryGetProperty("type", out var blockType)
|
||||
&& string.Equals(blockType.GetString(), "tool_use", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var input = contentBlock.TryGetProperty("input", out var inputProperty)
|
||||
? inputProperty.Clone()
|
||||
: JsonDocument.Parse("{}").RootElement.Clone();
|
||||
|
||||
yield return new SDKMessage.ToolUseStart(
|
||||
contentBlock.TryGetProperty("id", out var idProperty) ? idProperty.GetString() ?? string.Empty : string.Empty,
|
||||
contentBlock.TryGetProperty("name", out var nameProperty) ? nameProperty.GetString() ?? string.Empty : string.Empty,
|
||||
input);
|
||||
}
|
||||
|
||||
break;
|
||||
case "message_stop":
|
||||
case "response.completed":
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> ReadSseDataAsync(Stream stream, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: false);
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (line.Length == 0)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var data = line.AsSpan(5).TrimStart();
|
||||
if (data.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
buffer.Append(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/FreeCode.ApiProviders/FreeCode.ApiProviders.csproj
Normal file
10
src/FreeCode.ApiProviders/FreeCode.ApiProviders.csproj
Normal file
@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>FreeCode.ApiProviders</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FreeCode.Core\FreeCode.Core.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
17
src/FreeCode.ApiProviders/ServiceCollectionExtensions.cs
Normal file
17
src/FreeCode.ApiProviders/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddFreeCodeApiProviders(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<FreeCode.Core.Interfaces.IApiProviderRouter, ApiProviderRouter>();
|
||||
services.AddSingleton<AnthropicProvider>();
|
||||
services.AddSingleton<CodexProvider>();
|
||||
services.AddSingleton<BedrockProvider>();
|
||||
services.AddSingleton<VertexProvider>();
|
||||
services.AddSingleton<FoundryProvider>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
246
src/FreeCode.ApiProviders/VertexProvider.cs
Normal file
246
src/FreeCode.ApiProviders/VertexProvider.cs
Normal file
@ -0,0 +1,246 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FreeCode.Core.Interfaces;
|
||||
using FreeCode.Core.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace FreeCode.ApiProviders;
|
||||
|
||||
public sealed class VertexProvider : FreeCode.Core.Interfaces.IApiProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _projectId;
|
||||
private readonly string _location;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string? _accessToken;
|
||||
private readonly IRateLimitService? _rateLimitService;
|
||||
|
||||
public VertexProvider(HttpClient? httpClient = null, IConfiguration? configuration = null, IRateLimitService? rateLimitService = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_rateLimitService = rateLimitService;
|
||||
_projectId = GetSetting(configuration,
|
||||
"GOOGLE_CLOUD_PROJECT",
|
||||
"GCP_PROJECT",
|
||||
"GOOGLE_PROJECT_ID",
|
||||
"Vertex:ProjectId")
|
||||
?? "unknown-project";
|
||||
_location = GetSetting(configuration, "VERTEX_LOCATION", "GOOGLE_CLOUD_LOCATION", "Vertex:Location")
|
||||
?? "us-central1";
|
||||
_baseUrl = GetSetting(configuration, "VERTEX_BASE_URL", "Vertex:BaseUrl") ?? "https://aiplatform.googleapis.com";
|
||||
_accessToken = ResolveAccessToken(configuration);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SDKMessage> StreamAsync(ApiRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var model = request.Model ?? "claude-sonnet-4-6";
|
||||
var url = new Uri(new Uri(_baseUrl.TrimEnd('/')), $"/v1/projects/{Uri.EscapeDataString(_projectId)}/locations/{Uri.EscapeDataString(_location)}/publishers/anthropic/models/{Uri.EscapeDataString(model)}:streamRawPredict");
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
if (!string.IsNullOrWhiteSpace(_accessToken))
|
||||
{
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
system = request.SystemPrompt,
|
||||
contents = request.Messages,
|
||||
tools = request.Tools,
|
||||
stream = true
|
||||
};
|
||||
|
||||
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
var responseHeaders = ToHeaderDictionary(response.Headers, response.Content.Headers);
|
||||
if (_rateLimitService?.CanProceed(responseHeaders) == false)
|
||||
{
|
||||
throw CreateRateLimitException(responseHeaders);
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await foreach (var data in ReadSseDataAsync(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), ct).ConfigureAwait(false))
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (typeProperty.GetString())
|
||||
{
|
||||
case "content_block_delta":
|
||||
if (root.TryGetProperty("delta", out var delta) && delta.TryGetProperty("text", out var text))
|
||||
{
|
||||
yield return new SDKMessage.StreamingDelta(text.GetString() ?? string.Empty);
|
||||
}
|
||||
|
||||
break;
|
||||
case "content_block_start":
|
||||
if (root.TryGetProperty("content_block", out var contentBlock)
|
||||
&& contentBlock.TryGetProperty("type", out var blockType)
|
||||
&& string.Equals(blockType.GetString(), "tool_use", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var input = contentBlock.TryGetProperty("input", out var inputProperty)
|
||||
? inputProperty.Clone()
|
||||
: JsonDocument.Parse("{}").RootElement.Clone();
|
||||
|
||||
yield return new SDKMessage.ToolUseStart(
|
||||
contentBlock.TryGetProperty("id", out var idProperty) ? idProperty.GetString() ?? string.Empty : string.Empty,
|
||||
contentBlock.TryGetProperty("name", out var nameProperty) ? nameProperty.GetString() ?? string.Empty : string.Empty,
|
||||
input);
|
||||
}
|
||||
|
||||
break;
|
||||
case "message_stop":
|
||||
case "response.completed":
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveAccessToken(IConfiguration? configuration)
|
||||
{
|
||||
var accessToken = GetSetting(configuration,
|
||||
"GOOGLE_OAUTH_ACCESS_TOKEN",
|
||||
"GCP_ACCESS_TOKEN",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS_ACCESS_TOKEN",
|
||||
"Vertex:AccessToken");
|
||||
if (!string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
var credentialsJson = GetSetting(configuration, "GOOGLE_APPLICATION_CREDENTIALS_JSON", "Vertex:CredentialsJson");
|
||||
if (!string.IsNullOrWhiteSpace(credentialsJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(credentialsJson);
|
||||
if (document.RootElement.TryGetProperty("access_token", out var tokenElement))
|
||||
{
|
||||
return tokenElement.GetString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var credentialsPath = GetSetting(configuration, "GOOGLE_APPLICATION_CREDENTIALS", "Vertex:CredentialsPath");
|
||||
if (!string.IsNullOrWhiteSpace(credentialsPath) && File.Exists(credentialsPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(credentialsPath));
|
||||
if (document.RootElement.TryGetProperty("access_token", out var tokenElement))
|
||||
{
|
||||
return tokenElement.GetString();
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetSetting(IConfiguration? configuration, params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(key) ?? configuration?[key];
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ToHeaderDictionary(HttpResponseHeaders responseHeaders, HttpContentHeaders contentHeaders)
|
||||
{
|
||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var header in responseHeaders)
|
||||
{
|
||||
headers[header.Key] = string.Join(",", header.Value);
|
||||
}
|
||||
|
||||
foreach (var header in contentHeaders)
|
||||
{
|
||||
headers[header.Key] = string.Join(",", header.Value);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private Exception CreateRateLimitException(IReadOnlyDictionary<string, string> headers)
|
||||
{
|
||||
var retryAfter = _rateLimitService?.GetRetryAfter(headers as IDictionary<string, string> ?? new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase));
|
||||
return retryAfter is { } delay && delay > TimeSpan.Zero
|
||||
? new HttpRequestException($"Vertex rate limit exceeded. Retry after {delay.TotalSeconds:F0} seconds.")
|
||||
: new HttpRequestException("Vertex rate limit exceeded.");
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> ReadSseDataAsync(Stream stream, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: false);
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (line.Length == 0)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var data = line.AsSpan(5).TrimStart();
|
||||
if (data.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
buffer.Append(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/FreeCode.Bridge/BridgeApiClient.cs
Normal file
120
src/FreeCode.Bridge/BridgeApiClient.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System.Net.Http.Json;
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Bridge;
|
||||
|
||||
public sealed class BridgeApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public BridgeApiClient(HttpClient? httpClient = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
public async Task<BridgeStatusInfo> GetStatusAsync(BridgeConfig config, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(Root(config, "bridge/status"), ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ReadJsonAsync<BridgeStatusInfo>(response, ct).ConfigureAwait(false) ?? new BridgeStatusInfo(BridgeStatus.Idle);
|
||||
}
|
||||
|
||||
public async Task PostEventAsync(BridgeConfig config, object payload, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, "bridge/events"), payload, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task SendSessionUpdateAsync(BridgeConfig config, string sessionId, object payload, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/sessions/{Uri.EscapeDataString(sessionId)}/update"), payload, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<object?> RequestPermissionAsync(BridgeConfig config, string sessionId, object payload, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/sessions/{Uri.EscapeDataString(sessionId)}/permissions/request"), payload, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ReadJsonAsync<object>(response, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<BridgeStatusInfo> RegisterBridgeEnvironmentAsync(BridgeConfig config, BridgeEnvironment environment, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, "bridge/environments"), environment, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ReadJsonAsync<BridgeStatusInfo>(response, ct).ConfigureAwait(false) ?? new BridgeStatusInfo(BridgeStatus.Registered);
|
||||
}
|
||||
|
||||
public async Task<WorkItem?> PollForWorkAsync(BridgeConfig config, string environmentId, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(Root(config, $"bridge/environments/{Uri.EscapeDataString(environmentId)}/work"), ct).ConfigureAwait(false);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ReadJsonAsync<WorkItem>(response, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task AcknowledgeWorkAsync(BridgeConfig config, string workId, string sessionToken, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/work/{Uri.EscapeDataString(workId)}/ack"), new { sessionToken }, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<SessionHandle> SpawnSessionAsync(BridgeConfig config, SessionSpawnOptions options, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, "bridge/sessions"), options, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ReadJsonAsync<SessionHandle>(response, ct).ConfigureAwait(false) ?? new SessionHandle(options.Environment.Id, string.Empty);
|
||||
}
|
||||
|
||||
public async Task SendPermissionResponseAsync(BridgeConfig config, string sessionId, PermissionResponse responseBody, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/sessions/{Uri.EscapeDataString(sessionId)}/permissions"), responseBody, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task HeartbeatAsync(BridgeConfig config, string workId, string sessionToken, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/work/{Uri.EscapeDataString(workId)}/heartbeat"), new { sessionToken }, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task StopWorkAsync(BridgeConfig config, string workId, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await _httpClient.DeleteAsync(Root(config, $"bridge/work/{Uri.EscapeDataString(workId)}"), ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task DeregisterEnvironmentAsync(BridgeConfig config, string environmentId, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await _httpClient.DeleteAsync(Root(config, $"bridge/environments/{Uri.EscapeDataString(environmentId)}"), ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendJsonAsync(HttpMethod method, Uri uri, object payload, CancellationToken ct)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, uri)
|
||||
{
|
||||
Content = JsonContent.Create(payload)
|
||||
};
|
||||
|
||||
return await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Uri Root(BridgeConfig config, string relativePath)
|
||||
{
|
||||
var baseUri = new Uri(config.BaseUrl.TrimEnd('/') + "/", UriKind.Absolute);
|
||||
return new Uri(baseUri, relativePath);
|
||||
}
|
||||
|
||||
private static async Task<T?> ReadJsonAsync<T>(HttpResponseMessage response, CancellationToken ct)
|
||||
{
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
|
||||
return (T?)await response.Content.ReadFromJsonAsync(typeof(T), SourceGenerationContext.Default, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
300
src/FreeCode.Bridge/BridgeService.cs
Normal file
300
src/FreeCode.Bridge/BridgeService.cs
Normal file
@ -0,0 +1,300 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Interfaces;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Bridge;
|
||||
|
||||
public sealed class BridgeService : IBridgeService, IAsyncDisposable
|
||||
{
|
||||
private const string SessionTokenKey = "sessionToken";
|
||||
private const string PromptKey = "prompt";
|
||||
private const string ModelKey = "model";
|
||||
private const string AgentTypeKey = "agentType";
|
||||
private const string WorkingDirectoryKey = "workingDirectory";
|
||||
|
||||
private readonly BridgeApiClient _client;
|
||||
private readonly BridgeConfig _config;
|
||||
private readonly ConcurrentDictionary<string, SessionHandle> _activeSessions = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _startGate = new(1, 1);
|
||||
private CancellationTokenSource? _runCts;
|
||||
private Task? _runLoop;
|
||||
private BridgeEnvironment? _environment;
|
||||
private SessionHandle? _session;
|
||||
private BridgeStatusInfo _status = new(BridgeStatus.Idle);
|
||||
|
||||
public BridgeService()
|
||||
: this(new BridgeApiClient(), new BridgeConfig(Environment.GetEnvironmentVariable("FREECODE_BRIDGE_URL") ?? "http://127.0.0.1:8787"))
|
||||
{
|
||||
}
|
||||
|
||||
public BridgeService(BridgeApiClient client, BridgeConfig config)
|
||||
{
|
||||
_client = client;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public BridgeStatus Status => _status.Status;
|
||||
|
||||
public async Task<BridgeEnvironment> RegisterEnvironmentAsync()
|
||||
{
|
||||
var environment = _environment ??= new BridgeEnvironment(Guid.NewGuid().ToString("N"), Environment.MachineName, SpawnMode.SingleSession, Environment.CurrentDirectory);
|
||||
_status = await _client.RegisterBridgeEnvironmentAsync(_config, environment).ConfigureAwait(false);
|
||||
return environment;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _startGate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_runLoop is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
_runLoop = Task.Run(() => RunAsync(_runCts.Token), CancellationToken.None);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_startGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (_runCts is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runCts.Cancel();
|
||||
if (_runLoop is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _runLoop.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
if (!(_runCts?.IsCancellationRequested ?? false))
|
||||
{
|
||||
Console.Error.WriteLine($"Bridge run loop error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Bridge run loop error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_runLoop = null;
|
||||
_runCts.Dispose();
|
||||
_runCts = null;
|
||||
}
|
||||
|
||||
public async Task<WorkItem?> PollForWorkAsync(CancellationToken ct)
|
||||
{
|
||||
var environment = _environment;
|
||||
if (environment is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await _client.PollForWorkAsync(_config, environment.Id, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SessionHandle> SpawnSessionAsync(SessionSpawnOptions options)
|
||||
{
|
||||
_session = await _client.SpawnSessionAsync(_config, options).ConfigureAwait(false);
|
||||
_activeSessions[_session.SessionId] = _session;
|
||||
_status = new BridgeStatusInfo(BridgeStatus.Attached);
|
||||
return _session;
|
||||
}
|
||||
|
||||
public async Task AcknowledgeWorkAsync(string workId, string sessionToken)
|
||||
{
|
||||
await _client.AcknowledgeWorkAsync(_config, workId, sessionToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SendPermissionResponseAsync(string sessionId, PermissionResponse response)
|
||||
{
|
||||
await _client.SendPermissionResponseAsync(_config, sessionId, response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task HeartbeatAsync(string workId, string sessionToken)
|
||||
{
|
||||
await _client.HeartbeatAsync(_config, workId, sessionToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StopWorkAsync(string workId)
|
||||
{
|
||||
await _client.StopWorkAsync(_config, workId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeregisterEnvironmentAsync()
|
||||
{
|
||||
if (_environment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _client.DeregisterEnvironmentAsync(_config, _environment.Id).ConfigureAwait(false);
|
||||
_status = new BridgeStatusInfo(BridgeStatus.Idle);
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct = default)
|
||||
{
|
||||
await RegisterEnvironmentAsync().ConfigureAwait(false);
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var work = await PollForWorkAsync(ct).ConfigureAwait(false);
|
||||
if (work is null)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), ct).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var secret = DecodeWorkSecret(work);
|
||||
await AcknowledgeWorkAsync(work.Id, secret.SessionToken).ConfigureAwait(false);
|
||||
|
||||
var sessionDirectory = PrepareSessionDirectory(work, secret.WorkingDirectory);
|
||||
var environment = GetSessionEnvironment(sessionDirectory);
|
||||
var session = await SpawnSessionAsync(new SessionSpawnOptions(environment, secret.Prompt, secret.Model, secret.AgentType)).ConfigureAwait(false);
|
||||
|
||||
_activeSessions[work.Id] = session;
|
||||
_ = MonitorSessionAsync(work, session, secret.SessionToken, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorSessionAsync(WorkItem work, SessionHandle session, string sessionToken, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
_session = session;
|
||||
await HeartbeatAsync(work.Id, sessionToken).ConfigureAwait(false);
|
||||
await _client.SendSessionUpdateAsync(_config, session.SessionId, new { workId = work.Id, status = "running" }, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
await SafeSendSessionUpdateAsync(session.SessionId, new { workId = work.Id, status = "cancelled" }).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SafeSendSessionUpdateAsync(session.SessionId, new { workId = work.Id, status = "failed", error = ex.Message }).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activeSessions.TryRemove(work.Id, out _);
|
||||
|
||||
if (_session?.SessionId == session.SessionId)
|
||||
{
|
||||
_session = null;
|
||||
}
|
||||
|
||||
await SafeSendSessionUpdateAsync(session.SessionId, new { workId = work.Id, status = "completed" }).ConfigureAwait(false);
|
||||
_status = _activeSessions.IsEmpty ? new BridgeStatusInfo(BridgeStatus.Registered) : new BridgeStatusInfo(BridgeStatus.Attached);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PollForCommandsAsync(CancellationToken ct = default)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var work = await PollForWorkAsync(ct).ConfigureAwait(false);
|
||||
if (work is null)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), ct).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(work.SessionToken))
|
||||
{
|
||||
await AcknowledgeWorkAsync(work.Id, work.SessionToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendResponseAsync(object response)
|
||||
{
|
||||
if (_session is not null)
|
||||
{
|
||||
await _client.SendSessionUpdateAsync(_config, _session.SessionId, response).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BroadcastEventAsync(object evt)
|
||||
{
|
||||
await _client.PostEventAsync(_config, evt).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private BridgeEnvironment GetSessionEnvironment(string sessionDirectory)
|
||||
{
|
||||
var environment = _environment ?? new BridgeEnvironment(Guid.NewGuid().ToString("N"), Environment.MachineName, SpawnMode.SingleSession, Environment.CurrentDirectory);
|
||||
return environment with { WorkingDirectory = sessionDirectory };
|
||||
}
|
||||
|
||||
private string PrepareSessionDirectory(WorkItem work, string? workingDirectory)
|
||||
{
|
||||
var rootDirectory = string.IsNullOrWhiteSpace(workingDirectory)
|
||||
? _environment?.WorkingDirectory ?? Environment.CurrentDirectory
|
||||
: workingDirectory;
|
||||
|
||||
var safeWorkId = string.Concat(work.Id.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '_' : ch));
|
||||
var sessionDirectory = Path.Combine(rootDirectory, ".freecode-bridge", safeWorkId);
|
||||
Directory.CreateDirectory(sessionDirectory);
|
||||
return sessionDirectory;
|
||||
}
|
||||
|
||||
private WorkSecret DecodeWorkSecret(WorkItem work)
|
||||
{
|
||||
var sessionToken = TryGetString(work.Payload, SessionTokenKey) ?? work.SessionToken;
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
{
|
||||
throw new InvalidOperationException($"Work item '{work.Id}' does not include a session token.");
|
||||
}
|
||||
|
||||
return new WorkSecret(
|
||||
sessionToken,
|
||||
TryGetString(work.Payload, PromptKey),
|
||||
TryGetString(work.Payload, ModelKey),
|
||||
TryGetString(work.Payload, AgentTypeKey),
|
||||
TryGetString(work.Payload, WorkingDirectoryKey));
|
||||
}
|
||||
|
||||
private async Task SafeSendSessionUpdateAsync(string sessionId, object payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.SendSessionUpdateAsync(_config, sessionId, payload).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Bridge session update failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryGetString(JsonElement payload, string propertyName)
|
||||
{
|
||||
return payload.ValueKind == JsonValueKind.Object
|
||||
&& payload.TryGetProperty(propertyName, out var value)
|
||||
&& value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await StopAsync().ConfigureAwait(false);
|
||||
await DeregisterEnvironmentAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private sealed record WorkSecret(
|
||||
string SessionToken,
|
||||
string? Prompt,
|
||||
string? Model,
|
||||
string? AgentType,
|
||||
string? WorkingDirectory);
|
||||
}
|
||||
8
src/FreeCode.Bridge/BridgeTypes.cs
Normal file
8
src/FreeCode.Bridge/BridgeTypes.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Bridge;
|
||||
|
||||
public sealed record BridgeConfig(string BaseUrl, string? ApiKey = null);
|
||||
|
||||
public sealed record BridgeStatusInfo(BridgeStatus Status);
|
||||
10
src/FreeCode.Bridge/FreeCode.Bridge.csproj
Normal file
10
src/FreeCode.Bridge/FreeCode.Bridge.csproj
Normal file
@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>FreeCode.Bridge</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FreeCode.Core\FreeCode.Core.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Services\FreeCode.Services.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
14
src/FreeCode.Bridge/ServiceCollectionExtensions.cs
Normal file
14
src/FreeCode.Bridge/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FreeCode.Bridge;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddFreeCodeBridge(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<BridgeService>();
|
||||
services.AddSingleton<IBridgeService>(sp => sp.GetRequiredService<BridgeService>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
17
src/FreeCode.Bridge/SourceGenerationContext.cs
Normal file
17
src/FreeCode.Bridge/SourceGenerationContext.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Bridge;
|
||||
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
[JsonSerializable(typeof(BridgeStatusInfo))]
|
||||
[JsonSerializable(typeof(BridgeEnvironment))]
|
||||
[JsonSerializable(typeof(WorkItem))]
|
||||
[JsonSerializable(typeof(SessionHandle))]
|
||||
[JsonSerializable(typeof(SessionSpawnOptions))]
|
||||
[JsonSerializable(typeof(PermissionResponse))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class SourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
13
src/FreeCode.Commands/AddDirCommand.cs
Normal file
13
src/FreeCode.Commands/AddDirCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class AddDirCommand : CommandBase
|
||||
{
|
||||
public override string Name => "add-dir";
|
||||
public override string Description => "Add a working directory.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/AdvisorCommand.cs
Normal file
14
src/FreeCode.Commands/AdvisorCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class AdvisorCommand : CommandBase
|
||||
{
|
||||
public override string Name => "advisor";
|
||||
public override string Description => "Get advice.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/AgentsCommand.cs
Normal file
13
src/FreeCode.Commands/AgentsCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class AgentsCommand : CommandBase
|
||||
{
|
||||
public override string Name => "agents";
|
||||
public override string Description => "List available agents.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/AntTraceCommand.cs
Normal file
14
src/FreeCode.Commands/AntTraceCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class AntTraceCommand : CommandBase
|
||||
{
|
||||
public override string Name => "ant-trace";
|
||||
public override string Description => "Trace operations.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/AssistantCommand.cs
Normal file
13
src/FreeCode.Commands/AssistantCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class AssistantCommand : CommandBase
|
||||
{
|
||||
public override string Name => "assistant";
|
||||
public override string Description => "Manage assistant profiles.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/BranchCommand.cs
Normal file
13
src/FreeCode.Commands/BranchCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class BranchCommand : CommandBase
|
||||
{
|
||||
public override string Name => "branch";
|
||||
public override string Description => "Manage git branches.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/BreakCacheCommand.cs
Normal file
13
src/FreeCode.Commands/BreakCacheCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class BreakCacheCommand : CommandBase
|
||||
{
|
||||
public override string Name => "break-cache";
|
||||
public override string Description => "Break the prompt cache.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/BridgeKickCommand.cs
Normal file
14
src/FreeCode.Commands/BridgeKickCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class BridgeKickCommand : CommandBase
|
||||
{
|
||||
public override string Name => "bridge-kick";
|
||||
public override string Description => "Kick the bridge session.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
57
src/FreeCode.Commands/BriefCommand.cs
Normal file
57
src/FreeCode.Commands/BriefCommand.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class BriefCommand : CommandBase
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public override string Name => "brief";
|
||||
public override string Description => "Toggle brief output mode.";
|
||||
public override CommandCategory Category => CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
{
|
||||
var config = LoadConfig();
|
||||
var currentBrief = config["briefMode"]?.GetValue<bool>() ?? false;
|
||||
var newBrief = !currentBrief;
|
||||
|
||||
config["briefMode"] = newBrief;
|
||||
SaveConfig(config);
|
||||
|
||||
return Task.FromResult(new CommandResult(true, $"Brief mode: {(newBrief ? "ON" : "OFF")}"));
|
||||
}
|
||||
|
||||
private static JsonObject LoadConfig()
|
||||
{
|
||||
var path = GetConfigPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonNode.Parse(File.ReadAllText(path)) as JsonObject ?? [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveConfig(JsonObject config)
|
||||
{
|
||||
var path = GetConfigPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
File.WriteAllText(path, config.ToJsonString(JsonOptions));
|
||||
}
|
||||
|
||||
private static string GetConfigPath()
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".free-code", "config.json");
|
||||
}
|
||||
15
src/FreeCode.Commands/BtwCommand.cs
Normal file
15
src/FreeCode.Commands/BtwCommand.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class BtwCommand : CommandBase
|
||||
{
|
||||
public override string Name => "btw";
|
||||
public override string Description => "Buddy companion interaction.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override bool IsEnabled() => true;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/BughunterCommand.cs
Normal file
14
src/FreeCode.Commands/BughunterCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class BughunterCommand : CommandBase
|
||||
{
|
||||
public override string Name => "bughunter";
|
||||
public override string Description => "Bug hunting mode.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/ChromeCommand.cs
Normal file
13
src/FreeCode.Commands/ChromeCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ChromeCommand : CommandBase
|
||||
{
|
||||
public override string Name => "chrome";
|
||||
public override string Description => "Chrome extension integration.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/ClearCommand.cs
Normal file
13
src/FreeCode.Commands/ClearCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ClearCommand : CommandBase
|
||||
{
|
||||
public override string Name => "clear";
|
||||
public override string Description => "Clear the current terminal view.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> Task.FromResult(new CommandResult(true));
|
||||
}
|
||||
13
src/FreeCode.Commands/ColorCommand.cs
Normal file
13
src/FreeCode.Commands/ColorCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ColorCommand : CommandBase
|
||||
{
|
||||
public override string Name => "color";
|
||||
public override string Description => "Set the color scheme.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
16
src/FreeCode.Commands/CommandBase.cs
Normal file
16
src/FreeCode.Commands/CommandBase.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Interfaces;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public abstract class CommandBase : ICommand
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public virtual string[]? Aliases => null;
|
||||
public abstract string Description { get; }
|
||||
public abstract CommandCategory Category { get; }
|
||||
public virtual CommandAvailability Availability => CommandAvailability.Always;
|
||||
public virtual bool IsEnabled() => true;
|
||||
public abstract Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default);
|
||||
}
|
||||
1708
src/FreeCode.Commands/CommandExecutionHelper.cs
Normal file
1708
src/FreeCode.Commands/CommandExecutionHelper.cs
Normal file
File diff suppressed because it is too large
Load Diff
101
src/FreeCode.Commands/CommandRegistry.cs
Normal file
101
src/FreeCode.Commands/CommandRegistry.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using FreeCode.Core.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CommandRegistry(IServiceProvider serviceProvider) : ICommandRegistry
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private IReadOnlyList<ICommand>? _cachedCommands;
|
||||
|
||||
public Task<IReadOnlyList<ICommand>> GetCommandsAsync()
|
||||
=> Task.FromResult(GetOrCreateCommands());
|
||||
|
||||
public Task<IReadOnlyList<ICommand>> GetEnabledCommandsAsync()
|
||||
{
|
||||
var commands = GetOrCreateCommands();
|
||||
var authService = serviceProvider.GetService(typeof(IAuthService)) as IAuthService;
|
||||
var enabled = new List<ICommand>(commands.Count + 4);
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (!command.IsEnabled())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsAvailable(command, authService))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
enabled.Add(command);
|
||||
}
|
||||
|
||||
foreach (var pluginCommand in GetPluginCommands())
|
||||
{
|
||||
if (!pluginCommand.IsEnabled())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsAvailable(pluginCommand, authService))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
enabled.Add(pluginCommand);
|
||||
}
|
||||
|
||||
enabled.Sort(static (left, right) =>
|
||||
{
|
||||
var categoryComparison = Comparer<FreeCode.Core.Enums.CommandCategory>.Default.Compare(left.Category, right.Category);
|
||||
return categoryComparison != 0
|
||||
? categoryComparison
|
||||
: StringComparer.OrdinalIgnoreCase.Compare(left.Name, right.Name);
|
||||
});
|
||||
|
||||
return Task.FromResult<IReadOnlyList<ICommand>>(enabled);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ICommand> GetOrCreateCommands()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_cachedCommands is not null)
|
||||
{
|
||||
return _cachedCommands;
|
||||
}
|
||||
|
||||
var commands = serviceProvider.GetServices<ICommand>().ToArray();
|
||||
|
||||
_cachedCommands = commands
|
||||
.OrderBy(command => command.Category)
|
||||
.ThenBy(command => command.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
return _cachedCommands;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAvailable(ICommand command, IAuthService? authService)
|
||||
=> command.Availability switch
|
||||
{
|
||||
FreeCode.Core.Enums.CommandAvailability.Always => true,
|
||||
FreeCode.Core.Enums.CommandAvailability.RequiresAuth => authService?.IsAuthenticated ?? false,
|
||||
FreeCode.Core.Enums.CommandAvailability.ClaudeAiOnly => authService?.IsClaudeAiUser ?? false,
|
||||
FreeCode.Core.Enums.CommandAvailability.InternalOnly => authService?.IsInternalUser ?? false,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private IReadOnlyList<ICommand> GetPluginCommands()
|
||||
{
|
||||
var pluginManager = serviceProvider.GetService(typeof(IPluginManager));
|
||||
if (pluginManager is FreeCode.Plugins.PluginManager concreteManager)
|
||||
{
|
||||
return concreteManager.GetPluginCommands().OfType<ICommand>().ToArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
13
src/FreeCode.Commands/CommitCommand.cs
Normal file
13
src/FreeCode.Commands/CommitCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CommitCommand : CommandBase
|
||||
{
|
||||
public override string Name => "commit";
|
||||
public override string Description => "Generate and create a git commit.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Prompt;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/CommitPushPrCommand.cs
Normal file
14
src/FreeCode.Commands/CommitPushPrCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CommitPushPrCommand : CommandBase
|
||||
{
|
||||
public override string Name => "commit-push-pr";
|
||||
public override string[]? Aliases => new[] { "cpp" };
|
||||
public override string Description => "Commit, push, and open a pull request.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Prompt;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/CompactCommand.cs
Normal file
13
src/FreeCode.Commands/CompactCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CompactCommand : CommandBase
|
||||
{
|
||||
public override string Name => "compact";
|
||||
public override string Description => "Compact the conversation context.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/ConfigCommand.cs
Normal file
13
src/FreeCode.Commands/ConfigCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ConfigCommand : CommandBase
|
||||
{
|
||||
public override string Name => "config";
|
||||
public override string Description => "Get, set, or list configuration.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.LocalDialog;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/ContextCommand.cs
Normal file
13
src/FreeCode.Commands/ContextCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ContextCommand : CommandBase
|
||||
{
|
||||
public override string Name => "context";
|
||||
public override string Description => "Show current context information.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/CopyCommand.cs
Normal file
13
src/FreeCode.Commands/CopyCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CopyCommand : CommandBase
|
||||
{
|
||||
public override string Name => "copy";
|
||||
public override string Description => "Copy the last response to the clipboard.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/CostCommand.cs
Normal file
14
src/FreeCode.Commands/CostCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CostCommand : CommandBase
|
||||
{
|
||||
public override string Name => "cost";
|
||||
public override string Description => "Show token cost estimates.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.RequiresAuth;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/CtxVizCommand.cs
Normal file
14
src/FreeCode.Commands/CtxVizCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class CtxVizCommand : CommandBase
|
||||
{
|
||||
public override string Name => "ctx-viz";
|
||||
public override string Description => "Visualize the context.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/DebugToolCallCommand.cs
Normal file
14
src/FreeCode.Commands/DebugToolCallCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class DebugToolCallCommand : CommandBase
|
||||
{
|
||||
public override string Name => "debug-tool-call";
|
||||
public override string Description => "Debug tool calls.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/DesktopCommand.cs
Normal file
13
src/FreeCode.Commands/DesktopCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class DesktopCommand : CommandBase
|
||||
{
|
||||
public override string Name => "desktop";
|
||||
public override string Description => "Desktop integration.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/DiffCommand.cs
Normal file
13
src/FreeCode.Commands/DiffCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class DiffCommand : CommandBase
|
||||
{
|
||||
public override string Name => "diff";
|
||||
public override string Description => "Show the git diff.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/DoctorCommand.cs
Normal file
13
src/FreeCode.Commands/DoctorCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class DoctorCommand : CommandBase
|
||||
{
|
||||
public override string Name => "doctor";
|
||||
public override string Description => "Run diagnostics.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/EffortCommand.cs
Normal file
13
src/FreeCode.Commands/EffortCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class EffortCommand : CommandBase
|
||||
{
|
||||
public override string Name => "effort";
|
||||
public override string Description => "Set the effort level.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
28
src/FreeCode.Commands/EnvCommand.cs
Normal file
28
src/FreeCode.Commands/EnvCommand.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class EnvCommand : CommandBase
|
||||
{
|
||||
public override string Name => "env";
|
||||
public override string Description => "Show environment variables.";
|
||||
public override CommandCategory Category => CommandCategory.Local;
|
||||
public override CommandAvailability Availability => CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
{
|
||||
var variables = new List<string>();
|
||||
foreach (var entry in Environment.GetEnvironmentVariables().Cast<System.Collections.DictionaryEntry>().OrderBy(e => (string)e.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (entry.Key.ToString()?.StartsWith("FREE_CODE", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
variables.Add($"{entry.Key}={entry.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
return variables.Count == 0
|
||||
? Task.FromResult(new CommandResult(true, "No FREE_CODE environment variables set."))
|
||||
: Task.FromResult(new CommandResult(true, string.Join(Environment.NewLine, variables)));
|
||||
}
|
||||
}
|
||||
14
src/FreeCode.Commands/ExitCommand.cs
Normal file
14
src/FreeCode.Commands/ExitCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ExitCommand : CommandBase
|
||||
{
|
||||
public override string Name => "exit";
|
||||
public override string[]? Aliases => ["quit"];
|
||||
public override string Description => "Exit the application.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> Task.FromResult(new CommandResult(true, "Exit requested."));
|
||||
}
|
||||
13
src/FreeCode.Commands/ExportCommand.cs
Normal file
13
src/FreeCode.Commands/ExportCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ExportCommand : CommandBase
|
||||
{
|
||||
public override string Name => "export";
|
||||
public override string Description => "Export the conversation to a file.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/ExtraUsageCommand.cs
Normal file
14
src/FreeCode.Commands/ExtraUsageCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class ExtraUsageCommand : CommandBase
|
||||
{
|
||||
public override string Name => "extra-usage";
|
||||
public override string Description => "Show detailed usage information.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.RequiresAuth;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/FastCommand.cs
Normal file
13
src/FreeCode.Commands/FastCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class FastCommand : CommandBase
|
||||
{
|
||||
public override string Name => "fast";
|
||||
public override string Description => "Toggle fast mode.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/FeedbackCommand.cs
Normal file
13
src/FreeCode.Commands/FeedbackCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class FeedbackCommand : CommandBase
|
||||
{
|
||||
public override string Name => "feedback";
|
||||
public override string Description => "Submit feedback.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/FilesCommand.cs
Normal file
13
src/FreeCode.Commands/FilesCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class FilesCommand : CommandBase
|
||||
{
|
||||
public override string Name => "files";
|
||||
public override string Description => "Manage project files context.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
13
src/FreeCode.Commands/FreeCode.Commands.csproj
Normal file
13
src/FreeCode.Commands/FreeCode.Commands.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>FreeCode.Commands</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FreeCode.Core\FreeCode.Core.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Engine\FreeCode.Engine.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Plugins\FreeCode.Plugins.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.State\FreeCode.State.csproj" />
|
||||
<ProjectReference Include="..\FreeCode.Skills\FreeCode.Skills.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
14
src/FreeCode.Commands/GoodClaudeCommand.cs
Normal file
14
src/FreeCode.Commands/GoodClaudeCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class GoodClaudeCommand : CommandBase
|
||||
{
|
||||
public override string Name => "good-claude";
|
||||
public override string Description => "Rate the response.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
14
src/FreeCode.Commands/HeapdumpCommand.cs
Normal file
14
src/FreeCode.Commands/HeapdumpCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class HeapdumpCommand : CommandBase
|
||||
{
|
||||
public override string Name => "heapdump";
|
||||
public override string Description => "Trigger a heap dump.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
public override FreeCode.Core.Enums.CommandAvailability Availability => FreeCode.Core.Enums.CommandAvailability.InternalOnly;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
34
src/FreeCode.Commands/HelpCommand.cs
Normal file
34
src/FreeCode.Commands/HelpCommand.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using FreeCode.Core.Interfaces;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class HelpCommand(IServiceProvider services) : CommandBase
|
||||
{
|
||||
public override string Name => "help";
|
||||
public override string Description => "Show available commands.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Prompt;
|
||||
|
||||
public override async Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
{
|
||||
var commandRegistry = services.GetService(typeof(ICommandRegistry)) as ICommandRegistry;
|
||||
if (commandRegistry is null)
|
||||
{
|
||||
return new CommandResult(false, "Command registry is unavailable.");
|
||||
}
|
||||
|
||||
var commands = await commandRegistry.GetEnabledCommandsAsync().ConfigureAwait(false);
|
||||
var lines = new List<string>(commands.Count + 1) { "Available commands:" };
|
||||
|
||||
foreach (var command in commands.OrderBy(command => command.Category).ThenBy(command => command.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var aliasText = command.Aliases is { Length: > 0 }
|
||||
? $" ({string.Join(", ", command.Aliases)})"
|
||||
: string.Empty;
|
||||
|
||||
lines.Add($"/{command.Name}{aliasText} - {command.Description}");
|
||||
}
|
||||
|
||||
return new CommandResult(true, string.Join(Environment.NewLine, lines));
|
||||
}
|
||||
}
|
||||
13
src/FreeCode.Commands/HooksCommand.cs
Normal file
13
src/FreeCode.Commands/HooksCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class HooksCommand : CommandBase
|
||||
{
|
||||
public override string Name => "hooks";
|
||||
public override string Description => "Manage lifecycle hooks.";
|
||||
public override FreeCode.Core.Enums.CommandCategory Category => FreeCode.Core.Enums.CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
=> CommandExecutionHelper.ExecuteAsync(Name, context, args, ct);
|
||||
}
|
||||
236
src/FreeCode.Commands/IdeCommand.cs
Normal file
236
src/FreeCode.Commands/IdeCommand.cs
Normal file
@ -0,0 +1,236 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Interfaces;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class IdeCommand(IServiceProvider services) : CommandBase
|
||||
{
|
||||
private readonly IServiceProvider _services = services;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public override string Name => "ide";
|
||||
public override string Description => "Manage IDE integrations and show status.";
|
||||
public override CommandCategory Category => CommandCategory.Local;
|
||||
|
||||
public override Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
{
|
||||
var tokens = Tokenize(args);
|
||||
var subcommand = tokens.Count == 0 ? "status" : tokens[0].ToLowerInvariant();
|
||||
|
||||
return subcommand switch
|
||||
{
|
||||
"status" => Task.FromResult(GetStatus(context)),
|
||||
"connect" => ConnectAsync(context, tokens, ct),
|
||||
"disconnect" => DisconnectAsync(context, ct),
|
||||
"install" => InstallAsync(context, tokens, ct),
|
||||
_ => Task.FromResult(new CommandResult(false, "Usage: /ide [connect <vscode|jetbrains> [url]|disconnect|status|install <vscode|jetbrains>]"))
|
||||
};
|
||||
}
|
||||
|
||||
private static CommandResult GetStatus(CommandContext context)
|
||||
{
|
||||
var featureFlags = context.Services.GetService(typeof(IFeatureFlagService)) as IFeatureFlagService;
|
||||
var mcpManager = context.Services.GetService(typeof(IMcpClientManager)) as IMcpClientManager;
|
||||
var ideConnection = mcpManager?.GetConnections()
|
||||
.FirstOrDefault(connection => string.Equals(connection.Name, "ide", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"IDE integration feature: {(featureFlags?.IsEnabled("IDE_INTEGRATION") == true ? "enabled" : "disabled")}",
|
||||
$"Configured connection: {(ideConnection is null ? "none" : DescribeConnection(ideConnection))}"
|
||||
};
|
||||
|
||||
if (ideConnection?.Config is SseIdeServerConfig sse)
|
||||
{
|
||||
lines.Add($"URL: {sse.Url}");
|
||||
lines.Add($"IDE: {sse.IdeName}");
|
||||
}
|
||||
else if (ideConnection?.Config is WsIdeServerConfig ws)
|
||||
{
|
||||
lines.Add($"URL: {ws.Url}");
|
||||
lines.Add($"IDE: {ws.IdeName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add("Supported IDEs: VS Code, JetBrains");
|
||||
}
|
||||
|
||||
return new CommandResult(true, string.Join(Environment.NewLine, lines));
|
||||
}
|
||||
|
||||
private static async Task<CommandResult> ConnectAsync(CommandContext context, IReadOnlyList<string> tokens, CancellationToken ct)
|
||||
{
|
||||
if (tokens.Count < 2)
|
||||
{
|
||||
return new CommandResult(false, "Usage: /ide connect <vscode|jetbrains> [url]");
|
||||
}
|
||||
|
||||
var ide = NormalizeIde(tokens[1]);
|
||||
if (ide is null)
|
||||
{
|
||||
return new CommandResult(false, "Supported IDEs: vscode, jetbrains");
|
||||
}
|
||||
|
||||
var url = tokens.Count > 2 ? tokens[2] : GetDefaultIdeUrl(ide);
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != "ws" && uri.Scheme != "wss"))
|
||||
{
|
||||
return new CommandResult(false, $"Invalid IDE endpoint: {url}");
|
||||
}
|
||||
|
||||
var config = LoadConfig();
|
||||
var mcpServers = GetObject(config, "mcpServers");
|
||||
mcpServers["ide"] = new JsonObject
|
||||
{
|
||||
["type"] = uri.Scheme is "ws" or "wss" ? "ws-ide" : "sse-ide",
|
||||
["url"] = url,
|
||||
["ideName"] = ide,
|
||||
["scope"] = "User"
|
||||
};
|
||||
|
||||
config["mcpServers"] = mcpServers;
|
||||
SaveConfig(config);
|
||||
|
||||
var manager = context.Services.GetService(typeof(IMcpClientManager)) as IMcpClientManager;
|
||||
if (manager is not null)
|
||||
{
|
||||
await manager.ReloadAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new CommandResult(true, $"Connected IDE integration for {ide} at {url}.");
|
||||
}
|
||||
|
||||
private static async Task<CommandResult> DisconnectAsync(CommandContext context, CancellationToken ct)
|
||||
{
|
||||
var config = LoadConfig();
|
||||
var mcpServers = GetObject(config, "mcpServers");
|
||||
var removed = mcpServers.Remove("ide");
|
||||
config["mcpServers"] = mcpServers;
|
||||
SaveConfig(config);
|
||||
|
||||
var manager = context.Services.GetService(typeof(IMcpClientManager)) as IMcpClientManager;
|
||||
if (manager is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await manager.DisconnectServerAsync("ide").ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Warning: Failed to disconnect IDE integration: {ex.Message}");
|
||||
}
|
||||
|
||||
await manager.ReloadAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new CommandResult(true, removed ? "Disconnected IDE integration." : "No IDE integration was configured.");
|
||||
}
|
||||
|
||||
private static async Task<CommandResult> InstallAsync(CommandContext context, IReadOnlyList<string> tokens, CancellationToken ct)
|
||||
{
|
||||
if (tokens.Count < 2)
|
||||
{
|
||||
return new CommandResult(false, "Usage: /ide install <vscode|jetbrains>");
|
||||
}
|
||||
|
||||
var ide = NormalizeIde(tokens[1]);
|
||||
if (ide is null)
|
||||
{
|
||||
return new CommandResult(false, "Supported IDEs: vscode, jetbrains");
|
||||
}
|
||||
|
||||
var displayName = ide == "vscode" ? "VS Code" : "JetBrains";
|
||||
var config = LoadConfig();
|
||||
var mcpServers = GetObject(config, "mcpServers");
|
||||
mcpServers["ide"] = new JsonObject
|
||||
{
|
||||
["command"] = "free-code-ide-extension",
|
||||
["args"] = new JsonArray(ide, GetDefaultIdeUrl(ide)),
|
||||
["env"] = new JsonObject
|
||||
{
|
||||
["FREE_CODE_IDE"] = ide,
|
||||
["FREE_CODE_IDE_DISPLAY_NAME"] = displayName,
|
||||
["FREE_CODE_IDE_URL"] = GetDefaultIdeUrl(ide)
|
||||
},
|
||||
["scope"] = "User"
|
||||
};
|
||||
|
||||
config["mcpServers"] = mcpServers;
|
||||
SaveConfig(config);
|
||||
|
||||
var manager = context.Services.GetService(typeof(IMcpClientManager)) as IMcpClientManager;
|
||||
if (manager is not null)
|
||||
{
|
||||
await manager.ReloadAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new CommandResult(true, $"Installed IDE integration for {displayName} in {GetConfigPath()}. Next step: install or enable the {displayName} extension that provides the 'free-code-ide-extension' MCP bridge, then run /ide connect {ide} if you want to override the default endpoint.");
|
||||
}
|
||||
|
||||
private static string DescribeConnection(MCPServerConnection connection)
|
||||
{
|
||||
var status = connection.IsConnected ? "connected"
|
||||
: connection.IsPending ? "pending"
|
||||
: connection.IsFailed ? "failed"
|
||||
: connection.NeedsAuth ? "needs authentication"
|
||||
: connection.IsDisabled ? "disabled"
|
||||
: connection.ConnectionType;
|
||||
|
||||
return $"{connection.Name} ({status})";
|
||||
}
|
||||
|
||||
private static string? NormalizeIde(string value)
|
||||
=> value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"vscode" or "vs-code" or "vs_code" or "code" => "vscode",
|
||||
"jetbrains" or "jb" or "intellij" or "rider" => "jetbrains",
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string GetDefaultIdeUrl(string ide)
|
||||
=> ide == "jetbrains"
|
||||
? "ws://127.0.0.1:63342/free-code"
|
||||
: "http://127.0.0.1:7777/sse";
|
||||
|
||||
private static JsonObject LoadConfig()
|
||||
{
|
||||
var path = GetConfigPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return JsonNode.Parse(File.ReadAllText(path)) as JsonObject ?? [];
|
||||
}
|
||||
|
||||
private static void SaveConfig(JsonObject config)
|
||||
{
|
||||
var path = GetConfigPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
File.WriteAllText(path, config.ToJsonString(JsonOptions));
|
||||
}
|
||||
|
||||
private static JsonObject GetObject(JsonObject parent, string name)
|
||||
{
|
||||
if (parent[name] is JsonObject value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var created = new JsonObject();
|
||||
parent[name] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
private static List<string> Tokenize(string? args)
|
||||
=> string.IsNullOrWhiteSpace(args)
|
||||
? []
|
||||
: args.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
|
||||
private static string GetConfigPath()
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".free-code", "config.json");
|
||||
}
|
||||
61
src/FreeCode.Commands/InitCommand.cs
Normal file
61
src/FreeCode.Commands/InitCommand.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FreeCode.Core.Enums;
|
||||
using FreeCode.Core.Models;
|
||||
|
||||
namespace FreeCode.Commands;
|
||||
|
||||
public sealed class InitCommand(IServiceProvider services) : CommandBase
|
||||
{
|
||||
private readonly IServiceProvider _services = services;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public override string Name => "init";
|
||||
public override string Description => "Initialize a new project configuration.";
|
||||
public override CommandCategory Category => CommandCategory.Local;
|
||||
|
||||
public override async Task<CommandResult> ExecuteAsync(CommandContext context, string? args = null, CancellationToken ct = default)
|
||||
{
|
||||
var configDirectory = Path.Combine(context.WorkingDirectory, ".free-code");
|
||||
var configPath = Path.Combine(configDirectory, "config.json");
|
||||
Directory.CreateDirectory(configDirectory);
|
||||
|
||||
JsonObject config;
|
||||
if (File.Exists(configPath))
|
||||
{
|
||||
config = JsonNode.Parse(await File.ReadAllTextAsync(configPath, ct).ConfigureAwait(false)) as JsonObject ?? [];
|
||||
}
|
||||
else
|
||||
{
|
||||
config = [];
|
||||
}
|
||||
|
||||
config["projectName"] ??= Path.GetFileName(context.WorkingDirectory);
|
||||
config["workingDirectory"] = context.WorkingDirectory;
|
||||
config["createdAt"] ??= DateTimeOffset.UtcNow.ToString("O");
|
||||
config["commands"] ??= new JsonObject();
|
||||
config["contextFiles"] ??= new JsonArray();
|
||||
config["mcpServers"] ??= new JsonObject();
|
||||
config["voiceEnabled"] ??= false;
|
||||
config["voiceMode"] ??= "push-to-talk";
|
||||
|
||||
await File.WriteAllTextAsync(configPath, config.ToJsonString(JsonOptions), ct).ConfigureAwait(false);
|
||||
|
||||
var readmePath = Path.Combine(configDirectory, "README.md");
|
||||
if (!File.Exists(readmePath))
|
||||
{
|
||||
var markdown = new StringBuilder()
|
||||
.AppendLine("# free-code project config")
|
||||
.AppendLine()
|
||||
.AppendLine($"Initialized for `{Path.GetFileName(context.WorkingDirectory)}`.")
|
||||
.AppendLine()
|
||||
.AppendLine("This directory stores project-specific configuration for the .NET CLI port.")
|
||||
.ToString();
|
||||
|
||||
await File.WriteAllTextAsync(readmePath, markdown, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new CommandResult(true, $"Initialized project configuration in {configDirectory}.");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user