Files

1 line
32 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "boylees/ai-oj", "files": {"backend/internal/ai/handler.go": {"cc477e36a8df7af58777dfc84351841e87ea980b": {"t": 1785745638.069457, "n": 5289, "h": "926ece549c13a45f", "c": "package ai\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/boylees/ai-oj/internal/common/database\"\n\t\"github.com/boylees/ai-oj/internal/common/response\"\n)\n\ntype Handler struct {\n\tsvc *Service\n}\n\nfunc NewHandler(svc *Service) *Handler {\n\treturn &Handler{svc: svc}\n}\n\nfunc (h *Handler) GenerateTestCases(c *gin.Context) {\n\tif !h.checkAPIKey(c) {\n\t\treturn\n\t}\n\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tadminID := c.GetUint(\"user_id\")\n\n\tcases, cached, err := h.svc.GenerateTestCases(uint(problemID), adminID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"AI 调用失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tif cached {\n\t\tresponse.OK(c, gin.H{\n\t\t\t\"cached\": true,\n\t\t\t\"cases\": cases,\n\t\t\t\"msg\": \"该题目已有 AI 生成的测试用例,直接返回缓存结果\",\n\t\t})\n\t\treturn\n\t}\n\n\tisParseError := len(cases) == 1 && cases[0].Input == \"__PARSE_ERROR__\"\n\trawContent := \"\"\n\tif isParseError {\n\t\trawContent = cases[0].ExpectedOutput\n\t}\n\tresponse.OK(c, gin.H{\n\t\t\"cached\": false,\n\t\t\"cases\": cases,\n\t\t\"parse_error\": isParseError,\n\t\t\"raw_content\": rawContent,\n\t})\n}\n\nfunc (h *Handler) GenerateSolution(c *gin.Context) {\n\tif !h.checkAPIKey(c) {\n\t\treturn\n\t}\n\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tadminID := c.GetUint(\"user_id\")\n\n\tsolution, cached, err := h.svc.GenerateSolution(uint(problemID), adminID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"AI 调用失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.OK(c, gin.H{\n\t\t\"cached\": cached,\n\t\t\"solution\": solution,\n\t})\n}\n\nfunc (h *Handler) GenerateEditorial(c *gin.Context) {\n\tif !h.checkAPIKey(c) {\n\t\treturn\n\t}\n\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tadminID := c.GetUint(\"user_id\")\n\n\teditorial, cached, err := h.svc.GenerateEditorial(uint(problemID), adminID, true)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"AI 调用失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.OK(c, gin.H{\n\t\t\"cached\": cached,\n\t\t\"editorial\": editorial,\n\t})\n}\n\nfunc (h *Handler) AdoptTestCases(c *gin.Context) {\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tvar input struct {\n\t\tCases []GeneratedTestCase `json:\"cases\" binding:\"required\"`\n\t}\n\tif err := c.ShouldBindJSON(&input); err != nil {\n\t\tresponse.BadRequest(c, \"参数格式错误\")\n\t\treturn\n\t}\n\n\tcount, err := h.svc.AdoptTestCases(uint(problemID), input.Cases)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"保存测试用例失败\")\n\t\treturn\n\t}\n\n\tresponse.OK(c, gin.H{\"adopted\": count})\n}\n\nfunc (h *Handler) ParseProblem(c *gin.Context) {\n\tif !h.checkAPIKey(c) {\n\t\treturn\n\t}\n\n\tvar input struct {\n\t\tRawText string `json:\"raw_text\" binding:\"required\"`\n\t}\n\tif err := c.ShouldBindJSON(&input); err != nil {\n\t\tresponse.BadRequest(c, \"请输入题目文本\")\n\t\treturn\n\t}\n\n\tparsed, err := h.svc.ParseProblem(input.RawText)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"AI 解析失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.OK(c, parsed)\n}\n\nfunc (h *Handler) GenerateTestInputs(c *gin.Context) {\n\tif !h.checkAPIKey(c) {\n\t\treturn\n\t}\n\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tadminID := c.GetUint(\"user_id\")\n\n\tinputs, err := h.svc.GenerateTestInputs(uint(problemID), adminID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"AI 生成测试输入失败: \"+err.Error())\n\t\treturn\n\t}\n\n\tresponse.OK(c, gin.H{\"count\": len(inputs), \"inputs\": inputs})\n}\n\nfunc (h *Handler) GenerateWrapper(c *gin.Context) {\n\tif !h.checkAPIKey(c) {\n\t\treturn\n\t}\n\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tadminID := c.GetUint(\"user_id\")\n\n\twrapper, err := h.svc.GenerateWrapper(uint(problemID), adminID)\n\tif err != nil {\n\t\tresponse.InternalError(c, \"AI 生成包裹代码失败: \"+err.Error())\n\t\treturn\n\t}\n\n\t// Save wrapper as reference solution\n\th.svc.db.Model(&database.Problem{}).Where(\"id = ?\", uint(problemID)).\n\t\tUpdate(\"reference_solution\", wrapper)\n\n\tresponse.OK(c, gin.H{\"wrapper\": wrapper})\n}\n\n// SelfVerify runs the reference solution through the sandbox against all test cases\n// and returns per-case verdict comparison between expected and actual output.\nfunc (h *Handler) SelfVerify(c *gin.Context) {\n\tproblemID, _ := strconv.ParseUint(c.Param(\"id\"), 10, 64)\n\tresult, err := h.svc.SelfVerify(uint(problemID))\n\tif err != nil {\n\t\tresponse.InternalError(c, \"自校验失败: \"+err.Error())\n\t\treturn\n\t}\n\tresponse.OK(c, result)\n}\n\nfunc (h *Handler) GetAILogs(c *gin.Context) {\n\tvar logs []struct {\n\t\tID uint `json:\"id\"`\n\t\tProblemID uint `json:\"problem_id\"`\n\t\tType string `json:\"type\"`\n\t\tPromptTokens int `json:\"prompt_tokens\"`\n\t\tCompletionTokens int `json:\"completion_tokens\"`\n\t\tStatus string `json:\"status\"`\n\t\tCreatedAt int64 `json:\"created_at\"`\n\t}\n\th.svc.db.Table(\"ai_generation_logs\").\n\t\tSelect(\"id, problem_id, type, prompt_tokens, completion_tokens, status, created_at\").\n\t\tOrder(\"created_at DESC\").Limit(50).Scan(&logs)\n\n\tif logs == nil {\n\t\tlogs = []struct {\n\t\t\tID uint `json:\"id\"`\n\t\t\tProblemID uint `json:\"problem_id\"`\n\t\t\tType string `json:\"type\"`\n\t\t\tPromptTokens int `json:\"prompt_tokens\"`\n\t\t\tCompletionTokens int `json:\"completion_tokens\"`\n\t\t\tStatus string `json:\"status\"`\n\t\t\tCreatedAt int64 `json:\"created_at\"`\n\t\t}{}\n\t}\n\tresponse.OK(c, gin.H{\"logs\": logs})\n}\n\nfunc (h *Handler) checkAPIKey(c *gin.Context) bool {\n\tif h.svc.apiKey == \"\" {\n\t\tresponse.Error(c, 400, \"未配置 DEEPSEEK_API_KEY,请在 .env 中设置后重启服务\")\n\t\treturn false\n\t}\n\treturn true\n}\n"}}, "backend/internal/ai/service.go": {"cc477e36a8df7af58777dfc84351841e87ea980b": {"t": 1785746083.1911438, "n": 18962, "h": "01b61bbf75e1c484", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/boylees/ai-oj/internal/common/database\"\n\t\"github.com/boylees/ai-oj/internal/judge\"\n)\n\nconst (\n\tdefaultBaseURL = \"https://api.deepseek.com\"\n\tdefaultModel = \"deepseek-chat\"\n\tdefaultTimeout = 30 * time.Second\n\tdefaultMaxTokens = 4096\n)\n\ntype Service struct {\n\tdb *gorm.DB\n\tapiKey string\n\tbaseURL string\n\tmodel string\n\tclient *http.Client\n}\n\nfunc NewService(db *gorm.DB, apiKey, baseURL string) *Service {\n\tif baseURL == \"\" {\n\t\tbaseURL = defaultBaseURL\n\t}\n\treturn &Service{\n\t\tdb: db,\n\t\tapiKey: apiKey,\n\t\tbaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\tmodel: defaultModel,\n\t\tclient: &http.Client{Timeout: defaultTimeout},\n\t}\n}\n\n// ── Request / Response types ──\n\ntype chatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype chatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []chatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens\"`\n}\n\ntype chatChoice struct {\n\tMessage chatMessage `json:\"message\"`\n}\n\ntype chatUsage struct {\n\tPromptTokens int `json:\"prompt_tokens\"`\n\tCompletionTokens int `json:\"completion_tokens\"`\n}\n\ntype chatResponse struct {\n\tChoices []chatChoice `json:\"choices\"`\n\tUsage chatUsage `json:\"usage\"`\n}\n\ntype GeneratedTestCase struct {\n\tInput string `json:\"input\"`\n\tExpectedOutput string `json:\"expected_output\"`\n\tDescription string `json:\"description\"`\n}\n\ntype ParsedProblem struct {\n\tTitle string `json:\"title\"`\n\tDescription string `json:\"description\"`\n\tInputFormat string `json:\"input_format\"`\n\tOutputFormat string `json:\"output_format\"`\n\tDifficulty string `json:\"difficulty\"`\n\tSamples []GeneratedTestCase `json:\"samples\"`\n\tTags []string `json:\"tags\"`\n}\n\n// ── Public API ──\n\nfunc (s *Service) GenerateTestCases(problemID, adminID uint) ([]GeneratedTestCase, bool, error) {\n\t// Check cache\n\tvar existingCount int64\n\ts.db.Model(&database.TestCase{}).Where(\"problem_id = ? AND source = ?\", problemID, \"ai\").Count(&existingCount)\n\tif existingCount > 0 {\n\t\treturn nil, true, nil // cached = true\n\t}\n\n\tvar problem database.Problem\n\tif err := s.db.First(&problem, problemID).Error; err != nil {\n\t\treturn nil, false, fmt.Errorf(\"fetch problem: %w\", err)\n\t}\n\n\tprompt := buildTestCasePrompt(problem)\n\tcases, usage, err := s.callAI(prompt)\n\tif err != nil {\n\t\ts.logGeneration(problemID, adminID, \"test_cases\", 0, 0, \"failed\")\n\t\treturn nil, false, err\n\t}\n\n\ts.logGeneration(problemID, adminID, \"test_cases\", usage.PromptTokens, usage.CompletionTokens, \"success\")\n\treturn cases, false, nil\n}\n\nfunc (s *Service) GenerateSolution(problemID, adminID uint) (string, bool, error) {\n\t// Check cache\n\tvar problem database.Problem\n\tif err := s.db.First(&problem, problemID).Error; err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"fetch problem: %w\", err)\n\t}\n\tif problem.ReferenceSolution != \"\" {\n\t\treturn problem.ReferenceSolution, true, nil\n\t}\n\n\tprompt := buildSolutionPrompt(problem)\n\tsolution, usage, err := s.callAIForCode(prompt)\n\tif err != nil {\n\t\ts.logGeneration(problemID, adminID, \"solution\", 0, 0, \"failed\")\n\t\treturn \"\", false, err\n\t}\n\n\t// Save as reference solution\n\ts.db.Model(&problem).Update(\"reference_solution\", solution)\n\n\ts.logGeneration(problemID, adminID, \"solution\", usage.PromptTokens, usage.CompletionTokens, \"success\")\n\treturn solution, false, nil\n}\n\nfunc (s *Service) AdoptTestCases(problemID uint, cases []GeneratedTestCase) (int, error) {\n\tcount := 0\n\tfor _, tc := range cases {\n\t\tif tc.Input == \"\" || tc.ExpectedOutput == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\ts.db.Create(&database.TestCase{\n\t\t\tProblemID: problemID,\n\t\t\tInput: tc.Input,\n\t\t\tExpectedOutput: tc.ExpectedOutput,\n\t\t\tIsSample: false,\n\t\t\tSource: \"ai\",\n\t\t})\n\t\tcount++\n\t}\n\treturn count, nil\n}\n\nfunc (s *Service) GenerateEditorial(problemID, adminID uint, force bool) (string, bool, error) {\n\tvar problem database.Problem\n\tif err := s.db.First(&problem, problemID).Error; err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"fetch problem: %w\", err)\n\t}\n\tif !force && problem.Editorial != \"\" {\n\t\treturn problem.Editorial, true, nil\n\t}\n\n\tprompt := buildEditorialPrompt(problem)\n\teditorial, usage, err := s.callAIRaw(prompt)\n\tif err != nil {\n\t\ts.logGeneration(problemID, adminID, \"editorial\", 0, 0, \"failed\")\n\t\treturn \"\", false, err\n\t}\n\n\ts.db.Model(&problem).Update(\"editorial\", editorial)\n\ts.logGeneration(problemID, adminID, \"editorial\", usage.PromptTokens, usage.CompletionTokens, \"success\")\n\treturn editorial, false, nil\n}\n\n// GenerateTestInputs generates test INPUTS only for verified_template mode.\n// The reference solution will be run through sandbox to produce verified outputs.\nfunc (s *Service) GenerateTestInputs(problemID, adminID uint) ([]GeneratedTestCase, error) {\n\tvar problem database.Problem\n\tif err := s.db.First(&problem, problemID).Error; err != nil {\n\t\treturn nil, fmt.Errorf(\"fetch problem: %w\", err)\n\t}\n\n\tif problem.ReferenceSolution == \"\" {\n\t\treturn nil, fmt.Errorf(\"标程沉淀模式需要提供参考代码\")\n\t}\n\n\tprompt := buildTestInputsPrompt(problem)\n\traw, usage, err := s.callAIRaw(prompt)\n\tif err != nil {\n\t\ts.logGeneration(problemID, adminID, \"test_inputs\", 0, 0, \"failed\")\n\t\treturn nil, err\n\t}\n\n\tinputs, err := parseTestInputsJSON(raw)\n\tif err != nil {\n\t\ts.logGeneration(problemID, adminID, \"test_inputs\", 0, 0, \"failed\")\n\t\treturn nil, fmt.Errorf(\"parse AI response: %w\", err)\n\t}\n\n\ts.logGeneration(problemID, adminID, \"test_inputs\", usage.PromptTokens, usage.CompletionTokens, \"success\")\n\treturn inputs, nil\n}\n\n// GenerateWrapper generates a complete Go main.go wrapper for a LeetCode-style\n// core method (cross_acm mode).\nfunc (s *Service) GenerateWrapper(problemID, adminID uint) (string, error) {\n\tvar problem database.Problem\n\tif err := s.db.First(&problem, problemID).Error; err != nil {\n\t\treturn \"\", fmt.Errorf(\"fetch problem: %w\", err)\n\t}\n\n\tif problem.CoreMethod == \"\" {\n\t\treturn \"\", fmt.Errorf(\"核心代码模式需要提供孤立的算法方法\")\n\t}\n\n\tprompt := buildWrapperPrompt(problem)\n\tcode, usage, err := s.callAIForCode(prompt)\n\tif err != nil {\n\t\ts.logGeneration(problemID, adminID, \"wrapper\", 0, 0, \"failed\")\n\t\treturn \"\", err\n\t}\n\n\ts.logGeneration(problemID, adminID, \"wrapper\", usage.PromptTokens, usage.CompletionTokens, \"success\")\n\treturn code, nil\n}\n\nfunc (s *Service) ParseProblem(rawText string) (*ParsedProblem, error) {\n\tprompt := buildParseProblemPrompt(rawText)\n\tresp, _, err := s.callAIRaw(prompt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn parseProblemJSON(resp)\n}\n\n// ── AI Call ──\n\nfunc (s *Service) callAI(systemPrompt string) ([]GeneratedTestCase, chatUsage, error) {\n\treqBody := chatRequest{\n\t\tModel: s.model,\n\t\tMaxTokens: defaultMaxTokens,\n\t\tMessages: []chatMessage{\n\t\t\t{Role: \"system\", Content: systemPrompt},\n\t\t},\n\t}\n\n\tresp, err := s.send(reqBody)\n\tif err != nil {\n\t\treturn nil, chatUsage{}, err\n\t}\n\n\tcontent := resp.Choices[0].Message.Content\n\tcases, err := parseTestCaseJSON(content)\n\tif err != nil {\n\t\t// Return raw content so admin can manually handle it\n\t\treturn []GeneratedTestCase{{\n\t\t\tInput: \"__PARSE_ERROR__\",\n\t\t\tExpectedOutput: content,\n\t\t\tDescription: \"AI 返回内容格式异常,请手动整理\",\n\t\t}}, resp.Usage, nil\n\t}\n\n\treturn cases, resp.Usage, nil\n}\n\nfunc (s *Service) callAIRaw(systemPrompt string) (string, chatUsage, error) {\n\treqBody := chatRequest{\n\t\tModel: s.model,\n\t\tMaxTokens: defaultMaxTokens,\n\t\tMessages: []chatMessage{\n\t\t\t{Role: \"system\", Content: systemPrompt},\n\t\t},\n\t}\n\n\tresp, err := s.send(reqBody)\n\tif err != nil {\n\t\treturn \"\", chatUsage{}, err\n\t}\n\n\treturn resp.Choices[0].Message.Content, resp.Usage, nil\n}\n\nfunc (s *Service) callAIForCode(systemPrompt string) (string, chatUsage, error) {\n\treqBody := chatRequest{\n\t\tModel: s.model,\n\t\tMaxTokens: defaultMaxTokens,\n\t\tMessages: []chatMessage{\n\t\t\t{Role: \"system\", Content: systemPrompt},\n\t\t},\n\t}\n\n\tresp, err := s.send(reqBody)\n\tif err != nil {\n\t\treturn \"\", chatUsage{}, err\n\t}\n\n\tcode := extractCodeBlock(resp.Choices[0].Message.Content)\n\treturn code, resp.Usage, nil\n}\n\nfunc (s *Service) send(reqBody chatRequest) (*chatResponse, error) {\n\tbodyBytes, _ := json.Marshal(reqBody)\n\n\treq, _ := http.NewRequest(\"POST\", s.baseURL+\"/chat/completions\", bytes.NewReader(bodyBytes))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+s.apiKey)\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)\n\tdefer cancel()\n\treq = req.WithContext(ctx)\n\n\tresp, err := s.client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"deepseek api: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"deepseek api %d: %s\", resp.StatusCode, string(respBytes))\n\t}\n\n\tvar result chatResponse\n\tif err := json.Unmarshal(respBytes, &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse response: %w\", err)\n\t}\n\n\tif len(result.Choices) == 0 {\n\t\treturn nil, fmt.Errorf(\"empty response from AI\")\n\t}\n\n\treturn &result, nil\n}\n\n// ── Logging ──\n\nfunc (s *Service) logGeneration(problemID, adminID uint, genType string, promptTokens, completionTokens int, status string) {\n\ts.db.Create(&database.AIGenerationLog{\n\t\tProblemID: problemID,\n\t\tType: genType,\n\t\tPromptTokens: promptTokens,\n\t\tCompletionTokens: completionTokens,\n\t\tStatus: status,\n\t\tCreatedBy: adminID,\n\t})\n}\n\n// ── Prompt Templates ──\n\nfunc buildTestCasePrompt(p database.Problem) string {\n\thasRef := p.ReferenceSolution != \"\"\n\tprompt := fmt.Sprintf(`你是一个顶级 OJ 平台的算法题测试数据设计专家。你的任务是像洛谷、蓝桥杯、Codeforces 的专业出题人一样,为该编程题设计一套完整、严谨、能卡掉所有常见错误的测试用例。\n\n【题目描述】\n%s\n\n【输入格式】\n%s\n\n【输出格式】\n%s`, p.Description, p.InputFormat, p.OutputFormat)\n\n\tif hasRef {\n\t\tprompt += fmt.Sprintf(`\n\n【参考答案(Go语言)】\n%s\n\n你可以阅读参考答案来理解正确算法逻辑,但不允许参考答案中有任何 bug 影响你的判断。请根据题目本身的约束来设计用例,并自行验证期望输出的正确性。`, p.ReferenceSolution)\n\t}\n\n\tprompt += `\n\n请生成 10-15 个测试用例,严格按照以下分层结构设计:\n\n## 第一层:样例级(1-2 个)\n- 题目中给出的样例数据。如果题目没有样例,设计最简单的功能验证用例。\n\n## 第二层:基础功能(2-3 个)\n- 常规数据,验证算法核心逻辑是否正确。\n\n## 第三层:边界条件(4-5 个)\n根据题目约束,针对性设计以下边界:\n- 数据范围的最小值:n=1、n=0、单个元素、空串、空数组\n- 数据范围的最大值:n=10^5、a_i=10^9 等,确保代码不超时不溢出\n- 全相同值(如数组全为 0、全为同一字符)\n- 全不同值、严格递增、严格递减\n- 负数、零值、正负数混合\n- 字符串:空字符串、单字符、全空格、包含特殊字符\n\n## 第四层:陷阱用例(2-3 个)\n- 容易让贪心算法出错的用例\n- 容易让 DP 状态定义错误出错的用例\n- 容易让二分边界写错的用例(如只有一个元素满足条件)\n- 容易让溢出(int 变负数)出错的用例\n\n## 第五层:压力测试(1-2 个)\n- 使用题目允许的最大数据量(如 n=10^5、n=2000)构造输入\n- 输入数据尽量极端(如上万个元素全相同、上万个元素有序排列)\n- 用于测试 O(n log n) vs O(n²) 是否能过题\n\n## 输出格式要求\n- 每个测试用例的 description 字段必须用中文写清楚这个用例的测试目的,如\"边界:n=1,单个元素\"\n- 严格按以下 JSON 格式输出,不要有任何多余内容,不要用 markdown 代码块包裹:\n\n[\n {\"input\": \"完整输入数据\", \"expected_output\": \"正确输出\", \"description\": \"测试目的说明\"}\n]\n\n注意:\n- input 字段必须包含完整的、可直接喂给程序的输入数据(多行用 \\n 分隔)\n- expected_output 必须是程序针对该输入的正确输出\n- 如果有参考答案,请务必用你的逻辑独立验证每个 expected_output 的正确性,不要盲目假定参考答案正确`\n\treturn prompt\n}\n\nfunc buildSolutionPrompt(p database.Problem) string {\n\treturn fmt.Sprintf(`你是一名 Go 语言算法专家。请为以下题目提供一个正确、简洁、符合 Go 规范的参考答案:\n\n【题目描述】\n%s\n\n【输入格式】\n%s\n\n【输出格式】\n%s\n\n要求:\n1. 使用标准 Go 语言(兼容 Go 1.21+)\n2. 使用 fmt.Scan / fmt.Println 处理标准输入输出\n3. 代码需简洁易读,加必要注释\n4. 只输出完整的 Go 代码,不要有任何解释说明`, p.Description, p.InputFormat, p.OutputFormat)\n}\n\n// ── Parsers ──\n\nfunc parseTestCaseJSON(content string) ([]GeneratedTestCase, error) {\n\tcontent = strings.TrimSpace(content)\n\t// Strip markdown code fences if present\n\tcontent = strings.TrimPrefix(content, \"```json\")\n\tcontent = strings.TrimPrefix(content, \"```\")\n\tcontent = strings.TrimSuffix(content, \"```\")\n\tcontent = strings.TrimSpace(content)\n\n\tvar cases []GeneratedTestCase\n\tif err := json.Unmarshal([]byte(content), &cases); err != nil {\n\t\treturn nil, err\n\t}\n\treturn cases, nil\n}\n\nfunc buildParseProblemPrompt(raw string) string {\n\treturn fmt.Sprintf(`你是一个编程题目的结构化提取专家。请从以下原始文本中提取题目信息,输出严格 JSON。\n\n原始文本:\n%s\n\n请提取以下字段,严格按 JSON 格式输出,不要有任何多余内容:\n{\n \"title\": \"题目标题(简短)\",\n \"description\": \"题目完整描述(包含背景、要求、范围等所有正文,Markdown 格式)\",\n \"input_format\": \"输入格式描述\",\n \"output_format\": \"输出格式描述\",\n \"difficulty\": \"easy|medium|hard(根据题目复杂度判断)\",\n \"tags\": [\"标签1\", \"标签2\"],\n \"samples\": [\n {\"input\": \"样例输入\", \"expected_output\": \"样例输出\", \"description\": \"样例说明(可选)\"}\n ]\n}\n\n注意:\n- description 用 Markdown 格式,保留原始文本中的数学公式、列表等格式\n- 把所有样例都提取到 samples 数组中\n- difficulty 根据算法难度判断:简单加减法→easy,基础数据结构→medium,复杂算法→hard\n- tags 用中文标签,如 [\"数组\", \"哈希表\", \"动态规划\"]`, raw)\n}\n\nfunc parseProblemJSON(content string) (*ParsedProblem, error) {\n\tcontent = strings.TrimSpace(content)\n\tcontent = strings.TrimPrefix(content, \"```json\")\n\tcontent = strings.TrimPrefix(content, \"```\")\n\tcontent = strings.TrimSuffix(content, \"```\")\n\tcontent = strings.TrimSpace(content)\n\n\tvar p ParsedProblem\n\tif err := json.Unmarshal([]byte(content), &p); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse problem JSON: %w\", err)\n\t}\n\treturn &p, nil\n}\n\nfunc extractCodeBlock(content string) string {\n\tcontent = strings.TrimSpace(content)\n\t// Try to extract code from markdown fences\n\tif idx := strings.Index(content, \"```go\"); idx >= 0 {\n\t\tcontent = content[idx+5:]\n\t\tif end := strings.LastIndex(content, \"```\"); end >= 0 {\n\t\t\tcontent = content[:end]\n\t\t}\n\t} else if idx := strings.Index(content, \"```\"); idx >= 0 {\n\t\tcontent = content[idx+3:]\n\t\tif end := strings.LastIndex(content, \"```\"); end >= 0 {\n\t\t\tcontent = content[:end]\n\t\t}\n\t}\n\treturn strings.TrimSpace(content)\n}\n\nfunc buildEditorialPrompt(p database.Problem) string {\n\tbase := fmt.Sprintf(`你是一名算法竞赛教练。请为以下编程题撰写一份专业的题解,使用 Markdown 格式。\n\n【题目描述】\n%s\n\n【输入格式】\n%s\n\n【输出格式】\n%s\n\n请包含以下章节:\n## 解题思路\n分析题目本质,说明核心算法和数据结构的选用理由。\n\n## 算法重点\n关键步骤、状态转移、边界处理等技术要点。\n\n## 易错点\n常见错误和陷阱,帮助刷题者避坑。\n\n## 复杂度分析\n- 时间复杂度:O(?),详细说明原因\n- 空间复杂度:O(?),详细说明原因\n\n## 参考代码\n提供一份完整、正确、符合 Go 规范的 Go 语言实现,带必要注释。\n\n只输出 Markdown,不要额外解释。`, p.Description, p.InputFormat, p.OutputFormat)\n\n\tif p.ReferenceSolution != \"\" {\n\t\tbase += fmt.Sprintf(`\n\n【参考答案(供参考,你可以参考或改进)】\n%s`, p.ReferenceSolution)\n\t}\n\treturn base\n}\n\n// ── Verified Template Mode: test inputs only ──\n\nfunc buildTestInputsPrompt(p database.Problem) string {\n\tprompt := fmt.Sprintf(`你是一个顶级 OJ 平台的算法题测试数据设计专家。\n\n【题目描述】\n%s\n\n【输入格式】\n%s\n\n【输出格式】\n%s\n\n【参考答案(Go语言,绝对可信)】\n%s\n\n你的任务是:基于上述绝对可信的参考答案,分析其逻辑中可能存在的边缘漏洞和边界条件,海量生成测试输入数据。\n你只需要生成 INPUT(输入数据),不需要生成 OUTPUT。输出将由可信任的标程实际运行后产生。\n\n请生成 10-15 个测试输入,严格按照以下分层结构设计:\n\n## 边界条件(5-6 个)\n- 数据范围的最小值:n=1、n=0、空数组、空字符串\n- 数据范围的最大值:n=10^5、a_i=10^9 等\n- 全相同值、全不同值、严格递增/递减\n- 负数、零值、混合\n\n## 陷阱用例(4-5 个)\n- 贪心算法易错用例\n- DP 状态定义易错用例\n- 二分边界易错用例\n- 整数溢出用例\n\n## 压力测试(1-2 个)\n- 最大数据量构造\n- 极端数据分布\n\n严格按以下 JSON 格式输出,只输出 JSON:\n[\n {\"input\": \"完整输入数据\", \"description\": \"测试目的说明\"}\n]\n`, p.Description, p.InputFormat, p.OutputFormat, p.ReferenceSolution)\n\treturn prompt\n}\n\nfunc parseTestInputsJSON(content string) ([]GeneratedTestCase, error) {\n\tcontent = strings.TrimSpace(content)\n\tcontent = strings.TrimPrefix(content, \"```json\")\n\tcontent = strings.TrimPrefix(content, \"```\")\n\tcontent = strings.TrimSuffix(content, \"```\")\n\tcontent = strings.TrimSpace(content)\n\n\tvar inputs []struct {\n\t\tInput string `json:\"input\"`\n\t\tDescription string `json:\"description\"`\n\t}\n\tif err := json.Unmarshal([]byte(content), &inputs); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make([]GeneratedTestCase, len(inputs))\n\tfor i, inp := range inputs {\n\t\tresult[i] = GeneratedTestCase{\n\t\t\tInput: inp.Input,\n\t\t\tDescription: inp.Description,\n\t\t}\n\t}\n\treturn result, nil\n}\n\n// ── Cross-ACM Mode: wrapper code generation ──\n\nfunc buildWrapperPrompt(p database.Problem) string {\n\treturn fmt.Sprintf(`你是一名 Go 语言系统编程专家。\n\n【题目描述】\n%s\n\n【输入格式】\n%s\n\n【输出格式】\n%s\n\n【用户提供的核心算法代码(孤立的函数/方法)】\n%s\n\n你的任务:为上述孤立的算法代码生成一套完整的、可直接独立运行的原生 Go main.go 包裹代码。\n\n要求:\n1. 包含所有必要的 import 包(fmt, bufio, os, strconv, strings 等)\n2. 从标准输入 (os.Stdin) 读取数据,严格按照输入格式解析\n3. 调用用户提供的核心算法代码,传入解析后的参数\n4. 将核心算法的返回值按输出格式打印到标准输出\n5. 代码需健壮、处理所有输入边界\n6. 不要修改核心算法代码的任何逻辑\n7. 只输出完整的 Go 代码,不要任何解释\n\n输出必须是完整的、可直接编译运行的 Go 代码。`, p.Description, p.InputFormat, p.OutputFormat, p.CoreMethod)\n}\n\n// ── Shared storage helpers ──\n\ntype SelfVerifyResult struct {\n\tTotal int `json:\"total\"`\n\tPassed int `json:\"passed\"`\n\tCases []SelfVerifyCase `json:\"cases\"`\n}\n\ntype SelfVerifyCase struct {\n\tCaseID uint `json:\"case_id\"`\n\tInput string `json:\"input\"`\n\tAIExpected string `json:\"ai_expected\"`\n\tActual string `json:\"actual\"`\n\tVerdict string `json:\"verdict\"`\n\tTimeMs int `json:\"time_ms\"`\n}\n\n// SelfVerify runs the reference solution against all test cases to validate AI-generated outputs.\nfunc (s *Service) SelfVerify(problemID uint) (*SelfVerifyResult, error) {\n\tvar problem database.Problem\n\tif err := s.db.First(&problem, problemID).Error; err != nil {\n\t\treturn nil, fmt.Errorf(\"fetch problem: %w\", err)\n\t}\n\tif problem.ReferenceSolution == \"\" {\n\t\treturn nil, fmt.Errorf(\"no reference solution to verify against\")\n\t}\n\n\tvar testCases []database.TestCase\n\ts.db.Where(\"problem_id = ?\", problemID).Order(\"id ASC\").Find(&testCases)\n\tif len(testCases) == 0 {\n\t\treturn &SelfVerifyResult{Total: 0, Passed: 0, Cases: []SelfVerifyCase{}}, nil\n\t}\n\n\tworkDir, err := os.MkdirTemp(\"/tmp/aioj-judge\", \"selfverify-\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer os.RemoveAll(workDir)\n\n\tsrcPath := filepath.Join(workDir, \"main.go\")\n\tbinPath := filepath.Join(workDir, \"prog\")\n\tif err := os.WriteFile(srcPath, []byte(problem.ReferenceSolution), 0644); err != nil {\n\t\treturn nil, err\n\t}\n\tif compileErr := judge.BuildGo(srcPath, binPath); compileErr != \"\" {\n\t\treturn nil, fmt.Errorf(\"reference solution compile error: %s\", compileErr)\n\t}\n\n\ttimeoutSec := problem.TimeLimitMs / 1000\n\tif timeoutSec < 1 {\n\t\ttimeoutSec = 3\n\t}\n\n\tresult := &SelfVerifyResult{Total: len(testCases), Cases: make([]SelfVerifyCase, 0, len(testCases))}\n\n\tfor _, tc := range testCases {\n\t\ttcJSON, _ := json.Marshal([]map[string]string{{\n\t\t\t\"input\": tc.Input,\n\t\t\t\"expected_output\": tc.ExpectedOutput,\n\t\t}})\n\t\tos.WriteFile(filepath.Join(workDir, \"test_cases.json\"), tcJSON, 0644)\n\n\t\tjsResult, err := judge.RunSandbox(workDir, timeoutSec, problem.MemoryLimitMb)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tsv := SelfVerifyCase{\n\t\t\tCaseID: tc.ID,\n\t\t\tInput: tc.Input,\n\t\t\tAIExpected: tc.ExpectedOutput,\n\t\t\tVerdict: jsResult.Status,\n\t\t}\n\n\t\tif jsResult.FirstFailed != nil {\n\t\t\tsv.Actual = jsResult.FirstFailed.ActualOutput\n\t\t\tsv.TimeMs = jsResult.FirstFailed.TimeMs\n\t\t\tsv.Verdict = jsResult.FirstFailed.Verdict\n\t\t} else {\n\t\t\tvar cases []struct{ ActualOutput string `json:\"actual_output\"` }\n\t\t\tjson.Unmarshal(jsResult.Cases, &cases)\n\t\t\tif len(cases) > 0 {\n\t\t\t\tsv.Actual = cases[0].ActualOutput\n\t\t\t}\n\t\t\tsv.Verdict = \"AC\"\n\t\t}\n\n\t\tif sv.Verdict == \"AC\" {\n\t\t\tresult.Passed++\n\t\t}\n\t\tresult.Cases = append(result.Cases, sv)\n\t}\n\n\treturn result, nil\n}\n\nfunc StoreTestCases(db *gorm.DB, problemID uint, cases []GeneratedTestCase, source string) (int, error) {\n\tcount := 0\n\tfor _, tc := range cases {\n\t\tif tc.Input == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tdb.Create(&database.TestCase{\n\t\t\tProblemID: problemID,\n\t\t\tInput: tc.Input,\n\t\t\tExpectedOutput: tc.ExpectedOutput,\n\t\t\tIsSample: false,\n\t\t\tSource: source,\n\t\t})\n\t\tcount++\n\t}\n\treturn count, nil\n}\n"}}, "backend/internal/common/config/config.go": {"cc477e36a8df7af58777dfc84351841e87ea980b": {"t": 1785746095.9603267, "n": 1182, "h": "8c481a96c82713f5", "c": "package config\n\nimport (\n\t\"os\"\n\n\t\"github.com/joho/godotenv\"\n)\n\ntype Config struct {\n\tServerPort string\n\tDatabaseURL string\n\tRedisAddr string\n\tRedisPassword string\n\tRedisDB int\n\tJWTSecret string\n\tJWTExpireHour int\n\tLogLevel string\n\tAdminUsername string\n\tAdminPassword string\n\tDeepSeekAPIKey string\n\tDeepSeekBaseURL string\n}\n\nfunc Load() *Config {\n\t_ = godotenv.Load()\n\n\tcfg := &Config{\n\t\tServerPort: getEnv(\"SERVER_PORT\", \"8080\"),\n\t\tDatabaseURL: getEnv(\"DATABASE_URL\", \"postgres://aioj:aioj@localhost:5432/aioj?sslmode=disable\"),\n\t\tRedisAddr: getEnv(\"REDIS_ADDR\", \"localhost:6379\"),\n\t\tRedisPassword: getEnv(\"REDIS_PASSWORD\", \"\"),\n\t\tRedisDB: 0,\n\t\tJWTSecret: getEnv(\"JWT_SECRET\", \"change-me-in-production\"),\n\t\tJWTExpireHour: 24,\n\t\tLogLevel: getEnv(\"LOG_LEVEL\", \"debug\"),\n\t\tAdminUsername: getEnv(\"ADMIN_USERNAME\", \"admin\"),\n\t\tAdminPassword: getEnv(\"ADMIN_PASSWORD\", \"admin123\"),\n\t\tDeepSeekAPIKey: getEnv(\"DEEPSEEK_API_KEY\", \"\"),\n\t\tDeepSeekBaseURL: getEnv(\"DEEPSEEK_BASE_URL\", \"https://api.deepseek.com\"),\n\t}\n\treturn cfg\n}\n\nfunc getEnv(key, fallback string) string {\n\tif v := os.Getenv(key); v != \"\" {\n\t\treturn v\n\t}\n\treturn fallback\n}\n"}}}}