#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 #include #include #include 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 RequireAdminUserId( const drogon::HttpRequestPtr& req, std::function& 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&& cb) { auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildOpenApiSpec()); resp->setStatusCode(drogon::k200OK); cb(resp); } void MetaController::backendLogs( const drogon::HttpRequestPtr& req, std::function&& 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(running_jobs.size()); payload["queued_count_preview"] = static_cast(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&& 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&& 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&& 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&& 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