LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

.NET AI 实战篇:基于 Microsoft Agent Framework 集成钉钉机器⼈与云效项⽬管理

freeflydom
2026年9月2日 11:19 本文热度 50

前言

完整代码已经开源,搜索 Sky.DingTalk.AI 即可找到,欢迎 Star 和交流。

https://github.com/SkyChenSky/Sky.DingTalk.AI

不知道大家有没有类似的经历:业务群里,业务同事跟 QA、产品围绕一个问题来回沟通,几轮下来结论清晰了——这是个 Bug,要排期修;或者这是个新需求,要立项评估。讨论的热乎劲儿刚过,接下来却是谁都不爱干的一步:有人得把这几屏聊天记录消化掉,打开云效(或者Oncs)、选项目、选类型、把讨论提炼成标题和描述、指派负责人……讨论越充分,搬运越痛苦。

我们团队的日常沟通都在钉钉,项目管理的云效,中间隔着一层「手工搬运」。这活儿机械、重复、还容易漏字段。于是我最初的目标很朴素:讨论定论后,在群里 @机器人 说一句话,它就帮我把工作项建好,顺便把地址发回来。 正好最近 Microsoft Agent Framework(Microsoft.Agents.AI)GA 了,配合 .NET 10 一拍即合,这两天把这个 Demo 拉通了。

但做着做着你会发现,「把口语落成结构化工作项」这个环节一旦打通,它就不只是一个省事的建单工具——它是整条 AI 研发流水线的第一个闸口。工作项是研发过程的结构化锚点,它后面还可以挂一整串 Agent:

  • Bug 建完只是开始:Agent 根据工作项所属项目拉取代码仓库,让 AI 扫描相关模块,把「疑似出问题的文件、最近的变更记录」贴回工作项描述——开发还没打开 IDE,排查线索已经就位;
  • 需求立项即预估:结合历史相似需求做影响面分析,给出改动范围和涉及模块,辅助排期决策;
  • 订单(数据)查找:根据同事给的订单号,通过MCP由AI进行整理

用一张图表达这个愿景——本文打通的是第一环,后面的 Agent 都挂在「工作项」这个锚点上:

当信息能够在钉钉、云效、代码库之间自动流动,人就从「搬运工」退回到「决策者」的位置。 这篇文章先把这条流水线的第一环打通:从前期准备、钉钉机器人接入、AI Agent 集成,到云效 API 的封装,最后三者串起来跑通——后面那些宏大叙事,都建立在今天这块地基上。

先看最终效果,在钉钉群里 @机器人:

 

@云效助手 下单页在 iOS 上白屏了,项目是商城,严重的话帮我建个缺陷,给陈珙

机器人回复:

创建成功,工作项地址:https://devops.aliyun.com/projex/project/xxx/bug/xxxx

是不是有点意思?下面开工。

目的与作用

先明确目标,避免自嗨式开发。这个 Agent 要解决的核心问题是:

把群里口语化的反馈,自动落地成云效里结构化的工作项。

拆开来看,它干了三件事:

  1. 听懂人话:群消息是口语化的(「白屏了」「帮我建个缺陷」),AI 负责提取出结构化信息——项目名、类型(Bug/需求/任务)、标题、负责人、描述。
  2. 会干活:AI 不是只会聊天,它通过 Function Calling(工具调用)直接操作云效 API——查项目、查成员、建工作项、查列表。
  3. 有上下文:同一个群是多轮会话。你没说清是哪个项目时它会追问一句,你补一句「商城」,它就能接着上文的语境继续把工作项建好,而不是每次都从头再来。

整体架构非常朴素:

技术选型三件套:

  • Jusoft.DingtalkStream:钉钉官方 Stream 模式的 .NET 社区封装,免去自建 WebSocket 和暴露公网回调地址
  • Microsoft.Agents.AI:微软新出的 Agent 框架,ChatClientAgent + 工具注册,几行代码就有一个能调工具的 Agent
  • Sikiro.YunXiao:自己封装的云效 OpenAPI 客户端类库(独立项目,可复用)

前期准备:AI、钉钉、云效的配置获取

这一章没什么技术含量,但不做后面全卡。三家的「钥匙」都要先拿到手。

2.1 AI 接口(DeepSeek 为例)

Agent 的大脑需要一个 OpenAI 兼容的 Chat Completions 接口,并且必须支持工具调用(Function Calling),这是整个方案的地基。

以 DeepSeek 为例:

  1. 到开放平台注册并充值,创建一个 API Key(sk- 开头)
  2. 记下三样东西:ApiKeyEndpointhttps://api.deepseek.com)、Model(如 deepseek-chat

智谱、通义、Kimi 等国产模型都提供 OpenAI 兼容接口,换模型只改配置就行,这也是后面用 Microsoft.Extensions.AI.OpenAI 抽象的好处。

2.2 钉钉应用(Stream 模式机器人)

传统钉钉机器人要走 HTTP 回调,需要有公网地址,内网开发很痛苦。Stream 模式通过 WebSocket 长连接反向连接钉钉服务端,本地就能调试,这也是选 Jusoft.DingtalkStream 的原因。

  1. 钉钉开放平台创建一个企业内部应用
  2. 在「应用能力」里添加「机器人」
  3. 消息接收模式选择 Stream 模式
  4. 拿到应用的 ClientIdClientSecret(应用凭证页面)
  5. 发布应用,在群里把机器人添加进来

 

2.3 云效(阿里云 Yunxiao)

云效 Projex 的 OpenAPI 有两种认证方式,这里有个坑,后面封装时会展开:

方案凭证网关特点
方案一 AKAccessKeyId / Secretdevops.cn-hangzhou.aliyuncs.com阿里云 V2.0 签名,老版 ROA 接口居多
方案二 PAT个人访问令牌openapi-rdc.aliyuncs.comx-yunxiao-token 请求头,无需签名,oapi/v1 新接口

个人推荐 PAT 方案:在云效「个人设置 → 个人访问令牌」页面直接生成,不用折腾阿里云主账号 AK,而且新接口(工作项类型、字段定义、极简创建)都在 oapi/v1 下。

还需要记下 OrganizationId(组织 ID):打开云效任意页面,URL 里 organizations/{这串就是}/ 的那段。

 

 

2.4 配置外置

三家的凭证全部放进 appsettings.json,不进源码:

{
  "DingTalk": {
    "ClientId": "dingxxxxxxxx",
    "ClientSecret": "xxxxxxxx"
  },
  "Ai": {
    "ApiKey": "sk-xxxxxxxx",
    "Endpoint": "https://api.deepseek.com",
    "Model": "deepseek-chat"
  },
  "Yunxiao": {
    "OrganizationId": "xxxxxxxx",
    "PersonalAccessToken": "pt-xxxxxxxx"
  }
}

钉钉机器人与 AI 的集成

前期工作就绪后,我第一版 Demo 是「钉钉消息 → AI → 回复」先跑通,再把云效工具挂上去(对应仓库里「完成 demo」「调通了」那几个提交)。这个顺序建议大家都这么走:先让消息链路通,再让 Agent 有本事

3.1 消息处理器:模板方法模式

钉钉收到群消息后的处理骨架是固定的(模板模式):过滤 → 提取 → 生成回答 → 回复 → 应答。这里用了个模板方法模式的基类,把骨架钉死,子类只关心「怎么生成回答」:

public abstract class RobotMessageHandlerBase : IDingtalkStreamMessageHandler
{
    public async Task HandleMessage(MessageEventHanderArgs e)
    {
        if (!CanHandle(e)) return;                          // 1. 只处理机器人消息回调
        var message = e.GetRobotMessageData();
        var content = GetTextContent(message);             // 2. 提取消息文本
        var answer = await ProcessAsync(message, content); // 3. 生成回答(子类实现)
        await ReplyAsync(message, answer);                 // 4. sessionWebhook 回复
        await AckAsync(e);                                 // 5. ack 应答,否则服务端会重推
    }
    protected abstract Task<string> ProcessAsync(ReceivedRobotMessage message, string content);
}

有个细节值得一提:GetTextContent 要按消息类型分派。钉钉群里发文字是 text,转发、引用、Markdown 是 richText(富文本分段),纯图片之类的其他类型我们暂时不处理:

protected virtual string GetTextContent(ReceivedRobotMessage message)
    => message.MsgType?.ToLowerInvariant() switch
    {
        "text" => message.GetTextContent().Content ?? "",
        "richText" => string.Concat(message.GetRichTextContent().RichText?.Select(r => r.Text) ?? []),
        _ => "",
    };

具体的处理器就薄得只剩一行了:

public class DingTalkRobotMessageHandler(DingTalkRobotAgent agent)
    : RobotMessageHandlerBase
{
    protected override Task<string> ProcessAsync(ReceivedRobotMessage message, string content)
        => agent.AskAsync(message.ConversationId, content, message.SenderNick);
}

3.2 Agent:ChatClientAgent + 工具注册

主角登场。Microsoft.Agents.AIChatClientAgent 把「大模型 + 提示词 + 工具集」打包成一个 Agent 对象:

public DingTalkRobotAgent(IChatClient chatClient, YunxiaoClient yunxiao,
    ILogger<DingTalkRobotAgent> logger)
{
    _agent = new ChatClientAgent(
        chatClient,
        instructions: DefaultInstructions,   // 系统提示词:角色 + 参数提取规则 + 追问策略
        name: "dingtalk-robot-agent",
        description: "钉钉群机器人助手……",
        tools: YunxiaoAgentTools.Create(yunxiao));  // 云效工具集
}

其中 IChatClient 来自 Microsoft.Extensions.AI.OpenAI,任何 OpenAI 兼容的模型都能接:

services.AddSingleton<IChatClient>(_ =>
    new OpenAIClient(new ApiKeyCredential(ai.ApiKey), new OpenAIClientOptions
    {
        Endpoint = new Uri(ai.Endpoint),
    }).GetChatClient(ai.Model).AsIChatClient());

提示词是 Agent 的灵魂,我的策略是「能默认就默认,只有项目名完全无法确定才追问」——群里没人喜欢跟机器人一问一答填表单:

你是钉钉群里的「云效项目助手」,帮助团队成员把口语化的反馈落地成云效工作项。
处理用户消息的规则:
1. 从消息中提取:项目名、工作项类型(Bug 缺陷 / Req 需求 / Task 任务)、标题、负责人、描述。
2. 信息不全时优先用合理默认值,不要向用户二次确认:
   - 类型未提及 → 默认 Bug;
   - 描述未提及 → 把用户的原话整理成描述;
   - 负责人未提及 → 默认用消息标注的「发起人」(@机器人的用户)。
3. 只有当「项目名」完全无法确定时,才回复用户请他补充是哪个项目;
   用户补充后必须结合上下文继续处理,不要重复追问。
4. 不确定项目名是否真实存在时,先调用 ListProjects 核对(宁可多查一次,不要猜)。
5. 创建成功后,回复一句话结果并附上工作项地址(URL)。

3.3 多轮会话

每个群独立会话,用 ConcurrentDictionary<群ID, AgentSession> 按群保存即可。有个小提醒:CreateSessionAsync 如果传入 conversationId,会走框架的「服务端托管聊天历史」模式——历史存在模型服务商那边,客户端每次只带会话 ID。但这套模式要求后端协议本身支持「服务端存对话」:

协议代表服务端管历史?
Responses APIOpenAI / Azure OpenAI✅ 响应带 conversation_id,历史存在服务端
Chat CompletionsDeepSeek / 智谱 等 OpenAI 兼容端点❌ 无状态,每次请求要带全量历史

DeepSeek 走的是 Chat Completions,响应里没有会话 ID。框架每轮结束会「对账」:你声明了服务端管历史,但服务端没这个能力,于是直接抛 Service did not return a valid conversation id...——宁可报错也不静默丢历史。

解决办法是无参创建会话 + SetInMemoryChatHistory 在应用侧自己管历史——每轮把全量历史重新发给模型,恰好和 Chat Completions 的无状态协议是天生一对:

private async Task<AgentSession> GetOrCreateSessionAsync(string conversationId, CancellationToken ct)
{
    if (_sessions.TryGetValue(conversationId, out var session))
    {
        // 未超上限直接复用
        if (!session.TryGetInMemoryChatHistory(out var history) || history.Count <= MaxSessionMessages)
            return session;
        // 超过上限:丢弃最旧的消息,保留最近 N 条(滑动窗口)
        var trimmed = history.Skip(history.Count - MaxSessionMessages).ToList();
        // 截断处可能落在「工具调用 / 工具结果」中间,开头的孤儿 tool 消息会被接口拒绝,
        // 因此向前推进到第一条用户消息
        var firstUser = trimmed.FindIndex(m => m.Role == ChatRole.User);
        session.SetInMemoryChatHistory(firstUser > 0 ? trimmed.Skip(firstUser).ToList() : trimmed);
        return session;
    }
    session = await _agent.CreateSessionAsync(ct);   // 注意:必须无参
    session.SetInMemoryChatHistory([]);              // 应用侧托管聊天历史
    return _sessions[conversationId] = session;
}

每个群最多保留 40 条消息(约 20 轮),超过后丢弃最旧的、保留最近 40 条(滑动窗口),上下文不会突然全丢,token 也不会无限膨胀。截断时留意别切在一对「工具调用/工具结果」中间——开头留下孤儿 tool 消息会被接口拒绝,所以截断后从第一条用户消息开始保留。

云效 API 的封装

AI 有了,但它还只会说不会做。这一章把云效 OpenAPI 封装成独立的类库 Sikiro.YunXiao(对应仓库里「重构完成」「完成重构」两个提交——Demo 跑通后我把客户端从主工程拆成了独立项目,可复用)。

4.1 双认证方案:一个客户端兼容两套网关

前文提到的两套认证,在 YunxiaoClient 构造时二选一,之后所有方法自动路由:

  • 方案一(AK):用阿里云 V2.0 通用 SDK 做泛化调用(ROA 风格),不需要安装云效产品 SDK,SDK 自动完成 ACS V3 签名
  • 方案二(PAT):裸 HttpClient + x-yunxiao-token 请求头直连,无需任何签名
public YunxiaoClient(YunxiaoOptions options)
{
    if (!string.IsNullOrEmpty(options.AccessKeyId) && !string.IsNullOrEmpty(options.AccessKeySecret))
        _akClient = new Client(new Config { ... });   // 方案一
    else if (!string.IsNullOrEmpty(options.PersonalAccessToken))
        { }                                            // 方案二:PAT,无签名
    else
        throw new ArgumentException("必须提供 AK(方案一)或 PersonalAccessToken(方案二)");
}

两套网关不只是认证不同,接口路径和请求体结构也有差异(这是云效 API 的历史包袱,不是我们设计的问题)。比如创建工作项:

// 方案一:POST /organization/{orgId}/workitems/create
body["space"] = spaceId; body["spaceIdentifier"] = spaceId; body["spaceType"] = "Project";
body["descriptionFormat"] = "MARKDOWN";
// 方案二:POST /oapi/v1/projex/organizations/{orgId}/workitems
body["spaceId"] = spaceId;
body["formatType"] = "MARKDOWN";

对于这种「本质复杂度」,我的做法是老老实实在方法内分支,只把真正重复的部分(如单个工作项的 URL 拼接)抽成私有方法,不过度设计。

4.2 极简创建:QuickCreateWorkItemAsync

直接用 CreateWorkItemAsync 建一个 Bug,需要提供 spaceIdassignedTo(用户 ID)、workitemTypeId、必填自定义字段(比如 Bug 的「严重程度」选项 ID)……这对 AI 来说太不友好了。于是封装了一个极简版本,调用方只传大类、标题、项目名,其余全部自动推断:

public async Task<string> QuickCreateWorkItemAsync(
    YunxiaoWorkItemCategory category, string subject, string projectName,
    string? assignedTo = null, string? description = null, CancellationToken ct = default)
{
    // 1. 按名称找项目 → spaceId
    // 2. 负责人:传用户 ID 或姓名都行,自动解析;不传取项目第一个成员
    // 3. 类型:该大类下的默认类型
    // 4. 必填自定义字段(严重程度等):统一取字段配置的默认选项
    // 5. 创建并回查,返回工作项地址(可直接点开)
}

返回的是工作项 URL(https://devops.aliyun.com/projex/project/{spaceId}/bug/{id}),AI 拿到后直接贴在群里,体验闭环。

封装 API 给 AI 用时,「减少参数」和「返回人话」是两个关键设计原则——参数越少,模型越不容易填错;返回文本化,模型直接转述,不用二次理解 JSON。

4.3 把客户端变成 Agent 工具

Microsoft.Extensions.AIAIFunctionFactory.Create 能把普通方法变成 Agent 可调用的工具,[Description] 特性就是给大模型看的「说明书」,写得越清楚模型调用越准:

public static IList<AITool> Create(YunxiaoClient client)
{
    var functions = new YunxiaoToolFunctions(client);
    return
    [
        AIFunctionFactory.Create(functions.ListProjects),
        AIFunctionFactory.Create(functions.ListProjectMembers),
        AIFunctionFactory.Create(functions.CreateWorkItem),
        AIFunctionFactory.Create(functions.ListWorkItems),
    ];
}
private sealed class YunxiaoToolFunctions(YunxiaoClient client)
{
    [Description("在云效创建工作项(Bug 缺陷 / Req 需求 / Task 任务),成功后返回可直接打开的工作项地址。")]
    public async Task<string> CreateWorkItem(
        [Description("工作项类型:Bug=缺陷、Req=需求、Task=任务")] string category,
        [Description("标题:一句话概括问题或需求")] string subject,
        [Description("项目名称:必须是云效中真实存在的项目名,不确定时先用 ListProjects 查询")] string projectName,
        [Description("负责人姓名,可不传,默认取消息标注的发起人(@机器人的用户)")] string? assignedTo = null,
        [Description("详细描述(Markdown),可不传")] string? description = null)
    {
        if (!TryParseCategory(category, out var parsed))
            return $"创建失败:无法识别的工作项类型「{category}」";
        try
        {
            var url = await client.QuickCreateWorkItemAsync(parsed, subject, projectName, assignedTo, description);
            return $"创建成功,工作项地址:{url}";
        }
        catch (YunxiaoException ex)
        {
            return $"创建失败:{ex.Message}";   // 失败原因转成文本,模型会转告用户
        }
    }
}

注意所有工具的返回值都是中文文本而不是对象——失败时返回「创建失败:未找到项目 xxx」,模型会自然地转述给群里,不需要额外的错误处理链路。

类型参数还做了别名兼容(bug/缺陷/Bug 都认识),进一步降低模型出错的概率。

三者的集成:组装起飞

零件都齐了,最后用依赖注入把三者串起来。每个能力一个扩展方法,Program.cs 干净得像目录:

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((ctx, services) => services
        .AddYunxiao(o =>
        {
            o.OrganizationId = ctx.Configuration["Yunxiao:OrganizationId"]!;
            o.PersonalAccessToken = ctx.Configuration["Yunxiao:PersonalAccessToken"]!;
        })
        .AddDingTalkRobotAgent(ai =>
        {
            ai.ApiKey = ctx.Configuration["Ai:ApiKey"]!;
            ai.Endpoint = ctx.Configuration["Ai:Endpoint"]!;
            ai.Model = ctx.Configuration["Ai:Model"]!;
        })
        .AddDingTalkRobot<DingTalkRobotMessageHandler>(dingTalk =>
        {
            dingTalk.ClientId = ctx.Configuration["DingTalk:ClientId"]!;
            dingTalk.ClientSecret = ctx.Configuration["DingTalk:ClientSecret"]!;
        }))
    .Build();
Console.WriteLine("DingTalk Stream 机器人已启动(云效 AI Agent)");
await host.RunAsync();

注册顺序暗含依赖关系:AddYunxiao 提供 YunxiaoClientAddDingTalkRobotAgent 拿它构造 Agent → AddDingTalkRobot 挂上消息处理器,处理器构造函数注入 Agent。

一次消息的完整旅程:

我有话想说

这个 Agent 当前也有绕不开的能力边界,对应的后续计划:

  • 群聊总结建单:@机器人拿不到完整群聊历史,需结合钉钉 AI 小钉先做对话总结,再交给本 Agent 落单;
  • 图片上传:富文本里的图片嵌入工作项描述——downloadCode 只是下载凭证,得走完下载链路再传云效附件;
  • 上下文外置:会话历史迁到 Redis/数据库,重启不丢;
  • 代码初判:按项目拉取代码仓库,AI 扫描疑似模块、把线索贴回工作项——前言那张图里挂在工作项后面的 Agent。

最后,说点感触。

边界不是墙,是插座。 「拿不到群聊历史」看起来是这个 Agent 的短板,但换一个视角:AI 小钉负责对话总结,本 Agent 负责结构化落单,一个 Agent 的边界恰好是另一个 Agent 的接入点。Agent 时代的架构设计,拼的就是把这些边界编排起来——单体的能力有限,组合的想象无限。

说到底,自动化的终点从来不是替代人。妄言用 AI 代替人,本身就是一种狂妄——机器人建好单、AI 给出初判线索,最后拍板排期、定责、取舍的仍然是你。工具越强,人的决策越值钱。 让信息自动流动,让人专注于判断——这就是我做这个小东西的全部初衷。

让 AI 代替人,本是不负责任的狂妄——重复给流程,繁琐给 AI,决策与创造留给人。

与诸君共勉。

 

阅读原文:点击这里


该文章在 2026/9/2 11:19:28 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-9  粤公网安备44030602007207号