274 行
9.0 KiB
C++
274 行
9.0 KiB
C++
#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
|