[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-933":3,"consumer-news-interaction-933":41,"consumer-news-related-933":44},{"detail":4,"item":36},{"card":5,"schemaVersion":23,"fields":24,"content":30},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":15,"href":16,"sourceName":12,"meta":17,"metrics":20,"tags":21,"resolved":22},"NEWS_ARTICLE:933","news","NEWS_ARTICLE",933,"资讯","写给 C++ 工程师的 OpenClaw.NET 上手指南：用你熟悉的 C++ 思维，跑起一个生产级 AI Agent","博客园","它像一个「基于 boost.asio + REST 端点的常驻服务」——对外是 HTTP \u002F WebSocket \u002F 各 IM 的 webhook，对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent——只不过协程不用你手写 promise_type，内存不用你管 new\u002Fdelete。","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202609\u002F510-20260905070811424-1837144874.jpg","","\u002Fnews\u002F933",[18,19],"2026","软件开发",{},[19],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":25,"categoryName":19,"summary":13,"description":13,"publishTime":26,"updateTime":27,"sourceUrl":28,"language":29},"张善友","2026-09-10T21:38","2026-09-11T15:21:59","https:\u002F\u002Fwww.cnblogs.com\u002Fshanyou\u002Fp\u002F22851086","中文",{"format":31,"policy":32,"normalized":22,"html":33,"text":34,"wordCount":35,"hasBody":22},"HTML","NEWS_CONTENT_V1","\u003Cblockquote>\n \u003Cp>编译出单文件原生二进制、没有头文件、async 不用手写状态机、内存安全但不用和借用检查器格斗——一个 C++ 工程师视角下的 .NET AI Agent 运行时。\u003Cbr>\u003Cimg src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202609\u002F510-20260905070811424-1837144874.jpg?w=720&amp;quality=65&amp;strip=all\" alt=\"1\">\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>为什么写这篇\u003C\u002Fh2>\n\u003Cp>如果你是 C++ 工程师，你的世界观大概是：\u003Cstrong>产物必须是原生机器码、资源生命周期必须确定、抽象不该有隐藏开销\u003C\u002Fstrong>。Agent 框架的主流世界是 Python \u002F Node——解释执行、运行时依赖一堆、性能随运气——你大概率看不上。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>OpenClaw.NET 值得你多看一眼\u003C\u002Fstrong>：一个用 .NET 写的自托管 AI Agent 运行时 + 网关，鉴权、策略、记忆、可观测性、多渠道接入全部内置，关键是能用 NativeAOT 编译成\u003Cstrong>无依赖单文件原生二进制\u003C\u002Fstrong>——启动即原生码，没有 JIT 预热，没有运行时安装。它已经开源，仓库在 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fclawdotnet\u002Fopenclaw.net\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">github.com\u002Fclawdotnet\u002Fopenclaw.net\u003C\u002Fa>。\u003C\u002Fp>\n\u003Cp>用一句 C++ 话说：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>\u003Cstrong>它像一个「基于 boost.asio + REST 端点的常驻服务」——对外是 HTTP \u002F WebSocket \u002F 各 IM 的 webhook，对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent——只不过协程不用你手写 promise_type，内存不用你管 new\u002Fdelete。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>本文全程用 C++ 概念做类比。读完你能：看懂系统组成和消息流转、在本地把它跑起来、并写出你的第一个工具 \u002F 技能 \u002F 插件 \u002F 渠道。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>一、30 秒认识 OpenClaw.NET\u003C\u002Fh2>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>能力\u003C\u002Fth>\n   \u003Cth>说明\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>网关\u003C\u002Ftd>\n   \u003Ctd>HTTP \u002F WebSocket \u002F 浏览器 UI（\u003Ccode>\u002Fchat\u003C\u002Fcode>）\u002F 各 IM webhook \u002F OpenAI 兼容端点（\u003Ccode>\u002Fv1\u002F*\u003C\u002Fcode>）\u002F MCP（\u003Ccode>\u002Fmcp\u003C\u002Fcode>）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Agent 运行时\u003C\u002Ftd>\n   \u003Ctd>推理循环、工具执行、记忆、会话、技能、策略、审批、断路器\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>渠道\u003C\u002Ftd>\n   \u003Ctd>WebSocket、TG、Slack、Discord、Teams、WhatsApp，以及\u003Cstrong>飞书 \u002F 钉钉 \u002F 企业微信\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>扩展点\u003C\u002Ftd>\n   \u003Ctd>工具（Tool）、技能（Skill）、插件（Plugin）、渠道（Channel）、LLM Provider\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>客户端\u003C\u002Ftd>\n   \u003Ctd>浏览器 UI、CLI、Avalonia 桌面 App、Blazor WASM 运维面板、TUI\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cblockquote>\n \u003Cp>项目已开源：\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fclawdotnet\u002Fopenclaw.net\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">https:\u002F\u002Fgithub.com\u002Fclawdotnet\u002Fopenclaw.net\u003C\u002Fa>。仓库里解决方案叫 \u003Ccode>OpenClaw.Net.slnx\u003C\u002Fcode>，命名空间是 \u003Ccode>OpenClaw.*\u003C\u002Fcode>——和名字对得上，找代码不迷路。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Chr>\n\u003Ch2>二、C++ → C# 心智模型速查\u003C\u002Fh2>\n\u003Cp>这是全文最该先读的部分。看懂这张表，后面 90% 的代码你都能读：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>你在 C++ 里熟悉的\u003C\u002Fth>\n   \u003Cth>.NET 里的对应物\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>编译出原生二进制\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>NativeAOT\u003C\u002Fstrong>——同款产物；默认还有 JIT 模式\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>CMake \u002F Make + vcpkg \u002F conan\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>dotnet\u003C\u002Fcode> CLI + MSBuild（\u003Ccode>.csproj\u003C\u002Fcode>）+ NuGet\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>头文件 + 声明\u002F定义分离\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>没有头文件\u003C\u002Fstrong>——一份代码，编译器自己处理\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>模板（编译期展开）\u003C\u002Ftd>\n   \u003Ctd>泛型（运行时具体化）+ \u003Ccode>where T : ...\u003C\u002Fcode> 约束（≈ concepts 青春版）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>std::async\u003C\u002Fcode> \u002F C++20 协程 \u003Ccode>co_await\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>Task&lt;T&gt;\u003C\u002Fcode> + \u003Ccode>async\u003C\u002Fcode>\u002F\u003Ccode>await\u003C\u002Fcode>——\u003Cstrong>语言内置，不用手写 promise_type\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>std::stop_token\u003C\u002Fcode>（C++20）\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>CancellationToken\u003C\u002Fcode>——\u003Cstrong>就是同一个东西\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>std::optional&lt;T&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>string?\u003C\u002Fcode> vs \u003Ccode>string\u003C\u002Fcode>——\u003Cstrong>非空默认，可空显式标注\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>struct\u003C\u002Fcode> \u002F \u003Ccode>class\u003C\u002Fcode>（都在栈上或你管堆）\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>struct\u003C\u002Fcode> = 值类型 \u002F \u003Ccode>class\u003C\u002Fcode> = 堆上引用类型——\u003Cstrong>这个区分 C# 保留了\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>std::unique_ptr\u003C\u002Fcode> \u002F \u003Ccode>shared_ptr\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>不需要\u003C\u002Fstrong>——GC 接管，引用语义 ≈ 万物 \u003Ccode>shared_ptr\u003C\u002Fcode>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Cstrong>RAII\u003C\u002Fstrong>（析构函数确定性释放）\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>IDisposable\u003C\u002Fcode> + \u003Ccode>using\u003C\u002Fcode> 关键差异，见下\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>虚函数 \u002F 抽象基类\u003C\u002Ftd>\n   \u003Ctd>interface + \u003Ccode>virtual\u003C\u002Fcode>\u002F\u003Ccode>override\u003C\u002Fcode>（接口必须显式声明实现）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>异常 + RAII 回滚\u003C\u002Ftd>\n   \u003Ctd>异常 + \u003Ccode>try\u003C\u002Fcode>\u002F\u003Ccode>catch\u003C\u002Fcode>\u002F\u003Ccode>finally\u003C\u002Fcode>——模型几乎一样\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>boost.asio 的 io_context\u003C\u002Ftd>\n   \u003Ctd>async runtime 内置，无感\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>未定义行为（UB）\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>内存安全\u003C\u002Fstrong>——越界、悬垂引用编译期\u002F运行期拦死\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Google Test \u002F Catch2 + gmock\u003C\u002Ftd>\n   \u003Ctd>xUnit v3 + NSubstitute\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>#define\u003C\u002Fcode> \u002F 预处理器\u003C\u002Ftd>\n   \u003Ctd>基本没有预处理宏（条件编译仅限少数场景）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Ccode>constexpr\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>const\u003C\u002Fcode> \u002F \u003Ccode>static readonly\u003C\u002Fcode>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch3>语法上最容易愣住的三个点\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>\u002F\u002F 1) 异步：≈ C++20 协程，但编译器把所有状态机细节都包了\npublic async Task&lt;string&gt; RunAsync(Session session, string msg, CancellationToken ct)\n{\n    var result = await _llm.CallAsync(msg, ct);   \u002F\u002F ≈ co_await llm-&gt;Call(msg)\n    return result.Text;\n}\n\u002F\u002F CancellationToken ≈ std::stop_token：协作式取消，显式传参，务必往下传。\n\n\u002F\u002F 2) record + required：≈ 聚合初始化 + 编译期必填，带值相等语义（≈ 默认生成了 operator==）\npublic sealed record OutboundMessage\n{\n    public required string ChannelId { get; init; }   \u002F\u002F 不给就编译不过\n    public required string RecipientId { get; init; }\n}\n\n\u002F\u002F 3) 可空引用类型：string? 可空，string 不可空——≈ std::optional&lt;std::string&gt; vs std::string\nstring? maybe = GetOrNull();\nstring sure = maybe;                \u002F\u002F ⚠ 编译警告——本项目警告即错误（≈ -Werror）\nstring sure2 = maybe ?? \"default\";  \u002F\u002F ?? ≈ maybe.value_or(\"default\")\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>最大的思维转换：RAII → GC + using\u003C\u002Fh3>\n\u003Cp>这是 C++ 工程师最需要重建的习惯。C# 是 GC 语言：\u003Cstrong>对象内存的回收时机不确定\u003C\u002Fstrong>，析构函数（终结器）什么时候跑、跑不跑都不保证——RAII 里「离开作用域即释放」的直觉在这里对\u003Cstrong>内存\u003C\u002Fstrong>不成立。\u003C\u002Fp>\n\u003Cp>但对\u003Cstrong>非内存资源\u003C\u002Fstrong>（文件句柄、socket、锁），C# 给了显式机制：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>using var doc = JsonDocument.Parse(json);   \u002F\u002F ≈ 栈对象，离开作用域确定性 Dispose\n\u002F\u002F 异步版本：await using var stream = ...;  ≈ co_await 风格的确定性清理\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>心智模型这样切换：\u003Cstrong>内存交给 GC（你不用管），资源交给 \u003Ccode>using\u003C\u002Fcode>（你必须管）\u003C\u002Fstrong>。看到实现了 \u003Ccode>IDisposable\u003C\u002Fcode> 的类型，就像看到管理着 fd\u002Fhandle 的 RAII 类——必须包 \u003Ccode>using\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Ch3>性能纪律还在，只是换了位置\u003C\u002Fh3>\n\u003Cp>GC 不等于放弃性能意识。本项目的写法你会眼熟：\u003Ccode>ValueTask\u003C\u002Fcode>（避免堆分配的轻量 future）、\u003Ccode>Span&lt;T&gt;\u003C\u002Fcode>（≈ \u003Ccode>std::string_view\u003C\u002Fcode>，零拷贝切片）、\u003Ccode>ArrayPool\u003C\u002Fcode>（≈ 自己维护 freelist）。热路径上的分配纪律依然靠人——只是编译器不逼你。\u003C\u002Fp>\n\u003Ch3>NativeAOT 与裁剪：你的主场\u003C\u002Fh3>\n\u003Cp>「禁运行时反射、序列化走编译期代码生成」——C++ 工程师表示：\u003Cstrong>RTTI 我都经常关，这算什么约束\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>本项目开激进裁剪（\u003Ccode>TrimMode=link\u003C\u002Fcode>），裁剪器会删掉\"看起来没人引用\"的代码，所以 JSON 序列化用\u003Cstrong>源生成器\u003C\u002Fstrong>——为类型声明 \u003Ccode>JsonSerializerContext\u003C\u002Fcode>，编译期生成序列化代码：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>[JsonSerializable(typeof(ProblemDetails))]\n[JsonSerializable(typeof(OperatorAccountService.StoreState))]\ninternal partial class GatewayJsonContext : JsonSerializerContext;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这就是「编译期生成 vs 运行期内省」的老对立面，你一直站编译期这边。你新增 DTO 时，记得挂到某个 \u003Ccode>JsonSerializerContext\u003C\u002Fcode> 上。\u003C\u002Fp>\n\u003Cp>裁剪还引出贯穿全文的\u003Cstrong>两条运行时车道\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>\u003Ccode>aot\u003C\u002Fcode> 车道\u003C\u002Fstrong>：裁剪安全、低内存、无动态加载——≈ 静态链接的 release 构建，生产 Docker 镜像走这条。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>\u003Ccode>jit\u003C\u002Fcode> 车道\u003C\u002Fstrong>：完整 .NET，JIT 编译 + 支持运行时加载 DLL 插件——开发期默认走这条，≈ 支持 \u003Ccode>dlopen\u003C\u002Fcode> 插件的调试构建。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Ch2>三、一条消息的一生\u003C\u002Fh2>\n\u003Cp>这是理解整个系统的主线。中枢是 \u003Ccode>OpenClaw.Gateway\u003C\u002Fcode>——它在启动时把 Agent 运行时、消息管道、渠道适配器、插件宿主组合起来，统一路由所有流量。\u003C\u002Fp>\n\u003Cp>一条用户消息从进来到回复，共 11 步（C++ 类比已标注）：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>渠道收消息\u003C\u002Fstrong>：\u003Ccode>IChannelAdapter\u003C\u002Fcode> 把入站消息写进 \u003Ccode>MessagePipeline\u003C\u002Fcode>——≈ 一个有界的生产者-消费者队列\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Worker 取消息\u003C\u002Fstrong>：1~4 个 worker（上限 = CPU 核数）从队列读——≈ 你起 \u003Ccode>std::thread\u003C\u002Fcode> 池抢任务\u003C\u002Fli>\n \u003Cli>\u003Cstrong>会话加锁\u003C\u002Fstrong>：拿该会话的 \u003Ccode>SemaphoreSlim\u003C\u002Fcode>（≈ \u003Ccode>std::counting_semaphore&lt;1&gt;\u003C\u002Fcode>），同一会话不并发跑两轮\u003C\u002Fli>\n \u003Cli>\u003Cstrong>过中间件\u003C\u002Fstrong>：限流、token 预算，可短路拒绝——≈ 请求处理管线上的拦截器链\u003C\u002Fli>\n \u003Cli>\u003Cstrong>进 Agent 运行时\u003C\u002Fstrong>：\u003Ccode>MafAgentRuntime.RunAsync(...)\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>\u003Cstrong>准备上下文\u003C\u002Fstrong>：载入\u002F新建会话、裁剪历史、注入记忆召回\u003C\u002Fli>\n \u003Cli>\u003Cstrong>ReAct 循环\u003C\u002Fstrong>：调 LLM → 要工具就执行 → 结果回灌 → 再调 LLM……直到产出文本\u003C\u002Fli>\n \u003Cli>\u003Cstrong>工具执行\u003C\u002Fstrong>：一条完整链路——预设过滤 → 治理策略 → Hook → 人工审批 → 执行 → 审计\u003C\u002Fli>\n \u003Cli>\u003Cstrong>韧性\u003C\u002Fstrong>：LLM 调用自带指数退避重试、超时、断路器、降级模型级联\u003C\u002Fli>\n \u003Cli>\u003Cstrong>落库\u003C\u002Fstrong>：会话写入 \u003Ccode>IMemoryStore\u003C\u002Fcode>（dev 默认 sqlite）\u003C\u002Fli>\n \u003Cli>\u003Cstrong>回复出站\u003C\u002Fstrong>：按 \u003Ccode>ChannelId\u003C\u002Fcode> 找到渠道适配器投递\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>整个系统的「骨架接口」都在 \u003Ccode>src\u002FOpenClaw.Core\u002FAbstractions\u002F\u003C\u002Fcode>：\u003Ccode>ITool\u003C\u002Fcode>、\u003Ccode>IChannelAdapter\u003C\u002Fcode>、\u003Ccode>IAgentRuntime\u003C\u002Fcode>、\u003Ccode>IMemoryStore\u003C\u002Fcode>、\u003Ccode>IToolHook\u003C\u002Fcode>……\u003Cstrong>看懂它们 = 看懂系统的全部可扩展面\u003C\u002Fstrong>。扩展方式就是「实现接口（≈ 继承抽象基类、重写纯虚函数）+ 注册进 DI 容器（≈ 一个类型安全的工厂注册表）」。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>四、把它跑起来\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>前置\u003C\u002Fstrong>：.NET 10 SDK（必须，≈ 装一次编译器 toolchain）、可选 Node.js 20+（仅跑 TS\u002FJS 插件时需要）、一个 LLM API Key。\u003C\u002Fp>\n\u003Cpre>\u003Ccode># 先校验配置（≈ 启动前自检）\ndotnet run --project src\u002FOpenClaw.Gateway -c Release -- --doctor\n\n# 启动（≈ cmake --build --config Release &amp;&amp; .\u002Fopenclaw）\ndotnet run --project src\u002FOpenClaw.Gateway -c Release\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>默认监听 \u003Ccode>http:\u002F\u002F127.0.0.1:18789\u003C\u002Fcode>，浏览器打开 \u003Ccode>\u002Fchat\u003C\u002Fcode> 即可对话。\u003C\u002Fp>\n\u003Cp>最快的本地启动（三个环境变量 + 一条命令）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>export MODEL_PROVIDER_KEY=\"sk-...\"          # 你的 LLM key\nexport OPENCLAW_WORKSPACE=\"$PWD\u002Fworkspace\"  # 工作区根目录\nmkdir -p \"$OPENCLAW_WORKSPACE\"\ndotnet run --project src\u002FOpenClaw.Gateway -c Release\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>配置体系是「配置文件 + 环境变量覆盖」的分层套路：环境变量用双下划线映射层级，\u003Ccode>OpenClaw__Runtime__Mode\u003C\u002Fcode> ↔ 配置树 \u003Ccode>OpenClaw:Runtime:Mode\u003C\u002Fcode>。敏感字段支持 \u003Ccode>env:VAR_NAME\u003C\u002Fcode> 引用写法，生产环境推荐——不用自己写 getenv 解析层。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>本地避坑速查\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>必须 .NET 10，多 SDK 并存时用 \u003Ccode>global.json\u003C\u002Fcode> 钉版本（≈ \u003Ccode>toolchain file\u003C\u002Fcode> 指定编译器版本）\u003C\u002Fli>\n \u003Cli>出厂 \u003Ccode>appsettings.json\u003C\u002Fcode> 里的默认 \u003Ccode>AuthToken\u003C\u002Fcode> 和示例 API key \u003Cstrong>仅供本地回环\u003C\u002Fstrong>，对外部署务必改成 \u003Ccode>env:\u003C\u002Fcode> 引用，别把真实密钥提交进仓库\u003C\u002Fli>\n \u003Cli>公网绑定会被安全硬化拦截（缺鉴权 token、危险工具、\u003Ccode>raw:\u003C\u002Fcode> 密钥都会拒绝启动）——这是有意设计\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Ch2>五、动手扩展：四种方式，从轻到重\u003C\u002Fh2>\n\u003Ch3>① 写一个工具（Tool）—— 最常用\u003C\u002Fh3>\n\u003Cp>一个工具就是一个实现 \u003Ccode>ITool\u003C\u002Fcode> 的类。C++ 视角：\u003Cstrong>就是继承抽象基类、重写纯虚函数\u003C\u002Fstrong>——C# 接口同样需要显式声明实现：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>public interface ITool   \u002F\u002F ≈ class ITool { 全是纯虚函数 };\n{\n    string Name { get; }              \u002F\u002F LLM 用它来调用\n    string Description { get; }       \u002F\u002F 决定 LLM 何时调用它\n    string ParameterSchema { get; }   \u002F\u002F 参数的 JSON Schema\n    ValueTask&lt;string&gt; ExecuteAsync(string argumentsJson, CancellationToken ct);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>最小可用示例（字符串反转工具）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>public sealed class ReverseTextTool : ITool   \u002F\u002F ≈ class ReverseTextTool : public ITool\n{\n    public string Name =&gt; \"reverse_text\";\n    public string Description =&gt;\n        \"Reverse the characters of the given text.\";\n\n    public string ParameterSchema =&gt; \"\"\"\n    {\n      \"type\": \"object\",\n      \"properties\": {\n        \"text\": { \"type\": \"string\", \"description\": \"Text to reverse\" }\n      },\n      \"required\": [\"text\"]\n    }\n    \"\"\";\n\n    public ValueTask&lt;string&gt; ExecuteAsync(string argumentsJson, CancellationToken ct)\n    {\n        \u002F\u002F 用 JsonDocument 解析入参（AOT 安全）≈ 手写 JSON DOM 解析，不走反射\n        using var doc = JsonDocument.Parse(argumentsJson);   \u002F\u002F using ≈ RAII，确定性 Dispose\n        var text = doc.RootElement.GetProperty(\"text\").GetString() ?? \"\";\n        return new ValueTask&lt;string&gt;(new string(text.Reverse().ToArray()));\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然后把它加进内置工具列表（\u003Ccode>CreateBuiltInTools(...)\u003C\u002Fcode>，≈ 你在 main 里集中构造并 \u003Ccode>push_back\u003C\u002Fcode> 进 \u003Ccode>std::vector&lt;std::unique_ptr&lt;ITool&gt;&gt;\u003C\u002Fcode> 的那一段），重启网关，对它说「reverse the text hello」——工具调用会经过完整的审计\u002F治理\u002F审批链路。\u003C\u002Fp>\n\u003Ch3>② 写一个技能（Skill）—— 最轻，纯 Markdown\u003C\u002Fh3>\n\u003Cp>技能\u003Cstrong>不是代码\u003C\u002Fstrong>，而是一份「给 Agent 的操作手册」：教 Agent 遇到某类任务怎么组合调用已有工具。\u003C\u002Fp>\n\u003Cp>机制是\u003Cstrong>渐进式披露\u003C\u002Fstrong>：系统提示里只放技能索引（省 token）→ Agent 判断相关时拉取完整正文 → 需要时再读附属文件。\u003C\u002Fp>\n\u003Cp>创建只需一个文件夹 + 一个 \u003Ccode>SKILL.md\u003C\u002Fcode>，零编译：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>---\nname: pr-reviewer\ndescription: 当用户要求审查一个 Pull Request 或 diff 时使用。\n---\n\n## 步骤\n1. 用 `read_file` 或 `shell`（git diff）拿到改动\n2. 按正确性、边界、安全、可读性审查\n3. 输出分级意见：🔴 必须改 \u002F 🟡 建议 \u002F 🟢 可选\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>重启（或开热加载）即生效。\u003C\u002Fp>\n\u003Ch3>③ 写一个插件（Plugin）—— 打包一组能力\u003C\u002Fh3>\n\u003Cp>两条路：\u003Cstrong>原生 .NET 动态插件\u003C\u002Fstrong>（进程内 DLL 加载，仅 jit 车道）和 \u003Cstrong>JS\u002FTS 桥接插件\u003C\u002Fstrong>（Node.js 子进程 + JSON-RPC，两条车道都行）。\u003C\u002Fp>\n\u003Cp>这个取舍你闭着眼都懂：≈ \u003Cstrong>\u003Ccode>dlopen\u003C\u002Fcode> 加载 .so 进进程 vs 起子进程走 IPC\u003C\u002Fstrong>。前者零调用开销但 ABI\u002F生命周期\u002F崩溃隔离全是坑、且只能在 jit 车道用；后者有 IPC 成本但进程隔离、语言自由、崩了不拖垮宿主。原生契约仅 45 行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>public sealed class MyPlugin : INativeDynamicPlugin\n{\n    public void Register(INativeDynamicPluginContext context)\n    {\n        context.RegisterTool(new ReverseTextTool());\n        \u002F\u002F 还能 RegisterChannel \u002F RegisterHook \u002F RegisterProvider ...\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>生产环境（aot 车道）要跑自定义逻辑，走 TS 桥接插件。\u003C\u002Fp>\n\u003Ch3>④ 接一个渠道（Channel）—— 接你自己的 IM\u003C\u002Fh3>\n\u003Cp>契约是 \u003Ccode>IChannelAdapter\u003C\u002Fcode>（收 + 发），入站走「webhook → handler 校验解析 → 管道入队」，出站按 \u003Ccode>ChannelId\u003C\u002Fcode> 路由投递。照抄 Twilio SMS 的实现（最简单的参照）即可，6 步：配置类 → 适配器 → webhook handler → DI 注册 → 挂适配器 → 映射端点。\u003C\u002Fp>\n\u003Cp>webhook handler 的核心形态：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 验签 → 解析 → 白名单 → 入队，和你写的任何 webhook endpoint 一个结构\npublic async ValueTask&lt;WebhookResult&gt; HandleAsync(\n    string bodyText, string? signature,\n    Func&lt;InboundMessage, CancellationToken, ValueTask&gt; enqueue, CancellationToken ct)\n{\n    if (_config.ValidateSignature &amp;&amp; !IsValidSignature(bodyText, _secret, signature))\n        return WebhookResult.Unauthorized();\n    \u002F\u002F ... 解析、白名单校验 ...\n    await enqueue(new InboundMessage { ChannelId = \"myim\", SenderId = senderId, Text = text }, ct);\n    return WebhookResult.Ok();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>每个渠道都该有的安全面：签名校验（恒定时间比较，防时序侧信道）、发送者白名单、体积上限、去重窗口。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3>选型一图流\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>你的需求\u003C\u002Fth>\n   \u003Cth>用\u003C\u002Fth>\n   \u003Cth>要编译吗\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>加一个 Agent 能调用的动作\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>工具\u003C\u002Fstrong>\u003C\u002Ftd>\n   \u003Ctd>要\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>教 Agent 某类任务的处理流程\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>技能\u003C\u002Fstrong>\u003C\u002Ftd>\n   \u003Ctd>不要（纯 md）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>打包一组能力 \u002F 复用 TS 生态\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>插件\u003C\u002Fstrong>\u003C\u002Ftd>\n   \u003Ctd>原生要 \u002F 桥不要\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>接一个新的消息入口\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>渠道\u003C\u002Fstrong>\u003C\u002Ftd>\n   \u003Ctd>要\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Chr>\n\u003Ch2>六、开发约定：三个必须知道的红线\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\u003Cstrong>警告即错误\u003C\u002Fstrong>（\u003Ccode>TreatWarningsAsErrors=true\u003C\u002Fcode>）+ 可空性强制——≈ \u003Ccode>-Wall -Wextra -Werror\u003C\u002Fcode> 全开。第一次写会被编译器频繁拦，但它挡掉的就是你在 code review 里最不想看到的那类空指针路径。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>JSON 必须走源生成器\u003C\u002Fstrong>——对你而言这叫「编译期代码生成」，老朋友了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>数据安全铁律\u003C\u002Fstrong>：记忆\u002F会话默认落 \u003Ccode>.\u002Fmemory\u002F\u003C\u002Fcode>，严禁用「清空整库 \u002F DROP \u002F 删目录」做测试隔离——只删自己创建的数据，或用独立的 throwaway 路径。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>测试栈是 xUnit v3 + NSubstitute（≈ Google Test + gmock）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>dotnet test                                                # 全部（≈ ctest）\ndotnet test --filter \"FullyQualifiedName~ProcessToolTests\" # 单类（≈ gtest_filter）\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Chr>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cp>对 C++ 工程师来说，这套系统的读感大概是「熟悉中带着一丝轻松」：\u003Cstrong>单文件原生二进制、非空默认、显式接口实现、编译期代码生成、确定性的 \u003Ccode>using\u003C\u002Fcode> 资源管理、警告即错误\u003C\u002Fstrong>——你在乎的工程纪律都在。\u003C\u002Fp>\n\u003Cp>真正要适应的只有两件事：\u003Cstrong>内存从 RAII 换成 GC\u003C\u002Fstrong>（确定性析构变成了 \u003Ccode>using\u003C\u002Fcode> 显式管理非内存资源），以及\u003Cstrong>模板换成带约束的泛型\u003C\u002Fstrong>（编译期元编程的火力弱了，但编译速度快得不像话）。你失去一部分对内存的绝对控制，换来的是没有 UB、没有头文件、async 不用手写状态机——值不值，写两个工具你自有判断。\u003C\u002Fp>\n\u003Cp>想继续深挖，最可靠的三个源码入口：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Ccode>src\u002FOpenClaw.Gateway\u002FProgram.cs\u003C\u002Fcode> —— 启动主线（≈ 你的 \u003Ccode>main.cpp\u003C\u002Fcode>）\u003C\u002Fli>\n \u003Cli>\u003Ccode>src\u002FOpenClaw.Agent\u002FMafAgentRuntime.cs\u003C\u002Fcode> —— Agent 循环本体\u003C\u002Fli>\n \u003Cli>\u003Ccode>src\u002FOpenClaw.Core\u002FAbstractions\u002F\u003C\u002Fcode> —— 所有可扩展接口（≈ 项目里那堆抽象基类的头文件）\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cblockquote>\n \u003Cp>本文所有代码与结论均对照开源仓库 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fclawdotnet\u002Fopenclaw.net\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">clawdotnet\u002Fopenclaw.net\u003C\u002Fa> 当前源码（运行时 = MAF + jit）。如果你发现与代码不符——以代码为准，也欢迎提 PR。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>\u003Cstrong>觉得有用，欢迎去 GitHub 点个 Star ⭐，也欢迎点赞 \u002F 在看 \u002F 转发给你的 C++ 朋友 👋\u003C\u002Fstrong>\u003C\u002Fp>","编译出单文件原生二进制、没有头文件、async 不用手写状态机、内存安全但不用和借用检查器格斗——一个 C++ 工程师视角下的 .NET AI Agent 运行时。 为什么写这篇 如果你是 C++ 工程师，你的世界观大概是：产物必须是原生机器码、资源生命周期必须确定、抽象不该有隐藏开销。Agent 框架的主流世界是 Python \u002F Node——解释执行、运行时依赖一堆、性能随运气——你大概率看不上。 OpenClaw.NET 值得你多看一眼：一个用 .NET 写的自托管 AI Agent 运行时 + 网关，鉴权、策略、记忆、可观测性、多渠道接入全部内置，关键是能用 NativeAOT 编译成无依赖单文件原生二进制——启动即原生码，没有 JIT 预热，没有运行时安装。它已经开源，仓库在 github.com\u002Fclawdotnet\u002Fopenclaw.net。 用一句 C++ 话说： 它像一个「基于 boost.asio + REST 端点的常驻服务」——对外是 HTTP \u002F WebSocket \u002F 各 IM 的 webhook，对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent——只不过协程不用你手写 promise_type，内存不用你管 new\u002Fdelete。 本文全程用 C++ 概念做类比。读完你能：看懂系统组成和消息流转、在本地把它跑起来、并写出你的第一个工具 \u002F 技能 \u002F 插件 \u002F 渠道。 一、30 秒认识 OpenClaw.NET 能力 说明 网关 HTTP \u002F WebSocket \u002F 浏览器 UI（\u002Fchat）\u002F 各 IM webhook \u002F OpenAI 兼容端点（\u002Fv1\u002F*）\u002F MCP（\u002Fmcp） Agent 运行时 推理循环、工具执行、记忆、会话、技能、策略、审批、断路器 渠道 WebSocket、TG、Slack、Discord、Teams、WhatsApp，以及飞书 \u002F 钉钉 \u002F 企业微信 扩展点 工具（Tool）、技能（Skill）、插件（Plugin）、渠道（Channel）、LLM Provider 客户端 浏览器 UI、CLI、Avalonia 桌面 App、Blazor WASM 运维面板、TUI 项目已开源：https:\u002F\u002Fgithub.com\u002Fclawdotnet\u002Fopenclaw.net。仓库里解决方案叫 OpenClaw.Net.slnx，命名空间是 OpenClaw.*——和名字对得上，找代码不迷路。 二、C++ → C# 心智模型速查 这是全文最该先读的部分。看懂这张表，后面 90% 的代码你都能读： 你在 C++ 里熟悉的 .NET 里的对应物 编译出原生二进制 NativeAOT——同款产物；默认还有 JIT 模式 CMake \u002F Make + vcpkg \u002F conan dotnet CLI + MSBuild（.csproj）+ NuGet 头文件 + 声明\u002F定义分离 没有头文件——一份代码，编译器自己处理 模板（编译期展开） 泛型（运行时具体化）+ where T : ... 约束（≈ concepts 青春版） std::async \u002F C++20 协程 co_await Task\u003CT> + async\u002Fawait——语言内置，不用手写 promise_type std::stop_token（C++20） CancellationToken——就是同一个东西 std::optional\u003CT> string? vs string——非空默认，可空显式标注 struct \u002F class（都在栈上或你管堆） struct = 值类型 \u002F class = 堆上引用类型——这个区分 C# 保留了 std::unique_ptr \u002F shared_ptr 不需要——GC 接管，引用语义 ≈ 万物 shared_ptr RAII（析构函数确定性释放） IDisposable + using 关键差异，见下 虚函数 \u002F 抽象基类 interface + virtual\u002Foverride（接口必须显式声明实现） 异常 + RAII 回滚 异常 + try\u002Fcatch\u002Ffinally——模型几乎一样 boost.asio 的 io_context async runtime 内置，无感 未定义行为（UB） 内存安全——越界、悬垂引用编译期\u002F运行期拦死 Google Test \u002F Catch2 + gmock xUnit v3 + NSubstitute #define \u002F 预处理器 基本没有预处理宏（条件编译仅限少数场景） constexpr const \u002F static readonly 语法上最容易愣住的三个点 \u002F\u002F 1) 异步：≈ C++20 协程，但编译器把所有状态机细节都包了 public async Task\u003Cstring> RunAsync(Session session, string msg, CancellationToken ct) { var result = await _llm.CallAsync(msg, ct); \u002F\u002F ≈ co_await llm->Call(msg) return result.Text; } \u002F\u002F CancellationToken ≈ std::stop_token：协作式取消，显式传参，务必往下传。 \u002F\u002F 2) record + required：≈ 聚合初始化 + 编译期必填，带值相等语义（≈ 默认生成了 operator==） public sealed record OutboundMessage { public required string ChannelId { get; init; } \u002F\u002F 不给就编译不过 public required string RecipientId { get; init; } } \u002F\u002F 3) 可空引用类型：string? 可空，string 不可空——≈ std::optional\u003Cstd::string> vs std::string string? maybe = GetOrNull(); string sure = maybe; \u002F\u002F ⚠ 编译警告——本项目警告即错误（≈ -Werror） string sure2 = maybe ?? \"default\"; \u002F\u002F ?? ≈ maybe.value_or(\"default\") 最大的思维转换：RAII → GC + using 这是 C++ 工程师最需要重建的习惯。C# 是 GC 语言：对象内存的回收时机不确定，析构函数（终结器）什么时候跑、跑不跑都不保证——RAII 里「离开作用域即释放」的直觉在这里对内存不成立。 但对非内存资源（文件句柄、socket、锁），C# 给了显式机制： using var doc = JsonDocument.Parse(json); \u002F\u002F ≈ 栈对象，离开作用域确定性 Dispose \u002F\u002F 异步版本：await using var stream = ...; ≈ co_await 风格的确定性清理 心智模型这样切换：内存交给 GC（你不用管），资源交给 using（你必须管）。看到实现了 IDisposable 的类型，就像看到管理着 fd\u002Fhandle 的 RAII 类——必须包 using。 性能纪律还在，只是换了位置 GC 不等于放弃性能意识。本项目的写法你会眼熟：ValueTask（避免堆分配的轻量 future）、Span\u003CT>（≈ std::string_view，零拷贝切片）、ArrayPool（≈ 自己维护 freelist）。热路径上的分配纪律依然靠人——只是编译器不逼你。 NativeAOT 与裁剪：你的主场 「禁运行时反射、序列化走编译期代码生成」——C++ 工程师表示：RTTI 我都经常关，这算什么约束。 本项目开激进裁剪（TrimMode=link），裁剪器会删掉\"看起来没人引用\"的代码，所以 JSON 序列化用源生成器——为类型声明 JsonSerializerContext，编译期生成序列化代码： [JsonSerializable(typeof(ProblemDetails))] [JsonSerializable(typeof(OperatorAccountService.StoreState))] internal partial class GatewayJsonContext : JsonSerializerContext; 这就是「编译期生成 vs 运行期内省」的老对立面，你一直站编译期这边。你新增 DTO 时，记得挂到某个 JsonSerializerContext 上。 裁剪还引出贯穿全文的两条运行时车道： aot 车道：裁剪安全、低内存、无动态加载——≈ 静态链接的 release 构建，生产 Docker 镜像走这条。 jit 车道：完整 .NET，JIT 编译 + 支持运行时加载 DLL 插件——开发期默认走这条，≈ 支持 dlopen 插件的调试构建。 三、一条消息的一生 这是理解整个系统的主线。中枢是 OpenClaw.Gateway——它在启动时把 Agent 运行时、消息管道、渠道适配器、插件宿主组合起来，统一路由所有流量。 一条用户消息从进来到回复，共 11 步（C++ 类比已标注）： 渠道收消息：IChannelAdapter 把入站消息写进 MessagePipeline——≈ 一个有界的生产者-消费者队列 Worker 取消息：1~4 个 worker（上限 = CPU 核数）从队列读——≈ 你起 std::thread 池抢任务 会话加锁：拿该会话的 SemaphoreSlim（≈ std::counting_semaphore\u003C1>），同一会话不并发跑两轮 过中间件：限流、token 预算，可短路拒绝——≈ 请求处理管线上的拦截器链 进 Agent 运行时：MafAgentRuntime.RunAsync(...) 准备上下文：载入\u002F新建会话、裁剪历史、注入记忆召回 ReAct 循环：调 LLM → 要工具就执行 → 结果回灌 → 再调 LLM……直到产出文本 工具执行：一条完整链路——预设过滤 → 治理策略 → Hook → 人工审批 → 执行 → 审计 韧性：LLM 调用自带指数退避重试、超时、断路器、降级模型级联 落库：会话写入 IMemoryStore（dev 默认 sqlite） 回复出站：按 ChannelId 找到渠道适配器投递 整个系统的「骨架接口」都在 src\u002FOpenClaw.Core\u002FAbstractions\u002F：ITool、IChannelAdapter、IAgentRuntime、IMemoryStore、IToolHook……看懂它们 = 看懂系统的全部可扩展面。扩展方式就是「实现接口（≈ 继承抽象基类、重写纯虚函数）+ 注册进 DI 容器（≈ 一个类型安全的工厂注册表）」。 四、把它跑起来 前置：.NET 10 SDK（必须，≈ 装一次编译器 toolchain）、可选 Node.js 20+（仅跑 TS\u002FJS 插件时需要）、一个 LLM API Key。 # 先校验配置（≈ 启动前自检） dotnet run --project src\u002FOpenClaw.Gateway -c Release -- --doctor # 启动（≈ cmake --build --config Release && .\u002Fopenclaw） dotnet run --project src\u002FOpenClaw.Gateway -c Release 默认监听 http:\u002F\u002F127.0.0.1:18789，浏览器打开 \u002Fchat 即可对话。 最快的本地启动（三个环境变量 + 一条命令）： export MODEL_PROVIDER_KEY=\"sk-...\" # 你的 LLM key export OPENCLAW_WORKSPACE=\"$PWD\u002Fworkspace\" # 工作区根目录 mkdir -p \"$OPENCLAW_WORKSPACE\" dotnet run --project src\u002FOpenClaw.Gateway -c Release 配置体系是「配置文件 + 环境变量覆盖」的分层套路：环境变量用双下划线映射层级，OpenClaw__Runtime__Mode ↔ 配置树 OpenClaw:Runtime:Mode。敏感字段支持 env:VAR_NAME 引用写法，生产环境推荐——不用自己写 getenv 解析层。 本地避坑速查： 必须 .NET 10，多 SDK 并存时用 global.json 钉版本（≈ toolchain file 指定编译器版本） 出厂 appsettings.json 里的默认 AuthToken 和示例 API key 仅供本地回环，对外部署务必改成 env: 引用，别把真实密钥提交进仓库 公网绑定会被安全硬化拦截（缺鉴权 token、危险工具、raw: 密钥都会拒绝启动）——这是有意设计 五、动手扩展：四种方式，从轻到重 ① 写一个工具（Tool）—— 最常用 一个工具就是一个实现 ITool 的类。C++ 视角：就是继承抽象基类、重写纯虚函数——C# 接口同样需要显式声明实现： public interface ITool \u002F\u002F ≈ class ITool { 全是纯虚函数 }; { string Name { get; } \u002F\u002F LLM 用它来调用 string Description { get; } \u002F\u002F 决定 LLM 何时调用它 string ParameterSchema { get; } \u002F\u002F 参数的 JSON Schema ValueTask\u003Cstring> ExecuteAsync(string argumentsJson, CancellationToken ct); } 最小可用示例（字符串反转工具）： public sealed class ReverseTextTool : ITool \u002F\u002F ≈ class ReverseTextTool : public ITool { public string Name => \"reverse_text\"; public string Description => \"Reverse the characters of the given text.\"; public string ParameterSchema => \"\"\" { \"type\": \"object\", \"properties\": { \"text\": { \"type\": \"string\", \"description\": \"Text to reverse\" } }, \"required\": [\"text\"] } \"\"\"; public ValueTask\u003Cstring> ExecuteAsync(string argumentsJson, CancellationToken ct) { \u002F\u002F 用 JsonDocument 解析入参（AOT 安全）≈ 手写 JSON DOM 解析，不走反射 using var doc = JsonDocument.Parse(argumentsJson); \u002F\u002F using ≈ RAII，确定性 Dispose var text = doc.RootElement.GetProperty(\"text\").GetString() ?? \"\"; return new ValueTask\u003Cstring>(new string(text.Reverse().ToArray())); } } 然后把它加进内置工具列表（CreateBuiltInTools(...)，≈ 你在 main 里集中构造并 push_back 进 std::vector\u003Cstd::unique_ptr\u003CITool>> 的那一段），重启网关，对它说「reverse the text hello」——工具调用会经过完整的审计\u002F治理\u002F审批链路。 ② 写一个技能（Skill）—— 最轻，纯 Markdown 技能不是代码，而是一份「给 Agent 的操作手册」：教 Agent 遇到某类任务怎么组合调用已有工具。 机制是渐进式披露：系统提示里只放技能索引（省 token）→ Agent 判断相关时拉取完整正文 → 需要时再读附属文件。 创建只需一个文件夹 + 一个 SKILL.md，零编译： --- name: pr-reviewer description: 当用户要求审查一个 Pull Request 或 diff 时使用。 --- ## 步骤 1. 用 `read_file` 或 `shell`（git diff）拿到改动 2. 按正确性、边界、安全、可读性审查 3. 输出分级意见：🔴 必须改 \u002F 🟡 建议 \u002F 🟢 可选 重启（或开热加载）即生效。 ③ 写一个插件（Plugin）—— 打包一组能力 两条路：原生 .NET 动态插件（进程内 DLL 加载，仅 jit 车道）和 JS\u002FTS 桥接插件（Node.js 子进程 + JSON-RPC，两条车道都行）。 这个取舍你闭着眼都懂：≈ dlopen 加载 .so 进进程 vs 起子进程走 IPC。前者零调用开销但 ABI\u002F生命周期\u002F崩溃隔离全是坑、且只能在 jit 车道用；后者有 IPC 成本但进程隔离、语言自由、崩了不拖垮宿主。原生契约仅 45 行： public sealed class MyPlugin : INativeDynamicPlugin { public void Register(INativeDynamicPluginContext context) { context.RegisterTool(new ReverseTextTool()); \u002F\u002F 还能 RegisterChannel \u002F RegisterHook \u002F RegisterProvider ... } } 生产环境（aot 车道）要跑自定义逻辑，走 TS 桥接插件。 ④ 接一个渠道（Channel）—— 接你自己的 IM 契约是 IChannelAdapter（收 + 发），入站走「webhook → handler 校验解析 → 管道入队」，出站按 ChannelId 路由投递。照抄 Twilio SMS 的实现（最简单的参照）即可，6 步：配置类 → 适配器 → webhook handler → DI 注册 → 挂适配器 → 映射端点。 webhook handler 的核心形态： \u002F\u002F 验签 → 解析 → 白名单 → 入队，和你写的任何 webhook endpoint 一个结构 public async ValueTask\u003CWebhookResult> HandleAsync( string bodyText, string? signature, Func\u003CInboundMessage, CancellationToken, ValueTask> enqueue, CancellationToken ct) { if (_config.ValidateSignature && !IsValidSignature(bodyText, _secret, signature)) return WebhookResult.Unauthorized(); \u002F\u002F ... 解析、白名单校验 ... await enqueue(new InboundMessage { ChannelId = \"myim\", SenderId = senderId, Text = text }, ct); return WebhookResult.Ok(); } 每个渠道都该有的安全面：签名校验（恒定时间比较，防时序侧信道）、发送者白名单、体积上限、去重窗口。 选型一图流 你的需求 用 要编译吗 加一个 Agent 能调用的动作 工具 要 教 Agent 某类任务的处理流程 技能 不要（纯 md） 打包一组能力 \u002F 复用 TS 生态 插件 原生要 \u002F 桥不要 接一个新的消息入口 渠道 要 六、开发约定：三个必须知道的红线 警告即错误（TreatWarningsAsErrors=true）+ 可空性强制——≈ -Wall -Wextra -Werror 全开。第一次写会被编译器频繁拦，但它挡掉的就是你在 code review 里最不想看到的那类空指针路径。 JSON 必须走源生成器——对你而言这叫「编译期代码生成」，老朋友了。 数据安全铁律：记忆\u002F会话默认落 .\u002Fmemory\u002F，严禁用「清空整库 \u002F DROP \u002F 删目录」做测试隔离——只删自己创建的数据，或用独立的 throwaway 路径。 测试栈是 xUnit v3 + NSubstitute（≈ Google Test + gmock）： dotnet test # 全部（≈ ctest） dotnet test --filter \"FullyQualifiedName~ProcessToolTests\" # 单类（≈ gtest_filter） 写在最后 对 C++ 工程师来说，这套系统的读感大概是「熟悉中带着一丝轻松」：单文件原生二进制、非空默认、显式接口实现、编译期代码生成、确定性的 using 资源管理、警告即错误——你在乎的工程纪律都在。 真正要适应的只有两件事：内存从 RAII 换成 GC（确定性析构变成了 using 显式管理非内存资源），以及模板换成带约束的泛型（编译期元编程的火力弱了，但编译速度快得不像话）。你失去一部分对内存的绝对控制，换来的是没有 UB、没有头文件、async 不用手写状态机——值不值，写两个工具你自有判断。 想继续深挖，最可靠的三个源码入口： src\u002FOpenClaw.Gateway\u002FProgram.cs —— 启动主线（≈ 你的 main.cpp） src\u002FOpenClaw.Agent\u002FMafAgentRuntime.cs —— Agent 循环本体 src\u002FOpenClaw.Core\u002FAbstractions\u002F —— 所有可扩展接口（≈ 项目里那堆抽象基类的头文件） 本文所有代码与结论均对照开源仓库 clawdotnet\u002Fopenclaw.net 当前源码（运行时 = MAF + jit）。如果你发现与代码不符——以代码为准，也欢迎提 PR。 觉得有用，欢迎去 GitHub 点个 Star ⭐，也欢迎点赞 \u002F 在看 \u002F 转发给你的 C++ 朋友 👋",8402,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":16,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":40},"2026 · 软件开发","#2563eb","16 \u002F 10",[19],{"targetType":8,"targetId":9,"likedByMe":42,"likeCount":43,"commentCount":43,"contentLikeCount":43,"contentCommentCount":43,"sourceLikeCount":43,"sourceCommentCount":43},false,0,[45,52,58,65,72,78,84,91],{"id":46,"kind":7,"title":47,"summary":48,"image":49,"href":50,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":51},"NEWS_ARTICLE:927","HelloCrab - 短视频开源爬虫，仅供学习参考","HelloCrab 基于 Avalonia、Playwright、AI与 FFmpeg 的跨平台桌面采集器，支持9大平台，以及 Android、iOS、Browser 远程控制端。 Made By ChatGPT &amp; Vincent with ❤ 平台 是否接入 哔哩哔哩 ✅ 抖音 ✅ 快手","https:\u002F\u002Fwww.cnblogs.com\u002Fhupo376787\u002Fp\u002FScreenshot\u002FWindows.jpg","\u002Fnews\u002F927",[19],{"id":53,"kind":7,"title":54,"summary":55,"image":15,"href":56,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":57},"NEWS_ARTICLE:929",".NET 异常处理的\"暗门\"：代码里写满 catch，你依然能抓住它——从一个 AI Agent 运行时的源码说起","一个健壮的系统，必然到处都是有意的 catch；异常被消化不等于问题不存在。 观测与韧性，是一个硬币的两面——降级逻辑保证系统不崩，FirstChance 保证你能看见它为什么降级。","\u002Fnews\u002F929",[19],{"id":59,"kind":7,"title":60,"summary":61,"image":62,"href":63,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":64},"NEWS_ARTICLE:952",".NET 11 RC1 发布：拿到\"准生证\"，生产环境可以上了！","从 Preview 1 到 RC1，.NET 11 的拼图基本完整了：Runtime Async 反超状态机模型、CoreCLR 登陆 WebAssembly、CLI 全面 NativeAOT 化（dotnet tool list 快 5.5 倍）、C# 15 补齐 union 和 labeled","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202609\u002F510-20260909153805786-850851443.png","\u002Fnews\u002F952",[19],{"id":66,"kind":7,"title":67,"summary":68,"image":69,"href":70,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":71},"NEWS_ARTICLE:961","从零搭建ELK日志采集系统：Filebeat + Logstash + ES + Kibana 保姆级教程","轻量级架构，一份配置全搞定 一、前言 你是不是还在用 tail -f 和 grep 在多台服务器上翻日志？系统一出问题，就要登录三五台机器，来回切换窗口，定位一个Bug耗时半小时。 ELK 是业界最成熟的日志集中管理方案。本文将带你用 Docker Compose 一键部署 Filebeat + L","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1465907\u002F202609\u002F1465907-20260909162947190-195439361.png","\u002Fnews\u002F961",[19],{"id":73,"kind":7,"title":74,"summary":75,"image":14,"href":76,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":77},"NEWS_ARTICLE:980","写给 Rust 工程师的 OpenClaw.NET 上手指南：用你熟悉的 Rust 思维，跑起一个生产级 AI Agent","它像一个「axum 应用」——对外是 HTTP \u002F WebSocket \u002F 各 IM 的 webhook，对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent——只不过 async 不用选 runtime，也不用和借用检查器格斗。","\u002Fnews\u002F980",[19],{"id":79,"kind":7,"title":80,"summary":81,"image":15,"href":82,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":83},"NEWS_ARTICLE:999","Vane.Dispatch 1.0.0 发布：一个与容器、传输层零耦合的 .NET 服务分发引擎","Vane.Dispatch 1.0.0 发布：一个与容器、传输层零耦合的 .NET 服务分发引擎 它的前身，可以追溯到一个叫 Ndf 的项目。从那时算起，已经过去了十多年。今天，它以 Vane.Dispatch 1.0.0 的名字正式发布。 序：十年磨一剑 做后端的人大抵都绕不开同一个问题：如何把&","\u002Fnews\u002F999",[19],{"id":85,"kind":7,"title":86,"summary":87,"image":88,"href":89,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":90},"NEWS_ARTICLE:1002","从对标 Java 到对标 Go：Native AOT 的\"无痛化\"之路，走到哪一站了？","从 .NET 7 的 demo 到 .NET 12 的无痛化，这条路要走五年。慢吗？慢。但对比一下：Java 的 GraalVM Native Image 折腾了更久，至今 Spring 生态的 AOT 体验仍在打补丁；Go 则是天生就站在终点线上——.NET 是在背着二十年的反射遗产追赶一个轻装上","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202609\u002F510-20260907133134585-1733425989.png","\u002Fnews\u002F1002",[19],{"id":92,"kind":7,"title":93,"summary":94,"image":15,"href":95,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":96},"NEWS_ARTICLE:1012","java 多线程开发系列之八：玩转多线程（线程的中断）","之前的线程协作，讲的是通过wait和notify方法，多线程之间进行互相条件唤醒的办法。除此之外我们还需要进行中断操作。等待和唤醒可参考前文https:\u002F\u002Fwww.cnblogs.com\u002Fjilodream\u002Fp\u002F22770136设想这样一个场景：长工在给地主家干活，日出而作，日落而息。思路很简单：","\u002Fnews\u002F1012",[19]]