跳转到内容

Orleans

5 篇包含标签 "Orleans" 的文章

从 CLI 调用到 SDK 集成:GitHub Copilot 在 .NET 项目中的最佳实践

从 CLI 调用到 SDK 集成:GitHub Copilot 在 .NET 项目中的最佳实践

Section titled “从 CLI 调用到 SDK 集成:GitHub Copilot 在 .NET 项目中的最佳实践”

从命令行调用到官方 SDK 集成的升级之路,说起来也算是一段经历,今天就分享我们在 HagiCode 项目中踩过的坑和学到的东西。

GitHub Copilot SDK 在 2025 年正式发布后,我们开始将其集成到 AI 能力层中。在此之前,项目主要通过直接调用 Copilot CLI 命令行工具来使用 GitHub Copilot 能力,这种方式其实也存在几个明显问题:

  • 进程管理复杂:需要手动管理 CLI 进程的生命周期、启动超时和进程清理——毕竟进程这东西,说崩溃就崩溃了,也没什么预兆
  • 事件处理不完整:原始 CLI 调用难以捕获模型推理过程和工具执行的细粒度事件,就像只能看到结果,却看不到思考的过程
  • 会话管理困难:缺乏有效的会话复用和恢复机制,每次都得重新开始,想想也是挺累的
  • 兼容性问题:CLI 参数更新频繁,需要持续维护参数兼容性逻辑,这无异于和风车作战了

这些问题在日常开发中逐渐显现,特别是在需要实时追踪模型推理过程(thinking)和工具执行状态时,CLI 调用的局限性尤为明显。我们也算是想明白了,需要一个更底层、更完整的集成方式——毕竟,条条大路通罗马,只是有的路好走一点,有的路稍微曲折一点罢了。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个开源的 AI 代码助手项目,在开发过程中我们需要深度集成 GitHub Copilot 的各种能力——从基础的代码补全到复杂的多轮对话和工具调用。这些实际需求推动我们从 CLI 调用升级到了官方 SDK 集成。

如果你对本文的实践方案感兴趣,说明我们的工程实践可能对你有帮助——那么 HagiCode 项目本身也值得关注一下。或许在文末你会发现更多关于项目的信息和链接,谁知道呢…

项目采用了分层架构来解决 CLI 调用的问题:

┌─────────────────────────────────────────────────────────┐
│ hagicode-core (Orleans Grains + AI Provider Layer) │
│ - CopilotAIProvider: 将 AIRequest 转换为 CopilotOptions │
│ - GitHubCopilotGrain: Orleans 分布式执行接口 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ HagiCode.Libs (Shared Provider Layer) │
│ - CopilotProvider: CLI Provider 接口实现 │
│ - ICopilotSdkGateway: SDK 调用抽象 │
│ - GitHubCopilotSdkGateway: SDK 会话管理与事件分发 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ GitHub Copilot SDK (Official .NET SDK) │
│ - CopilotClient: SDK 客户端 │
│ - CopilotSession: 会话管理 │
│ - SessionEvent: 事件流 │
└─────────────────────────────────────────────────────────┘

这种分层设计带来的技术优势,其实也还挺实用的:

  1. 关注点分离:核心业务逻辑与 SDK 实现细节解耦——毕竟,什么层做什么事,井水不犯河水
  2. 可测试性:通过 ICopilotSdkGateway 接口可以轻松进行单元测试,测试起来也不那么费劲
  3. 复用性:HagiCode.Libs 可被多个项目引用,写一次,多处用
  4. 可维护性:SDK 升级只需修改 Gateway 层,上面的代码不用动,美得很

认证是 SDK 集成的第一步,也是最重要的一步——毕竟,门都进不去,后面的事情就免谈了。我们设计了一个灵活的认证配置,支持多种认证来源:

// CopilotProvider.cs - 认证来源配置
public class CopilotOptions
{
public bool UseLoggedInUser { get; set; } = true;
public string? GitHubToken { get; set; }
public string? CliUrl { get; set; }
}
// 转换为 SDK 请求
return new CopilotSdkRequest(
GitHubToken: options.AuthSource == CopilotAuthSource.GitHubToken
? options.GitHubToken
: null,
UseLoggedInUser: options.AuthSource != CopilotAuthSource.GitHubToken
);

这个设计的好处,其实也挺明显的:

  • 支持已登录用户模式(无需 token),适合桌面端场景——用户用自己的账号登录就行
  • 支持 GitHub Token 模式,适用于服务端部署——统一管理也方便
  • 支持 Copilot CLI URL 覆盖,方便企业代理配置——企业环境嘛,总有些特殊的规矩

在实际使用中,这种灵活的认证方式大大简化了不同部署场景的配置工作。桌面端可以使用用户自己的 Copilot 登录状态,服务端则可以通过 Token 进行统一管理。怎么说呢,各取所需罢了。

SDK 最强大的能力之一,应该就是对事件流的完整捕获了。我们实现了一个事件分发系统,能够实时处理各种 SDK 事件——毕竟,知道过程和只知道结果,感觉还是不一样的:

// GitHubCopilotSdkGateway.cs - 事件分发核心逻辑
internal static SessionEventDispatchResult DispatchSessionEvent(
SessionEvent evt, bool sawDelta)
{
switch (evt)
{
case AssistantReasoningEvent reasoningEvent:
// 捕获模型推理过程
events.Add(new CopilotSdkStreamEvent(
CopilotSdkStreamEventType.ReasoningDelta,
Content: reasoningEvent.Data.Content));
break;
case ToolExecutionStartEvent toolStartEvent:
// 捕获工具调用开始
events.Add(new CopilotSdkStreamEvent(
CopilotSdkStreamEventType.ToolExecutionStart,
ToolName: toolStartEvent.Data.ToolName,
ToolCallId: toolStartEvent.Data.ToolCallId));
break;
case ToolExecutionCompleteEvent toolCompleteEvent:
// 捕获工具调用完成及结果
events.Add(new CopilotSdkStreamEvent(
CopilotSdkStreamEventType.ToolExecutionEnd,
Content: ExtractToolExecutionContent(toolCompleteEvent)));
break;
default:
// 未处理事件作为 RawEvent 保留
events.Add(new CopilotSdkStreamEvent(
CopilotSdkStreamEventType.RawEvent,
RawEventType: evt.GetType().Name));
break;
}
}

这个实现带来的价值,怎么说呢:

  • 完整捕获模型推理过程(thinking):用户可以看到 AI 的思考过程,而不仅仅是最终结果——就像知道答案不如知道怎么思考出来的
  • 实时追踪工具执行状态:知道哪些工具正在运行、何时完成、返回了什么结果
  • 零事件丢失:通过 fallback 到 RawEvent 机制,确保所有事件都被记录,什么都不落下

在 HagiCode 的实际使用中,这些细粒度的事件让用户能够更深入地理解 AI 的工作过程,特别是在调试复杂任务时——这还是有点用处的。

从 CLI 调用迁移到 SDK 后,我们发现一些原有的 CLI 参数在 SDK 中不再适用。为了保持向后兼容,我们实现了一个参数过滤系统——毕竟,旧配置不能用,也挺让人头疼的:

// CopilotCliCompatibility.cs - 参数过滤
private static readonly Dictionary<string, string> RejectedFlags = new()
{
["--headless"] = "不支持的启动参数",
["--model"] = "通过 SDK 原生字段传递",
["--prompt"] = "通过 SDK 原生字段传递",
["--interactive"] = "由 provider 管理交互",
};
public static CopilotCliArgumentBuildResult BuildCliArgs(CopilotOptions options)
{
// 过滤不支持的参数,保留兼容参数
// 生成诊断信息
}

这样做的好处:

  • 自动过滤不兼容的 CLI 参数,避免运行时错误——程序崩溃可不是闹着玩的
  • 生成清晰的错误诊断信息,帮助开发者快速定位问题
  • 保证 SDK 稳定性,不受 CLI 参数变化的影响

在升级过程中,这个兼容性处理机制帮助我们平滑过渡,旧的配置文件仍然可以使用,只需要根据诊断信息逐步调整即可——也算是个渐进的过程了。

Copilot SDK 的会话创建成本较高,频繁创建和销毁会话会影响性能。我们实现了一个会话池管理系统——就像池子里的水,用完了再装,不如留着下次接着用:

// CopilotProvider.cs - 会话池管理
await using var lease = await _poolCoordinator.AcquireCopilotRuntimeAsync(
request,
async ct => await _gateway.CreateRuntimeAsync(sdkRequest, ct),
cancellationToken);
if (lease.IsWarmLease)
{
// 复用已有会话
yield return CreateSessionReusedMessage();
}
await foreach (var eventData in lease.Entry.Resource.SendPromptAsync(...))
{
yield return MapEvent(eventData);
}

会话池化的好处:

  • 会话复用:相同 sessionId 的请求可以复用已有会话,减少启动开销
  • 支持会话恢复:网络中断后可以恢复之前的会话状态——毕竟网络这东西,谁敢保证一直稳定呢
  • 自动池化管理:自动清理过期会话,避免资源泄漏

在 HagiCode 的实际使用中,会话池化显著提升了响应速度,特别是在处理连续对话时效果明显——这种提升还是能感觉到的。

HagiCode 使用 Orleans 作为分布式框架,我们将 Copilot SDK 集成到了 Orleans Grain 中——分布式这东西,说起来复杂,用起来倒也挺顺手:

// GitHubCopilotGrain.cs - 分布式执行
public async IAsyncEnumerable<GitHubCopilotResponse> ExecuteCommandStreamAsync(
string command,
CancellationToken token = default)
{
var provider = await aiProviderFactory.GetProviderAsync(AIProviderType.GitHubCopilot);
await foreach (var chunk in provider.SendMessageAsync(request, null, token))
{
// 映射为统一的响应格式
yield return BuildChunkResponse(chunk, startedAt);
}
}

Orleans 集成带来的优势:

  • 统一的 AI Provider 抽象:可以轻松切换不同的 AI 提供商——今天用这个,明天用那个,也挺灵活
  • 多租户隔离:不同用户的 Copilot 会话相互隔离,井水不犯河水
  • 持久化会话状态:会话状态可以跨服务器重启恢复,重启也不怕丢数据

对于需要处理大量并发请求的场景,Orleans 的分布式能力提供了很好的扩展性——毕竟,单机扛不住的时候,只能靠分布式顶上了。

以下是一个完整的配置示例——直接复制粘贴改改就能用:

{
"AI": {
"Providers": {
"Providers": {
"GitHubCopilot": {
"Enabled": true,
"ExecutablePath": "copilot",
"Model": "gpt-5",
"WorkingDirectory": "/path/to/project",
"Timeout": 7200,
"StartupTimeout": 30,
"UseLoggedInUser": true,
"NoAskUser": true,
"Permissions": {
"AllowAllTools": false,
"AllowedTools": ["Read", "Bash", "Grep"],
"DeniedTools": ["Edit"]
}
}
}
}
}
}

在实际使用中,我们总结了一些需要注意的地方——有些是踩坑得来的经验:

启动超时配置:首次启动 Copilot CLI 需要较长时间,建议设置 StartupTimeout 至少 30 秒。如果是首次登录,可能需要更长的时间——毕竟首次登录总得验证一下,这也没办法。

权限管理:生产环境避免使用 AllowAllTools: true。使用 AllowedTools 白名单控制可用工具,使用 DeniedTools 黑名单禁止危险操作。这样可以有效防止 AI 执行危险命令——安全这东西,小心点总是对的。

会话管理:相同 sessionId 的请求会自动复用会话。会话状态通过 ProviderSessionId 持久化。取消操作通过 CancellationTokenSource 传递——会话管理做得好,体验自然就好。

诊断输出:不兼容的 CLI 参数会生成 diagnostic 类型消息。原始 SDK 事件以 event.raw 类型保留。错误信息包含分类(启动超时、参数不兼容等),方便排查问题——出了问题能快速定位,也算是一种安慰了。

基于我们的实际经验,这里分享一些最佳实践——算是一些总结吧:

1. 使用工具白名单

var request = new AIRequest
{
Prompt = "分析这个文件",
AllowedTools = new[] { "Read", "Grep", "Bash(git:*)" }
};

通过白名单明确指定允许的工具,避免 AI 执行意外操作。特别是对于有写入权限的工具(如 Edit),需要格外谨慎——毕竟删库这种事,谁也不想经历。

2. 设置合理的超时

options.Timeout = 3600; // 1小时
options.StartupTimeout = 60; // 1分钟

根据任务的复杂度设置合适的超时时间。太短可能导致任务中断,太长则可能浪费资源等待无响应的请求——凡事适度,过犹不及。

3. 启用会话复用

options.SessionId = "my-session-123";

为相关任务设置相同的 sessionId,可以复用之前的会话上下文,提升响应速度——上下文这东西,有时候还挺重要的。

4. 处理流式响应

await foreach (var chunk in provider.StreamAsync(request))
{
switch (chunk.Type)
{
case StreamingChunkType.ThinkingDelta:
// 处理推理过程
break;
case StreamingChunkType.ToolCallDelta:
// 处理工具调用
break;
case StreamingChunkType.ContentDelta:
// 处理文本输出
break;
}
}

流式响应可以实时显示 AI 的处理进度,提升用户体验。特别是对于耗时任务,实时反馈非常重要——看着进度条总比干等着强。

5. 错误处理和重试

try
{
await foreach (var chunk in provider.StreamAsync(request))
{
// 处理响应
}
}
catch (CopilotSessionException ex)
{
// 处理会话异常
logger.LogError(ex, "Copilot session failed");
// 根据异常类型决定是否重试
}

适当的错误处理和重试机制可以提升系统的稳定性——谁也不能保证程序永远不出错,出了错能处理好就行。

从 CLI 调用到 SDK 集成的升级,为 HagiCode 项目带来了显著的价值——怎么说呢,这次升级还是挺值的:

  • 稳定性提升:SDK 提供了更稳定的接口,不受 CLI 版本变化影响——不用天天担心版本更新了
  • 功能完整性:能够捕获完整的事件流,包括推理过程和工具执行状态——过程和结果都能看到
  • 开发效率:类型安全的 SDK 接口让开发更高效,减少运行时错误——有类型检查,心里踏实
  • 用户体验:实时的事件反馈让用户更清晰地了解 AI 的工作过程——知道它在想什么,总比一无所知强

这次升级不仅仅是技术方案的替换,更是对整个 AI 能力层架构的优化。通过分层设计和抽象接口,我们获得了更好的可维护性和可扩展性——架构做好了,后面的事情就好办了。

如果你正在考虑将 GitHub Copilot 集成到你的 .NET 项目中,希望本文的实践经验能够帮助你少走一些弯路。官方 SDK 确实比 CLI 调用更加稳定和完整,值得投入时间去理解和掌握——毕竟,正确的工具能让事情事半功倍,这话也不是没有道理的。


如果本文对你有帮助:


写到这里也差不多了。技术文章嘛,总是写不完的,毕竟技术在发展,我们也在学习。如果你在使用 HagiCode 的过程中有什么问题或建议,欢迎随时联系我们。好了,就这样吧…

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。 本内容采用人工智能辅助协作,最终内容由作者审核并确认。

HagiCode Skill 系统技术解析:如何打造可扩展的 AI 技能管理平台

HagiCode Skill 系统技术解析:如何打造可扩展的 AI 技能管理平台

Section titled “HagiCode Skill 系统技术解析:如何打造可扩展的 AI 技能管理平台”

本文深入解析 HagiCode 项目中 Skill(技能)管理系统的架构设计与实现方案,涵盖本地全局管理、市场搜索、智能推荐、授信提供者管理四大核心功能的技术实现细节。

在 AI 代码助手这个领域,如何扩展 AI 的能力边界,其实一直是个核心课题。Claude Code 本身的代码辅助能力是挺强的,只是不同开发团队、不同技术栈,往往需要针对特定场景的专业能力——比如处理 Docker 部署、数据库优化、前端组件生成之类的。这时候,Skill(技能)系统就显得尤为重要了。

HagiCode 项目在开发过程中也遇到了类似的挑战:怎么让 Claude Code 能够像人一样「学会」新的专业技能,同时保持良好的用户体验和工程可维护性?毕竟这个问题,说难也难,说简单也简单。围绕这个问题,我们设计并实现了一套完整的 Skill 管理系统。

本文将详细解析这个系统的技术架构和核心实现,适合对 AI 扩展性、命令行工具集成感兴趣的开发者阅读。或许对你有用,也或许没用,但总归是写出来了。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个开源的 AI 代码助手项目,旨在帮助开发团队提升研发效率。项目的技术栈涵盖 ASP.NET Core、Orleans 分布式框架、TanStack Start + React 前端,以及本文要介绍的 Skill 管理子系统。

项目的 GitHub 地址是 HagiCode-org/site,如果你觉得本文介绍的技术方案有价值,欢迎给个 Star。毕竟Star多了,心情也会好一些。

Skill 系统采用前后端分离的架构设计,说起来也没什么特别的。

前端部分 使用 TanStack Start + React 构建用户界面,通过 Redux Toolkit 管理状态,四个主要功能分别对应四个 Tab 组件:本地技能、市场画廊、智能推荐、授信提供者。这样设计,其实也是为了用户体验罢了。

后端部分 基于 ASP.NET Core + ABP Framework,使用 Orleans Grain 实现分布式状态管理。在线 API 客户端封装了 IOnlineApiClient 接口,用于与远程技能目录服务通信。

整体架构的设计原则是「命令执行与业务逻辑分离」,通过适配器模式将 npm/npx 命令执行的细节屏蔽在独立模块中。毕竟谁愿意看到一堆命令行散落在代码各处呢?

本地全局管理是最基础的功能模块,负责列出已安装的技能并支持卸载操作。也没什么复杂的,就是把事情做好而已。

实现位置在 LocalSkillsTab.tsxLocalSkillCommandAdapter.cs。核心思路是封装 npx skills 命令,解析其 JSON 输出,转换为内部数据结构。说起来简单,做起来其实也简单。

public async Task<IReadOnlyList<LocalSkillInventoryResponseDto>> GetLocalSkillsAsync(
CancellationToken cancellationToken = default)
{
var result = await _commandAdapter.ListGlobalSkillsAsync(cancellationToken);
return result.Skills.Select(skill => new LocalSkillInventoryResponseDto
{
Name = skill.Name,
Version = skill.Version,
Source = skill.Source,
InstalledPath = skill.InstalledPath,
Description = skill.Description
}).ToList();
}

数据流非常清晰:前端发起请求 → SkillGalleryAppService 接收 → LocalSkillCommandAdapter 执行 npx 命令 → 解析 JSON 结果 → 返回 DTO 对象。一环扣一环,也没什么好说的。

卸载技能使用 npx skills remove -g <skillName> -y 命令,系统会自动处理依赖关系和清理工作。安装元数据存储在技能目录的 managed-install.json 中,记录了安装时间、来源版本等信息,便于后续更新和审计。毕竟有些东西,记下来总是好的。

技能安装涉及多个步骤的协调,怎么说呢,其实也不算太复杂:

public async Task<SkillInstallResultDto> InstallAsync(
SkillInstallRequestDto request,
CancellationToken cancellationToken = default)
{
// 1. 规范化安装引用
var normalized = _referenceNormalizer.Normalize(
request.SkillId,
request.Source,
request.SkillSlug,
request.Version);
// 2. 检查先决条件
await _prerequisiteChecker.CheckAsync(cancellationToken);
// 3. 获取安装锁
using var installLock = await _lockProvider.AcquireAsync(normalized.SkillId);
// 4. 执行安装命令
var result = await _installCommandRunner.ExecuteAsync(
new SkillInstallCommandExecutionRequest
{
Command = $"npx skills add {normalized.FullReference} -g -y",
Timeout = TimeSpan.FromMinutes(4)
},
cancellationToken);
// 5. 持久化安装元数据
await _metadataStore.WriteAsync(normalized.SkillPath, request);
return new SkillInstallResultDto { Success = result.Success };
}

这里用到了几个关键的设计模式:引用规范化器 负责将各种输入格式(如 tanweai/pua@opencode/docker-skill)转换为统一的内部表示;安装锁机制 确保同一技能同时只有一个安装操作在进行;流式输出 通过 Server-Sent Events 向前端实时推送安装进度,用户可以看到类似终端的实时日志。

这些设计模式,说到底,也还是为了让事情变得简单罢了。

市场搜索让用户能够发现和安装来自社区的技能。毕竟一个人的能力是有限的,众人的智慧才是无穷的。

搜索功能依赖在线 API https://api.hagicode.com/v1/skills/search。为了提升响应速度,系统实现了缓存机制。缓存这东西,就像记忆一样,有些东西记住了,下次就不用再费劲去想。

private async Task<IReadOnlyList<SkillGallerySkillDto>> SearchCatalogAsync(
string query,
CancellationToken cancellationToken,
IReadOnlySet<string>? allowedSources = null)
{
var cacheKey = $"skill_search:{query}:{string.Join(",", allowedSources ?? Array.Empty<string>())}";
if (_memoryCache.TryGetValue(cacheKey, out var cached))
return (IReadOnlyList<SkillGallerySkillDto>)cached!;
var response = await _onlineApiClient.SearchAsync(
new SearchSkillsRequest
{
Query = query,
Limit = _options.LimitPerQuery,
},
cancellationToken);
var results = response.Skills
.Where(skill => allowedSources is null || allowedSources.Contains(skill.Source))
.Select(skill => new SkillGallerySkillDto { ... })
.ToList();
_memoryCache.Set(cacheKey, results, TimeSpan.FromMinutes(10));
return results;
}

搜索结果支持按授信来源过滤,只显示用户信任的技能源。预置的种子查询用于初始化目录,比如「popular」、「recent」等,让用户在首次打开时就能看到推荐的热门技能。毕竟第一印象还是重要的。

智能推荐是系统中最复杂的功能,它能根据用户当前项目的情况,自动推荐最适合的技能。复杂归复杂,但做出来还是值得的。

整个推荐流程分为五个阶段:

1. 构建项目上下文
2. AI 生成搜索查询
3. 并行搜索在线目录
4. AI 对候选进行排名
5. 返回推荐列表

首先,系统分析项目的技术栈、编程语言、域名结构等特征,构建一个「项目画像」。这个画像,就像一个人的简历一样,记录着所有的特征。

然后,使用 AI Grain 生成针对性的搜索查询。这里的设计其实挺有意思——不是直接问 AI「推荐什么技能」,而是让它先思考「什么样的搜索词能找到相关技能」。毕竟有时候,问问题的方法比答案本身更重要:

var queryGeneration = await aiGrain.GenerateSkillRecommendationQueriesAsync(
projectContext, // 项目上下文
locale, // 用户语言偏好
maxQueries, // 最大查询数量
effectiveSearchHero); // AI 模型选择

接着,并行执行这些搜索查询,获取候选技能列表。并行处理,说到底也是为了节省时间罢了。

最后,使用另一个 AI Grain 对候选技能进行排名。这一步会综合考虑技能与项目的相关性、授信状态、用户历史偏好等因素:

var ranking = await aiGrain.RankSkillRecommendationsAsync(
projectContext,
candidates,
installedSkillNames,
locale,
maxRecommendations,
effectiveRankingHero);
response.Items = MergeRecommendations(projectContext, candidates, ranking, maxRecommendations);

AI 模型可能出现响应慢或暂时不可用的情况。毕竟再好的系统,也有掉链子的时候。为此,系统设计了确定性回退机制:当 AI 服务不可用时,使用基于规则启发式算法生成推荐,比如根据 package.json 中的依赖推断可能需要的技能。

这个回退机制,说穿了,也就是给系统留了一条后路罢了。

授信提供者管理允许用户控制哪些技能源是可信的。毕竟信任这东西,还是要自己把握的。

授信提供者支持两种匹配规则:精确匹配(exact)和前缀匹配(prefix)。

public static TrustedSkillProviderResolutionSnapshot Resolve(
TrustedSkillProviderSnapshot snapshot,
string source)
{
var normalizedSource = Normalize(source);
foreach (var entry in snapshot.Entries.OrderBy(e => e.SortOrder))
{
if (!entry.IsEnabled) continue;
foreach (var rule in entry.MatchRules)
{
bool isMatch = rule.MatchType switch
{
TrustedSkillProviderMatchRuleType.Exact
=> string.Equals(normalizedSource, Normalize(rule.Value),
StringComparison.OrdinalIgnoreCase),
TrustedSkillProviderMatchRuleType.Prefix
=> normalizedSource.StartsWith(Normalize(rule.Value) + "/",
StringComparison.OrdinalIgnoreCase),
_ => false
};
if (isMatch)
return new TrustedSkillProviderResolutionSnapshot
{
IsTrustedSource = true,
ProviderId = entry.ProviderId,
DisplayName = entry.DisplayName
};
}
}
return new TrustedSkillProviderResolutionSnapshot { IsTrustedSource = false };
}

预置的授信提供者包括 Vercel、Azure、anthropics、Microsoft、browser-use 等知名组织和项目。自定义提供者可以通过配置文件添加,指定提供者 ID、显示名称、徽章标签、匹配规则等。毕竟世界那么大,不可能只有几家是可信的。

授信配置使用 Orleans Grain 持久化存储:

public class TrustedSkillProviderGrain : Grain<TrustedSkillProviderState>,
ITrustedSkillProviderGrain
{
public async Task UpdateConfigurationAsync(TrustedSkillProviderSnapshot snapshot)
{
State.Snapshot = snapshot;
await WriteStateAsync();
}
public Task<TrustedSkillProviderSnapshot> GetConfigurationAsync()
{
return Task.FromResult(State.Snapshot);
}
}

这种方式的好处是配置变更会自动同步到所有节点,无需手动刷新缓存。毕竟自动化,说到底也是为了让人少操心罢了。

Skill 系统需要执行各种 npx 命令,如果把这些逻辑散落在各处,代码会变得难以维护。因此我们设计了适配器接口。设计模式这东西,说到底,也还是为了让代码更好维护而已:

public interface ISkillInstallCommandRunner
{
Task<SkillInstallCommandExecutionResult> ExecuteAsync(
SkillInstallCommandExecutionRequest request,
CancellationToken cancellationToken = default);
}

不同的命令有不同的执行器实现,全部实现同一个接口,便于测试和替换。

安装进度通过 Server-Sent Events 实时推送到前端:

public async Task InstallWithProgressAsync(
SkillInstallRequestDto request,
IServerStreamWriter<SkillInstallProgressEventDto> stream,
CancellationToken cancellationToken)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "npx",
Arguments = $"skills add {request.FullReference} -g -y",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
}
};
process.OutputDataReceived += async (sender, e) =>
{
await stream.WriteAsync(new SkillInstallProgressEventDto
{
EventType = "output",
Data = e.Data ?? string.Empty
});
};
process.Start();
process.BeginOutputReadLine();
await process.WaitForExitAsync(cancellationToken);
}

用户在前端可以看到类似终端的实时输出,体验非常直观。毕竟实时反馈,让人安心。

以安装 pua 技能为例(这是一个流行的社区技能):

  1. 打开 Skills 抽屉,切换到「Skill Gallery」标签
  2. 输入「pua」进行搜索
  3. 点击搜索结果查看技能详情
  4. 点击「Install」按钮安装
  5. 切换到「Local Skills」标签确认安装成功

安装命令是 npx skills add tanweai/pua -g -y,系统会自动处理所有细节。其实也没那么多步骤,一步步来就是了。

如果你的团队有自己的技能仓库,可以添加为授信来源:

providerId: "my-team"
displayName: "My Team Skills"
badgeLabel: "MyTeam"
isEnabled: true
sortOrder: 100
matchRules:
- matchType: "prefix"
value: "my-team/"
- matchType: "exact"
value: "my-team/special-skill"

这样来自你团队的所有技能都会显示授信徽章,用户可以更放心地安装。毕竟有标记的东西,总是让人安心一些。

创建自定义技能需要遵循以下结构:

my-skill/
├── SKILL.md # 技能元数据(YAML front matter)
├── index.ts # 技能入口
├── agents/ # 支持的代理配置
└── references/ # 参考资源

SKILL.md 的格式示例:

---
name: my-skill
description: A brief description of what this skill does
---
# My Skill
Detailed documentation...
  1. 网络要求:技能搜索和安装需要能访问 api.hagicode.com 和 npm registry
  2. Node.js 版本:建议使用 Node.js 18 或更高版本
  3. 权限要求:需要全局 npm 安装权限
  4. 并发控制:同一技能同时只能有一个安装或卸载操作在执行
  5. 超时设置:安装操作默认超时时间为 4 分钟,复杂场景可能需要调整

这些注意事项,说到底,也还是为了让事情顺利进行罢了。

本文介绍了 HagiCode 项目中 Skill 管理系统的完整实现。这个系统通过前后端分离的架构、适配器模式、Orleans 分布式状态管理等技术手段,实现了:

  • 本地全局管理:通过封装 npx skills 命令,提供统一的技能管理接口
  • 市场搜索:利用在线 API 和缓存机制,快速发现社区技能
  • 智能推荐:结合 AI 能力,根据项目上下文推荐最合适的技能
  • 授信管理:灵活的配置系统,让用户掌控信任边界

这套设计思路不仅适用于 Skill 管理,对于任何需要集成命令行工具、兼顾本地存储和在线服务的场景,都有参考价值。

如果本文对你有帮助,欢迎来 GitHub 给个 Star:github.com/HagiCode-org/site。也可以访问官网了解更多:hagicode.com

或许你也会觉得,这套系统设计得还行,或许你不会。但这都罢了,毕竟代码写出来,总有人会用,也总有人不会用…

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。 本内容采用人工智能辅助协作,最终内容由作者审核并确认。

HagiCode 平台的多 AI Provider 架构实践

HagiCode 平台的多 AI Provider 架构实践

Section titled “HagiCode 平台的多 AI Provider 架构实践”

本文分享了在 Orleans Grain 架构下,如何通过统一的 IAIProvider 接口集成 iflow 和 OpenCode 两个 AI 工具的技术方案,并详细对比了 WebSocket 和 HTTP 两种通信方式的实现差异。

其实也没什么特别的,就是做 HagiCode 的时候遇到了个挺实际的问题——用户想用不同的 AI 工具,这倒也不奇怪,毕竟每个人都有自己的习惯。有的喜欢 Claude Code,有的钟爱 GitHub Copilot,还有些团队用自己开发的工具。

最开始的方案也挺简单粗暴的,就是给每个 AI 工具写专门的对接代码。可后来问题就来了——代码里全是 if-else,改一个地方要到处测试,新工具接入还得重新写一堆逻辑,想想都觉得累。

后来我想明白了,不如做一个统一的 IAIProvider 接口,把所有 AI 提供者的能力都抽象出来。这样,不管底层用的是哪个工具,对上层来说都是一样的调用方式,岂不美哉?

最近项目要接入两个新工具:iflow 和 OpenCode。这两个都支持 ACP 协议,但通信方式不太一样——iflow 用 WebSocket,OpenCode 用 HTTP API。这也算是种考验吧,要在统一的接口下适配两种不同的通信模式,不过想想也挺有意思的。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个基于 Orleans Grain 架构的 AI 辅助开发平台,通过统一的 IAIProvider 接口与不同的 AI 提供者集成,让用户可以灵活选择自己喜欢的 AI 工具。

首先,定义了 IAIProvider 接口,把所有 AI 提供者需要实现的能力都抽象出来:

public interface IAIProvider
{
string Name { get; }
bool SupportsStreaming { get; }
ProviderCapabilities Capabilities { get; }
Task<AIResponse> ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
IAsyncEnumerable<AIStreamingChunk> StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<AIStreamingChunk> SendMessageAsync(AIRequest request, string? embeddedCommandPrompt = null, CancellationToken cancellationToken = default);
}

这个接口有几个关键方法:

  • ExecuteAsync:执行一次性的 AI 请求
  • StreamAsync:流式获取响应,支持实时展示
  • PingAsync:健康检查,验证 provider 是否可用
  • SendMessageAsync:发送消息,支持嵌入式命令

IFlowCliProvider:基于 WebSocket 的实现

Section titled “IFlowCliProvider:基于 WebSocket 的实现”

iflow 使用 WebSocket 进行 ACP 通信,整体架构是这样的:

IFlowCliProvider → ACPSessionManager → WebSocketAcpTransport → iflow CLI
动态端口分配 + 进程管理

核心流程也挺简单:

  1. ACPSessionManager 负责创建和管理 ACP 会话
  2. WebSocketAcpTransport 处理 WebSocket 通信
  3. 动态分配一个端口,用 iflow —experimental-acp —port 启动 iflow 进程
  4. 通过 IAIRequestToAcpMapper 和 IAcpToAIResponseMapper 做请求/响应的转换

来看看核心代码:

private async IAsyncEnumerable<AIStreamingChunk> StreamCoreAsync(
AIRequest request,
string? embeddedCommandPrompt,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// 解析工作目录
var resolvedWorkingDirectory = ResolveWorkingDirectory(request);
var effectiveRequest = ApplyEmbeddedCommandPrompt(request, embeddedCommandPrompt);
// 创建 ACP 会话
await using var session = await _sessionManager.CreateSessionAsync(
Name,
resolvedWorkingDirectory,
cancellationToken,
request.SessionId);
// 发送提示词
var prompt = _requestMapper.ToPromptString(effectiveRequest);
var promptResponse = await session.SendPromptAsync(prompt, cancellationToken);
// 接收流式响应
await foreach (var notification in session.ReceiveUpdatesAsync(cancellationToken))
{
if (_responseMapper.TryConvertToStreamingChunk(notification, out var chunk))
{
if (chunk.Type == StreamingChunkType.Metadata && chunk.IsComplete)
{
yield return chunk;
yield break;
}
yield return chunk;
}
}
}

这里有几个设计上的注意点,也算是一些小心得:

  • 用 await using 确保会话正确释放,避免资源泄漏,毕竟资源这东西,不用了就该放归自然
  • 流式响应通过 IAsyncEnumerable 返回,天然支持异步流
  • Metadata 类型的 chunk 判断是否完成,确保完整接收响应

OpenCodeCliProvider:基于 HTTP API 的实现

Section titled “OpenCodeCliProvider:基于 HTTP API 的实现”

OpenCode 用 HTTP API 方式提供服务,架构略有不同:

OpenCodeCliProvider → OpenCodeRuntimeManager → OpenCodeClient → OpenCode HTTP API
OpenCodeProcessManager → opencode 进程管理

OpenCode 的特点是用 SQLite 数据库持久化会话绑定关系,这样可以支持会话恢复和提示词响应恢复,这倒是挺贴心的设计:

private async Task<OpenCodePromptExecutionResult> ExecutePromptAsync(
AIRequest request,
string? embeddedCommandPrompt,
CancellationToken cancellationToken)
{
var prompt = BuildPrompt(request, embeddedCommandPrompt);
var resolvedWorkingDirectory = ResolveWorkingDirectory(request.WorkingDirectory);
var client = await _runtimeManager.GetClientAsync(resolvedWorkingDirectory, cancellationToken);
var bindingSessionId = request.SessionId;
var boundSession = TryGetBinding(bindingSessionId, resolvedWorkingDirectory);
// 尝试使用已绑定的会话
if (boundSession is not null)
{
try
{
return await PromptSessionAsync(
client,
boundSession,
BuildPromptRequest(request, prompt, CreatePromptMessageId()),
request.Model ?? _settings.Model,
cancellationToken);
}
catch (OpenCodeApiException ex) when (IsStaleBinding(ex))
{
// 会话已过期,移除绑定
RemoveBinding(bindingSessionId);
}
}
// 创建新会话
var session = await client.Session.CreateAsync(new OpenCodeSessionCreateRequest
{
Title = BuildSessionTitle(request)
}, cancellationToken);
BindSession(bindingSessionId, session.Id, resolvedWorkingDirectory);
return await PromptSessionAsync(client, session.Id, ...);
}

这个实现有几个亮点,或者说几个有趣的地方:

  • 会话绑定机制:同一个 SessionId 会复用 OpenCode 会话,避免重复创建,省得浪费资源
  • 过期处理:检测到会话过期时自动清理绑定,旧的不去,新的不来
  • 数据库持久化:通过 SQLite 存储绑定关系,重启后仍然有效,有些东西记住了就是记住了
方面IFlowCliProviderOpenCodeCliProvider
通信方式WebSocket (ACP)HTTP API
进程管理ACPSessionManagerOpenCodeProcessManager
端口分配动态端口无端口(使用 HTTP)
会话管理ACPSessionOpenCodeSession
持久化内存缓存SQLite 数据库
启动命令iflow —experimental-acp —portopencode
延迟更低(长连接)相对较高(HTTP 请求)

选择哪种方式主要看你的需求:WebSocket 适合实时性要求高的场景,HTTP API 则更简单、更容易调试。这就像选路一样,有的路快一点,有的路好走一点罢了。

先在配置文件里启用这两个 provider:

AI:
Providers:
IFlowCli:
Type: "IFlowCli"
Enabled: true
ExecutablePath: "iflow"
Model: null
WorkingDirectory: null
OpenCodeCli:
Type: "OpenCodeCli"
Enabled: true
ExecutablePath: "opencode"
Model: "anthropic/claude-sonnet-4"
WorkingDirectory: null
OpenCode:
Enabled: true
BaseUrl: "http://localhost:38376"
ExecutablePath: "opencode"
StartupTimeoutSeconds: 30
RequestTimeoutSeconds: 120
// 通过 Factory 获取 provider
var provider = await _providerFactory.GetProviderAsync(AIProviderType.IFlowCli);
// 执行 AI 请求
var request = new AIRequest
{
Prompt = "请帮我重构这个函数",
WorkingDirectory = "/path/to/project",
Model = "claude-sonnet-4"
};
// 获取完整响应
var response = await provider.ExecuteAsync(request, cancellationToken);
Console.WriteLine(response.Content);
// 或者用流式响应
await foreach (var chunk in provider.StreamAsync(request, cancellationToken))
{
if (chunk.Type == StreamingChunkType.ContentDelta)
{
Console.Write(chunk.Content);
}
}
// 通过 Factory 获取 provider
var provider = await _providerFactory.GetProviderAsync(AIProviderType.OpenCodeCli);
var request = new AIRequest
{
Prompt = "请帮我分析这个错误",
WorkingDirectory = "/path/to/project",
Model = "anthropic/claude-sonnet-4"
};
var response = await provider.ExecuteAsync(request, cancellationToken);
Console.WriteLine(response.Content);

在启动或使用前,可以先检查 provider 是否可用:

var iflowResult = await iflowProvider.PingAsync(cancellationToken);
if (!iflowResult.Success)
{
Console.WriteLine($"IFlow 不可用: {iflowResult.ErrorMessage}");
return;
}
var openCodeResult = await openCodeProvider.PingAsync(cancellationToken);
if (!openCodeResult.Success)
{
Console.WriteLine($"OpenCode 不可用: {openCodeResult.ErrorMessage}");
return;
}

两个 provider 都支持嵌入式命令,比如 /file:xxx 这样的命令:

var request = new AIRequest
{
Prompt = "分析这个文件的问题",
SystemMessage = "你是一个代码分析专家"
};
await foreach (var chunk in provider.SendMessageAsync(
request,
embeddedCommandPrompt: "/file:src/main.cs",
cancellationToken))
{
Console.Write(chunk.Content);
}

IFlow 用 WebSocket 长连接,所以资源管理要特别注意:

  • 用 await using 确保会话正确释放,不用了就放手
  • 取消操作会触发进程清理
  • ACPSessionManager 支持最大会话数限制

OpenCode 的进程管理相对简单,OpenCodeRuntimeManager 会自动处理,省心不少。

两个 provider 都有完善的错误处理:

  • IFlow 的错误通过 ACP 会话更新传播
  • OpenCode 的错误通过 OpenCodeApiException 抛出
  • 建议在调用方捕获并处理这些异常,毕竟错误总会发生的
  • IFlow 的 WebSocket 通信比 HTTP 有更低的延迟
  • OpenCode 的会话复用可以减少 HTTP 请求开销
  • Factory 的缓存机制可以避免重复创建 provider
  • 高并发场景下,要关注进程数和连接数的限制,别到时候撑不住了

启动时会验证可执行文件路径,但运行时也可能出问题。PingAsync 是个好工具,可以验证配置是否正确:

// 启动时检查
var provider = await _providerFactory.GetProviderAsync(providerType);
var result = await provider.PingAsync(cancellationToken);
if (!result.Success)
{
_logger.LogError("Provider {ProviderType} 不可用: {Error}", providerType, result.ErrorMessage);
}

本文分享了 HagiCode 平台在集成 iflow 和 OpenCode 两个 AI 工具时的技术方案。通过统一的 IAIProvider 接口,实现了对不同通信方式(WebSocket 和 HTTP)的适配,同时保持了上层调用的一致性。

核心思路其实挺简单的:

  1. 定义统一的接口抽象
  2. 对不同实现做适配层
  3. 通过工厂模式统一管理

这样扩展性就很好,以后有新的 AI 工具要接入,只需要实现 IAIProvider 接口就行,不用改动太多现有代码。想想也挺合理的,就像搭积木一样,有统一的接口,想怎么拼都行。

如果你也在做多 AI 工具的集成,希望本文对你有帮助。不过话说回来,技术这东西,能帮到人就好,其他的也不必太在意…


如果本文对你有帮助:

AI Compose Commit:用 AI 智能重构 Git 提交工作流

AI Compose Commit:用 AI 智能重构 Git 提交工作流

Section titled “AI Compose Commit:用 AI 智能重构 Git 提交工作流”

在软件开发过程中,提交代码是程序员每天都要面对的日常工作。可是你有没有经历过这样的场景:一天工作结束后,打开 Git 看到几十个未暂存的修改文件,却不知道该如何将它们组织成合理的提交?

传统的方式是手动将文件分批暂存、逐个提交、撰写提交信息,这个过程既耗时又容易出错。咱们就常常在这上面浪费了不少时间,毕竟谁也不想在已经疲惫的晚上还要为这些琐事烦心。

我们在 HagiCode 项目中推出了一项新功能——AI Compose Commit,旨在彻底改变这个工作流程。它通过 AI 智能分析工作区中的所有未提交变更,自动将它们分组为多个逻辑提交,并执行符合规范的提交操作。本文将深入探讨这个功能的实现原理、技术架构以及我们在实践中遇到的挑战与解决方案。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。

Git 作为版本控制系统,为开发者提供了强大的代码管理能力。但在实际使用中,提交操作往往成为开发流程中的瓶颈:

  1. 手动分组耗时: 当有大量文件变更时,开发者需要逐个检查文件内容,判断哪些属于同一个功能,这需要耗费大量脑力
  2. 提交信息质量参差: 撰写符合 Conventional Commits 规范的提交信息需要经验和技巧,新手常常写出不规范的提交
  3. 多仓库管理复杂: 在 monorepo 环境中,需要在不同仓库间切换,增加了操作复杂度
  4. 工作流被打断: 提交代码会打断开发思路,影响编码效率

这些问题在大型项目和团队协作环境中尤为明显。一个优秀的开发工具应该让开发者专注于核心的编码工作,而不是被繁琐的提交流程所困扰。

近年来,AI 技术在软件开发领域的应用日益广泛。从代码补全、错误检测到自动生成文档,AI 正在逐步渗透到开发的各个环节。在 Git 工作流方面,虽然已有一些工具提供提交信息生成的功能,但大多局限于单次提交的场景,缺乏对整个工作区变更的智能分析和分组能力。

其实 HagiCode 在开发过程中也遇到了这些痛点,我们曾尝试过多种工具,但都或多或少存在一些局限性。要么是功能不够完善,要么是用户体验不够好。这也是为什么我们最终决定自己实现 AI Compose Commit 功能的原因。

HagiCode 的 AI Compose Commit 功能正是为了填补这一空白而生,它不仅是生成提交信息,而是完整接管从文件分析到执行提交的整个流程。

在实现 AI Compose Commit 功能的过程中,我们面临了多个技术挑战:

  1. 文件语义理解: AI 需要理解文件变更的语义关系,判断哪些文件属于同一个功能模块。这需要深入分析文件内容、目录结构以及变更的上下文。

  2. 提交分组策略: 如何定义合理的分组标准?是按功能、按模块,还是按文件类型?不同的项目可能适用不同的策略。

  3. 实时反馈与异步处理: Git 操作可能需要较长时间,特别是处理大量文件时。如何在保证用户体验的同时完成复杂操作?

  4. 多仓库支持: 在 monorepo 架构下,需要在主仓库和子仓库之间正确路由操作。

  5. 错误处理与回滚: 如果某个提交失败,如何处理已执行的提交?是否需要回滚已暂存的文件?

  6. 提交信息一致性: 生成的提交信息需要符合项目现有的风格,保持历史提交的格式一致。

AI 处理大量文件变更会消耗显著的时间和计算资源。我们需要在以下方面进行优化:

  • 减少不必要的 AI 调用
  • 优化文件上下文的构建方式
  • 实现高效的 Git 操作批处理

这些问题在 HagiCode 的实际使用中都真实出现过,我们通过不断的迭代和优化才找到了相对完美的解决方案。如果你也在开发类似的工具,希望我们的经验能给你一些启发。

我们采用了分层架构来实现 AI Compose Commit 功能,确保系统具有良好的可扩展性和可维护性:

GitController 提供了 POST /api/git/auto-compose-commit 端点,作为功能入口。为了优化用户体验,我们采用了 Fire-and-Forget 异步模式:

  • 客户端发起请求后,服务器立即返回 HTTP 202 Accepted
  • 实际的 AI 处理在后台异步执行
  • 处理完成后通过 SignalR 通知客户端

这种设计确保了即使 AI 处理需要几分钟,用户也能立即得到响应,不会感觉系统卡顿。

GitAppService 负责核心业务逻辑:

  • 仓库检测:支持 monorepo 中的多仓库管理
  • 锁管理:防止并发操作导致的冲突
  • 文件暂存协调:与 AI 处理流程的交互
  • 错误回滚:处理失败场景下的状态恢复

AIGrain 作为 AI 操作的核心执行单元,实现了 IAIGrain 接口中的 AutoComposeCommitAsync 方法:

// 定义 AI 自动组合提交的接口方法
// 参数说明:
// - projectId: 项目唯一标识符
// - unstagedFiles: 未暂存文件列表,包含文件路径和状态信息
// - projectPath: 项目根目录路径(可选),用于访问项目上下文
// 返回值: 包含执行结果的响应对象,包括成功/失败状态和详细信息
[Alias("AutoComposeCommitAsync")]
[ResponseTimeout("00:20:00")] // 20 分钟超时,适用于处理大型变更集
Task<AutoComposeCommitResponseDto> AutoComposeCommitAsync(
string projectId,
GitFileStatusDto[] unstagedFiles,
string? projectPath = null);

这个方法设置了 20 分钟的超时时间,以处理大型变更集。HagiCode 在实际使用中发现,有些项目的单次变更可能涉及上百个文件,需要更长的处理时间。

通过抽象的 IAIService 接口,我们实现了 AI 服务的可插拔架构。目前使用 Claude Helper 服务,但可以轻松切换到其他 AI 提供商。

AI 需要了解每个文件的状态才能做出智能决策。我们通过 BuildFileChangesXml 方法构建文件上下文:

/// <summary>
/// 构建文件变更的 XML 表示形式,用于为 AI 提供完整的文件上下文信息
/// </summary>
/// <param name="stagedFiles">已暂存的文件列表,包含文件路径、状态和旧路径(针对重命名操作)</param>
/// <returns>格式化的 XML 字符串,包含所有文件的元数据信息</returns>
private static string BuildFileChangesXml(GitFileStatusDto[] stagedFiles)
{
var sb = new StringBuilder();
sb.AppendLine("<files>");
foreach (var file in stagedFiles)
{
sb.AppendLine(" <file>");
// 使用 XML 转义确保特殊字符不会破坏 XML 结构
sb.AppendLine($" <path>{System.Security.SecurityElement.Escape(file.Path)}</path>");
sb.AppendLine($" <status>{System.Security.SecurityElement.Escape(file.Status)}</status>");
// 处理文件重命名场景,记录旧路径以便 AI 理解变更关系
if (!string.IsNullOrEmpty(file.OldPath))
{
sb.AppendLine($" <oldPath>{System.Security.SecurityElement.Escape(file.OldPath)}</oldPath>");
}
sb.AppendLine(" </file>");
}
sb.AppendLine("</files>");
return sb.ToString();
}

这个 XML 格式的上下文包含文件路径、状态和旧路径(针对重命名操作),为 AI 提供了完整的元数据。通过结构化的 XML 格式,我们确保了 AI 能够准确理解每个文件的状态和变更类型。

为了让 AI 能够直接执行 Git 操作,我们配置了全面的工具权限:

// 定义 AI 可以使用的工具集合,包括文件操作和 Git 命令执行权限
// Read/Write/Edit: 文件读写和编辑能力
// Bash(git:*): 执行所有 Git 命令的权限
// 其他 Bash 命令: 用于查看文件内容和目录结构,辅助 AI 理解上下文
var allowedTools = new[]
{
"Read", "Write", "Edit",
"Bash(git:*)", "Bash(cat:*)", "Bash(ls:*)", "Bash(find:*)",
"Bash(grep:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(wc:*)"
};
// 构建完整的 AI 请求对象
var request = new AIRequest
{
Prompt = prompt, // 完整的 Prompt 模板,包含任务指令和约束条件
WorkingDirectory = projectPath ?? GetTempDirectory(), // 工作目录,确保 AI 在正确的项目上下文中执行
AllowedTools = allowedTools, // 允许使用的工具集合
PermissionMode = PermissionMode.bypassPermissions, // 绕过权限检查,允许直接执行 Git 操作
LanguagePreference = languagePreference // 语言偏好设置,确保生成符合用户期望的提交信息
};

这里使用了 PermissionMode.bypassPermissions 模式,允许 AI 直接执行 Git 命令而无需用户确认。这是功能设计的核心,但同时也需要严格的输入验证来防止滥用。HagiCode 在实际部署中,通过后端的参数验证和日志监控,确保了这个机制的安全性。

AI 执行完成后,会返回结构化的结果。我们实现了双重解析策略以确保兼容性:

/// <summary>
/// 解析 AI 返回的提交执行结果,支持分隔符格式和正则表达式格式
/// </summary>
/// <param name="aiResponse">AI 返回的原始响应内容</param>
/// <returns>解析后的提交结果列表,每个结果包含提交哈希和执行状态</returns>
private List<CommitResultDto> ParseCommitExecutionResults(string aiResponse)
{
var results = new List<CommitResultDto>();
// 优先使用分隔符解析(新格式),这种格式更加明确和可靠
if (aiResponse.Contains("---"))
{
logger.LogDebug("Using delimiter-based parsing for AI response");
results = ParseDelimitedFormat(aiResponse);
if (results.Count > 0)
{
return results; // 成功解析,直接返回结果
}
logger.LogWarning("Delimiter-based parsing produced no results, falling back to regex");
}
else
{
logger.LogDebug("No delimiter found, using legacy regex-based parsing");
}
// 回退到正则表达式解析(旧格式),确保向后兼容性
return ParseLegacyFormat(aiResponse);
}

分隔符格式使用 --- 作为提交之间的分隔,格式清晰且易于解析:

---
Commit 1: abc123def456
feat(auth): add user login functionality
Implement JWT-based authentication with login form and API endpoints.
Co-Authored-By: Hagicode <noreply@hagicode.com>
---
Commit 2: 789ghi012jkl
docs(readme): update installation instructions
Add new setup steps for Docker environment.
Co-Authored-By: Hagicode <noreply@hagicode.com>
---

这种格式设计让解析变得简单可靠,同时人类阅读也很清晰。

为了防止并发操作导致的状态冲突,我们实现了仓库锁机制:

// 获取仓库锁,防止并发操作
// 参数说明:
// - fullPath: 仓库的完整路径,用于标识不同的仓库实例
// - requestedBy: 请求者标识,用于追踪和日志记录
await _autoComposeLockService.AcquireLockAsync(fullPath, requestedBy);
try
{
// 执行 AI Compose Commit 操作
// 这部分代码会调用 Orleans Grain 的方法,执行实际的 AI 处理和 Git 操作
await aiGrain.AutoComposeCommitAsync(projectId, unstagedFiles, projectPath);
}
finally
{
// 确保锁被释放,无论操作成功或失败
// 使用 finally 块可以保证异常情况下也能释放锁,避免死锁
await _autoComposeLockService.ReleaseLockAsync(fullPath);
}

锁具有 20 分钟的超时时间,与 AI 操作的超时设置保持一致。如果操作失败或超时,系统会自动释放锁,避免永久阻塞。HagiCode 在实际使用中发现,这个锁机制非常重要,特别是在团队协作环境中,多个开发者可能同时触发 AI Compose Commit 操作。

处理完成后,系统通过 SignalR 向前端发送通知:

/// <summary>
/// 发送自动组合提交完成的通知
/// </summary>
/// <param name="projectId">项目标识符,用于路由通知到正确的客户端</param>
/// <param name="totalCount">总提交数量,包括成功和失败</param>
/// <param name="successCount">成功提交的数量</param>
/// <param name="failureCount">失败提交的数量</param>
/// <param name="success">整体操作是否成功标志</param>
/// <param name="error">错误信息(如果操作失败)</param>
private async Task SendAutoComposeCommitNotificationAsync(
string projectId,
int totalCount,
int successCount,
int failureCount,
bool success,
string? error)
{
try
{
// 构建通知数据传输对象,包含详细的执行结果
var notification = new AutoComposeCommitCompletedDto
{
ProjectId = projectId,
TotalCount = totalCount,
SuccessCount = successCount,
FailureCount = failureCount,
Success = success,
Error = error
};
// 通过 SignalR Hub 广播通知到所有连接的客户端
await messageService.SendAutoComposeCommitCompletedAsync(notification);
logger.LogInformation(
"Auto compose commit notification sent for project {ProjectId}: {SuccessCount}/{TotalCount} succeeded",
projectId, successCount, totalCount);
}
catch (Exception ex)
{
// 记录通知错误但不影响主操作流程
// 通知失败不应该导致整个操作失败
logger.LogError(ex, "Failed to send auto compose commit notification for project {ProjectId}", projectId);
}
}

前端收到通知后可以更新 UI,显示提交成功或失败的状态,提升用户体验。这种实时反馈机制在 HagiCode 的使用中获得了很好的用户反馈,用户可以清楚地知道操作何时完成以及结果如何。

AI 的行为完全由 Prompt 决定,我们精心设计了 Auto Compose Commit 的 Prompt 模板。以中文版本为例(auto-compose-commit.zh-CN.hbs):

Prompt 开头明确声明支持非交互式运行模式,这是 CI/CD 和自动化脚本的关键需求:

**重要提示**:此提示词可能在非交互式环境中运行(如 CI/CD、自动化脚本)。
**非交互式模式**:
- 禁止使用 AskUserQuestion 或任何交互式工具
- 当需要用户输入时:
- 使用合理的默认值(如提交类型使用 feat)
- 跳过可选的确认步骤
- 记录所做的假设

这个设计确保了 AI Compose Commit 功能不仅能在交互式 IDE 环境中使用,也能集成到 CI/CD 流程中,实现完全自动化的提交流程。

为了防止 AI 执行危险操作,我们在 Prompt 中添加了严格的分支保护规则:

**分支保护**:
- 禁止执行任何分支切换操作(git checkout、git switch)
- 所有 git commit 命令必须在当前分支上执行
- 不得创建、删除或重命名分支
- 不得修改未跟踪文件或未暂存变更
- 如果需要分支切换才能完成操作,应返回错误而非执行

这些规则通过约束 AI 的工具使用范围,确保操作的安全性。HagiCode 在实际测试中验证了这些约束的有效性,AI 在遇到需要分支切换的场景时会安全地返回错误,而不是执行危险操作。

Prompt 中详细定义了文件分组的决策逻辑:

**文件分组决策树**:
├── 是否为配置文件(package.json、tsconfig.json、.env 等)?
│ ├── 是 → 独立提交(类型:chore 或 build)
│ └── 否 → 继续
├── 是否为文档文件(README.md、*.md、docs/**)?
│ ├── 是 → 独立提交(类型:docs)
│ └── 否 → 继续
├── 是否与同一功能相关?
│ ├── 是 → 合并到同一提交
│ └── 否 → 分别提交
└── 是否为跨模块变更?
├── 是 → 按模块分组
└── 否 → 按功能分组

这个决策树为 AI 提供了清晰的分组逻辑,确保生成的提交符合语义合理性。HagiCode 在实际使用中发现,这个决策树能够处理绝大多数常见场景,生成的分组结果符合开发者预期。

为了让提交信息与项目历史保持一致,Prompt 要求 AI 在生成前分析最近的提交历史:

**历史格式一致性**:在生成提交信息之前,你**必须**分析当前仓库的提交历史以匹配现有风格
1. 使用 git log -n 15 --pretty=format:"%H|%s|%b%n---%n" 获取最近的提交历史
2. 分析提交以识别:
- 结构模式:项目是否使用多段落?是否有 "Changes:" 或 "Capabilities:" 部分?
- 语言模式:提交信息是英文、中文还是混合?
- 常用类型:最常使用哪些提交类型(feat、fix、docs 等)?
- 特殊格式:是否有 Co-Authored-By 行?其他项目特定的约定?
3. 生成遵循检测到的模式的提交信息

这个分析确保了 AI 生成的提交信息不会显得突兀,而是与项目的提交历史保持风格一致。在 HagiCode 的多语言项目中,这个功能特别重要,它能够根据项目的提交历史自动选择合适的语言和格式。

每个提交必须包含 Co-Authored-By 信息:

**重要**:每个提交必须添加 Co-Authored-By 信息
- 使用以下格式:git commit -m "type(scope): subject" -m "" -m "Co-Authored-By: Hagicode <noreply@hagicode.com>"
- 或者直接在提交信息中包含 Co-Authored-By 行

这不仅是为了贡献规范,也是为了追踪 AI 辅助的提交历史。HagiCode 将这个要求作为强制规则,确保所有 AI 生成的提交都带有明确的来源标识。

完整的 AI Compose Commit 工作流程如下:

  1. 用户触发: 用户在 Git Status 面板或 Quick Actions Zone 点击”AI Auto Compose Commit”按钮
  2. API 请求: 前端发送 POST 请求到 /api/git/auto-compose-commit 端点
  3. 立即响应: 服务器返回 HTTP 202 Accepted,不等待处理完成
  4. 后台处理:
    • GitAppService 获取仓库锁
    • 调用 AIGrain 的 AutoComposeCommitAsync 方法
    • 构建文件上下文 XML
    • 执行 AI Prompt,让 AI 分析并执行提交
  5. AI 执行:
    • 使用 Git 命令获取所有未暂存变更
    • 读取文件内容理解变更性质
    • 按语义关系对文件分组
    • 对每组执行 git addgit commit 操作
  6. 结果解析: 解析 AI 返回的执行结果
  7. 通知发送: 通过 SignalR 通知前端
  8. 锁释放: 无论成功或失败,都释放仓库锁

这个流程的设计确保了用户可以在发起操作后立即继续其他工作,而不需要等待 AI 处理完成。HagiCode 的用户反馈表明,这种异步处理方式大大提升了工作流体验。

我们实现了多层级的错误处理:

// 验证请求参数的有效性,防止无效请求到达后端处理逻辑
if (request.UnstagedFiles == null || request.UnstagedFiles.Count == 0)
{
return BadRequest(new
{
message = "No unstaged files provided. Please make changes in the working directory first.",
status = "validation_failed"
});
}

如果 AI 处理过程中出现错误,系统会执行回滚操作,将已暂存的文件取消暂存,避免留下不一致的状态。这个机制在 HagiCode 的实际使用中挽救了多次意外中断,确保了仓库状态的完整性。

20 分钟的超时设置确保了长时间运行的操作不会无限期阻塞资源。超时后,系统会释放锁并通知用户操作失败。HagiCode 在实际使用中发现,大部分操作能够在 2-5 分钟内完成,只有处理超大型变更集时才会接近超时限制。

AI Compose Commit 最适合以下场景:

  • 一天工作结束后,批量处理多个文件的变更
  • 重构操作后,多个相关文件需要分别提交
  • 功能开发完成,需要将相关变更分组提交

不适合以下场景:

  • 单个文件的快速提交(直接使用普通提交更快)
  • 需要精确控制提交内容的场景
  • 包含敏感信息的提交(需要人工审核)

虽然 AI 智能分组很强大,但开发者仍应审查生成的提交:

  • 检查提交的分组是否符合预期
  • 验证提交信息的准确性
  • 确认没有遗漏或错误包含文件

如果发现不合理的分组,可以使用 git reset --soft HEAD~N 撤销后重新分组。HagiCode 的经验表明,即使 AI 分组很智能,人工审查仍然是有价值的,特别是在重要的功能提交时。

确保项目的 Git 配置支持 Conventional Commits:

Terminal window
# 安装 commitlint
npm install -g @commitlint/cli @commitlint/config-conventional
# 配置 commitlint
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

这样可以在 CI/CD 流程中验证提交信息格式,与 AI Compose Commit 生成的格式保持一致。

如果你想在项目中实现类似的 AI 辅助提交功能,以下是我们的建议:

先实现单次提交信息生成,再逐步扩展到多提交分组功能。这样更容易验证和迭代。HagiCode 也是按照这个路径逐步完善功能的,早期版本只支持单次提交,后来才扩展到多提交智能分组。

不要自己实现 AI 调用逻辑,使用现有的 SDK 可以减少开发时间和潜在 bug。我们使用了 Claude Helper 服务,它提供了稳定的接口和完善的错误处理。

Prompt 的质量直接决定了 AI 输出的质量。投入时间设计详细的 Prompt,包括:

  • 明确的任务描述
  • 具体的输出格式要求
  • 边界情况的处理规则
  • 示例说明

HagiCode 在 Prompt 设计上投入了大量时间,这是功能成功的关键因素之一。

AI 操作可能因为各种原因失败(网络问题、API 限流、内容审查等)。确保你的系统能够优雅地处理这些错误,并提供有意义的错误信息。

不要完全自动化,给用户保留控制权。提供查看分组结果、调整分组、手动编辑提交信息等选项,平衡自动化与灵活性。HagiCode 虽然实现了自动执行,但仍然保留了预览和调整的能力。

在构建文件上下文时,过滤掉不需要 AI 分析的文件:

// 过滤掉自动生成的文件和过大的文件,减少 AI 处理负担
var relevantFiles = stagedFiles
.Where(f => !IsGeneratedFile(f.Path))
.Where(f => !IsLargeFile(f.Path))
.ToArray();

如果支持多个独立仓库,可以并行处理不同仓库的提交,提高整体效率。

缓存项目提交历史分析结果,避免每次都重新分析。可以在配置文件中存储历史格式偏好,减少 AI 调用次数。

AI Compose Commit 功能代表了 AI 技术在软件开发工具中的深度应用。通过智能分析文件变更、自动分组提交、生成规范的提交信息,它显著提升了 Git 工作流的效率,让开发者能够更专注于核心的编码工作。

在实现过程中,我们学到了几个重要的经验:

  1. 用户反馈是关键: 早期版本采用同步等待方式,用户反馈体验不佳,改为 Fire-and-Forget 模式后满意度大幅提升
  2. Prompt 设计决定质量: 一个精心设计的 Prompt 比复杂的算法更能保证 AI 输出的质量
  3. 安全永远是第一位的: 虽然赋予 AI 直接执行 Git 命令的权限带来了效率提升,但必须配合严格的约束和验证
  4. 渐进式改进: 从简单场景开始,逐步增加复杂度,比一次性实现所有功能更容易成功

未来,我们计划进一步优化 AI Compose Commit 功能,包括:

  • 支持更多提交分组策略(按时间、按开发者等)
  • 集成代码审查流程,在提交前自动触发审查
  • 支持自定义提交信息模板,满足不同项目的个性化需求

如果你觉得本文分享的方案有价值,不妨也试试 HagiCode,体验一下这个功能在实际开发中的效果。毕竟实践是检验真理的唯一标准嘛。


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。

.NET Core 双数据库实战:优雅融合 PostgreSQL 与 SQLite 的最佳实践

.NET Core 双数据库实战:让 PostgreSQL 与 SQLite 和平共处

Section titled “.NET Core 双数据库实战:让 PostgreSQL 与 SQLite 和平共处”

在构建现代化应用时,我们经常面临这样的抉择:开发环境渴望轻量便捷,而生产环境则需要高并发与高可用。本文将分享如何在 .NET Core 项目中优雅地同时支持 PostgreSQL 和 SQLite,实现“开发用 SQLite,生产用 PG”的最佳实践。

在软件开发中,环境差异化一直是困扰开发团队的难题之一。以我们正在构建的 HagiCode 平台为例,这是一个基于 ASP.NET Core 10 和 React 的 AI 辅助开发系统,内部集成了 Orleans 进行分布式状态管理,技术栈相当现代且复杂。

在项目初期,我们遇到了一个典型的工程痛点:开发人员希望本地环境能够“开箱即用”,不希望安装和配置繁重的 PostgreSQL 数据库;但在生产环境中,我们需要处理高并发写入和复杂的 JSON 查询,这时轻量级的 SQLite 又显得力不从心。

如何在保持代码库统一的前提下,让应用既能像客户端软件一样利用 SQLite 的便携性,又能像企业级服务一样发挥 PostgreSQL 的强悍性能?这就是本文要探讨的核心问题。

本文分享的双数据库适配方案,直接来源于我们在 HagiCode 项目中的实战经验。HagiCode 是一个集成了 AI 提示词管理和 OpenSpec 工作流的下一代开发平台。正是为了兼顾开发者的体验和生产环境的稳定性,我们探索出了这套行之有效的架构模式。

欢迎访问我们的 GitHub 仓库了解项目全貌:HagiCode-org/site

核心内容一:架构设计与统一抽象

Section titled “核心内容一:架构设计与统一抽象”

要在 .NET Core 中实现双数据库支持,核心思想是“依赖抽象而非具体实现”。我们需要把数据库的选择权从业务代码中剥离出来,交给配置层决定。

  1. 统一接口:所有的业务逻辑都应依赖于 DbContext 基类或自定义的接口,而不是具体的 PostgreSqlDbContext
  2. 配置驱动:通过 appsettings.json 中的配置项,在应用启动时动态决定加载哪个数据库提供程序。
  3. 特性隔离:针对 PostgreSQL 特有的功能(如 JSONB)进行适配处理,确保在 SQLite 中也能降级运行。

在 ASP.NET Core 的 Program.cs 中,我们不应硬编码 UseNpgsqlUseSqlite。相反,我们应该读取配置来动态决定。

首先,定义配置类:

public class DatabaseSettings
{
public const string SectionName = "Database";
// 数据库类型:PostgreSQL 或 SQLite
public string DbType { get; set; } = "PostgreSQL";
// 连接字符串
public string ConnectionString { get; set; } = string.Empty;
}

然后,在 Program.cs 中根据配置注册服务:

// 读取配置
var databaseSettings = builder.Configuration.GetSection(DatabaseSettings.SectionName).Get<DatabaseSettings>();
// 注册 DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
if (databaseSettings?.DbType?.ToLower() == "sqlite")
{
// SQLite 配置
options.UseSqlite(databaseSettings.ConnectionString);
// SQLite 的并发写入限制处理
// 注意:在生产环境中建议开启 WAL 模式以提高并发性能
}
else
{
// PostgreSQL 配置(默认)
options.UseNpgsql(databaseSettings.ConnectionString, npgsqlOptions =>
{
// 开启 JSONB 支持,这在处理 AI 对话记录时非常有用
npgsqlOptions.UseJsonNet();
});
// 配置连接池重连策略
options.EnableRetryOnFailure(3);
}
});

核心内容二:处理差异性与迁移策略

Section titled “核心内容二:处理差异性与迁移策略”

PostgreSQL 和 SQLite 虽然都支持 SQL 标准,但在具体特性和行为上存在显著差异。如果不处理好这些差异,很可能会出现“本地跑得通,上线就报错”的尴尬情况。

在 HagiCode 中,我们需要存储大量的提示词和 AI 元数据,这通常涉及 JSON 列。

  • PostgreSQL:拥有原生的 JSONB 类型,查询性能极佳。
  • SQLite:没有原生的 JSON 类型(新版本有 JSON1 扩展,但对象映射上仍有差异),通常存储为 TEXT。

解决方案: 在 EF Core 的实体映射中,我们将其配置为可转换的类型。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置实体
modelBuilder.Entity<PromptTemplate>(entity =>
{
entity.Property(e => e.Metadata)
.HasColumnType("jsonb") // PG 使用 jsonb
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Dictionary<string, object>>(v, (JsonSerializerOptions)null)
);
});
}

当使用 SQLite 时,虽然 HasColumnType("jsonb") 会被忽略或产生警告,但由于配置了 HasConversion,数据会被正确地序列化和反序列化为字符串存入 TEXT 字段,从而保证了兼容性。

绝对不要试图让同一套 Migration 脚本同时适配 PG 和 SQLite。由于主键生成策略、索引语法等的不同,这必然会导致失败。

推荐实践: 维护两个迁移分支或项目。在 HagiCode 的开发流中,我们是这样处理的:

  1. 开发阶段:主要在 SQLite 下工作。使用 Add-Migration Init_Sqlite -OutputDir Migrations/Sqlite
  2. 适配阶段:开发完一段功能后,切换连接字符串指向 PostgreSQL,执行 Add-Migration Init_Postgres -OutputDir Migrations/Postgres
  3. 自动化脚本:编写一个简单的 PowerShell 或 Bash 脚本,根据当前环境变量自动应用对应的迁移。
Terminal window
# 简单的部署逻辑伪代码
if [ "$DATABASE_PROVIDER" = "PostgreSQL" ]; then
dotnet ef database update --project Migrations.Postgres
else
dotnet ef database update --project Migrations.Sqlite
fi

核心内容三:HagiCode 的实战经验总结

Section titled “核心内容三:HagiCode 的实战经验总结”

在将 HagiCode 从单一数据库重构为双数据库支持的过程中,我们踩过一些坑,也总结了一些关键的经验,希望能给大家避坑。

PostgreSQL 是服务端-客户端架构,支持高并发写入,事务隔离级别非常强大。而 SQLite 是文件锁机制,写入操作会锁定整个数据库文件(除非开启 WAL 模式)。

建议: 在编写涉及频繁写入的业务逻辑时(例如实时保存用户的编辑状态),一定要考虑到 SQLite 的锁机制。在设计 HagiCode 的 OpenSpec 协作模块时,我们引入了“写前合并”机制,减少数据库的直接写入频率,从而在两种数据库下都能保持高性能。

PostgreSQL 的连接建立成本较高,依赖连接池。而 SQLite 连接非常轻量,但如果不及时释放,文件锁可能会导致后续操作超时。

Program.cs 中,我们可以针对不同数据库做精细化调整:

if (databaseSettings?.DbType?.ToLower() == "sqlite")
{
// SQLite:保持连接开启能提升性能,但要注意文件锁
options.UseSqlite(connectionString, sqliteOptions =>
{
// 设置命令超时时间
sqliteOptions.CommandTimeout(30);
});
}
else
{
// PG:利用连接池
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.MaxBatchSize(100);
npgsqlOptions.CommandTimeout(30);
});
}

很多开发者(包括我们团队早期的成员)容易犯一个错误:只在开发环境(通常是 SQLite)跑单元测试。

我们在 HagiCode 的 CI/CD 流水线中强制加入了 GitHub Action 步骤,确保每次 Pull Request 都要跑过 PostgreSQL 的集成测试。

# .github/workflows/test.yml 示例片段
- name: Run Integration Tests (PostgreSQL)
run: |
docker-compose up -d db_postgres
dotnet test --filter "Category=Integration"

这帮我们拦截了无数次关于 SQL 语法差异、大小写敏感性的 Bug。

通过引入抽象层和配置驱动的依赖注入,我们在 HagiCode 项目中成功实现了 PostgreSQL 和 SQLite 的“双轨制”运行。这不仅极大降低了新开发者的上手门槛(不需要装 PG),也为生产环境提供了坚实的性能保障。

回顾一下关键点:

  1. 抽象至上:业务代码不依赖具体数据库实现。
  2. 配置分离:开发和生产使用不同的 appsettings.json
  3. 迁移分离:不要尝试一套 Migration 走天下。
  4. 特性降级:在 SQLite 中以兼容性优先,在 PostgreSQL 中以性能优先。

这种架构模式不仅适用于 HagiCode,也适用于任何需要在轻量级开发和重量级生产之间寻找平衡的 .NET 项目。


如果本文对你有帮助,欢迎来 GitHub 给个 Star,或者直接体验 HagiCode 带来的高效开发流程:

公测已开始,欢迎安装体验!


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮👍,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。