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,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