feat: rebuild CSP practice workflow, UX and automation

这个提交包含在:
Codex CLI
2026-02-13 15:49:05 +08:00
父节点 d33deed4c5
当前提交 e2ab522b78
修改 105 个文件,包含 15669 行新增428 行删除

查看文件

@@ -0,0 +1,133 @@
#include "csp/controllers/contest_controller.h"
#include "csp/app_state.h"
#include "csp/domain/json.h"
#include "csp/services/contest_service.h"
#include "http_auth.h"
#include <exception>
#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;
}
} // namespace
void ContestController::list(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
services::ContestService svc(csp::AppState::Instance().db());
const auto contests = svc.ListContests();
Json::Value arr(Json::arrayValue);
for (const auto& c : contests) arr.append(domain::ToJson(c));
cb(JsonOk(arr));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ContestController::getById(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id) {
try {
services::ContestService svc(csp::AppState::Instance().db());
const auto contest = svc.GetContest(contest_id);
if (!contest.has_value()) {
cb(JsonError(drogon::k404NotFound, "contest not found"));
return;
}
Json::Value data;
data["contest"] = domain::ToJson(*contest);
Json::Value problems(Json::arrayValue);
for (const auto& p : svc.ListContestProblems(contest_id)) {
problems.append(domain::ToJson(p));
}
data["problems"] = problems;
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (user_id.has_value()) {
data["registered"] = svc.IsRegistered(contest_id, *user_id);
}
cb(JsonOk(data));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ContestController::registerForContest(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id) {
try {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
cb(JsonError(drogon::k401Unauthorized, auth_error));
return;
}
services::ContestService svc(csp::AppState::Instance().db());
if (!svc.GetContest(contest_id).has_value()) {
cb(JsonError(drogon::k404NotFound, "contest not found"));
return;
}
svc.Register(contest_id, *user_id);
Json::Value data;
data["contest_id"] = Json::Int64(contest_id);
data["user_id"] = Json::Int64(*user_id);
data["registered"] = true;
cb(JsonOk(data));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ContestController::leaderboard(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id) {
try {
services::ContestService svc(csp::AppState::Instance().db());
if (!svc.GetContest(contest_id).has_value()) {
cb(JsonError(drogon::k404NotFound, "contest not found"));
return;
}
const auto rows = svc.Leaderboard(contest_id);
Json::Value arr(Json::arrayValue);
for (const auto& r : rows) arr.append(domain::ToJson(r));
cb(JsonOk(arr));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,37 @@
#pragma once
#include "csp/app_state.h"
#include "csp/services/auth_service.h"
#include <drogon/HttpRequest.h>
#include <optional>
#include <string>
namespace csp::controllers {
inline std::optional<int64_t> GetAuthedUserId(const drogon::HttpRequestPtr& req,
std::string& error) {
const std::string authz = req->getHeader("Authorization");
const std::string prefix = "Bearer ";
if (authz.rfind(prefix, 0) != 0) {
error = "missing Authorization: Bearer <token>";
return std::nullopt;
}
const std::string token = authz.substr(prefix.size());
if (token.empty()) {
error = "empty bearer token";
return std::nullopt;
}
services::AuthService auth(csp::AppState::Instance().db());
const auto user_id = auth.VerifyToken(token);
if (!user_id.has_value()) {
error = "invalid or expired token";
return std::nullopt;
}
return static_cast<int64_t>(*user_id);
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,190 @@
#include "csp/controllers/import_controller.h"
#include "csp/app_state.h"
#include "csp/services/import_runner.h"
#include "csp/services/import_service.h"
#include <algorithm>
#include <exception>
#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));
}
Json::Value ToJson(const services::ImportJob& job) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["status"] = job.status;
j["trigger"] = job.trigger;
j["total_count"] = job.total_count;
j["processed_count"] = job.processed_count;
j["success_count"] = job.success_count;
j["failed_count"] = job.failed_count;
j["options_json"] = job.options_json;
j["last_error"] = job.last_error;
j["started_at"] = Json::Int64(job.started_at);
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["created_at"] = Json::Int64(job.created_at);
return j;
}
Json::Value ToJson(const services::ImportJobItem& item) {
Json::Value j;
j["id"] = Json::Int64(item.id);
j["job_id"] = Json::Int64(item.job_id);
j["source_path"] = item.source_path;
j["status"] = item.status;
j["title"] = item.title;
j["difficulty"] = item.difficulty;
if (item.problem_id.has_value()) {
j["problem_id"] = Json::Int64(*item.problem_id);
} else {
j["problem_id"] = Json::nullValue;
}
j["error_text"] = item.error_text;
if (item.started_at.has_value()) {
j["started_at"] = Json::Int64(*item.started_at);
} else {
j["started_at"] = Json::nullValue;
}
if (item.finished_at.has_value()) {
j["finished_at"] = Json::Int64(*item.finished_at);
} else {
j["finished_at"] = Json::nullValue;
}
j["updated_at"] = Json::Int64(item.updated_at);
j["created_at"] = Json::Int64(item.created_at);
return j;
}
} // namespace
void ImportController::latestJob(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
services::ImportService svc(csp::AppState::Instance().db());
Json::Value payload;
const auto job = svc.GetLatestJob();
payload["runner_running"] = services::ImportRunner::Instance().IsRunning();
if (job.has_value()) {
payload["job"] = ToJson(*job);
} else {
payload["job"] = Json::nullValue;
}
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ImportController::jobById(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t job_id) {
try {
services::ImportService svc(csp::AppState::Instance().db());
const auto job = svc.GetById(job_id);
if (!job.has_value()) {
cb(JsonError(drogon::k404NotFound, "import job not found"));
return;
}
Json::Value payload = ToJson(*job);
payload["runner_running"] = services::ImportRunner::Instance().IsRunning();
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ImportController::jobItems(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t job_id) {
try {
services::ImportJobItemQuery query;
query.status = req->getParameter("status");
query.page = ParsePositiveInt(req->getParameter("page"), 1, 1, 100000);
query.page_size = ParsePositiveInt(req->getParameter("page_size"), 100, 1, 500);
services::ImportService svc(csp::AppState::Instance().db());
const auto rows = svc.ListItems(job_id, query);
Json::Value arr(Json::arrayValue);
for (const auto& row : rows) arr.append(ToJson(row));
Json::Value payload;
payload["items"] = arr;
payload["page"] = query.page;
payload["page_size"] = query.page_size;
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 ImportController::runJob(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
services::ImportRunOptions opts;
const auto json = req->getJsonObject();
if (json) {
opts.clear_all_problems =
(*json).isMember("clear_all_problems") &&
(*json)["clear_all_problems"].asBool();
}
const bool started =
services::ImportRunner::Instance().TriggerAsync("manual", opts);
if (!started) {
cb(JsonError(drogon::k409Conflict, "import job already running"));
return;
}
Json::Value payload;
payload["started"] = true;
payload["running"] = true;
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,81 @@
#include "csp/controllers/kb_controller.h"
#include "csp/app_state.h"
#include "csp/domain/json.h"
#include "csp/services/kb_service.h"
#include <exception>
#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;
}
} // namespace
void KbController::listArticles(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
services::KbService svc(csp::AppState::Instance().db());
const auto rows = svc.ListArticles();
Json::Value arr(Json::arrayValue);
for (const auto& a : rows) arr.append(domain::ToJson(a));
cb(JsonOk(arr));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void KbController::getArticle(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
std::string slug) {
try {
services::KbService svc(csp::AppState::Instance().db());
const auto detail = svc.GetBySlug(slug);
if (!detail.has_value()) {
cb(JsonError(drogon::k404NotFound, "article not found"));
return;
}
Json::Value data;
data["article"] = domain::ToJson(detail->article);
Json::Value rel(Json::arrayValue);
for (const auto& p : detail->related_problems) {
Json::Value item;
item["problem_id"] = Json::Int64(p.first);
item["title"] = p.second;
rel.append(item);
}
data["related_problems"] = rel;
cb(JsonOk(data));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,57 @@
#include "csp/controllers/leaderboard_controller.h"
#include "csp/app_state.h"
#include "csp/domain/json.h"
#include "csp/services/user_service.h"
#include <algorithm>
#include <exception>
#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 JsonOkArray(const Json::Value& arr) {
Json::Value j;
j["ok"] = true;
j["data"] = arr;
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
resp->setStatusCode(drogon::k200OK);
return resp;
}
} // namespace
void LeaderboardController::global(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
int limit = 100;
const auto limit_str = req->getParameter("limit");
if (!limit_str.empty()) {
limit = std::max(1, std::min(500, std::stoi(limit_str)));
}
services::UserService users(csp::AppState::Instance().db());
const auto rows = users.GlobalLeaderboard(limit);
Json::Value arr(Json::arrayValue);
for (const auto& r : rows) arr.append(domain::ToJson(r));
cb(JsonOkArray(arr));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,146 @@
#include "csp/controllers/me_controller.h"
#include "csp/app_state.h"
#include "csp/domain/json.h"
#include "csp/services/user_service.h"
#include "csp/services/wrong_book_service.h"
#include "http_auth.h"
#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;
}
std::optional<int64_t> RequireAuth(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;
}
return user_id;
}
} // namespace
void MeController::profile(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
services::UserService users(csp::AppState::Instance().db());
const auto user = users.GetById(*user_id);
if (!user.has_value()) {
cb(JsonError(drogon::k404NotFound, "user not found"));
return;
}
cb(JsonOk(domain::ToPublicJson(*user)));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::listWrongBook(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
services::WrongBookService wrong_book(csp::AppState::Instance().db());
const auto rows = wrong_book.ListByUser(*user_id);
Json::Value arr(Json::arrayValue);
for (const auto& row : rows) {
Json::Value item = domain::ToJson(row.item);
item["problem_title"] = row.problem_title;
arr.append(item);
}
cb(JsonOk(arr));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::upsertWrongBookNote(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
const auto json = req->getJsonObject();
if (!json) {
cb(JsonError(drogon::k400BadRequest, "body must be json"));
return;
}
const std::string note = (*json).get("note", "").asString();
if (note.size() > 4000) {
cb(JsonError(drogon::k400BadRequest, "note too long"));
return;
}
services::WrongBookService wrong_book(csp::AppState::Instance().db());
wrong_book.UpsertNote(*user_id, problem_id, note);
Json::Value data;
data["user_id"] = Json::Int64(*user_id);
data["problem_id"] = Json::Int64(problem_id);
data["note"] = note;
cb(JsonOk(data));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::deleteWrongBookItem(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
services::WrongBookService wrong_book(csp::AppState::Instance().db());
wrong_book.Remove(*user_id, problem_id);
Json::Value data;
data["user_id"] = Json::Int64(*user_id);
data["problem_id"] = Json::Int64(problem_id);
data["deleted"] = true;
cb(JsonOk(data));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,273 @@
#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/problem_gen_runner.h"
#include "csp/services/problem_service.h"
#include "csp/services/submission_service.h"
#include <algorithm>
#include <exception>
#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;
}
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/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/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::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

查看文件

@@ -0,0 +1,299 @@
#include "csp/controllers/problem_controller.h"
#include "csp/app_state.h"
#include "csp/domain/json.h"
#include "csp/services/problem_service.h"
#include "csp/services/problem_solution_runner.h"
#include "csp/services/problem_workspace_service.h"
#include "http_auth.h"
#include <algorithm>
#include <cctype>
#include <exception>
#include <sstream>
#include <string>
#include <vector>
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::vector<std::string> ParseCsv(const std::string& raw) {
auto trim = [](std::string s) {
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
s.erase(s.begin());
}
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
s.pop_back();
}
return s;
};
std::vector<std::string> out;
std::stringstream ss(raw);
std::string item;
while (std::getline(ss, item, ',')) {
item = trim(item);
if (item.empty()) continue;
out.push_back(item);
}
return out;
}
} // namespace
void ProblemController::list(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
services::ProblemQuery q;
q.q = req->getParameter("q");
q.tag = req->getParameter("tag");
q.tags = ParseCsv(req->getParameter("tags"));
q.source_prefix = req->getParameter("source_prefix");
q.difficulty = ParsePositiveInt(req->getParameter("difficulty"), 0, 0, 10);
q.page = ParsePositiveInt(req->getParameter("page"), 1, 1, 100000);
q.page_size = ParsePositiveInt(req->getParameter("page_size"), 20, 1, 200);
q.order_by = req->getParameter("order_by");
q.order = req->getParameter("order");
services::ProblemService svc(csp::AppState::Instance().db());
const auto result = svc.List(q);
Json::Value arr(Json::arrayValue);
for (const auto& p : result.items) arr.append(domain::ToJson(p));
Json::Value payload;
payload["items"] = arr;
payload["total_count"] = result.total_count;
payload["page"] = q.page;
payload["page_size"] = q.page_size;
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 ProblemController::getById(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
services::ProblemService svc(csp::AppState::Instance().db());
const auto p = svc.GetById(problem_id);
if (!p.has_value()) {
cb(JsonError(drogon::k404NotFound, "problem not found"));
return;
}
cb(JsonOk(domain::ToJson(*p)));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ProblemController::getDraft(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
cb(JsonError(drogon::k401Unauthorized, auth_error));
return;
}
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
const auto draft = svc.GetDraft(*user_id, problem_id);
if (!draft.has_value()) {
cb(JsonError(drogon::k404NotFound, "draft not found"));
return;
}
Json::Value payload;
payload["language"] = draft->language;
payload["code"] = draft->code;
payload["stdin"] = draft->stdin_text;
payload["updated_at"] = Json::Int64(draft->updated_at);
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ProblemController::saveDraft(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
cb(JsonError(drogon::k401Unauthorized, auth_error));
return;
}
const auto json = req->getJsonObject();
if (!json) {
cb(JsonError(drogon::k400BadRequest, "body must be json"));
return;
}
const std::string language = (*json).get("language", "cpp").asString();
const std::string code = (*json).get("code", "").asString();
const std::string stdin_text = (*json).get("stdin", "").asString();
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
if (!svc.ProblemExists(problem_id)) {
cb(JsonError(drogon::k404NotFound, "problem not found"));
return;
}
svc.SaveDraft(*user_id, problem_id, language, code, stdin_text);
Json::Value payload;
payload["saved"] = true;
cb(JsonOk(payload));
} catch (const std::runtime_error& e) {
cb(JsonError(drogon::k400BadRequest, e.what()));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ProblemController::listSolutions(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
if (!svc.ProblemExists(problem_id)) {
cb(JsonError(drogon::k404NotFound, "problem not found"));
return;
}
const auto rows = svc.ListSolutions(problem_id);
const auto latest_job = svc.GetLatestSolutionJob(problem_id);
Json::Value arr(Json::arrayValue);
for (const auto& item : rows) {
Json::Value j;
j["id"] = Json::Int64(item.id);
j["problem_id"] = Json::Int64(item.problem_id);
j["variant"] = item.variant;
j["title"] = item.title;
j["idea_md"] = item.idea_md;
j["explanation_md"] = item.explanation_md;
j["code_cpp"] = item.code_cpp;
j["complexity"] = item.complexity;
j["tags_json"] = item.tags_json;
j["source"] = item.source;
j["created_at"] = Json::Int64(item.created_at);
j["updated_at"] = Json::Int64(item.updated_at);
arr.append(j);
}
Json::Value payload;
payload["items"] = arr;
payload["runner_running"] =
services::ProblemSolutionRunner::Instance().IsRunning(problem_id);
if (latest_job.has_value()) {
Json::Value j;
j["id"] = Json::Int64(latest_job->id);
j["problem_id"] = Json::Int64(latest_job->problem_id);
j["status"] = latest_job->status;
j["progress"] = latest_job->progress;
j["message"] = latest_job->message;
j["created_at"] = Json::Int64(latest_job->created_at);
if (latest_job->started_at.has_value()) {
j["started_at"] = Json::Int64(*latest_job->started_at);
} else {
j["started_at"] = Json::nullValue;
}
if (latest_job->finished_at.has_value()) {
j["finished_at"] = Json::Int64(*latest_job->finished_at);
} else {
j["finished_at"] = Json::nullValue;
}
payload["latest_job"] = j;
} else {
payload["latest_job"] = Json::nullValue;
}
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void ProblemController::generateSolutions(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
cb(JsonError(drogon::k401Unauthorized, auth_error));
return;
}
int max_solutions = 3;
const auto json = req->getJsonObject();
if (json && (*json).isMember("max_solutions")) {
max_solutions = std::max(1, std::min(5, (*json)["max_solutions"].asInt()));
}
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
if (!svc.ProblemExists(problem_id)) {
cb(JsonError(drogon::k404NotFound, "problem not found"));
return;
}
const int64_t job_id = svc.CreateSolutionJob(problem_id, *user_id, max_solutions);
const bool started = services::ProblemSolutionRunner::Instance().TriggerAsync(
problem_id, job_id, max_solutions);
if (!started) {
cb(JsonError(drogon::k409Conflict, "solution generation is already running"));
return;
}
Json::Value payload;
payload["started"] = true;
payload["job_id"] = Json::Int64(job_id);
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,80 @@
#include "csp/controllers/problem_gen_controller.h"
#include "csp/services/problem_gen_runner.h"
#include <exception>
#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;
}
} // namespace
void ProblemGenController::status(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
auto& runner = services::ProblemGenRunner::Instance();
Json::Value payload;
payload["running"] = runner.IsRunning();
payload["last_command"] = runner.LastCommand();
if (runner.LastExitCode().has_value()) {
payload["last_exit_code"] = *runner.LastExitCode();
} 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 ProblemGenController::run(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
int count = 1;
const auto json = req->getJsonObject();
if (json && (*json).isMember("count")) {
count = (*json)["count"].asInt();
}
auto& runner = services::ProblemGenRunner::Instance();
const bool started = runner.TriggerAsync("manual", count);
if (!started) {
cb(JsonError(drogon::k409Conflict, "problem generation job already running"));
return;
}
Json::Value payload;
payload["started"] = true;
payload["count"] = count;
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -0,0 +1,192 @@
#include "csp/controllers/submission_controller.h"
#include "csp/app_state.h"
#include "csp/domain/enum_strings.h"
#include "csp/domain/json.h"
#include "csp/services/contest_service.h"
#include "csp/services/submission_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 ParseClampedInt(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> ParseOptionalInt64(const std::string& s) {
if (s.empty()) return std::nullopt;
return std::stoll(s);
}
} // namespace
void SubmissionController::submitProblem(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id) {
try {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
cb(JsonError(drogon::k401Unauthorized, auth_error));
return;
}
const auto json = req->getJsonObject();
if (!json) {
cb(JsonError(drogon::k400BadRequest, "body must be json"));
return;
}
services::SubmissionCreateRequest create;
create.user_id = *user_id;
create.problem_id = problem_id;
create.language = (*json).get("language", "cpp").asString();
create.code = (*json).get("code", "").asString();
if ((*json).isMember("contest_id") && !(*json)["contest_id"].isNull()) {
create.contest_id = (*json)["contest_id"].asInt64();
services::ContestService contest(csp::AppState::Instance().db());
if (!contest.GetContest(*create.contest_id).has_value()) {
cb(JsonError(drogon::k400BadRequest, "contest not found"));
return;
}
if (!contest.ContainsProblem(*create.contest_id, problem_id)) {
cb(JsonError(drogon::k400BadRequest, "problem not in contest"));
return;
}
if (!contest.IsRegistered(*create.contest_id, *user_id)) {
cb(JsonError(drogon::k403Forbidden, "user is not registered for contest"));
return;
}
if (!contest.IsRunning(*create.contest_id)) {
cb(JsonError(drogon::k403Forbidden, "contest is not running"));
return;
}
}
services::SubmissionService svc(csp::AppState::Instance().db());
auto s = svc.CreateAndJudge(create);
cb(JsonOk(domain::ToJson(s)));
} catch (const std::invalid_argument&) {
cb(JsonError(drogon::k400BadRequest, "invalid numeric field"));
} catch (const std::out_of_range&) {
cb(JsonError(drogon::k400BadRequest, "numeric field out of range"));
} catch (const std::runtime_error& e) {
cb(JsonError(drogon::k400BadRequest, e.what()));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void SubmissionController::listSubmissions(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto user_id = ParseOptionalInt64(req->getParameter("user_id"));
const auto problem_id = ParseOptionalInt64(req->getParameter("problem_id"));
const auto contest_id = ParseOptionalInt64(req->getParameter("contest_id"));
const int page = ParseClampedInt(req->getParameter("page"), 1, 1, 100000);
const int page_size =
ParseClampedInt(req->getParameter("page_size"), 20, 1, 200);
services::SubmissionService svc(csp::AppState::Instance().db());
const auto rows = svc.List(user_id, problem_id, contest_id, page, page_size);
Json::Value arr(Json::arrayValue);
for (const auto& s : rows) arr.append(domain::ToJson(s));
Json::Value payload;
payload["items"] = arr;
payload["page"] = page;
payload["page_size"] = page_size;
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 SubmissionController::getSubmission(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t submission_id) {
try {
services::SubmissionService svc(csp::AppState::Instance().db());
const auto s = svc.GetById(submission_id);
if (!s.has_value()) {
cb(JsonError(drogon::k404NotFound, "submission not found"));
return;
}
cb(JsonOk(domain::ToJson(*s)));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void SubmissionController::runCpp(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto json = req->getJsonObject();
if (!json) {
cb(JsonError(drogon::k400BadRequest, "body must be json"));
return;
}
const std::string code = (*json).get("code", "").asString();
const std::string input = (*json).get("input", "").asString();
services::SubmissionService svc(csp::AppState::Instance().db());
const auto r = svc.RunOnlyCpp(code, input);
Json::Value payload;
payload["status"] = domain::ToString(r.status);
payload["time_ms"] = r.time_ms;
payload["stdout"] = r.stdout_text;
payload["stderr"] = r.stderr_text;
payload["compile_log"] = r.compile_log;
cb(JsonOk(payload));
} catch (const std::runtime_error& e) {
cb(JsonError(drogon::k400BadRequest, e.what()));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers