using System.Text;
using System.Text.Json;
using Dpz.Core.Infrastructure;
using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.MessageQueue.Models;
using Dpz.Core.Public.ViewModel.Messages;
using Dpz.Core.Service.Mcp;
using Dpz.Core.Service.Network;
using Dpz.Core.Service.RepositoryService;
using Dpz.Core.Web.Jobs.Services;
using JetBrains.Annotations;
using Medallion.Threading;
using OpenAI.Chat;
using ZiggyCreatures.Caching.Fusion;

namespace Dpz.Core.Web.Jobs.MessageHandlers;

/// <summary>
/// 代码分析消息处理器
/// </summary>
[UsedImplicitly]
public class AnalyzeCodeHandler(
    IOpenAiService openAiService,
    ICodeFileSystemEntryService codeFileSystemEntryService,
    IInProcessMcpSessionFactory mcpSessionFactory,
    IDistributedLockProvider distributedLockProvider,
    IFusionCache fusionCache,
    IPushMessage pushMessage,
    ILogger<AnalyzeCodeHandler> logger
) : IMessageHandler<AnalyzeCodeMessage>
{
    /// <summary>
    /// 流式文本增量推送间隔(毫秒)
    /// </summary>
    private const int DeltaFlushIntervalMilliseconds = 100;

    /// <summary>
    /// 待推送的流式文本增量缓冲
    /// </summary>
    private readonly List<string> _pendingDeltaBuffer = [];

    private DateTime _lastDeltaFlushTime = DateTime.UtcNow;

    /// <summary>
    /// 当前分析文件的进度标识(与代码浏览页面路由一致)
    /// </summary>
    private string _progressFileKey = "";

    public async Task<MessageHandlerResult> HandleAsync(
        AnalyzeCodeMessage message,
        CancellationToken cancellationToken
    )
    {
        var dedupeKey = string.Empty;
        try
        {
            // 1. 基本校验:代码行数必须 >= 50
            if (GetContentLineCount(message.CodeContent) < 50)
            {
                logger.LogInformation(
                    "代码行数不足50行,跳过分析。Path:{@Path} File:{FileName} Lines:{Lines}",
                    message.Path,
                    message.FileName,
                    GetContentLineCount(message.CodeContent)
                );
                return MessageHandlerResult.Ok();
            }

            // 2. 生成去重键
            var keyParts = new List<string>(message.Path) { message.FileName, message.FileHash };
            var subKey = Convert.ToBase64String(
                Encoding.UTF8.GetBytes(string.Join("\0", keyParts))
            );
            dedupeKey = $"AI_Analyze.InProgress:{subKey}";

            // 3. 检查是否已有进行中的分析任务
            var wasSet = await fusionCache.TryGetAsync<bool>(dedupeKey, token: cancellationToken);
            if (wasSet.HasValue && wasSet.Value)
            {
                logger.LogInformation(
                    "已有分析任务进行中,跳过重复处理。Path:{@Path} File:{FileName} Hash:{Hash}",
                    message.Path,
                    message.FileName,
                    message.FileHash
                );
                return MessageHandlerResult.Ok();
            }

            // 4. 设置进行中标记
            var cacheOptions = new FusionCacheEntryOptions
            {
                Duration = TimeSpan.FromMinutes(30),
                AllowBackgroundDistributedCacheOperations = false,
            };
            await fusionCache.SetAsync(dedupeKey, true, cacheOptions, cancellationToken);

            // 5. 分布式锁
            var pathParts = new List<string>(message.Path) { message.FileName };
            var lockSubKey = Convert.ToBase64String(
                Encoding.UTF8.GetBytes(string.Join("\0", pathParts))
            );
            var lockKey = $"AI_Analyze.Code.Lock:{lockSubKey}";

            await using (
                await distributedLockProvider.AcquireLockAsync(
                    lockKey,
                    cancellationToken: cancellationToken
                )
            )
            {
                // 6. 再次检查文件是否存在及是否需要分析
                var entryPath = pathParts.ToArray();
                var entry = await codeFileSystemEntryService.FindByPathWithoutCacheAsync(
                    entryPath,
                    cancellationToken
                );

                if (entry == null)
                {
                    logger.LogWarning(
                        "文件不存在,跳过分析。Path:{@Path} File:{FileName}",
                        message.Path,
                        message.FileName
                    );
                    return MessageHandlerResult.Ok();
                }

                // 7. 检查是否已有最新分析结果(哈希值匹配)
                var hasFreshAnalyzeResult =
                    !string.IsNullOrWhiteSpace(entry.AiAnalyzeResult)
                    && string.Equals(
                        entry.AiAnalyzeHash,
                        message.FileHash,
                        StringComparison.OrdinalIgnoreCase
                    );

                if (hasFreshAnalyzeResult)
                {
                    logger.LogInformation(
                        "已有最新分析结果,跳过。Path:{@Path} File:{FileName} Hash:{Hash}",
                        message.Path,
                        message.FileName,
                        message.FileHash
                    );
                    return MessageHandlerResult.Ok();
                }

                // 8. 通知前端分析开始,并调用 AI 分析(允许模型通过 MCP 查询相关文件)
                var filePath = string.Join('/', pathParts);
                _progressFileKey = filePath;
                await PushProgressStageAsync(
                    CodeAnalyzeStage.Started,
                    $"开始分析代码文件:{message.FileName}"
                );

                var prompt = BuildAnalysisPrompt(filePath);
                await using var mcpSession = await mcpSessionFactory.CreateAsync(cancellationToken);
                var result = await openAiService.SendMessageWithMcpAsync(
                    messages:
                    [
                        new SystemChatMessage(prompt),
                        new UserChatMessage(
                            $"当前文件路径:{filePath}\n\n```\n{message.CodeContent}\n```"
                        ),
                    ],
                    mcpClient: mcpSession.Client,
                    onDeltaContentAsync: PushContentDeltaAsync,
                    onToolCallAsync: PushToolCallAsync,
                    options: x =>
                    {
                        x.Model = "deepseek-v4-flash";
                    },
                    maxToolRounds: 16,
                    cancellationToken: cancellationToken
                );

                // 推送剩余未刷出的流式增量
                await FlushContentAsync(cancellationToken);

                // 9. 保存分析结果
                if (result is { Success: true, Data: not null })
                {
                    var content = result.Data.Content;
                    if (string.IsNullOrWhiteSpace(content))
                    {
                        logger.LogWarning(
                            "AI分析未产出文本,跳过保存. Path:{@Path} File:{FileName}",
                            message.Path,
                            message.FileName
                        );
                        await PushProgressStageAsync(
                            CodeAnalyzeStage.Failed,
                            "AI 分析未产出文本,已跳过保存。"
                        );
                        return MessageHandlerResult.Fail("AI分析未产出文本结果");
                    }

                    // 10. 再次验证文件未变更(关键!防止在 AI 调用期间文件被修改)
                    var latestEntry = await codeFileSystemEntryService.FindByPathWithoutCacheAsync(
                        entryPath,
                        cancellationToken
                    );

                    if (
                        latestEntry == null
                        || !string.Equals(
                            latestEntry.Hash,
                            message.FileHash,
                            StringComparison.OrdinalIgnoreCase
                        )
                    )
                    {
                        logger.LogInformation(
                            "文件已变更,跳过保存过期分析结果。Path:{@Path} File:{FileName} MessageHash:{MessageHash} CurrentHash:{CurrentHash}",
                            message.Path,
                            message.FileName,
                            message.FileHash,
                            latestEntry?.Hash
                        );
                        await PushProgressStageAsync(
                            CodeAnalyzeStage.Completed,
                            "分析期间文件内容已变更,正在重新加载最新内容。"
                        );
                        return MessageHandlerResult.Ok();
                    }

                    // 11. 保存 AI 分析结果到数据库
                    await codeFileSystemEntryService.SaveAiAnalyzeResultAsync(
                        message.Path,
                        message.FileName,
                        content,
                        message.FileHash,
                        cancellationToken
                    );

                    await PushProgressStageAsync(CodeAnalyzeStage.Completed, "代码分析完成");

                    logger.LogInformation(
                        "AI分析成功。Path:{@Path} File:{FileName} Hash:{Hash}",
                        message.Path,
                        message.FileName,
                        message.FileHash
                    );
                }
                else
                {
                    logger.LogWarning(
                        "AI分析失败:{Message} | Path:{@Path} File:{FileName} Args:{@Arguments}",
                        result.Message,
                        message.Path,
                        message.FileName,
                        result.Arguments
                    );
                    await PushProgressStageAsync(
                        CodeAnalyzeStage.Failed,
                        $"AI 分析失败:{result.Message ?? "未知错误"}"
                    );
                    return MessageHandlerResult.Fail($"AI分析失败: {result.Message ?? "未知错误"}");
                }
            }

            return MessageHandlerResult.Ok();
        }
        catch (Exception ex)
        {
            logger.LogError(
                ex,
                "代码分析异常。Path:{@Path} File:{FileName} Hash:{Hash}",
                message.Path,
                message.FileName,
                message.FileHash
            );
            // 把流式中断前已产生的增量补推,便于前端展示部分内容
            await FlushContentAsync(cancellationToken);
            await PushProgressStageAsync(CodeAnalyzeStage.Failed, $"代码分析异常:{ex.Message}");
            return MessageHandlerResult.Fail($"代码分析异常: {ex.Message}");
        }
        finally
        {
            // 12. 清除进行中标记
            if (!string.IsNullOrEmpty(dedupeKey))
            {
                await fusionCache.RemoveAsync(dedupeKey, token: cancellationToken);
            }
        }
    }

    /// <summary>
    /// 推送分析进度阶段消息(推送失败不影响分析主流程)
    /// </summary>
    private async Task PushProgressStageAsync(int stage, string message)
    {
        if (string.IsNullOrEmpty(_progressFileKey))
        {
            return;
        }

        try
        {
            await pushMessage.PushCodeAnalyzeProgressAsync(
                new CodeAnalyzeProgressMessage
                {
                    FileKey = _progressFileKey,
                    Stage = stage,
                    Message = message,
                }
            );
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "推送代码分析进度失败 Stage:{Stage}", stage);
        }
    }

    /// <summary>
    /// 将 MCP 工具调用转换为中文进度说明并推送
    /// </summary>
    private Task PushToolCallAsync(
        string toolName,
        string argumentsJson,
        CancellationToken cancellationToken
    )
    {
        var target = ExtractToolTarget(argumentsJson);
        var message = (toolName, target) switch
        {
            ("list_directory", not null) => $"正在查看目录:{target}",
            ("list_directory", null) => "正在查看目录内容…",
            ("read_file", not null) => $"正在读取文件:{target}",
            ("read_file", null) => "正在读取相关文件…",
            ("search_files", not null) => $"正在查找文件:{target}",
            ("search_files", null) => "正在查找相关文件…",
            ("search_content", not null) => $"正在搜索代码:{target}",
            ("search_content", null) => "正在搜索相关代码…",
            ("get_file_info", not null) => $"正在获取文件信息:{target}",
            ("get_file_info", null) => "正在获取文件信息…",
            _ => $"正在调用工具:{toolName}",
        };
        return PushProgressStageAsync(CodeAnalyzeStage.Tool, message);
    }

    /// <summary>
    /// 从工具参数 JSON 中提取目标路径或关键字用于进度展示
    /// </summary>
    private static string? ExtractToolTarget(string argumentsJson)
    {
        try
        {
            using var document = JsonDocument.Parse(argumentsJson);
            var root = document.RootElement;
            foreach (var propertyName in new[] { "path", "directory", "pattern", "keyword" })
            {
                if (
                    root.TryGetProperty(propertyName, out var property)
                    && property.ValueKind == JsonValueKind.String
                )
                {
                    return property.GetString();
                }
            }
        }
        catch (JsonException)
        {
            // 参数不是合法 JSON 时忽略
        }

        return null;
    }

    /// <summary>
    /// 缓冲流式文本增量,按间隔批量推送
    /// </summary>
    private async Task PushContentDeltaAsync(string delta, CancellationToken cancellationToken)
    {
        _pendingDeltaBuffer.Add(delta);
        if (
            (DateTime.UtcNow - _lastDeltaFlushTime).TotalMilliseconds
            < DeltaFlushIntervalMilliseconds
        )
        {
            return;
        }

        await FlushContentAsync(cancellationToken);
    }

    /// <summary>
    /// 将缓冲的流式文本一次性推送
    /// </summary>
    private async Task FlushContentAsync(CancellationToken cancellationToken)
    {
        if (_pendingDeltaBuffer.Count == 0 || string.IsNullOrEmpty(_progressFileKey))
        {
            return;
        }

        var content = string.Concat(_pendingDeltaBuffer);
        _pendingDeltaBuffer.Clear();
        _lastDeltaFlushTime = DateTime.UtcNow;

        try
        {
            await pushMessage.PushCodeAnalyzeProgressAsync(
                new CodeAnalyzeProgressMessage
                {
                    FileKey = _progressFileKey,
                    Stage = CodeAnalyzeStage.Content,
                    Content = content,
                }
            );
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "推送代码分析流式内容失败");
        }
    }

    /// <summary>
    /// 构建 AI 分析提示词
    /// </summary>
    /// <param name="filePath">当前正在分析的文件相对路径</param>
    private static string BuildAnalysisPrompt(string filePath)
    {
        return new StringBuilder()
            .AppendLine("你是一个经验丰富且专业的开发人员。")
            .AppendLine()
            .AppendLine($"当前需要解释的文件路径是:{filePath}")
            .AppendLine("用户消息中包含该文件的完整内容。")
            .AppendLine()
            .AppendLine("代码如果足够简单,直接分析即可。")
            .AppendLine(
                "代码如果复杂,不要猜测含义。请主动使用提供的 MCP 工具,MCP工具调用限制在16轮以内:"
            )
            .AppendLine("- list_directory:查看同目录其它文件")
            .AppendLine("- read_file:阅读 import/using 或引用指向的相关文件")
            .AppendLine("- search_files:按文件名查找类型或模块")
            .AppendLine("- search_content:在源码内容中搜索符号或字符串")
            .AppendLine("- get_file_info:查看元数据和已有 AI 分析结果")
            .AppendLine()
            .AppendLine("在掌握足够上下文后,解释上述代码的用途、职责以及它如何与相关文件协作。")
            .AppendLine("回复要求:")
            .AppendLine("- 使用简体中文")
            .AppendLine("- 引用相关文件时使用Markdown链接格式,形如: [Y.cs](X/Y.cs)")
            .AppendLine("- 未通过工具确认的符号或调用关系不要臆断,直接说明")
            .AppendLine(
                "- 对于超长、足够复杂、存在明显执行/调用流程时的代码,使用mermaid画出流程图,流程图必须放在 mermaid 代码块中"
            )
            .AppendLine()
            .AppendLine("最终回复必须是对该文件描述的自然语言的报告,不要再发起工具调用。")
            .AppendLine()
            .ToString();
    }

    /// <summary>
    /// 获取代码内容行数
    /// </summary>
    private static int GetContentLineCount(string codeContent)
    {
        return codeContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None).Length;
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

已完成对 AnalyzeCodeHandler.cs 及其协作文件的阅读与交叉核对,下面是分析报告。

一、类的职责定位

AnalyzeCodeHandler 是「代码浏览 + AI 分析」链路里的消费端执行器,运行在 Dpz.Core.Web.Jobs(后台任务宿主)中。它实现了泛型消息处理接口 IMessageHandler<AnalyzeCodeMessage>(见 IMessageHandler.cs),通过 RegisterDependencyInjectionExtensions.cs 中的 AddMessageConsumer<AnalyzeCodeMessage, AnalyzeCodeHandler>() 注册为 RabbitMQ 消费者。

职责一句话概括:收到 AnalyzeCodeMessage 后,调用大模型对指定源码文件做 AI 解读(过程中允许模型通过进程内 MCP 去查库、读文件、搜代码),把流式输出实时推送到浏览器,最后把完整分析结果和文件 hash 写回仓库数据库。

消息从哪来:CodeController.cs(以及 WebApi 侧)在访问 /code/{**path} 时,当文件可预览、行数 ≥ 50、语言符合白名单且 ShouldAnalyzeAsync 判定需要分析(结果为空或 hash 不一致)时,发布 AnalyzeCodeMessage。消息体定义见 AnalyzeCodeMessage.cs,包含:目录分段 Path[]FileName、用于去重/版本校验的 FileHash、完整 CodeContent

二、构造函数注入的依赖

依赖类型用途
IOpenAiServiceIOpenAiService.cs发送支持 MCP 工具循环的对话请求(SendMessageWithMcpAsync
ICodeFileSystemEntryServiceICodeFileSystemEntryService.cs绕过缓存查库(FindByPathWithoutCacheAsync)、保存结果(SaveAiAnalyzeResultAsync
IInProcessMcpSessionFactoryIInProcessMcpSessionFactory.cs创建进程内 MCP 会话(内存管道对接服务端/客户端),供模型调用 list_directory/read_file 等工具
IDistributedLockProviderMedallion.Threading分布式锁,保证同一文件同时只有一个分析在执行
IFusionCacheZiggyCreatures缓存“进行中”去重标记,避免重复投递重复分析
IPushMessageIPushMessage.cs把阶段/流式进度推到 Web 的 SignalR Hub
ILogger-日志

三、处理流程(HandleAsync 主流程)

1. 前置过滤与去重

  • 代码行数 < 50 直接跳过(GetContentLineCount 按换行符切分计数)。
  • Path + FileName + FileHash 拼 NUL 分隔再 Base64,生成 AI_Analyze.InProgress:{subKey} 作为 FusionCache 标记(TTL 30 分钟)。已存在且为 true 则直接 Ok 返回,防止同一内容重复触发。

2. 分布式锁内二次校验

  • Path + FileName 生成 AI_Analyze.Code.Lock:{subKey}AcquireLockAsync 获得锁。
  • 锁内绕过缓存重新查文件实体(无缓存版本,防旧缓存误判);文件不存在则跳过。
  • 检查是否已有新鲜结果:AiAnalyzeResult 非空且 AiAnalyzeHash 与消息中的 FileHash 一致则跳过。实体字段见 CodeFileSystemEntryResponse.cs

3. AI 分析(MCP 加持 + 流式)

  • Path+FileName/ 连接成 filePath 赋给 _progressFileKey(此值与代码浏览页面的路由一致,是前端过滤推送的关键,见下)。
  • Started 阶段;组装 System Prompt(BuildAnalysisPrompt)→ 通过 mcpSessionFactory.CreateAsync 建会话 → 调 openAiService.SendMessageWithMcpAsync
    • System 消息为分析提示词;
    • User 消息为 "当前文件路径:{filePath}\n\n```\n{CodeContent}\n```"
    • 指定模型 deepseek-v4-flashmaxToolRounds: 16
    • 两个回调:onDeltaContentAsync → 流式文本增量缓冲;onToolCallAsync → 工具调用实时转成中文进度行推送。
  • 结束后把残留缓冲补推(FlushContentAsync)。

4. 结果落库与并发防护

  • 若结果 Success && Data != null:文本为空则按失败处理;
  • 关键防过期写入:保存前再次 FindByPathWithoutCacheAsync 并把实体 Hash 与消息 FileHash 比对——AI 分析耗时长,若期间文件被改/删除则放弃保存旧结果,只提示“内容已变更”并正常返回(不重试);
  • 校验通过则 SaveAiAnalyzeResultAsync 落库(记录结果 + 源 hash),推 Completed
  • 失败时记录日志并推 Failed,返回 MessageHandlerResult.Fail(...)(消息队列据此触发重试,见 MessageHandlerResult.cs)。

5. 异常与收尾

  • catch:记录异常、把中断前已缓冲的流式增量补推(尽量让前端看到部分内容)、推 Failed
  • finally:从 FusionCache 移除去重标记,保证下次同内容还能重新触发。

四、进度/流式推送机制(前端联动)

分析全过程是用户可见的实时直播:Jobs 通过 IPushMessage.cs 的实现 PushMessage.cs,建立到 Web 端 SignalR Hub(/notification)的连接并调用 PushCodeAnalyzeProgress,把 CodeAnalyzeProgressMessage.cs(含常量类 CodeAnalyzeStage:Started=0 / Tool=1 / Content=2 / Completed=3 / Failed=4)推送出去。

推送内容有两类语义,且都携带 FileKey

  1. 阶段/工具行Stage 0/1/3/4 + Message):开始、失败、完成,以及 PushToolCallAsync 把 MCP 工具调用翻译成友好中文(如“正在读取文件:xxx”,通过解析参数 JSON 里的 path/directory/pattern/keyword 取目标)。
  2. 流式正文Stage=2 + Content):模型逐字输出的 Markdown 增量,由 PushContentDeltaAsync 进入 _pendingDeltaBuffer,按 100ms 节流批量 flush,减少推送次数。

浏览器侧由 CodeAiLive.ts 订阅 codeAnalyzeProgress,仅当页面 data-file-key 与推送 fileKey 相等时才渲染(多开/导航不串台),流式内容按 120ms 节流渲染 markdown、失败时移除直播面板并恢复静态结果。这与 _progressFileKey = string.Join('/', pathParts) 的设计一一对应。

五、提示词与工具闭环

BuildAnalysisPrompt 生成的系统提示词要求模型“像资深开发一样解释该文件”,并主动使用 MCP 工具(最多 16 轮):list_directoryread_filesearch_filessearch_contentget_file_info,最后产出简体中文、带 Markdown 相对链接的报告。这正是仓库内“用 AI 分析解释自己源码文件”的元循环设计,工具集合与 ICodeFileSystemEntryService.cs 暴露的仓库检索能力一一对应,经由 IInProcessMcpSessionFactory.cs 提供的内存管道会话完成握手与工具发现。

六、主流程时序图

flowchart TD
    A[收到 AnalyzeCodeMessage] --> B{代码行数 >= 50?}
    B -- 否 --> B1[记日志并跳过] --> END
    B -- 是 --> C[生成去重键<br/>AI_Analyze.InProgress:subKey]
    C --> D{FusionCache 已有进行中标记?}
    D -- 是 --> D1[记日志跳过重复] --> END
    D -- 否 --> E[写入 30 分钟进行中标记]
    E --> F[获取分布式锁<br/>AI_Analyze.Code.Lock:subKey]
    F --> G[绕过缓存查询文件实体]
    G --> H{文件存在?}
    H -- 否 --> H1[跳过] --> END
    H -- 是 --> I{AiAnalyzeResult 非空<br/>且 AiAnalyzeHash == FileHash?}
    I -- 是 --> I1[已有新鲜结果跳过] --> END
    I -- 否 --> J[设 _progressFileKey<br/>推送 Started 阶段]
    J --> K[构建提示词 + 建进程内 MCP 会话]
    K --> L[SendMessageWithMcpAsync<br/>模型: deepseek-v4-flash / 16轮]
    L --> M[onToolCall: 推送工具进度行<br/>onDelta: 100ms节流缓冲推送]
    M --> N[flush 残留缓冲]
    N --> O{结果成功且有内容?}
    O -- 否 --> O1[推送 Failed] --> FAIL_RET
评论加载中...