feat: add daily tasks and fix /admin139 admin entry
这个提交包含在:
@@ -3,9 +3,13 @@
|
||||
#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 "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
@@ -25,6 +29,24 @@ drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode 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 BuildOpenApiSpec() {
|
||||
Json::Value root;
|
||||
root["openapi"] = "3.1.0";
|
||||
@@ -54,11 +76,28 @@ Json::Value BuildOpenApiSpec() {
|
||||
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/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 入口";
|
||||
|
||||
@@ -130,6 +169,205 @@ void MetaController::openapi(
|
||||
cb(resp);
|
||||
}
|
||||
|
||||
void MetaController::backendLogs(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
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<int>(running_jobs.size());
|
||||
payload["queued_count_preview"] = static_cast<int>(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<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
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<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
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;
|
||||
}
|
||||
|
||||
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<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
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 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<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户