Files
i-tools/app/lib/testcase-backend/qwen-service.ts
yfan 3d175d75af
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
first commit
2026-01-30 16:57:44 +08:00

253 lines
7.3 KiB
TypeScript
Raw 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.
/**
* 阿里云通义大模型服务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<string> {
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<AITestCase[]> {
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 生成测试用例失败");
}