文件
csp/backend/src/controllers/meta_controller.cc

529 行
19 KiB
C++
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#include "csp/controllers/meta_controller.h"
#include "csp/app_state.h"
#include "csp/domain/enum_strings.h"
#include "csp/domain/json.h"
#include "csp/services/kb_import_runner.h"
#include "csp/services/problem_gen_runner.h"
#include "csp/services/problem_service.h"
#include "csp/services/problem_solution_runner.h"
#include "csp/services/problem_workspace_service.h"
#include "csp/services/submission_service.h"
#include "csp/services/user_service.h"
#include "http_auth.h"
#include <algorithm>
#include <exception>
#include <optional>
#include <string>
namespace csp::controllers {
namespace {
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
const std::string& msg) {
Json::Value j;
j["ok"] = false;
j["error"] = msg;
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
resp->setStatusCode(code);
return resp;
}
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
Json::Value j;
j["ok"] = true;
j["data"] = data;
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
resp->setStatusCode(drogon::k200OK);
return resp;
}
int ParsePositiveInt(const std::string& s,
int default_value,
int min_value,
int max_value) {
if (s.empty()) return default_value;
const int v = std::stoi(s);
return std::max(min_value, std::min(max_value, v));
}
std::optional<int64_t> RequireAdminUserId(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>& cb) {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
cb(JsonError(drogon::k401Unauthorized, auth_error));
return std::nullopt;
}
services::UserService users(csp::AppState::Instance().db());
const auto user = users.GetById(*user_id);
if (!user.has_value() || user->username != "admin") {
cb(JsonError(drogon::k403Forbidden, "admin only"));
return std::nullopt;
}
return user_id;
}
Json::Value BuildOpenApiSpec() {
Json::Value root;
root["openapi"] = "3.1.0";
root["info"]["title"] = "CSP Platform API";
root["info"]["version"] = "1.0.0";
root["info"]["description"] =
"CSP 训练平台接口文档,含认证、题库、评测、导入、MCP。";
root["servers"][0]["url"] = "/";
auto& paths = root["paths"];
paths["/api/health"]["get"]["summary"] = "健康检查";
paths["/api/health"]["get"]["responses"]["200"]["description"] = "OK";
paths["/api/v1/auth/login"]["post"]["summary"] = "登录";
paths["/api/v1/auth/register"]["post"]["summary"] = "注册";
paths["/api/v1/problems"]["get"]["summary"] = "题库列表";
paths["/api/v1/problems/{id}"]["get"]["summary"] = "题目详情";
paths["/api/v1/problems/{id}/submit"]["post"]["summary"] = "提交评测";
paths["/api/v1/problems/{id}/draft"]["get"]["summary"] = "读取代码草稿";
paths["/api/v1/problems/{id}/draft"]["put"]["summary"] = "保存代码草稿";
paths["/api/v1/problems/{id}/solutions"]["get"]["summary"] = "题解列表/任务状态";
paths["/api/v1/problems/{id}/solutions/generate"]["post"]["summary"] =
"异步生成题解";
paths["/api/v1/run/cpp"]["post"]["summary"] = "C++ 试运行";
paths["/api/v1/submissions"]["get"]["summary"] = "提交记录";
paths["/api/v1/submissions/{id}"]["get"]["summary"] = "提交详情";
paths["/api/v1/submissions/{id}/analysis"]["post"]["summary"] = "提交评测建议LLM";
paths["/api/v1/admin/users"]["get"]["summary"] = "管理员用户列表";
paths["/api/v1/admin/users/{id}/rating"]["patch"]["summary"] = "管理员修改用户积分";
paths["/api/v1/admin/users/{id}"]["delete"]["summary"] = "管理员删除用户";
paths["/api/v1/admin/redeem-items"]["get"]["summary"] = "管理员查看积分兑换物品";
paths["/api/v1/admin/redeem-items"]["post"]["summary"] = "管理员新增积分兑换物品";
paths["/api/v1/admin/redeem-items/{id}"]["patch"]["summary"] = "管理员修改积分兑换物品";
paths["/api/v1/admin/redeem-items/{id}"]["delete"]["summary"] = "管理员下架积分兑换物品";
paths["/api/v1/admin/redeem-records"]["get"]["summary"] = "管理员查看兑换记录";
paths["/api/v1/me/redeem/items"]["get"]["summary"] = "我的可兑换物品列表";
paths["/api/v1/me/redeem/records"]["get"]["summary"] = "我的兑换记录";
paths["/api/v1/me/redeem/records"]["post"]["summary"] = "创建兑换记录";
paths["/api/v1/me/daily-tasks"]["get"]["summary"] = "我的每日任务列表";
paths["/api/v1/import/jobs/latest"]["get"]["summary"] = "最新导入任务";
paths["/api/v1/import/jobs/run"]["post"]["summary"] = "触发导入任务";
paths["/api/v1/problem-gen/status"]["get"]["summary"] = "CSP-J 生成任务状态";
paths["/api/v1/problem-gen/run"]["post"]["summary"] = "触发生成新题RAG+去重)";
paths["/api/v1/backend/logs"]["get"]["summary"] = "后台日志(题解任务队列)";
paths["/api/v1/backend/kb/refresh"]["get"]["summary"] = "知识库资料更新状态";
paths["/api/v1/backend/kb/refresh"]["post"]["summary"] = "手动一键更新知识库资料";
paths["/api/v1/backend/solutions/generate-missing"]["post"]["summary"] =
"异步补全所有缺失题解";
paths["/api/v1/mcp"]["post"]["summary"] = "MCP JSON-RPC 入口";
auto& components = root["components"];
components["securitySchemes"]["bearerAuth"]["type"] = "http";
components["securitySchemes"]["bearerAuth"]["scheme"] = "bearer";
components["securitySchemes"]["bearerAuth"]["bearerFormat"] = "JWT";
return root;
}
Json::Value BuildMcpError(const Json::Value& id,
int code,
const std::string& message) {
Json::Value out;
out["jsonrpc"] = "2.0";
out["id"] = id;
out["error"]["code"] = code;
out["error"]["message"] = message;
return out;
}
Json::Value BuildMcpOk(const Json::Value& id, const Json::Value& result) {
Json::Value out;
out["jsonrpc"] = "2.0";
out["id"] = id;
out["result"] = result;
return out;
}
Json::Value McpToolsList() {
Json::Value tools(Json::arrayValue);
Json::Value t1;
t1["name"] = "health";
t1["description"] = "Get backend health";
tools.append(t1);
Json::Value t2;
t2["name"] = "list_problems";
t2["description"] = "List problems with filters";
tools.append(t2);
Json::Value t3;
t3["name"] = "get_problem";
t3["description"] = "Get problem by id";
tools.append(t3);
Json::Value t4;
t4["name"] = "run_cpp";
t4["description"] = "Run C++ code with input";
tools.append(t4);
Json::Value t5;
t5["name"] = "generate_cspj_problem";
t5["description"] = "Trigger RAG-based CSP-J problem generation";
tools.append(t5);
return tools;
}
} // namespace
void MetaController::openapi(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildOpenApiSpec());
resp->setStatusCode(drogon::k200OK);
cb(resp);
}
void MetaController::backendLogs(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
if (!RequireAdminUserId(req, cb).has_value()) return;
const int limit =
ParsePositiveInt(req->getParameter("limit"), 100, 1, 500);
const int running_limit =
ParsePositiveInt(req->getParameter("running_limit"), 20, 1, 200);
const int queued_limit =
ParsePositiveInt(req->getParameter("queued_limit"), 100, 1, 1000);
services::ProblemWorkspaceService workspace(csp::AppState::Instance().db());
const auto jobs = workspace.ListRecentSolutionJobs(limit);
const auto running_jobs = workspace.ListSolutionJobsByStatus(
"running", running_limit);
const auto queued_jobs = workspace.ListSolutionJobsByStatus(
"queued", queued_limit);
auto& runner = services::ProblemSolutionRunner::Instance();
Json::Value items(Json::arrayValue);
for (const auto& job : jobs) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["problem_id"] = Json::Int64(job.problem_id);
j["problem_title"] = job.problem_title;
j["status"] = job.status;
j["progress"] = job.progress;
j["message"] = job.message;
j["created_by"] = Json::Int64(job.created_by);
j["max_solutions"] = job.max_solutions;
j["created_at"] = Json::Int64(job.created_at);
if (job.started_at.has_value()) {
j["started_at"] = Json::Int64(*job.started_at);
} else {
j["started_at"] = Json::nullValue;
}
if (job.finished_at.has_value()) {
j["finished_at"] = Json::Int64(*job.finished_at);
} else {
j["finished_at"] = Json::nullValue;
}
j["updated_at"] = Json::Int64(job.updated_at);
j["runner_pending"] = runner.IsRunning(job.problem_id);
items.append(j);
}
Json::Value running_items(Json::arrayValue);
Json::Value running_problem_ids(Json::arrayValue);
for (const auto& job : running_jobs) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["problem_id"] = Json::Int64(job.problem_id);
j["problem_title"] = job.problem_title;
j["status"] = job.status;
j["progress"] = job.progress;
j["message"] = job.message;
j["updated_at"] = Json::Int64(job.updated_at);
if (job.started_at.has_value()) {
j["started_at"] = Json::Int64(*job.started_at);
} else {
j["started_at"] = Json::nullValue;
}
running_items.append(j);
running_problem_ids.append(Json::Int64(job.problem_id));
}
Json::Value queued_items(Json::arrayValue);
Json::Value queued_problem_ids(Json::arrayValue);
for (const auto& job : queued_jobs) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["problem_id"] = Json::Int64(job.problem_id);
j["problem_title"] = job.problem_title;
j["status"] = job.status;
j["progress"] = job.progress;
j["message"] = job.message;
j["updated_at"] = Json::Int64(job.updated_at);
queued_items.append(j);
queued_problem_ids.append(Json::Int64(job.problem_id));
}
Json::Value payload;
payload["items"] = items;
payload["running_jobs"] = running_items;
payload["queued_jobs"] = queued_items;
payload["running_problem_ids"] = running_problem_ids;
payload["queued_problem_ids"] = queued_problem_ids;
payload["running_count"] = static_cast<int>(running_jobs.size());
payload["queued_count_preview"] = static_cast<int>(queued_jobs.size());
payload["pending_jobs"] = Json::UInt64(runner.PendingCount());
payload["missing_problems"] = workspace.CountProblemsWithoutSolutions();
payload["limit"] = limit;
payload["running_limit"] = running_limit;
payload["queued_limit"] = queued_limit;
cb(JsonOk(payload));
} catch (const std::invalid_argument&) {
cb(JsonError(drogon::k400BadRequest, "invalid query parameter"));
} catch (const std::out_of_range&) {
cb(JsonError(drogon::k400BadRequest, "query parameter out of range"));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MetaController::kbRefreshStatus(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
if (!RequireAdminUserId(req, cb).has_value()) return;
const auto& runner = services::KbImportRunner::Instance();
Json::Value payload;
payload["running"] = runner.IsRunning();
payload["last_command"] = runner.LastCommand();
payload["last_trigger"] = runner.LastTrigger();
if (const auto rc = runner.LastExitCode(); rc.has_value()) {
payload["last_exit_code"] = *rc;
} else {
payload["last_exit_code"] = Json::nullValue;
}
payload["last_started_at"] = Json::Int64(runner.LastStartedAt());
payload["last_finished_at"] = Json::Int64(runner.LastFinishedAt());
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MetaController::triggerKbRefresh(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
if (!RequireAdminUserId(req, cb).has_value()) return;
auto& runner = services::KbImportRunner::Instance();
const bool started = runner.TriggerAsync("manual");
Json::Value payload;
payload["started"] = started;
payload["message"] = started ? "已触发异步资料更新" : "当前已有资料更新任务在运行中";
payload["running"] = runner.IsRunning();
payload["last_command"] = runner.LastCommand();
payload["last_trigger"] = runner.LastTrigger();
if (const auto rc = runner.LastExitCode(); rc.has_value()) {
payload["last_exit_code"] = *rc;
} else {
payload["last_exit_code"] = Json::nullValue;
}
payload["last_started_at"] = Json::Int64(runner.LastStartedAt());
payload["last_finished_at"] = Json::Int64(runner.LastFinishedAt());
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MetaController::triggerMissingSolutions(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto user_id = RequireAdminUserId(req, cb);
if (!user_id.has_value()) return;
int limit = 50000;
int max_solutions = 3;
const auto json = req->getJsonObject();
if (json && (*json).isMember("limit")) {
limit = std::max(1, std::min(200000, (*json)["limit"].asInt()));
}
if (json && (*json).isMember("max_solutions")) {
max_solutions = std::max(1, std::min(5, (*json)["max_solutions"].asInt()));
}
auto& db = csp::AppState::Instance().db();
auto& runner = services::ProblemSolutionRunner::Instance();
const auto summary = runner.TriggerMissingAsync(
db, *user_id, max_solutions, limit);
Json::Value payload;
payload["started"] = true;
payload["missing_total"] = summary.missing_total;
payload["candidate_count"] = summary.candidate_count;
payload["queued_count"] = summary.queued_count;
payload["pending_jobs"] = Json::UInt64(runner.PendingCount());
payload["limit"] = limit;
payload["max_solutions"] = max_solutions;
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MetaController::mcp(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto json = req->getJsonObject();
if (!json) {
auto resp = drogon::HttpResponse::newHttpJsonResponse(
BuildMcpError(Json::nullValue, -32700, "body must be json"));
resp->setStatusCode(drogon::k400BadRequest);
cb(resp);
return;
}
const Json::Value id = (*json).isMember("id") ? (*json)["id"] : Json::nullValue;
const std::string method = (*json).get("method", "").asString();
const Json::Value params = (*json).isMember("params") ? (*json)["params"] : Json::Value();
if (method == "initialize") {
Json::Value result;
result["server_name"] = "csp-platform-mcp";
result["server_version"] = "1.0.0";
result["capabilities"]["tools"] = true;
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
if (method == "tools/list") {
Json::Value result;
result["tools"] = McpToolsList();
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
if (method != "tools/call") {
auto resp = drogon::HttpResponse::newHttpJsonResponse(
BuildMcpError(id, -32601, "method not found"));
cb(resp);
return;
}
const std::string tool_name = params.get("name", "").asString();
const Json::Value args = params.isMember("arguments") ? params["arguments"] : Json::Value();
if (tool_name == "health") {
Json::Value result;
result["ok"] = true;
result["service"] = "csp-backend";
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
if (tool_name == "list_problems") {
services::ProblemQuery q;
q.q = args.get("q", "").asString();
q.page = std::max(1, args.get("page", 1).asInt());
q.page_size = std::max(1, std::min(100, args.get("page_size", 20).asInt()));
q.source_prefix = args.get("source_prefix", "").asString();
q.difficulty = std::max(0, std::min(10, args.get("difficulty", 0).asInt()));
services::ProblemService svc(csp::AppState::Instance().db());
const auto rows = svc.List(q);
Json::Value items(Json::arrayValue);
for (const auto& row : rows.items) {
items.append(domain::ToJson(row));
}
Json::Value result;
result["items"] = items;
result["total_count"] = rows.total_count;
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
if (tool_name == "get_problem") {
const int64_t problem_id = args.get("problem_id", 0).asInt64();
services::ProblemService svc(csp::AppState::Instance().db());
const auto p = svc.GetById(problem_id);
if (!p.has_value()) {
auto resp = drogon::HttpResponse::newHttpJsonResponse(
BuildMcpError(id, -32004, "problem not found"));
cb(resp);
return;
}
Json::Value result = domain::ToJson(*p);
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
if (tool_name == "run_cpp") {
const std::string code = args.get("code", "").asString();
const std::string input = args.get("input", "").asString();
services::SubmissionService svc(csp::AppState::Instance().db());
const auto r = svc.RunOnlyCpp(code, input);
Json::Value result;
result["status"] = domain::ToString(r.status);
result["time_ms"] = r.time_ms;
result["stdout"] = r.stdout_text;
result["stderr"] = r.stderr_text;
result["compile_log"] = r.compile_log;
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
if (tool_name == "generate_cspj_problem") {
const int count = std::max(1, std::min(5, args.get("count", 1).asInt()));
const bool started =
services::ProblemGenRunner::Instance().TriggerAsync("mcp", count);
Json::Value result;
result["started"] = started;
result["count"] = count;
result["running"] = services::ProblemGenRunner::Instance().IsRunning();
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
cb(resp);
return;
}
auto resp = drogon::HttpResponse::newHttpJsonResponse(
BuildMcpError(id, -32602, "unknown tool"));
cb(resp);
} catch (const std::runtime_error& e) {
auto resp = drogon::HttpResponse::newHttpJsonResponse(
BuildMcpError(Json::nullValue, -32000, e.what()));
resp->setStatusCode(drogon::k400BadRequest);
cb(resp);
} catch (const std::exception& e) {
auto resp = drogon::HttpResponse::newHttpJsonResponse(
BuildMcpError(Json::nullValue, -32000, e.what()));
resp->setStatusCode(drogon::k500InternalServerError);
cb(resp);
}
}
} // namespace csp::controllers