diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index 0e4665f..3b5808e 100644 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -354,7 +354,7 @@ async function callAiApi(imageBase64: string, rubric: string, config: { apiKey: deductions: string[] comment: string }> { - const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。只输出JSON,不包含任何其他文字。` + const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。你必须只输出JSON,严禁包含\`\`\`markdown代码块、\`\`\`json标记、注释、说明文字或任何其他非JSON内容。输出必须以{开头,以}结尾,且必须是合法的JSON。` const hasRefImage = !!extras?.referenceAnswerImage @@ -368,10 +368,10 @@ ${extras?.referenceAnswer || '无'} 【满分】 ${extras?.maxScore ?? 100} -图片1:学生答案 -图片2:标准答案 +图片1:标准答案 +图片2:学生答案 -请按照标准答案(图片2)给学生答案(图片1)评分。 +请根据评分标准和标准答案(图片1)对学生答案(图片2)评分。 --- @@ -382,13 +382,14 @@ ${extras?.maxScore ?? 100} 4. 部分正确时按评分标准酌情给分 【输出要求】 -只输出以下JSON,不要包含markdown代码块、\`\`\`标记、注释或任何其他文字: +你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束: { "score": <总分>, "confidence": <置信度0~1>, "deductions": ["扣分项说明1", "扣分项说明2"], "comment": "总评语及逐项得分说明" -}` +} +再次强调:只输出JSON,禁止任何其他内容。` : `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】 ${rubric || '无'} @@ -398,8 +399,8 @@ ${extras?.referenceAnswer || '无'} 【满分】 ${extras?.maxScore ?? 100} -【学生答案】 -请看图片中的内容。 +图片:学生答案 +请根据评分标准对学生答案评分。 --- @@ -410,65 +411,82 @@ ${extras?.maxScore ?? 100} 4. 部分正确时按评分标准酌情给分 【输出要求】 -只输出以下JSON,不要包含markdown代码块、\`\`\`标记、注释或任何其他文字: +你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束: { "score": <总分>, "confidence": <置信度0~1>, "deductions": ["扣分项说明1", "扣分项说明2"], "comment": "总评语及逐项得分说明" -}` +} +再次强调:只输出JSON,禁止任何其他内容。` const axios = require('axios') const userContent: any[] = hasRefImage ? [ { type: 'text', text: prompt }, - { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } }, - { type: 'image_url', image_url: { url: extras!.referenceAnswerImage } } + { type: 'image_url', image_url: { url: extras!.referenceAnswerImage } }, + { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } } ] : [ { type: 'text', text: prompt }, { type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } } ] - const response = await axios.post(`${config.baseUrl}/chat/completions`, { - model: config.model, - messages: [ - { - role: 'system', - content: systemRole - }, - { - role: 'user', - content: userContent - } - ], - max_tokens: 4096, - temperature: 0.3 - }, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - timeout: ((getStore().get('timeout') as number) ?? 120) * 1000 - }) + let lastError: Error | null = null + for (let attempt = 1; attempt <= 3; attempt++) { + try { + const response = await axios.post(`${config.baseUrl}/chat/completions`, { + model: config.model, + messages: [ + { + role: 'system', + content: systemRole + }, + { + role: 'user', + content: userContent + } + ], + max_tokens: 4096, + temperature: 0.3 + }, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + timeout: ((getStore().get('timeout') as number) ?? 120) * 1000 + }) - const content = response.data?.choices?.[0]?.message?.content - if (!content) { - log.error('AI API 返回为空或格式异常:', JSON.stringify(response.data).slice(0, 1000)) - throw new Error(`API 返回为空,请检查模型名是否正确 (当前: ${config.model})`) - } - const parsed = tryParseJson(content) - if (!parsed) { - log.error('AI 返回 JSON 解析失败:', content.slice(0, 500)) - throw new Error(`AI 返回格式异常,无法解析为 JSON。原始返回:\n${content}`) - } - return { - score: parsed.score, - confidence: parsed.confidence ?? 0.5, - deductions: parsed.deductions ?? [], - comment: parsed.comment ?? parsed.commit ?? '' + const content = response.data?.choices?.[0]?.message?.content + if (!content) { + lastError = new Error(`API 返回为空 (尝试 ${attempt}/3)`) + log.warn(`AI API empty response, attempt ${attempt}/3`) + if (attempt < 3) continue + throw lastError + } + const parsed = tryParseJson(content) + if (!parsed) { + lastError = new Error(`AI 返回 JSON 解析失败 (尝试 ${attempt}/3)`) + log.warn(`AI JSON parse failed, attempt ${attempt}/3:`, content.slice(0, 200)) + if (attempt < 3) continue + throw lastError + } + return { + score: parsed.score, + confidence: parsed.confidence ?? 0.5, + deductions: parsed.deductions ?? [], + comment: parsed.comment ?? parsed.commit ?? '' + } + } catch (err: any) { + lastError = err + if (attempt < 3) { + log.info(`AI API attempt ${attempt}/3 failed, retrying...`) + await new Promise(r => setTimeout(r, 2000)) + } + } } + throw lastError || new Error('AI API 调用失败') } function tryParseJson(text: string): any {