1 line
32 KiB
JSON
1 line
32 KiB
JSON
{"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"}}}} |