feat: rebuild CSP practice workflow, UX and automation
这个提交包含在:
@@ -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
|
||||
在新工单中引用
屏蔽一个用户