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;
}
}
已完成对 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。
二、构造函数注入的依赖
| 依赖 | 类型 | 用途 |
|---|---|---|
IOpenAiService | IOpenAiService.cs | 发送支持 MCP 工具循环的对话请求(SendMessageWithMcpAsync) |
ICodeFileSystemEntryService | ICodeFileSystemEntryService.cs | 绕过缓存查库(FindByPathWithoutCacheAsync)、保存结果(SaveAiAnalyzeResultAsync) |
IInProcessMcpSessionFactory | IInProcessMcpSessionFactory.cs | 创建进程内 MCP 会话(内存管道对接服务端/客户端),供模型调用 list_directory/read_file 等工具 |
IDistributedLockProvider | Medallion.Threading | 分布式锁,保证同一文件同时只有一个分析在执行 |
IFusionCache | ZiggyCreatures | 缓存“进行中”去重标记,避免重复投递重复分析 |
IPushMessage | IPushMessage.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-flash,maxToolRounds: 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:
- 阶段/工具行(
Stage0/1/3/4 +Message):开始、失败、完成,以及PushToolCallAsync把 MCP 工具调用翻译成友好中文(如“正在读取文件:xxx”,通过解析参数 JSON 里的path/directory/pattern/keyword取目标)。 - 流式正文(
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_directory、read_file、search_files、search_content、get_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