Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9c99441
feat: 配置 Kestrel gRPC 端点支持 HTTPS
Cyrene2008 Jun 5, 2026
79438fd
fix: 服务端修复 - Kestrel HTTPS gRPC、拦截器豁免注册、移除重定向、CoreVersion、前端代理配置
Cyrene2008 Jun 6, 2026
b9a6173
feat: 修复分页、同步共享模型、实现 Audit/ConfigUpload 服务
Cyrene2008 Jun 6, 2026
b9a4161
docs: 远程命令执行 + WebSocket + 看板设计文档
Cyrene2008 Jun 6, 2026
3e8a58f
feat: WebSocket 实时推送 + 远程命令执行 + 首页看板 API
Cyrene2008 Jun 6, 2026
3a0f006
feat: 添加远程命令执行页面、审计日志页面、修复看板 API 路径
Cyrene2008 Jun 6, 2026
0222103
feat: WebSocket 数据更新推送 + 远程命令页面改进
Cyrene2008 Jun 6, 2026
7a37ca9
feat: 远程协助 PIN 验证机制
Cyrene2008 Jun 6, 2026
f44ea7f
fix: 远程命令页面使用 AssigneeList + 远程协助逻辑修复
Cyrene2008 Jun 6, 2026
aa9175f
fix: PIN 加载、客户端选择器、文案修复
Cyrene2008 Jun 6, 2026
7eb60ce
feat: 远程命令改用 gRPC 双向流推送(同广播机制)
Cyrene2008 Jun 6, 2026
3fc14ef
chore: 移除临时文件,更新 gitignore
Cyrene2008 Jun 6, 2026
a5f8f03
fix: 课表编辑器修复
Cyrene2008 Jun 7, 2026
e256d66
fix: 课表列表 API 500 错误(重复 key 异常)
Cyrene2008 Jun 7, 2026
43c16d3
feat: 自动化管理 API + WebUI + 课表显示修复
Cyrene2008 Jun 7, 2026
8c51808
feat: 组件配置和自动化配置可视化编辑器
Cyrene2008 Jun 7, 2026
ae587c6
fix: 组件编辑器 n-select 添加后重置,支持连续添加组件
Cyrene2008 Jun 7, 2026
3a852eb
feat: 规则集编辑器 - 集成到自动化和组件编辑器
Cyrene2008 Jun 7, 2026
e03a8ef
feat: 插件管理服务端 API + WebUI
Cyrene2008 Jun 7, 2026
e9dc39e
fix: 组件/自动化编辑器属性编辑弹窗修复
Cyrene2008 Jun 7, 2026
4492d50
fix: component push ConfigType=3, automation push uses PushConfig cmd
Cyrene2008 Jun 7, 2026
d6bea15
fix: ConfigUpload routes to correct tables, PendingConfigRequest trac…
Cyrene2008 Jun 8, 2026
1d2fbf5
refactor: remove WebSocket, remote command execution, and remote assi…
Cyrene2008 Jun 18, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,5 @@ thumb
sketch

ClassIsland.ManagementServer.Server/data/appsettings.json
data/
start-vite.bat
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ public static class Roles

public const string ClientsDelete = "ClientsDelete";

public const string CommandsUser = "CommandsUser";

public const string UsersManager = "UsersManager";

public static readonly List<string> AllRoles = [Admin, ObjectsWrite, ObjectsDelete, ClientsWrite, ClientsDelete, CommandsUser, UsersManager];
public static readonly List<string> AllRoles = [Admin, ObjectsWrite, ObjectsDelete, ClientsWrite, ClientsDelete, UsersManager];
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ public ManagementServerContext(DbContextOptions<ManagementServerContext> options

public virtual DbSet<Setting> Settings { get; set; }

public virtual DbSet<AuditLog> AuditLogs { get; set; }

public virtual DbSet<ClientConfigSnapshot> ClientConfigSnapshots { get; set; }

public virtual DbSet<ComponentTemplate> ComponentTemplates { get; set; }

public virtual DbSet<ClientComponentConfig> ClientComponentConfigs { get; set; }

public virtual DbSet<ClientAutomationConfig> ClientAutomationConfigs { get; set; }

public virtual DbSet<ClientPluginInfo> ClientPluginInfos { get; set; }

public virtual DbSet<ClientPluginInstallRequest> ClientPluginInstallRequests { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
MapJsonConverter(modelBuilder.Entity<ProfileClassplan>().Property(e => e.AttachedObjects));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ClassIsland.ManagementServer.Server.Context;
using ClassIsland.ManagementServer.Server.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ClassIsland.ManagementServer.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/v1/audit")]
public class AuditController(
ILogger<AuditController> logger,
ManagementServerContext dbContext) : ControllerBase
{
public ILogger<AuditController> Logger { get; } = logger;
public ManagementServerContext DbContext { get; } = dbContext;

[HttpGet]
public async Task<IActionResult> ListLogs([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20)
{
var result = await DbContext.AuditLogs
.ToPaginatedListAsync(pageIndex, pageSize, decreasing: true, orderByUpdatedTime: true);
return Ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using ClassIsland.ManagementServer.Server.Authorization;
using ClassIsland.ManagementServer.Server.Context;
using ClassIsland.ManagementServer.Server.Entities;
using ClassIsland.ManagementServer.Server.Enums;
using ClassIsland.ManagementServer.Server.Models;
using ClassIsland.ManagementServer.Server.Services;
using ClassIsland.Shared.Protobuf.Command;
using ClassIsland.Shared.Protobuf.Enum;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ClassIsland.ManagementServer.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/v1/clients/{cuid}/automation")]
public class AutomationController(
ILogger<AutomationController> logger,
ManagementServerContext dbContext,
ClientCommandDeliverService commandDeliverService,
PendingConfigRequestService pendingConfigRequests) : ControllerBase
{
public ILogger<AutomationController> Logger { get; } = logger;
public ManagementServerContext DbContext { get; } = dbContext;
public ClientCommandDeliverService CommandDeliverService { get; } = commandDeliverService;
public PendingConfigRequestService PendingConfigRequests { get; } = pendingConfigRequests;

/// <summary>
/// 获取客户端自动化配置
/// </summary>
[HttpGet]
public async Task<IActionResult> GetConfig(Guid cuid)
{
var config = await DbContext.Set<ClientAutomationConfig>().FindAsync(cuid);
if (config == null)
return Ok(new ClientAutomationConfig { ClientCuid = cuid, WorkflowsJson = "[]" });
return Ok(config);
}

/// <summary>
/// 更新客户端自动化配置并推送到客户端
/// </summary>
[HttpPut]
[Authorize(Roles = Roles.ObjectsWrite)]
public async Task<IActionResult> UpdateConfig(Guid cuid, [FromBody] UpdateAutomationRequest request)
{
var config = await DbContext.Set<ClientAutomationConfig>().FindAsync(cuid);
if (config == null)
{
config = new ClientAutomationConfig
{
ClientCuid = cuid,
WorkflowsJson = request.WorkflowsJson ?? "[]",
CreatedTime = DateTime.Now,
UpdatedTime = DateTime.Now
};
await DbContext.Set<ClientAutomationConfig>().AddAsync(config);
}
else
{
config.WorkflowsJson = request.WorkflowsJson ?? config.WorkflowsJson;
config.UpdatedTime = DateTime.Now;
}

await DbContext.SaveChangesAsync();

// 推送到客户端
var pushPayload = new PushConfig
{
ConfigType = 4, // CurrentAutomation
ConfigJson = config.WorkflowsJson
};
await CommandDeliverService.DeliverCommandAsync(
CommandTypes.PushConfig,
pushPayload,
new ObjectsAssignee
{
AssigneeType = AssigneeTypes.ClientUid,
TargetClientCuid = cuid
});

Logger.LogInformation("已更新客户端 {Cuid} 的自动化配置", cuid);
return Ok(config);
}

/// <summary>
/// 请求客户端上报当前自动化配置
/// </summary>
[HttpPost("request-upload")]
[Authorize(Roles = Roles.ObjectsWrite)]
public async Task<IActionResult> RequestUpload(Guid cuid)
{
var payload = new GetClientConfig
{
RequestGuid = Guid.NewGuid().ToString(),
ConfigType = ConfigTypes.CurrentAutomation
};

PendingConfigRequests.TrackRequest(payload.RequestGuid, ConfigTypes.CurrentAutomation, cuid);

await CommandDeliverService.DeliverCommandAsync(
CommandTypes.GetClientConfig,
payload,
new ObjectsAssignee
{
AssigneeType = AssigneeTypes.ClientUid,
TargetClientCuid = cuid
});

Logger.LogInformation("已请求客户端 {Cuid} 上报自动化配置", cuid);
return Ok(new { success = true, requestId = payload.RequestGuid });
}
}

public class UpdateAutomationRequest
{
public string? WorkflowsJson { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
namespace ClassIsland.ManagementServer.Server.Controllers;

[ApiController]
[Authorize(Roles = Roles.CommandsUser)]
[Authorize(Roles = Roles.ClientsWrite)]
[Route("api/v1/client-commands/")]
public class ClientCommandDeliverController(ClientCommandDeliverService clientCommandDeliverService) : ControllerBase
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using ClassIsland.ManagementServer.Server.Authorization;
using ClassIsland.ManagementServer.Server.Context;
using ClassIsland.ManagementServer.Server.Entities;
using ClassIsland.ManagementServer.Server.Enums;
using ClassIsland.ManagementServer.Server.Services;
using ClassIsland.Shared.Protobuf;
using ClassIsland.Shared.Protobuf.Command;
using ClassIsland.Shared.Protobuf.Enum;
using Google.Protobuf;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ClassIsland.ManagementServer.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/v1/clients/{cuid}/plugins")]
public class ClientPluginsController(
ILogger<ClientPluginsController> logger,
ManagementServerContext dbContext,
ClientCommandDeliverService commandDeliverService,
PendingConfigRequestService pendingConfigRequests) : ControllerBase
{
public ILogger<ClientPluginsController> Logger { get; } = logger;
public ManagementServerContext DbContext { get; } = dbContext;
public ClientCommandDeliverService CommandDeliverService { get; } = commandDeliverService;
public PendingConfigRequestService PendingConfigRequests { get; } = pendingConfigRequests;

/// <summary>
/// 获取客户端已安装的插件列表
/// </summary>
[HttpGet]
public async Task<IActionResult> GetPlugins(Guid cuid)
{
var plugins = await DbContext.ClientPluginInfos
.Where(p => p.ClientCuid == cuid)
.OrderBy(p => p.PluginName)
.ToListAsync();
return Ok(plugins);
}

/// <summary>
/// 请求客户端上报插件列表
/// </summary>
[HttpPost("request-upload")]
[Authorize(Roles = Roles.ObjectsWrite)]
public async Task<IActionResult> RequestUpload(Guid cuid)
{
var payload = new GetClientConfig
{
RequestGuid = Guid.NewGuid().ToString(),
ConfigType = ConfigTypes.PluginList
};

PendingConfigRequests.TrackRequest(payload.RequestGuid, ConfigTypes.PluginList, cuid);

await CommandDeliverService.DeliverCommandAsync(
CommandTypes.GetClientConfig,
payload,
new ObjectsAssignee
{
AssigneeType = AssigneeTypes.ClientUid,
TargetClientCuid = cuid
});

Logger.LogInformation("已请求客户端 {Cuid} 上报插件列表", cuid);
return Ok(new { success = true, requestId = payload.RequestGuid });
}

/// <summary>
/// 远程安装插件(上传 .cipx 文件)
/// </summary>
[HttpPost("install")]
[Authorize(Roles = Roles.ObjectsWrite)]
public async Task<IActionResult> InstallPlugin(Guid cuid, IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest(new { error = "请上传 .cipx 文件" });

if (!file.FileName.EndsWith(".cipx", StringComparison.OrdinalIgnoreCase))
return BadRequest(new { error = "文件必须是 .cipx 格式" });

var uploadsDir = Path.Combine("data", "plugin-uploads");
Directory.CreateDirectory(uploadsDir);
var fileName = $"{cuid}_{DateTime.Now:yyyyMMddHHmmss}_{file.FileName}";
var filePath = Path.Combine(uploadsDir, fileName);

await using (var stream = System.IO.File.Create(filePath))
{
await file.CopyToAsync(stream);
}

var installRequest = new ClientPluginInstallRequest
{
ClientCuid = cuid,
FileName = fileName,
FilePath = filePath,
Status = 0,
CreatedTime = DateTime.Now,
UpdatedTime = DateTime.Now
};
await DbContext.ClientPluginInstallRequests.AddAsync(installRequest);
await DbContext.SaveChangesAsync();

var command = new PluginCommand
{
Operation = "install",
PluginId = "",
RequestId = installRequest.Id
};
await CommandDeliverService.DeliverCommandAsync(
CommandTypes.ManagePlugin,
command,
new ObjectsAssignee
{
AssigneeType = AssigneeTypes.ClientUid,
TargetClientCuid = cuid
});

Logger.LogInformation("已发送插件安装命令到客户端 {Cuid}, 文件: {FileName}", cuid, fileName);
return Ok(new { success = true, requestId = installRequest.Id });
}

/// <summary>
/// 获取插件安装包(供客户端拉取)
/// </summary>
[HttpGet("install/{requestId}/package")]
[AllowAnonymous]
public async Task<IActionResult> GetPluginPackage(Guid cuid, long requestId)
{
var request = await DbContext.ClientPluginInstallRequests.FindAsync(requestId);
if (request == null || request.ClientCuid != cuid)
return NotFound();

if (!System.IO.File.Exists(request.FilePath))
return NotFound(new { error = "安装包不存在" });

var fileBytes = await System.IO.File.ReadAllBytesAsync(request.FilePath);
request.Status = 1;
request.UpdatedTime = DateTime.Now;
await DbContext.SaveChangesAsync();

return File(fileBytes, "application/octet-stream", "plugin.cipx");
}

/// <summary>
/// 卸载插件
/// </summary>
[HttpPost("{pluginId}/uninstall")]
[Authorize(Roles = Roles.ObjectsWrite)]
public async Task<IActionResult> UninstallPlugin(Guid cuid, string pluginId)
{
var command = new PluginCommand
{
Operation = "uninstall",
PluginId = pluginId
};
await CommandDeliverService.DeliverCommandAsync(
CommandTypes.ManagePlugin,
command,
new ObjectsAssignee
{
AssigneeType = AssigneeTypes.ClientUid,
TargetClientCuid = cuid
});

Logger.LogInformation("已发送插件卸载命令到客户端 {Cuid}, 插件: {PluginId}", cuid, pluginId);
return Ok(new { success = true });
}

/// <summary>
/// 启用/禁用插件
/// </summary>
[HttpPost("{pluginId}/toggle")]
[Authorize(Roles = Roles.ObjectsWrite)]
public async Task<IActionResult> TogglePlugin(Guid cuid, string pluginId, [FromQuery] bool enable)
{
var operation = enable ? "enable" : "disable";
var command = new PluginCommand
{
Operation = operation,
PluginId = pluginId
};
await CommandDeliverService.DeliverCommandAsync(
CommandTypes.ManagePlugin,
command,
new ObjectsAssignee
{
AssigneeType = AssigneeTypes.ClientUid,
TargetClientCuid = cuid
});

Logger.LogInformation("已发送插件{Operation}命令到客户端 {Cuid}, 插件: {PluginId}", enable ? "启用" : "禁用", cuid, pluginId);
return Ok(new { success = true });
}
}
Loading