目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

一、当TypeScript遇上C#:一场跨语言的"相亲"

想象一下,你手里攥着一张用古埃及象形文字写的藏宝图(TypeScript版Codex SDK),而你的开发团队全是一群只认识简体汉字的盗墓贼(C#程序员)。这张藏宝图确实精美,每一个细节都标注得清清楚楚,但你的队伍愣是看不懂。怎么办?总不能现场让所有人去考古系进修吧?

这就是2025年10月Codex GA发布后,.NET开发者面临的尴尬处境。OpenAI甩出的官方SDK华丽得让人眼馋——支持Slack集成、MCP协议、多轮会话管理,但翻开源码一看,满屏的.ts后缀让人直呼"告辞"。就像去西餐厅发现菜单全是法文,虽然知道肯定好吃,但点菜都得靠猜。

不过话说回来,TypeScript和C#这俩语言,简直就是失散多年的亲兄弟。一个戴着谷歌出品的"类型安全"手表,一个挂着微软颁发的"面向对象"徽章,骨子里都是强类型、静态检查的学霸人设。既然基因如此相似,咱们何不搞一场"语言移植手术",把TypeScript的精华完整提取出来,注入到.NET的躯壳里?

二、Codex SDK解剖课:先看懂TypeScript版的骨架

在动手翻译之前,咱们得先当个"法医",把TypeScript版SDK的尸体(哦不,源码)解剖明白。根据OpenAI官方放出的Agents SDK集成文档,Codex CLI暴露的核心接口其实就俩"快递员":

  1. codex():负责接单,开启一段新对话
  2. codex-reply():负责续单,在多轮对话里继续传话

这俩接口跑在MCP(Model Context Protocol)协议上。你可以把MCP想象成AI界的USB-C接口——以前每个AI工具都是自家专用的充电口,MCP一出,大家终于能用同一根线对话了。

TypeScript版SDK的架构说白了就三层:

  • 传输层:通过stdio或者HTTP跟Codex CLI进程通信
  • 协议层:把JSON-RPC消息包成MCP格式,处理生命周期和工具调用
  • 应用层:封装成人类能看懂的generateCode()、reviewPR()这类业务方法

咱们移植的目标,就是把这三层原封不动地"翻译"成C#,让.NET程序能直接new CodexClient()就能使唤这个AI码农。

三、搭建.NET版的脚手架:从NuGet包开始

咱们不玩虚的,直接新建一个类库项目,取名OpenAI.Codex。首先得把基础设施搭起来。TypeScript版依赖@modelcontextprotocol/sdk,咱们C#圈里有对应的ModelContextProtocolNuGet包(社区维护版),这相当于借来了MCP协议的"解码器"。

但等等,移植不是简单的复制粘贴。TypeScript那种"随便写,类型推断自动补"的洒脱,到了C#这儿得变成"万物皆接口"的严谨。咱们得定义好这些"翻译字典":

// 对应TypeScript的CodexSettings接口
public record CodexConfiguration
{
    public string Model { get; init; } = "gpt-5.2-codex";
    public string ApprovalMode { get; init; } = "on-failure"; // 人工审核触发时机
    public string SandboxMode { get; init; } = "workspace-write"; // 沙箱权限
    public bool Verbose { get; init; } = false;
    public IReadOnlyDictionary<string, McpServerConfig> McpServers { get; init; } =
        new Dictionary<string, McpServerConfig>();
}

// MCP服务器配置,对应TypeScript的McpServerConfig
public record McpServerConfig
{
    public string Command { get; init; } = "npx";
    public IReadOnlyList<string> Args { get; init; } = new[] { "-y", "@openai/codex", "mcp" };
    public IReadOnlyDictionary<string, string> Env { get; init; } =
        new Dictionary<string, string>();
}

看到没?TypeScript里随便一个const config = { ... }的对象字面量,到了C#里得变成规规矩矩的record。这就像把街头嘻哈翻译成新闻联播——意思还是那个意思,但腔调得正。

四、核心通信层:用Process玩转MCP协议

Codex SDK最妙的设计,是把CLI当成一个长期驻留的MCP服务器来用。TypeScript里用spawn启动子进程,咱们C#有System.Diagnostics.Process,功能一模一样,但写法得更讲究。

public class CodexMcpClient : IAsyncDisposable
{
    private readonly Process _process;
    private readonly StreamWriter _stdin;
    private readonly StreamReader _stdout;
    private readonly JsonSerializerOptions _jsonOptions;
    private int _requestId = 0;
    private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonElement>> _pendingRequests;

    public CodexMcpClient(CodexConfiguration config)
    {
        // 启动Codex CLI作为MCP服务器进程
        _process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "npx",
                Arguments = $"-y @openai/codex@latest mcp --model {config.Model}",
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true,
                Environment = 
                {
                    ["OPENAI_API_KEY"] = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? "",
                    ["CODEX_SANDBOX_MODE"] = config.SandboxMode
                }
            }
        };

        _process.Start();
        _stdin = _process.StandardInput;
        _stdout = _process.StandardOutput;
        _pendingRequests = new ConcurrentDictionary<int, TaskCompletionSource<JsonElement>>();
        
        _jsonOptions = new JsonSerializerOptions 
        { 
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower 
        };

        // 启动后台线程监听响应
        _ = Task.Run(ListenForResponsesAsync);
    }

    private async Task ListenForResponsesAsync()
    {
        while (!_process.HasExited)
        {
            var line = await _stdout.ReadLineAsync();
            if (string.IsNullOrWhiteSpace(line)) continue;

            try
            {
                var message = JsonSerializer.Deserialize<JsonElement>(line, _jsonOptions);
                if (message.TryGetProperty("id", out var idElement))
                {
                    var id = idElement.GetInt32();
                    if (_pendingRequests.TryRemove(id, out var tcs))
                    {
                        tcs.SetResult(message);
                    }
                }
            }
            catch { /* 忽略解析失败的噪音 */ }
        }
    }

    public async Task<JsonElement> SendRequestAsync(string method, object @params)
    {
        var id = Interlocked.Increment(ref _requestId);
        var request = new
        {
            jsonrpc = "2.0",
            id,
            method,
            @params
        };

        var tcs = new TaskCompletionSource<JsonElement>();
        _pendingRequests[id] = tcs;

        var json = JsonSerializer.Serialize(request, _jsonOptions);
        await _stdin.WriteLineAsync(json);
        await _stdin.FlushAsync();

        // 设置超时,Codex任务可能跑很久(最长30分钟)
        using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(30));
        await using (cts.Token.Register(() => tcs.TrySetCanceled()))
        {
            return await tcs.Task;
        }
    }

    public async ValueTask DisposeAsync()
    {
        _process?.Kill(true);
        _process?.Dispose();
        _stdin?.Dispose();
        _stdout?.Dispose();
    }
}

这段代码就是整个SDK的"心脏"。TypeScript版本可能用几行async/await就搞定的事儿,C#版本得显式管理进程生命周期、处理JSON-RPC协议、还得防着内存泄漏。这就像同样是搬家,TypeScript开发者叫个搬家公司就躺平了,C#开发者得亲自检查每箱东西有没有磕碰,但好处是绝不会丢三落四。

五、工具调用层:把AI的"想法"变成代码

MCP协议的核心是Tool/Resource概念。Codex CLI作为MCP服务器,暴露了两个主要工具:codexcodex-reply。咱们得在C#里封装成正常人能用的API。

public class CodexAgent
{
    private readonly CodexMcpClient _client;
    private string? _currentConversationId;
    private readonly CodexConfiguration _config;

    public CodexAgent(CodexConfiguration config)
    {
        _config = config;
        _client = new CodexMcpClient(config);
    }

    // 对应TypeScript的codex()工具调用
    public async Task<CodexResponse> StartTaskAsync(
        string prompt, 
        string[]? files = null,
        string? approvalMode = null)
    {
        var parameters = new Dictionary<string, object>
        {
            ["prompt"] = prompt,
            ["approval_mode"] = approvalMode ?? _config.ApprovalMode,
            ["verbose"] = _config.Verbose
        };

        if (files?.Length > 0)
        {
            // 读取文件内容作为上下文
            var fileContents = await Task.WhenAll(
                files.Select(async f => 
                    new { path = f, content = await File.ReadAllTextAsync(f) })
            );
            parameters["files"] = fileContents;
        }

        var result = await _client.SendRequestAsync("tools/codex", parameters);
        
        // 从响应中提取会话ID和结果
        _currentConversationId = result.GetProperty("result")
            .GetProperty("conversation_id").GetString();
            
        return new CodexResponse
        {
            ConversationId = _currentConversationId,
            Output = result.GetProperty("result").GetProperty("output").GetString(),
            FilesModified = result.GetProperty("result")
                .GetProperty("files_modified").EnumerateArray()
                .Select(x => x.GetString()!).ToList()
        };
    }

    // 对应TypeScript的codex-reply()工具调用
    public async Task<CodexResponse> ContinueTaskAsync(string followUpPrompt)
    {
        if (_currentConversationId == null)
            throw new InvalidOperationException("先调用StartTaskAsync开启对话");

        var parameters = new
        {
            conversation_id = _currentConversationId,
            reply = followUpPrompt,
            approval_mode = _config.ApprovalMode
        };

        var result = await _client.SendRequestAsync("tools/codex-reply", parameters);
        
        return new CodexResponse
        {
            ConversationId = _currentConversationId,
            Output = result.GetProperty("result").GetProperty("output").GetString(),
            FilesModified = result.GetProperty("result")
                .GetProperty("files_modified").EnumerateArray()
                .Select(x => x.GetString()!).ToList()
        };
    }
}

public record CodexResponse
{
    public string? ConversationId { get; init; }
    public string? Output { get; init; }
    public IReadOnlyList<string> FilesModified { get; init; } = new List<string>();
}

注意到没?这里咱们把TypeScript那种"回调地狱"式的流式输出,改成了C#熟悉的async/await模式。Codex执行任务可能要跑好几分钟(甚至半小时),TypeScript可能用EventEmitter不断吐数据,但C#开发者更喜欢Task<CodexResponse>这种"先干活,完事叫我"的契约式编程。

六、流式响应改造:让C#也能"边看边聊"

不过,有些场景咱们确实得边干活边汇报进度。比如Codex在改一个大型项目,改了50个文件,用户肯定不想干等30分钟最后看个结果,而是想看"正在修改UserController.cs…正在生成单元测试…"。

TypeScript里用ReadableStream搞流式,C#有IAsyncEnumerable<T>,这才是真·流式处理的王者。

public async IAsyncEnumerable<CodexStreamChunk> StreamTaskAsync(
    string prompt,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    // 启动带流式标记的任务
    var parameters = new
    {
        prompt,
        stream = true,  // 要求流式返回
        approval_mode = _config.ApprovalMode
    };
    // 这里简化处理,实际需通过MCP的notification机制实现
    var response = await _client.SendRequestAsync("tools/codex", parameters);
    var conversationId = response.GetProperty("result")
        .GetProperty("conversation_id").GetString();

    // 模拟监听流式输出(实际实现需通过MCP的stdio长连接读取多行JSON)
    var progressReader = Task.Run(async () =>
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var line = await _client.ReadNextNotificationAsync(); // 假设的读取方法
            if (line.ValueKind == JsonValueKind.Undefined) break;
            
            yield return new CodexStreamChunk
            {
                Type = line.GetProperty("type").GetString(), // "progress", "file_change", "complete"
                Message = line.GetProperty("message").GetString(),
                Data = line.TryGetProperty("data", out var d) ? d : null
            };
        }
    }, cancellationToken);

    await foreach (var chunk in progressReader.AsAsyncEnumerable().WithCancellation(cancellationToken))
    {
        yield return chunk;
    }
}

public record CodexStreamChunk
{
    public string? Type { get; init; }
    public string? Message { get; init; }
    public JsonElement? Data { get; init; }
}

这就像是把TypeScript的"直播弹幕"模式,改成了C#的"新闻联播滚动字幕"——信息还是一条条过,但更符合.NET生态的审美。

七、企业级增强:让SDK像个成熟的打工人

单纯翻译TypeScript版的功能那是"小学水平",咱们得给.NET版本加上企业级开发的标配:日志、重试、熔断、健康检查。

public class ResilientCodexClient
{
    private readonly CodexAgent _innerAgent;
    private readonly ILogger<ResilientCodexClient> _logger;
    private readonly AsyncPolicyWrap _policy;

    public ResilientCodexClient(
        CodexConfiguration config, 
        ILogger<ResilientCodexClient> logger)
    {
        _innerAgent = new CodexAgent(config);
        _logger = logger;

        // 重试策略:Codex API偶尔抽风,重试3次
        var retry = Policy.Handle<TimeoutException>()
            .WaitAndRetryAsync(3, retryAttempt => 
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                (exception, timeSpan, retryCount, ctx) =>
                {
                    _logger.LogWarning("Codex调用失败,{RetryCount}秒后第{RetryCount}次重试...", 
                        timeSpan.TotalSeconds, retryCount);
                });

        // 熔断策略:连续5次失败就歇30秒,别死磕
        var circuitBreaker = Policy.Handle<Exception>()
            .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30),
                (ex, duration) => _logger.LogError("熔断器打开,Codex服务暂时不可用"),
                () => _logger.LogInformation("熔断器关闭,恢复服务"));

        _policy = Policy.WrapAsync(circuitBreaker, retry);
    }

    public async Task<CodexResponse> ExecuteWithResilienceAsync(
        Func<CodexAgent, Task<CodexResponse>> action)
    {
        return await _policy.ExecuteAsync(() => action(_innerAgent));
    }
}

用上Polly库做策略包装,这就是.NET生态的"护城河"。TypeScript开发者可能还在手写try/catch嵌套,C#这边已经用AOP思想把容错机制织入SDK了。就像同样是开车,一个是手动挡裸奔,一个是自动挡带ABS+ESP。

八、完整实战:写一个能跑通的控制台Demo

光说不练假把式,咱们来个完整的控制台项目,演示如何用这SDK让Codex帮咱们重构一个烂代码文件。

using Microsoft.Extensions.Logging;
using OpenAI.Codex;
using Serilog;

// 配置日志,.NET开发者就爱看这花花绿绿的控制台输出
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();
var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog());
var logger = loggerFactory.CreateLogger<Program>();

// 初始化配置,这里用的是"宽松模式"——只在失败时人工确认
var config = new CodexConfiguration
{
    Model = "gpt-5.2-codex",
    ApprovalMode = "on-failure",  // 或者"never"全自动,"on-request"每次询问
    SandboxMode = "workspace-write", // 允许写工作区文件,但别乱动系统目录
    Verbose = true,
    McpServers = new Dictionary<string, McpServerConfig>
    {
        ["filesystem"] = new McpServerConfig
        {
            Command = "npx",
            Args = new[] { "-y", "@modelcontextprotocol/server-filesystem", "/tmp" }
        }
    }
};

// 创建带韧性的客户端
var client = new ResilientCodexClient(config,
    loggerFactory.CreateLogger<ResilientCodexClient>());

try
{
    Console.WriteLine("🚀 启动Codex任务:重构Legacy代码...");
    // 让Codex分析并重构一个垃圾代码文件
    var response = await client.ExecuteWithResilienceAsync(agent => 
        agent.StartTaskAsync(
            prompt: "分析这个C#类,应用SOLID原则重构,提取接口,消除重复代码,添加异常处理",
            files: new[] { "./LegacyCode.cs" },
            approvalMode: "on-failure"  // 如果Codex想删库,拦住它
        ));

    Console.WriteLine($"✅ 任务完成!会话ID: {response.ConversationId}");
    Console.WriteLine($"📝 AI的输出报告:\n{response.Output}");
    Console.WriteLine($"📁 修改了 {response.FilesModified.Count} 个文件:");

    foreach (var file in response.FilesModified)
    {
        Console.WriteLine($"   - {file}");
    }

    // 多轮对话:继续让AI生成单元测试
    Console.WriteLine("\n🔄 继续生成单元测试...");
    var testResponse = await client.ExecuteWithResilienceAsync(agent => 
        agent.ContinueTaskAsync("为刚才重构的代码生成xUnit单元测试,覆盖率要到80%以上"));

    Console.WriteLine($"✅ 测试代码生成完成!\n{testResponse.Output}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("⏱️ 任务超时或被取消");
}
catch (Exception ex)
{
    logger.LogError(ex, "💥 调用Codex时发生灾难性错误");
}
finally
{
    await Log.CloseAndFlushAsync();
}

把这个程序跑起来,你会看到Codex CLI在后台启动,像一个真正的初级程序员一样吭哧吭哧改代码。不同的是,这"哥们儿"不会喊累,不会抱怨需求变更,更不会在代码里偷偷埋雷(只要你的approvalMode设置得当)。

九、避坑指南:移植过程中那些让人头秃的细节

移植SDK不是一帆风顺的,有几个TypeScript到C#的"文化冲突"得特别留意:

  1. JSON序列化的大小写问题
    TypeScript生态习惯camelCase(conversationId),但Codex CLI底层某些字段居然用snake_case(conversation_id)。C#的System.Text.Json默认是camelCase,得显式设置JsonNamingPolicy.SnakeCaseLower才能对上号,否则RPC调用会报invalid params。

  2. 进程生命周期管理
    TypeScript的Node.js进程模型跟.NET的Process类在信号处理上有差异。Codex CLI作为长期运行的MCP服务器,如果C#这边Dispose时直接Kill进程,可能会留下僵尸进程。建议用CancellationToken优雅通知,给5秒缓冲期再强制Kill。

  3. 文件路径的跨平台地狱
    TypeScript开发者大多在Mac/Linux上开发,路径用/;C#开发者很多在Windows上,Path.Combine出来的是\。Codex CLI在Docker沙箱里运行,只认POSIX路径。所以传文件路径给Codex前,得统一转换成/分隔符,不然AI找不到文件会瞎猜。

  4. 编码问题
    Codex CLI的stdio通信默认UTF-8,但Windows控制台默认GBK(中文系统)。如果C#程序用默认编码读取stdout,中文输出会变成"锟斤拷"。务必在ProcessStartInfo里设置StandardOutputEncoding = Encoding.UTF8

十、结语:当.NET生态拥抱AI Agent

咱们这趟移植之旅,本质上是在修一座桥——让.NET生态的开发者能无缝驶入AI Agent的高速路。TypeScript版Codex SDK像是一辆改装过的跑车,速度快但底盘低(依赖Node生态);咱们C#版则更像是一辆SUV,稳重、耐操、能适应各种企业级地形。

从技术趋势看,OpenAI已经在Agents SDK里明确支持通过MCP协议调用Codex,这说明MCP正在成为AI工具互联的事实标准。咱们这套.NET移植版,不仅复刻了功能,更重要的是把MCP协议的原生支持做进了SDK里。这意味着以后不管Codex CLI怎么升级,只要它遵循MCP协议,咱们的C# SDK不用改一行代码就能兼容。

最后说句掏心窝子的:写代码这活儿,正在从"手工作坊"变成"指挥交响乐团"。以前你得亲自拉每一把小提琴,现在你只需要挥挥指挥棒(自然语言描述),Codex这样的AI Agent就能帮你把代码写出来、测起来、甚至部署上去。而咱们做的这个SDK,就是给.NET开发者打造的那根"智能指挥棒"。

所以,还在等什么?赶紧去GitHub搜搜看有没有现成的OpenAI.CodexNuGet包(如果真有人发布了记得给我打钱),或者直接用本文的代码撸一个属于自己的版本。毕竟,在AI吃掉世界之前,先让AI帮你把代码写了,才是正经事。

目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐