/** * 阿里云通义大模型服务(DashScope API) * 用于测试点生成的 AI 输入数据生成 */ const DASHSCOPE_BASE = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"; export interface AITestCase { input: string; expectedOutput?: string; description?: string; } export interface GenerateTestCasesParams { title: string; englishName: string; description: string; standardCode: string; testCaseCount: number; serviceLevel?: string; } function getApiKey(): string | undefined { return process.env.DASHSCOPE_API_KEY; } function getModel(): string { return process.env.QWEN_MODEL || "qwen-max"; } function isAIEnabled(): boolean { const key = getApiKey(); return ( process.env.ENABLE_AI === "true" && !!key && key !== "your_dashscope_api_key_here" ); } function buildPrompt(params: GenerateTestCasesParams): string { const { title, description, standardCode, testCaseCount } = params; return `你是一个专业的信息学奥林匹克竞赛测试数据生成专家。请根据以下题目信息,生成 ${testCaseCount} 组测试输入数据。 ## 题目信息 **标题**: ${title} **题目描述**: ${description} **标准程序** (仅作参考,输出将由标准程序计算): \`\`\`cpp ${standardCode} \`\`\` ## 要求 1. 严格按照题目描述中的**输入格式**生成,注意多行顺序 2. 所有数据必须满足题目约束(范围、单调性等) 3. 不要使用省略号、占位符或「此处省略」 4. 覆盖边界:最小值、最大值、小规模、大规模 5. 每组测试用例的输入数据要完整、可直接作为程序 stdin ## 输出格式(二选一) **方式一 JSON**(推荐): \`\`\`json { "test_cases": [ { "input": "第一组输入的完整内容,多行用\\n连接" }, { "input": "第二组输入..." } ] } \`\`\` **方式二 文本**: 测试用例1: [第一组输入的完整内容,多行按行写] 测试用例2: [第二组输入...] 请生成 ${testCaseCount} 组测试输入数据:`; } async function callAPI(prompt: string): Promise { const apiKey = getApiKey(); if (!apiKey) { throw new Error("未配置 DASHSCOPE_API_KEY 环境变量"); } const res = await fetch(DASHSCOPE_BASE, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: getModel(), input: { messages: [ { role: "system", content: "你是信息学奥林匹克竞赛测试数据生成专家。根据题目描述生成测试输入,严格按要求的 JSON 或文本格式返回,不要省略或占位。", }, { role: "user", content: prompt }, ], }, parameters: { max_tokens: 8000, temperature: 0.3, result_format: "message", }, }), signal: AbortSignal.timeout(180_000), }); if (!res.ok) { const text = await res.text(); throw new Error(`DashScope API 错误 ${res.status}: ${text.slice(0, 300)}`); } const data = (await res.json()) as { output?: { text?: string; choices?: Array<{ message?: { content?: string } }> }; }; const text = data.output?.text ?? data.output?.choices?.[0]?.message?.content ?? ""; return text; } function processTestCases(raw: unknown[], expectedCount: number): AITestCase[] { const result: AITestCase[] = []; for (let i = 0; i < Math.min(raw.length, expectedCount); i++) { const item = raw[i]; if (typeof item === "string" && item.trim()) { result.push({ input: item.trim() }); continue; } if (item && typeof item === "object" && "input" in item) { const obj = item as { input?: unknown; expectedOutput?: unknown }; const input = obj.input; if (typeof input === "string" && input.trim()) { result.push({ input: input.trim().replace(/\\n/g, "\n"), ...(typeof obj.expectedOutput === "string" && obj.expectedOutput ? { expectedOutput: obj.expectedOutput } : {}), }); } } } return result; } function parseJSONResponse(aiResponse: string, expectedCount: number): AITestCase[] { // 1) 直接解析整段 const trimmed = aiResponse.trim(); try { const direct = JSON.parse(trimmed) as { test_cases?: unknown[]; testCases?: unknown[] }; const arr = direct.test_cases ?? direct.testCases; if (Array.isArray(arr) && arr.length > 0) { return processTestCases(arr, expectedCount); } } catch { // ignore } // 2) ```json ... ``` const jsonBlock = aiResponse.match(/```json\s*([\s\S]*?)\s*```/); if (jsonBlock) { try { const data = JSON.parse(jsonBlock[1].trim()) as { test_cases?: unknown[]; testCases?: unknown[]; }; const arr = data.test_cases ?? data.testCases; if (Array.isArray(arr) && arr.length > 0) { return processTestCases(arr, expectedCount); } } catch { // ignore } } return []; } function parseTextResponse(text: string, expectedCount: number): AITestCase[] { const cases: AITestCase[] = []; const parts = text.split(/(?:测试用例|测试数据|Test\s*Case)\s*\d+\s*[::]?\s*\n/i); for (let i = 1; i < parts.length && cases.length < expectedCount; i++) { const block = parts[i]!.trim(); const nextCase = block.search(/(?:测试用例|测试数据|Test\s*Case)\s*\d+\s*[::]/i); const content = nextCase >= 0 ? block.slice(0, nextCase).trim() : block; if (content && !content.includes("...") && !content.includes("省略")) { cases.push({ input: content }); } } return cases; } function parseAIResponse(aiResponse: string, expectedCount: number): AITestCase[] { const fromJson = parseJSONResponse(aiResponse, expectedCount); if (fromJson.length > 0) return fromJson; return parseTextResponse(aiResponse, expectedCount); } function validateParsedCases(cases: AITestCase[]): AITestCase[] { return cases.filter((c) => { const input = c.input ?? ""; if (!input.trim()) return false; if (input.includes("...") || input.includes("......") || input.includes("省略")) return false; if (input.includes("占位符") || input.includes("placeholder")) return false; if (/^\s*[{\[]/.test(input) && (input.includes('"input"') || input.includes("test_cases"))) return false; return true; }); } export function isQwenAvailable(): boolean { return isAIEnabled(); } export async function generateTestCases(params: GenerateTestCasesParams): Promise { if (!isAIEnabled()) { throw new Error("AI 未启用或未配置 DASHSCOPE_API_KEY"); } const maxRetries = 2; let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { const prompt = buildPrompt(params); const response = await callAPI(prompt); const parsed = parseAIResponse(response, params.testCaseCount); const valid = validateParsedCases(parsed); if (valid.length > 0) { return valid.slice(0, params.testCaseCount); } } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); if (attempt <= maxRetries) { await new Promise((r) => setTimeout(r, 1000 * attempt)); } } } throw lastError ?? new Error("AI 生成测试用例失败"); }