跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

开发指南

最近更新:

从源码运行,理解架构并扩展能力。

本章面向准备修改 AGW 源码或接入 API 的开发者。只想安装和使用 AGW,可以先阅读快速开始。

建议先完成开发环境,再用架构与模块边界判断代码应放在哪里。接入客户端时阅读API 与执行协议,新增能力时阅读扩展 Tools 与 Integrations。提交改动前,按测试与贡献约定完成相关验证。

1 - 开发环境与运行

最近更新:

安装依赖,独立运行后端、Web、Desktop 或 Mobile。

前提:.NET 10 SDK、Node.js 24、pnpm 12.5.1(版本由 src/clients/package.json 的 packageManager 指定)和 Git。只有构建容器镜像时才需要 Docker Buildx。以下应用命令在 AGW 仓库执行,文档站本身不依赖这些工具链。

后端

git clone https://github.com/zxyao145/agw.git
cd agw
git config core.hooksPath .githooks
dotnet restore Agw.slnx
dotnet tool restore
dotnet run --project src/server/Agw.Standalone.Host

在 http://localhost:30816/setup 初始化。热重载可将 dotnet run 改为 dotnet watch。

客户端

另开终端,从仓库根进入 workspace:

cd src/clients
pnpm install
pnpm dev:web

打开 http://localhost:3001。Desktop 使用 pnpm dev:desktop,其独立 renderer 运行在 3000,不需要同时运行 Web。Mobile 使用 pnpm dev:mobile,或 pnpm android:mobile / pnpm ios:mobile;原生工程由 Expo CNG 生成。

启动后应该看到什么

保留后端终端运行,再启动需要的客户端。Web 页面能打开后,先确认 Server 初始化和登录,再按第一次对话配置模型。仅看到前端页面还不能说明后端连接正常。

后端默认端口是 30816,Web 开发端口是 3001,Desktop 界面开发端口是 3000。端口被占用时先确认是否已有开发进程,不要误把其他服务当成 AGW。手机上的 localhost 指手机本身,真机连接需使用手机可访问的电脑地址。

验证

确认后端初始化完成,客户端能连接并运行一条简单消息。移动真机需要可达的后端地址;外部 CLI 需在执行进程环境下可用。

运行站点需要 Hugo Extended 与 Go,运行检查脚本 scripts/check-site.py 还需要 Python 3,命令见 site/README.md。不要将站点加入客户端 Turborepo,也不要让 Web/Desktop 消费站点产物。

实现与参考

2 - 架构与模块边界

最近更新:

理解模块化单体、数据所有权和客户端包分工。

本页帮助开发者判断一项改动应放在哪个模块、哪一层。AGW 按业务拆分代码,例如 Agents、Projects 和 Jobs;它们可以共享一个部署进程和数据库,但各自负责自己的数据和操作。

先完成源码运行,再选择一个具体功能沿请求追踪,通常比一次阅读所有项目更容易理解。

后端组织

AGW 是模块化单体。Agw.Host 提供共享 Hosting,Control Plane、Data Plane 和 Standalone 组合所需模块。业务模块遵守 Api → Application → Domain ← Infrastructure,只创建实际需要的层。

flowchart LR
    API[Api] --> APP[Application]
    APP --> DOMAIN[Domain]
    INFRA[Infrastructure] --> DOMAIN
层负责什么阅读时关注什么
Api接收请求并返回响应路由、输入和返回值
Application完成一次业务操作身份检查、查询、事务和调用顺序
Domain保存业务数据并表达规则Entity、Behavior 和 DomainService
Infrastructure连接数据库和外部系统持久化、外部服务适配和具体实现

Domain 中的 Entity 保存状态。单个 Aggregate 的业务规则由 Behavior 处理;需要其他 Aggregate 信息的规则由 DomainService 处理。Application 读取数据、协调调用并保存变更。普通增删改查由 Application 直接完成。

数据所有权

每张表只有一个负责它的模块。即使多个模块共用实体类型和数据库,也要通过所属模块提供的接口访问数据,不能直接查询或修改其他模块的表。

拥有数据表的模块在 Application/Persistence 中声明自己的持久化接口 I<Module>DbContext,目前共有九个:Agents、Auth、Integrations、Jobs、Projects、Providers、Settings、Skills 和 Tools;Files、Setup 和 A2A 不拥有数据表,也没有这类接口。同一次请求中,这些接口由同一个 AgwDbContext 实例实现。这样既能共用数据库连接和事务,又能限制模块可见的数据范围。跨模块调用使用 Contracts 中的公开约定,必要的跨模块事务放在经过批准的 Infrastructure 适配器中。

Agw.Agents.Execution → Agw.Agents 单向依赖,两程序集属于同一 Agents 模块。Agentflow 的选择性 DDD 不扩展到普通 CRUD 模块。

以修改 Agentflow 为例

请求先进入 Api,再由 Application 检查用户能否访问该流程,并加载流程及全部节点和连线。Policy 判断新图是否符合规则,返回 Decision;Behavior 将有效变更应用到已加载的对象,最后由 Application 保存。

因此,修改连线规则时应查看 Agentflow 的 Policy 和 Topology;调整权限、加载或保存顺序时应查看 Application;修改数据库实现时再进入 Infrastructure。这个分工让业务规则与网络、数据库细节分开,也便于分别测试。

客户端

Web 与 Desktop 各自拥有路由壳和构建,业务包位于 src/clients/packages。chat-core 负责消息语义,chat-runtime 负责执行连接和状态,chat 负责 DOM 呈现。Mobile 通过 chat-native 与 RN-safe 包接入,不直接依赖 DOM 包。

增加功能前确认所属模块与公开入口;修改边界后运行 pnpm test:boundaries 和后端架构测试 dotnet test tests/Agw.Architecture.Tests。

实现与参考

3 - API 与执行协议

最近更新:

区分管理 JSON API、SignalR 执行与 A2A 协议。

AGW 的管理操作和任务执行使用不同接口。创建或查询配置使用普通 HTTP JSON API;持续接收 Agent 回复和状态使用 SignalR 执行连接;对接其他 Agent 系统时可以使用 A2A。

接入前,先准备可访问的开发 Server 和有效的认证身份,例如 API Key 或浏览器登录会话。具体参数以运行实例的 OpenAPI 及所属模块 Contracts 中的类型定义为准,避免照抄与运行版本不一致的请求。

协议边界

接口用途与约定
管理 JSON APIBens.Results ApiResult 封装,客户端 typed helpers 解包
/api/hubs/execSignalR 执行命令、状态和事件;仅 Data Plane 和 Standalone 映射,只接受 WebSocket 传输,官方客户端以 skipNegotiation: true 直接连接
/api/agents/permission-capabilities查询目标支持的权限能力
/api/auth/oidc/providers查询已启用的登录提供商,登录界面据此显示按钮
/api/auth/oidc/login跳转到提供商完成验证,client 取 web 或 desktop
/api/auth/desktop/exchangeDesktop 用一次性代码和自身校验值换取 API Key
A2A协议专用响应,仅 Data Plane 和 Standalone 映射
/openapi/*OpenAPI 接口描述,与 Scalar API 参考页一起只在 Development 环境中由 Control Plane 或 Standalone 提供

第三方登录相关的路由由 Control Plane 和 Standalone 提供。Desktop 换取到的 API Key 与手动创建的 API Key 用法相同,按创建者身份访问资源。

接入流程

  1. 自动化使用 API Key,通过 Authorization: Bearer 请求头发送;浏览器使用 Cookie 发送 POST、PUT、DELETE 等修改请求时,还要按现有客户端流程处理 CSRF 防护,防止其他网站借用登录状态发起操作。
  2. 通过管理 API 获取当前用户可访问的资源,保留其稳定标识。
  3. 启动执行前查询目标权限能力,按现有执行协议发起命令并订阅事件。
  4. 断线重连时恢复会话/执行状态,不把连接断开当作任务完成。

新增接口默认通过 query/body 传标识,遵守仓库规则。具体接口的路由和参数以当前 OpenAPI 与 Contracts 为准。

客户端应如何处理结果

管理 JSON API 使用 Bens.Results 统一响应格式。仓库的 @agw/api 已提供类型化辅助方法来提取业务数据,调用方应复用这些方法,并分别处理请求失败和业务错误。

执行连接会持续返回事件。客户端需要保留会话与执行标识,展示工具活动和等待输入状态,并在重连后查询实际进度。收到部分文字不代表执行结束,连接断开也不代表执行已经取消。协议消息及顺序见执行协议说明。

Agent 的结构化响应字段

responseSchema 保存 Agent 配置的 JSON Schema 原文,出现在完整的 Agent 响应中:GET /api/agents/{id}、GET /api/agents/paged、POST /api/agents、PUT /api/agents/{id} 和 PUT /api/agents/enabled。选择器使用的 GET /api/agents 不包含该字段。

两类响应都包含 resultFormat,取值为 markdown 或 json,由是否配置 Schema 推导。客户端据此决定最终结果按 Markdown 还是 JSON 显示,无需自行解析 Schema 内容。

更新时,字段缺失表示保持原值,null 或只包含空白的字符串表示清空,其他字符串表示替换。服务端要求内容是合法 JSON 且根节点为对象,否则返回参数错误;Pi Agent 设置任何 Schema 都会返回参数错误。Schema 只作为文本保存和传递。

契约变更

DTO(请求和响应的数据类型)放在所属模块的 Contracts 中。预期的业务错误使用 AgwException 和稳定的七位 ErrorCode,再由 API 或协议入口转换成响应。WebSocket、OAuth 跳转、A2A 和静态文件使用各自的协议格式。

后端接口定义更新后,先把 Development 环境的 OpenAPI 文档导出到 src/clients/packages/api/openapi.json,再从 src/clients 运行 pnpm gen:api,最后验证调用方。gen:api 只转换这个本地文件,不会自动从 Server 获取最新文档。不要手写修改生成的 openapi.d.ts,也不要向日志输出实际的 API Key。

实现与参考

4 - 扩展 Tools 与 Integrations

最近更新:

选择能力归属,使用编译期工具声明与用户连接。

扩展前先确定要增加什么:一个具体操作可以写成 Tool,一组任务说明和专属工具可以通过 Skill 提供,需要用户连接外部账号的能力则适合 Integration。

先阅读模块边界,确认能力由哪个模块负责。本页说明放置代码、注册能力和验证调用的顺序;具体声明写法可参考文末的工具示例。

工具扩展路径

  1. 手写的 IAgwTool、IContextualTool 和 IToolBlock 实现放在 Agw.Tools,全局目录只扫描这个程序集中的手写工具。业务工具放在所属模块的 Application/Tools,以 Attribute 容器声明或通过 Skill 提供,DTO 放 Contracts/Tools。
  2. 引用 Agw.Tools.Abstractions,需要 Attribute 声明时将 Agw.Tools.Generators 作为 Analyzer 引用。
  3. 显式声明权限、参数说明和返回类型。独立工具及使用 Attribute 声明的工具容器不能保存会话状态;状态放在 Provider、会话对象或所属存储中。
  4. 在所属模块注册所需服务与生成声明。选择通过 Skill 提供,或显式加入全局目录。
  5. 验证工具发现、参数、权限、项目绑定和错误映射。

生成器输出元数据、JSON Schema 和直接调用委托。不要加入运行时反射扫描兜底。Skill 工具有两个来源:IAgentSkillRegistration.Tools 提供手写的 IProjectScopedAgwTool 实例,ToolTypes 提供 Attribute 容器类型(由 IAgwToolSet<T> 生成)。它们在执行时绑定 Project,不因注册生成模块就自动进入全局目录。

集成扩展路径

IPluginCatalog 拥有 Plugin、Connector、认证和能力源定义。定义是代码/内容资产;用户设置是 PluginInstallation,可选账号或端点是 Connection。不要将它们合成一张全局配置表。

先增加目录定义和必要的工具来源,再验证用户完成设置、账号进入 Ready 状态、绑定 Agent 和实际调用的全过程。凭据由 Infrastructure 加密保存并在调用时读取;读取和执行都要检查账号归属。通过 HTTP/SSE 发送凭据时使用 HTTPS。

选择全局工具还是 Skill 专属工具

如果工具是可单独选用的通用操作,可显式加入全局目录。如果它只服务于某个 Skill,就通过该 Skill 注册,使说明和工具一起提供给 Agent。例如,agw-job 的任务管理工具属于 Jobs 模块,并随 Skill 提供。

工具成功编译后,还要确认 Agent 实际能发现它。若目录中没有出现,应先检查生成声明和注册位置;若调用时失败,再检查 Project 绑定、权限及参数。编译通过并不等于已经完成运行时接入。

方式一:通过接口定义 Tool

适合一个类负责一个可独立调用的操作。实现 IAgwTool,声明稳定名称、分类、Plan 可用性和权限,并实现唯一必需的成员 ToAITool()。仓库工具的写法是把操作放在 Execute 方法中,再在 ToAITool() 中用 AgwAIFunctionFactory.CreateParameterObjectFunction 包装成模型可调用的函数;这个工厂是 Agw.Tools 的内部类型,所以示例放在 Agw.Tools 中。下面是一个不读写外部资源的最小示例:

using System.ComponentModel;
using Agw.Tools.Abstractions;
using Agw.Tools.Infrastructure;
using Microsoft.Extensions.AI;

public sealed class EchoInput
{
    [Description("Text to return unchanged.")]
    public string Text { get; set; } = "";
}

public sealed class EchoTool : IAgwTool
{
    public string Name => "echo";
    public string Category => "Examples";
    public bool AllowInPlanMode => true;
    public AgwToolPermission RequiredPermission => AgwToolPermission.None;

    [Description("Return the supplied text unchanged.")]
    public string Execute(EchoInput input) => input.Text;

    public AITool ToAITool()
    {
        Func<EchoInput, string> func = Execute;
        return AgwAIFunctionFactory.CreateParameterObjectFunction(func, Name);
    }
}

IAgwTool 继承元数据接口 IAgwToolMeta,ToAITool() 把执行方法包装成模型可调用的函数。参数 DTO 和方法上的 Description 帮助模型理解何时调用、如何填参。实际异步 I/O 应参考仓库工具使用异步方法并传递 CancellationToken;业务 DTO 放在所属模块的 Contracts/Tools。

接入与使用

  1. 将实现放到 Agw.Tools/Impl/Tools。全局目录只扫描 Agw.Tools 程序集中的手写工具,放在业务模块中的 IAgwTool 不会被发现;业务模块改用方式二的 Attribute 容器,或把 IProjectScopedAgwTool 实例放进 Skill 的 IAgentSkillRegistration.Tools。声明对象保持无状态,不在字段中保存当前 Project、用户或会话数据。
  2. 在 src/server/Agw.Shared/Tooling/ToolValueObject.cs 中把工具名称加入 ToolDefinitionNames 及其 All 列表,并加入具体 ToolDefinition、[JsonDerivedType] 名称映射和空的或实际的 Options 类型,保持定义与实现一一对应。名称没有登记时,注册会报错 “does not have a registered ToolDefinition”。
  3. 在所属模块 DI 入口登记依赖,确认工具出现在 /api/tools;工具绑定到 Agent 或 Project 后,才供该执行目标使用。
  4. 在 Chat 让 Agent 调用该操作,检查输入和输出。直接调用 C# 方法适合单元测试,但不能证明运行时权限和 Project 绑定已接通。

有 Project 或运行时目录依赖的独立工具,应参考 IContextualTool.MaterializeAsync(接口位于 Agw.Tools/Contracts/Abstractions,与其他手写工具一样只从 Agw.Tools 程序集发现),把已校验的上下文绑定到本次贡献的函数中。不要让模型通过一个随意填写的 Project ID 决定资源归属。

方式二:通过 Attribute 定义 Tool

适合把一个服务中的多个操作声明为工具,也适合随 Skill 提供的业务能力。先添加抽象项目和生成器引用,路径按项目位置调整:

<ItemGroup>
  <ProjectReference Include="../Agw.Tools.Abstractions/Agw.Tools.Abstractions.csproj" />
  <ProjectReference Include="../Agw.Tools.Generators/Agw.Tools.Generators.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

下面与接口示例实现同一个 echo 操作,二选一使用,不要同时注册同名工具:

using System.ComponentModel;
using Agw.Tools.Abstractions;
using Agw.Tools.Abstractions.Attributes;

[AgwToolContainer(AgwToolPermission.None,
    DefaultCategory = "Examples", AllowInPlanMode = true)]
public sealed class TextTools
{
    [AgwTool("echo", AgwToolPermission.None)]
    [Description("Return the supplied text unchanged.")]
    public static string Echo([Description("Text to return.")] string text)
    {
        return text;
    }

    [AgwToolIgnore]
    public static string FormatForDisplay(string text) => text.Trim();
}

示例容器是普通的 sealed class,其中的方法都是静态方法;后面的 IAgwToolSet<TextTools> 要求类型参数是非静态类,所以容器不能声明为 static class。AgwToolContainer 批量选择类型直接声明的 public 普通方法;AgwTool 指定名称和权限;AgwToolIgnore 排除辅助方法。没有显式名称时,默认使用方法名并移除末尾的 Async。容器及每个操作都应明确声明或继承权限;AllowInPlanMode 是独立设置。

实例容器使用显式构造函数注入,并在 DI 中注册实例类型。静态方法可通过标记 [AgwToolService] 的参数取得服务。服务参数与 CancellationToken 不进入模型可填写的参数 Schema;每次调用都有独立的异步 DI scope。

生成、注册与选用

编译器生成元数据、输入输出 Schema 和直接调用委托。以程序集 My.Module 为例,在模块组合入口登记生成模块;需要全局选用时再选择容器类型:

// Example assembly name: My.Module
services.AddSingleton<IAgwGeneratedToolModule>(
    Agw.Generated.My.Module.AgwToolModule.Instance);
// Only for tools intended for the global catalog:
services.AddToolCatalogTypes(typeof(TextTools));

上述代码片段需要生成声明命名空间及所属注册扩展的引用。只包含静态方法的容器不必登记实例;包含实例方法的容器还需 services.AddScoped<YourToolContainer>()。生成模块只登记声明,不会自动公开全部工具。全局工具仍要完成具体 ToolDefinition 和 JSON 多态映射。

若工具只服务于 Skill,让 Skill 注册类使用 partial 并实现 IAgwToolSet<TextTools>,生成器补齐 ToolTypes;同时按 IAgentSkillRegistration 完成 Id、说明和创建逻辑。在所属模块的 DI 入口用 services.AddSingleton<IAgentSkillRegistration, YourSkillRegistration>() 注册 Skill,并登记生成模块和实例容器,写法参考 Jobs 模块的 JobManagementSkillRegistration。将该 Skill 绑定到 Agent/Project 后,其工具才参与运行时组合。手写的 Skill 工具实现 IProjectScopedAgwTool,放进 IAgentSkillRegistration.Tools,不应额外放进全局目录。

生成失败时先处理编译诊断:不支持的签名是 AGWTOOL001,无效声明是 AGWTOOL002。不要用运行时反射绕过诊断。完整的实例容器和 Skill 示例见 工具抽象说明。

ToolBlock:定义共享状态的一组工具

当多个操作需要维护同一份状态,例如添加 Todo、完成 Todo 和查看待办列表,应把它们作为一个 ToolBlock 整组选用。Attribute 容器只是声明方式,本身不提供会话状态隔离。

下面是仓库的 TodoToolBlock 完整实现:

using Agw.Tools.ToolBlocks;

namespace Agw.Tools.Impl.ToolBlocks.Todo;

public sealed class TodoToolBlock : IToolBlock
{
    public ToolBlockDescriptor Descriptor { get; } =
        new(
            ToolBlockNames.Todo,
            "Todo",
            "Tracks multi-step work with a persistent todo list.",
            ToolBlockScope.Agent | ToolBlockScope.Project,
            [
                new("todos_add", AgwToolPermission.None, allowInPlanMode: true),
                new("todos_complete", AgwToolPermission.None, allowInPlanMode: true),
                new("todos_remove", AgwToolPermission.None, allowInPlanMode: true),
                new("todos_get_remaining", AgwToolPermission.ReadOnly, allowInPlanMode: true),
                new("todos_get_all", AgwToolPermission.ReadOnly, allowInPlanMode: true),
            ]
        );

    public ValueTask<ToolContribution> MaterializeAsync(
        ToolBlockDefinition definition,
        ToolMaterializationContext context,
        CancellationToken cancellationToken
    )
    {
        var contribution = new ToolContribution();
        contribution.PlanModeAllowedToolNames.UnionWith(
            Descriptor.Members.Where(static member => member.AllowInPlanMode).Select(static member => member.Name)
        );
        contribution.ContextProviders.Add(new AgwTodoProvider());
        var evaluatorOptions = context.EnabledToolBlockNames.Contains(ToolBlockNames.Mode)
            ? new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] }
            : null;
        contribution.LoopEvaluators.Add(new TodoCompletionLoopEvaluator(evaluatorOptions));
        return ValueTask.FromResult(contribution);
    }
}

Todo 的状态与生命周期

  • Descriptor.Members 声明五个成员、各自权限和 Plan 可用性。成员不能单独注册成全局工具。
  • MaterializeAsync 创建本次能力组合使用的 AgwTodoProvider,放进 ToolContribution.ContextProviders,同时提供循环检查器。
  • AgwTodoProvider 把 AgwTodoState 保存到当前 AgentSession.StateBag,并通过 StateKeys 声明状态键。调用时从当前 session 读取、修改并保存,不能用 static 列表或共享单例保存所有人的 Todo。
  • 同一 session 的后续调用可继续使用已有 Todo;不同 session 的状态分开。持久化恢复依赖外层会话保存与恢复流程,不能仅因为 Provider 中有字段就认为状态可以恢复。
  • TodoCompletionLoopEvaluator 检查待办是否完成;与 Mode 一起启用时只在 Execute 阶段参与检查。自定义 ToolBlock 不一定需要循环检查器,按具体任务添加。

开发自己的有状态 ToolBlock

  1. 定义数据对象和状态范围:回合、会话或项目长期存储。会话状态参考 AgwTodoState,项目长期状态参考 Project Memory。
  2. 在 Agw.Shared/Tooling/ToolValueObject.cs 中把名称加入 ToolBlockDefinitionNames 及其 All 列表,增加具体 ToolBlockDefinition、Options 和 [JsonDerivedType] 名称映射,再在 ToolBlockNames 中加入引用该常量的运行时名称。启动时的覆盖检查会拒绝缺少定义或缺少实现的 ToolBlock。
  3. 实现 IToolBlock,一次声明全部成员,明确每个成员的权限与 allowInPlanMode。
  4. 在 MaterializeAsync 创建 Provider,将函数和状态操作绑定到当前上下文;把生命周期交给 ToolContribution,不要缓存跨用户的 Provider 或 scoped 服务。
  5. 在目录和定义解析流程接通整组选用,再测试:新增、完成、移除、不同 session 隔离、保存恢复、Plan 限制与审批。

参考 Todo Provider 和 Todo 状态,不要只复制描述符而遗漏状态的读取与保存。

Plugin:从目录定义到实际调用

这里的 Plugin 是 AGW 集成目录中的代码与内容定义。当前内建示例是 GitHub;新增 Plugin 需要修改并构建服务端,不是上传任意插件包即可运行。界面用 Available integrations 展示目录,用 Configured integrations 展示用户配置的账号或端点。

开发对象定义内容
PluginDefinition稳定 Id、版本、显示名、Connector 和可选 Skill 内容
ConnectorDefinition服务或协议变体,例如 GitHub Cloud
AuthSchemeDefinition认证方式、用户配置字段及 OAuth 流程设置
CapabilitySourceDefinition工具由内部 C# Provider 创建,或从 MCP 服务取得
PluginInstallation当前用户的安装设置,例如 OAuth Client ID/Secret
Connection当前用户配置的具体账号、凭据和 Ready 状态,绑定时使用 ConnectionId

定义目录与认证

下面是现有 GitHub 目录实现,可作为开发新集成时的完整结构参考。OAuth 地址、scope 和字段需按目标服务的协议设置,不能照搬 GitHub 的值:

using Agw.Integrations.Application.Plugins;
using Agw.Integrations.Domain.Plugins;

namespace Agw.Integrations.Infrastructure.Plugins;

/// <summary>
/// 所有可以使用的 plugin 列表
/// </summary>
public sealed class BuiltInPluginCatalog : IPluginCatalog
{
    private static readonly IReadOnlyList<PluginDefinition> Plugins =
    [
        new PluginDefinition
        {
            Id = "github",
            Version = "1.0.0",
            DisplayName = "GitHub",
            Description = "Connect GitHub accounts and use repository capabilities.",
            Tags = ["Git", "Coding"],
            Connectors =
            [
                new ConnectorDefinition
                {
                    Id = "github-cloud",
                    DisplayName = "GitHub Cloud",
                    Description = "Connect a GitHub.com account using OAuth.",
                    AuthSchemes =
                    [
                        new AuthSchemeDefinition
                        {
                            Id = "oauth2",
                            DisplayName = "OAuth 2.0",
                            Type = AuthSchemeType.OAuth2,
                            OAuth2AuthorizationCode = new OAuth2AuthorizationCodeSettings
                            {
                                AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
                                TokenEndpoint = "https://github.com/login/oauth/access_token",
                                UserInfoEndpoint = "https://api.github.com/user",
                                ClientIdFieldId = "client-id",
                                ClientSecretFieldId = "client-secret",
                                SubjectResolution = new OAuthSubjectResolutionDefinition
                                {
                                    Source = OAuthSubjectSource.UserInfo,
                                    Field = "login",
                                },
                                UsePkce = true,
                                ClientAuthenticationMethod = OAuth2ClientAuthenticationMethod.Body,
                                SupportsRefresh = false,
                                Scopes = ["repo", "read:user", "read:org"],
                            },
                            InstallationFields =
                            [
                                new FormFieldDefinition
                                {
                                    Id = "client-id",
                                    Label = "Client ID",
                                    Type = FormFieldType.Text,
                                    IsRequired = true,
                                },
                                new FormFieldDefinition
                                {
                                    Id = "client-secret",
                                    Label = "Client secret",
                                    Type = FormFieldType.Secret,
                                    IsRequired = true,
                                },
                            ],
                        },
                    ],
                    CapabilitySources =
                    [
                        // Agw 内部 C# Provider 创建工具。
                        new NativeCapabilitySourceDefinition { Id = "github-native", Provider = "github" },
                    ],
                },
            ],
            Skills = [new PluginSkillDefinition { ContentPath = "Plugins/github/skills/github/SKILL.md" }],
        },
    ];

    public BuiltInPluginCatalog()
    {
        PluginCatalogValidator.Validate(Plugins);
    }

    public IReadOnlyList<PluginDefinition> List()
    {
        return Plugins;
    }

    public PluginDefinition? Find(string pluginId)
    {
        return Plugins.FirstOrDefault(plugin => string.Equals(plugin.Id, pluginId, StringComparison.OrdinalIgnoreCase));
    }
}

新增定义时保持 Plugin、Connector、认证方式和能力源的 Id 稳定,并让 PluginCatalogValidator 验证整个目录。InstallationFields 用于每用户的安装配置;账号级字段应放到相应认证方案的连接字段中。秘密字段使用 Secret 类型,不能把真实凭据写入目录定义。

实现能力源

Native:定义中的 Provider = "github" 对应 IConnectionNativeCapabilityProvider.Provider。实现 CreateTools(ConnectionNativeCapabilityContext),用已解析的 ConnectionId、Alias 和 ProjectId 创建工具。工具名称通常为 {alias}__{operation},防止多个账号的工具混淆。

参考 GitHubConnectionNativeCapabilityProvider:生成函数绑定账号和 Project,实际调用时创建 scope、解析 IGitHubConnectionInvoker;Invoker 再执行账号归属、Ready 状态与凭据检查。不得让模型指定任意 ConnectionId,或在单例 Provider 中缓存账号密钥。为新增操作接入当前能力源的权限元数据与执行审批流程。

MCP:使用 McpCapabilitySourceDefinition,选择 stdio、HTTP 或 SSE Transport,再通过 CredentialBindings 把安装字段、连接字段或 OAuth Token 注入所需环境变量或 HTTP Header。经过网络注入凭据时使用 HTTPS;字段引用必须与认证定义匹配。MCP 路径复用连接授权与运行时校验,不是直接把未验证的 URL 交给 Agent。

注册、内容与测试

  1. 在 Integrations 的 DI 入口维护目录注册;Native Provider 注册为 IConnectionNativeCapabilityProvider,调用服务按其生命周期登记,例如 GitHub 的 scoped Invoker。
  2. 若附带 Skill,将内容放入 Plugin 内容目录,并通过 PluginSkillDefinition.ContentPath 指向 SKILL.md。它提供使用说明,不自动执行第三方脚本;同时确认构建产物包含内容文件。
  3. 在 Available integrations 找到定义,为测试用户配置安装字段和账号。完成认证后确认 Ready,再绑定 Agent 或 Project。
  4. 验证一次读取和一次受控写入,检查工具名称、参数、权限及错误处理。测试使用真实实现,不使用 mock 或 fake 实现;需要真实账号或 OAuth 授权的测试不放进默认测试套件。
  5. 覆盖跨用户 ConnectionId、未就绪账号、失效凭据、目录字段错误、同名工具和配置变更。修改用户安装设置只应影响该用户的账号连接。

当前没有远程 Marketplace 的下载、签名与自动升级机制。完整调用链参考 GitHub Native Provider、GitHub Invoker 和 能力源定义。

验证

至少覆盖成功调用、非法参数、权限不足、外来 Connection 和未就绪 Connection。保持编译期诊断有效。测试使用真实实现,不使用 mock 或 fake 实现;依赖真实账号或外部 CLI 的测试需要显式开启,不放进默认测试套件。

实现与参考

5 - 测试与贡献约定

最近更新:

按影响范围验证改动,遵守格式、边界和迁移规则。

前提:依赖已安装。修改前阅读根目录 AGENTS.md 和 docs/human/ 中的相关规则,保留无关的本地改动。

后端检查

从仓库根运行:

dotnet build Agw.slnx
dotnet test Agw.slnx
dotnet csharpier check .

测试项目使用 xUnit v3,并通过根目录 global.json 使用 Microsoft.Testing.Platform 运行。定位问题时先运行相关测试,例如 dotnet test tests/Agw.Files.Tests。单元和组合测试使用真实实现和纯选项辅助方法,不使用 mock 或 fake 实现。构造 CodexAIAgent 或 ClaudeCodeAIAgent 时会探测 CLI,这类测试作为真实 CLI 测试运行,需要显式启用并确认可执行文件可用,不放进默认测试套件。

修改错误码或异常规则后运行 dotnet test tests/Agw.Shared.Tests;修改模块依赖后运行后端架构测试 dotnet test tests/Agw.Architecture.Tests。

持久化执行的 PostgreSQL 测试(租约保护、事件顺序、活动执行升级和调度容量)通过 AGW_TEST_POSTGRES_CONNECTION_STRING 连接独立的测试实例,测试用户需要创建数据库的权限;Redis 事件投影测试使用 AGW_TEST_REDIS_CONNECTION_STRING。没有设置这些变量时,对应测试会跳过。CI 使用 PostgreSQL 18 运行 PostgreSQL 测试,并检查 TRX 结果,确认必需的测试全部执行成功;完整命令见开发文档。

改动登录相关代码时,运行 dotnet test tests/Agw.Auth.Tests。这些测试默认使用受控的提供商和各自独立的 SQLite 数据库。需要在 PostgreSQL 上验证时,把 AGW_TEST_OIDC_POSTGRES 设为测试服务器的管理连接字符串,该账号需要能够创建数据库;不要指向生产服务器。Desktop 主进程的登录与凭据存储测试使用 pnpm --filter @agw/desktop test。

客户端检查

从 src/clients 运行:

pnpm lint
pnpm test
pnpm fmt:check
pnpm build

使用 oxlint/oxfmt,不是 ESLint/Prettier。修改包边界后必须通过 pnpm test:boundaries;改变 API 后先把 Development 环境的 OpenAPI 文档导出到 src/clients/packages/api/openapi.json,再运行 pnpm gen:api 重新生成 typed client,并验证调用方。

组件渲染测试通过共享的 @agw/test-harness 建立 DOM 环境;需要 API 响应时,用其中的 startApiServer 启动真实的本地 HTTP 服务,让组件走自身的请求路径。Web 的浏览器测试先运行 pnpm --filter @agw/web exec playwright install chromium,再运行 pnpm --filter @agw/web test:e2e;Playwright 会在 127.0.0.1:3101 启动独立的 Web 服务,不需要后端。

数据与提交

模型变更需要配套 SQLite 与 PostgreSQL 迁移,但只有明确授权后才生成或应用。生成时分别以 src/server/Agw.Migrations.Sqlite 和 src/server/Agw.Migrations.Postgres 为迁移项目、src/server/Agw.Standalone.Host 为启动项目,并在命令末尾传入 -- --provider sqlite 或 -- --provider postgres,完整命令见开发文档。dotnet tool restore 只安装 CSharpier,dotnet ef 需要另行安装。NoForeignKeyModelDiffer 禁止生成数据库外键,引用验证和清理由应用层/基础设施负责。

C# 使用显式构造函数,禁止 primary constructor;日期使用 DateTimeOffset。遵守根目录 AGENTS.md。提交需显式授权,并使用 Conventional Commits。

按改动选择检查

改动至少确认
修复后端行为相关项目构建通过,能重现原问题的验证通过
修改接口或 DTO导出 OpenAPI 文档并重新生成 API 类型,检查调用方及错误处理
调整模块或包依赖后端架构测试或客户端边界检查通过
修改错误码tests/Agw.Shared.Tests 通过
修改界面检查实际操作、不同屏幕宽度和相关测试;涉及 Web 浏览器行为时运行 test:e2e
修改本站文档Hugo 严格构建、链接与中英文对应检查通过,页面显示正常

验证失败时保留错误信息,修复后重跑受影响的检查。提交说明应写清改了什么、如何验证,以及是否影响数据库或部署。

完成标准

缺陷修复应有可复现验证,行为改动覆盖相关成功和失败路径。文档站只改 site 时运行其 Hugo、链接和浏览器检查,无需为纯文档改动启动模型服务或执行数据库初始化。

实现与参考