Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions MSLX.Daemon/Controllers/AiAssistantController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MSLX.Daemon.Services;
using MSLX.SDK.Models;

namespace MSLX.Daemon.Controllers;

[ApiController]
[Route("api/ai")]
public class AiAssistantController : ControllerBase
{
private readonly AiService _aiService;

public AiAssistantController(AiService aiService)
{
_aiService = aiService;
}

[HttpPost("chat")]
[Authorize(Roles = "admin")]
public async Task Chat([FromBody] ChatRequest request)
{
Response.Headers.Append("Content-Type", "text/event-stream");
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");

if (request.Messages == null || request.Messages.Count == 0)
{
await SendSseEventAsync("message", "请输入有效的对话内容。");
await SendSseEventAsync("done", "[DONE]");
return;
}

try
{
var jsonMessages = new JsonArray();
foreach (var m in request.Messages)
{
jsonMessages.Add(new JsonObject
{
["role"] = m.Role,
["content"] = m.Content
});
}

await _aiService.ProcessChatAsync(
jsonMessages,
async (chunkText) =>
{
await SendSseEventAsync("message", chunkText);
},
async (toolName, toolData) =>
{
var toolPayload = System.Text.Json.JsonSerializer.Serialize(new
{
tool = toolName,
data = toolData
});
await SendSseEventAsync("tool_executed", toolPayload);
}
);
}
catch (Exception ex)
{
await SendSseEventAsync("error", ex.Message);
}
finally
{
await SendSseEventAsync("done", "[DONE]");
}
}

private async Task SendSseEventAsync(string eventType, string data)
{
try
{
var formattedData = data.Replace("\r", "").Replace("\n", "\\n");
await Response.WriteAsync($"event: {eventType}\ndata: {formattedData}\n\n");
await Response.Body.FlushAsync();
}
catch (Exception ex)
{
// 客户端已断开(如用户中断对话),忽略写入异常
Console.WriteLine($"SSE 写入失败 (客户端可能已断开): {ex.Message}");
}
}

[HttpPost("confirm-tool")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> ConfirmTool([FromBody] ConfirmToolRequest request)
{
string? toolName = null;
object? toolData = null;

var (success, message, _) = await _aiService.ConfirmPendingToolAsync(
request.ConfirmationId,
request.Approved,
async (name, data) =>
{
toolName = name;
toolData = data;
await Task.CompletedTask;
});

return Ok(new ApiResponse<object>
{
Code = success ? 200 : 400,
Message = message,
Data = new
{
success,
message,
tool = toolName,
data = toolData
}
});
}
}

public class ConfirmToolRequest
{
public string ConfirmationId { get; set; } = "";
public bool Approved { get; set; }
}

public class ChatRequest
{
public List<ChatMessageDto>? Messages { get; set; }
}

public class ChatMessageDto
{
public string Role { get; set; } = "user";
public string Content { get; set; } = "";
}
54 changes: 54 additions & 0 deletions MSLX.Daemon/Controllers/AiSettingsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MSLX.Daemon.Utils.ConfigUtils;
using MSLX.SDK.Models;

namespace MSLX.Daemon.Controllers;

[ApiController]
[Route("api/settings/ai")]
public class AiSettingsController : ControllerBase
{
[HttpGet]
[Authorize(Roles = "admin")]
public IActionResult GetAiSettings()
{
var config = IConfigBase.Config.ReadConfig();
return Ok(new ApiResponse<object>
{
Code = 200,
Message = "获取成功",
Data = new
{
AiEnabled = (bool?)(config["aiEnabled"]) ?? false,
AiApiKey = (string?)(config["aiApiKey"]) ?? "",
AiBaseUrl = (string?)(config["aiBaseUrl"]) ?? "https://api.deepseek.com/v1",
AiModelName = (string?)(config["aiModelName"]) ?? "deepseek-chat"
}
});
}

[HttpPost]
[Authorize(Roles = "admin")]
public IActionResult UpdateAiSettings([FromBody] AiSettingsRequest request)
{
IConfigBase.Config.WriteConfigKey("aiEnabled", request.AiEnabled);
IConfigBase.Config.WriteConfigKey("aiApiKey", request.AiApiKey ?? "");
IConfigBase.Config.WriteConfigKey("aiBaseUrl", request.AiBaseUrl ?? "https://api.deepseek.com/v1");
IConfigBase.Config.WriteConfigKey("aiModelName", request.AiModelName ?? "deepseek-chat");

return Ok(new ApiResponse<object>
{
Code = 200,
Message = "AI 配置更新成功"
});
}
}

public class AiSettingsRequest
{
public bool AiEnabled { get; set; }
public string? AiApiKey { get; set; }
public string? AiBaseUrl { get; set; }
public string? AiModelName { get; set; }
}
74 changes: 74 additions & 0 deletions MSLX.Daemon/Hubs/AiChatHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using MSLX.Daemon.Services;
using System.Text.Json.Nodes;

namespace MSLX.Daemon.Hubs;

[Authorize(Roles = "admin")]
public class AiChatHub : Hub
{
private readonly AiService _aiService;
private readonly ILogger<AiChatHub> _logger;

public AiChatHub(AiService aiService, ILogger<AiChatHub> logger)
{
_aiService = aiService;
_logger = logger;
}

public async Task SendMessage(JsonArray messages)
{
_logger.LogInformation("收到 SignalR AI 对话请求,ConnectionId: {ConnectionId}", Context.ConnectionId);

try
{
await _aiService.ProcessChatAsync(
messages,
async (chunk) =>
{
await Clients.Caller.SendAsync("ChatChunk", chunk);
},
async (toolName, toolData) =>
{
await Clients.Caller.SendAsync("ToolExecuted", toolName, toolData);
}
);

await Clients.Caller.SendAsync("ChatComplete");
}
catch (Exception ex)
{
_logger.LogError(ex, "SignalR AI 对话处理异常");
try
{
await Clients.Caller.SendAsync("ChatError", ex.Message);
}
catch (Exception sendEx)
{
_logger.LogWarning(sendEx, "向已断开的 SignalR 客户端发送 ChatError 失败");
}
}
}

public async Task<object> ConfirmToolAction(string confirmationId, bool approved)
{
try
{
var (success, message, data) = await _aiService.ConfirmPendingToolAsync(
confirmationId,
approved,
async (toolName, toolData) =>
{
await Clients.Caller.SendAsync("ToolExecuted", toolName, toolData);
});

return new { success, message, data };
}
catch (Exception ex)
{
_logger.LogError(ex, "SignalR AI 敏感操作确认异常");
return new { success = false, message = ex.Message, data = (object?)null };
}
}
}
4 changes: 4 additions & 0 deletions MSLX.Daemon/MSLX.Daemon.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@
<ItemGroup>
<ProjectReference Include="..\MSLX.SDK\MSLX.SDK.csproj" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Skills\**\*.md" />
</ItemGroup>
</Project>
4 changes: 3 additions & 1 deletion MSLX.Daemon/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
Expand Down Expand Up @@ -190,6 +190,7 @@
builder.Services.AddSingleton<IDockerService,DockerService>();
builder.Services.AddSingleton<SystemMonitor>();
builder.Services.AddSingleton<CreationTaskTracker>();
builder.Services.AddSingleton<AiService>();
// 插件的一些服务
var pluginManager = new PluginManager();
builder.Services.AddSingleton(pluginManager);
Expand Down Expand Up @@ -435,6 +436,7 @@
app.MapHub<InstanceConsoleHub>("/api/hubs/instanceControlHub");
app.MapHub<SystemMonitorHub>("/api/hubs/system");
app.MapHub<DaemonUpdateHub>("/api/hubs/daemonUpdate");
app.MapHub<AiChatHub>("/api/hubs/aiChatHub");
app.MapControllers();


Expand Down
Loading
Loading