feat: auto LLM feedback runner + problem link + 5xx retry

- Add SubmissionFeedbackRunner: async background queue for auto LLM feedback
- Enqueue feedback generation after each submission in submitProblem()
- Register runner in main.cc with CSP_FEEDBACK_AUTO_RUN env var
- Add problem_title to GET /api/v1/submissions/{id} response
- Frontend: clickable problem link on submission detail page
- Enhance LLM prompt with richer analysis dimensions
- Add 5xx/connection error retry (max 5 attempts) in Python LLM script

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
这个提交包含在:
cryptocommuniums-afk
2026-02-16 15:13:35 +08:00
父节点 bc2e085c70
当前提交 7860414ae5
修改 37 个文件,包含 312 行新增5343 行删除

查看文件

@@ -6,6 +6,7 @@
#include "csp/services/contest_service.h"
#include "csp/services/problem_service.h"
#include "csp/services/solution_access_service.h"
#include "csp/services/submission_feedback_runner.h"
#include "csp/services/submission_feedback_service.h"
#include "csp/services/submission_service.h"
#include "http_auth.h"
@@ -130,6 +131,10 @@ void SubmissionController::submitProblem(
services::SubmissionService svc(csp::AppState::Instance().db());
auto s = svc.CreateAndJudge(create);
// Auto-enqueue LLM feedback generation in background.
services::SubmissionFeedbackRunner::Instance().Enqueue(s.id);
cb(JsonOk(domain::ToJson(s)));
} catch (const std::invalid_argument&) {
cb(JsonError(drogon::k400BadRequest, "invalid numeric field"));
@@ -187,6 +192,14 @@ void SubmissionController::getSubmission(
Json::Value payload = domain::ToJson(*s);
payload["code"] = s->code;
// Attach problem title for frontend linking.
{
services::ProblemService psvc(csp::AppState::Instance().db());
if (const auto p = psvc.GetById(s->problem_id); p.has_value()) {
payload["problem_title"] = p->title;
}
}
services::SolutionAccessService access_svc(csp::AppState::Instance().db());
const auto stats =
access_svc.QueryUserProblemViewStats(s->user_id, s->problem_id);

查看文件

@@ -6,6 +6,7 @@
#include "csp/services/kb_import_runner.h"
#include "csp/services/problem_gen_runner.h"
#include "csp/services/problem_solution_runner.h"
#include "csp/services/submission_feedback_runner.h"
#include <cstdlib>
#include <filesystem>
@@ -20,6 +21,7 @@ int main(int argc, char** argv) {
csp::services::KbImportRunner::Instance().Configure(db_path);
csp::services::ProblemSolutionRunner::Instance().Configure(db_path);
csp::services::ProblemGenRunner::Instance().Configure(db_path);
csp::services::SubmissionFeedbackRunner::Instance().Configure(db_path);
// Optional seed admin user for dev/test.
{
@@ -48,6 +50,9 @@ int main(int argc, char** argv) {
csp::AppState::Instance().db());
// Auto-generate one CSP-J new problem (RAG + dedupe) on startup by default.
csp::services::ProblemGenRunner::Instance().AutoStartIfEnabled();
// Auto-queue submission feedback generation for submissions without feedback.
csp::services::SubmissionFeedbackRunner::Instance().AutoStartIfEnabled(
csp::AppState::Instance().db());
// CORS (dev-friendly). In production, prefer reverse proxy same-origin.
drogon::app().registerPreRoutingAdvice([](const drogon::HttpRequestPtr& req,

查看文件

@@ -0,0 +1,180 @@
#include "csp/services/submission_feedback_runner.h"
#include "csp/services/problem_service.h"
#include "csp/services/submission_feedback_service.h"
#include "csp/services/submission_service.h"
#include <drogon/drogon.h>
#include <sqlite3.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdlib>
#include <thread>
#include <vector>
namespace csp::services {
namespace {
bool EnvBool(const char* key, bool default_value) {
const char* raw = std::getenv(key);
if (!raw) return default_value;
std::string val(raw);
for (auto& c : val)
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
if (val == "1" || val == "true" || val == "yes" || val == "on") return true;
if (val == "0" || val == "false" || val == "no" || val == "off") return false;
return default_value;
}
int EnvInt(const char* key, int default_value) {
const char* raw = std::getenv(key);
if (!raw) return default_value;
try {
return std::stoi(raw);
} catch (...) {
return default_value;
}
}
} // namespace
SubmissionFeedbackRunner& SubmissionFeedbackRunner::Instance() {
static SubmissionFeedbackRunner inst;
return inst;
}
void SubmissionFeedbackRunner::Configure(std::string db_path) {
std::lock_guard<std::mutex> lock(mu_);
db_path_ = std::move(db_path);
RecoverPendingLocked();
StartWorkerIfNeededLocked();
}
bool SubmissionFeedbackRunner::Enqueue(int64_t submission_id) {
std::lock_guard<std::mutex> lock(mu_);
if (db_path_.empty()) return false;
queue_.push_back(submission_id);
++pending_jobs_;
StartWorkerIfNeededLocked();
return true;
}
void SubmissionFeedbackRunner::AutoStartIfEnabled(db::SqliteDb& db) {
if (!EnvBool("CSP_FEEDBACK_AUTO_RUN", false)) {
LOG_INFO << "submission feedback auto-run disabled";
return;
}
const int limit =
std::max(1, std::min(10000, EnvInt("CSP_FEEDBACK_AUTO_LIMIT", 500)));
// Find submissions without feedback.
sqlite3_stmt* stmt = nullptr;
const char* sql =
"SELECT s.id FROM submissions s "
"LEFT JOIN submission_feedback f ON f.submission_id=s.id "
"WHERE f.id IS NULL "
"ORDER BY s.id DESC LIMIT ?";
if (sqlite3_prepare_v2(db.raw(), sql, -1, &stmt, nullptr) != SQLITE_OK) {
LOG_ERROR << "feedback runner: failed to query pending submissions";
return;
}
sqlite3_bind_int(stmt, 1, limit);
int queued = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const int64_t sid = sqlite3_column_int64(stmt, 0);
Enqueue(sid);
++queued;
}
sqlite3_finalize(stmt);
LOG_INFO << "submission feedback auto-run queued=" << queued
<< ", pending=" << PendingCount();
}
size_t SubmissionFeedbackRunner::PendingCount() const {
std::lock_guard<std::mutex> lock(mu_);
return pending_jobs_;
}
void SubmissionFeedbackRunner::StartWorkerIfNeededLocked() {
if (worker_running_ || queue_.empty()) return;
worker_running_ = true;
std::thread([this]() { WorkerLoop(); }).detach();
}
void SubmissionFeedbackRunner::WorkerLoop() {
while (true) {
int64_t submission_id = 0;
std::string db_path;
{
std::lock_guard<std::mutex> lock(mu_);
if (queue_.empty()) {
worker_running_ = false;
return;
}
submission_id = queue_.front();
queue_.pop_front();
db_path = db_path_;
}
try {
db::SqliteDb db = db::SqliteDb::OpenFile(db_path);
SubmissionFeedbackService feedback_svc(db);
// Skip if feedback already exists.
if (feedback_svc.GetBySubmissionId(submission_id).has_value()) {
std::lock_guard<std::mutex> lock(mu_);
if (pending_jobs_ > 0) --pending_jobs_;
continue;
}
SubmissionService sub_svc(db);
const auto submission = sub_svc.GetById(submission_id);
if (!submission.has_value()) {
LOG_WARN << "feedback runner: submission " << submission_id
<< " not found, skipping";
std::lock_guard<std::mutex> lock(mu_);
if (pending_jobs_ > 0) --pending_jobs_;
continue;
}
ProblemService prob_svc(db);
const auto problem = prob_svc.GetById(submission->problem_id);
if (!problem.has_value()) {
LOG_WARN << "feedback runner: problem " << submission->problem_id
<< " not found for submission " << submission_id;
std::lock_guard<std::mutex> lock(mu_);
if (pending_jobs_ > 0) --pending_jobs_;
continue;
}
feedback_svc.GenerateAndSave(*submission, *problem, false);
LOG_INFO << "feedback runner: generated feedback for submission "
<< submission_id;
} catch (const std::exception& e) {
LOG_ERROR << "feedback runner: submission " << submission_id
<< " failed: " << e.what();
}
{
std::lock_guard<std::mutex> lock(mu_);
if (pending_jobs_ > 0) --pending_jobs_;
}
// Small delay between jobs to avoid overwhelming LLM API / SQLite.
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
void SubmissionFeedbackRunner::RecoverPendingLocked() {
if (recovered_ || db_path_.empty()) return;
recovered_ = true;
// Recovery is handled by AutoStartIfEnabled; no separate DB state to recover.
}
} // namespace csp::services