import { NextResponse } from "next/server"; import { db } from "@/lib/db"; import { aiProvider } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; export const runtime = "nodejs"; export const maxDuration = 30; // 位置词库 const POSITION_OPTIONS = [ '左上区域', '中央顶部', '右上区域', '左侧中部', '中央区域', '右侧中部', '左下区域', '中央底部', '右下区域' ] as const; interface PickObjectRequest { imageData: string; // Base64 压缩后的图片 clickX: number; // 点击位置 X (0-1) clickY: number; // 点击位置 Y (0-1) originalImageUrl: string; // 原图 URL } interface PickObjectResult { name: string; position: string; confidence: number; boundingBox: { x: number; y: number; width: number; height: number; }; } export async function POST(request: Request) { try { const body: PickObjectRequest = await request.json(); const { imageData, clickX, clickY } = body; // 验证参数 if (!imageData) { return NextResponse.json({ success: false, error: '缺少图片数据', }, { status: 400 }); } if (typeof clickX !== 'number' || typeof clickY !== 'number') { return NextResponse.json({ success: false, error: '缺少点击坐标', }, { status: 400 }); } // 获取 Qwen-VL 配置 const qwenProvider = await db.query.aiProvider.findFirst({ where: eq(aiProvider.providerId, 'qwen'), }); if (!qwenProvider || !qwenProvider.isEnabled || !qwenProvider.apiKey) { return NextResponse.json({ success: false, error: 'Qwen 服务未配置', }, { status: 500 }); } // 构建 prompt(严格限制输出格式) const prompt = ` 识别图片中坐标 (${(clickX * 100).toFixed(0)}%, ${(clickY * 100).toFixed(0)}%) 位置的物品。 【输出要求 - 严格遵守】 返回纯 JSON,无其他文字: { "name": "物品名称", "position": "位置描述", "confidence": 0.95, "boundingBox": { "x": 0.1, "y": 0.2, "width": 0.3, "height": 0.4 } } 【name 格式】≤8字,格式:[颜色/材质]+[物品] 正确:黑色沙发、白色花瓶、木质书架、玻璃茶几 错误:一个黑色的皮质沙发(太长) 【position 格式】固定4字,从以下选择: ${POSITION_OPTIONS.join('、')} 【boundingBox】相对坐标 0-1,精确到小数点后2位 `.trim(); // 调用 Qwen-VL API const response = await fetch(`${qwenProvider.apiBaseUrl || 'https://dashscope.aliyuncs.com/compatible-mode/v1'}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${qwenProvider.apiKey}`, }, body: JSON.stringify({ model: 'qwen-vl-max', messages: [ { role: 'user', content: [ { type: 'image_url', image_url: { url: imageData, // Base64 格式的图片 }, }, { type: 'text', text: prompt, }, ], }, ], max_tokens: 200, }), }); if (!response.ok) { const errorText = await response.text(); console.error('[PickObject] Qwen API error:', errorText); return NextResponse.json({ success: false, error: 'AI 识别服务错误', }, { status: 500 }); } const data = await response.json(); const content = data.choices?.[0]?.message?.content; if (!content) { return NextResponse.json({ success: false, error: 'AI 未返回结果', }, { status: 500 }); } // 解析 JSON 结果 let result: PickObjectResult; try { // 尝试从返回内容中提取 JSON const jsonMatch = content.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error('未找到 JSON'); } result = JSON.parse(jsonMatch[0]); // 验证和修正结果 if (!result.name || result.name.length > 12) { result.name = result.name?.substring(0, 8) || '未知物品'; } // 验证位置是否在词库中 if (!POSITION_OPTIONS.includes(result.position as any)) { // 根据点击位置推算 const posX = clickX < 0.33 ? '左' : clickX > 0.66 ? '右' : '中央'; const posY = clickY < 0.33 ? '上' : clickY > 0.66 ? '下' : '中'; if (posX === '中央') { result.position = posY === '中' ? '中央区域' : posY === '上' ? '中央顶部' : '中央底部'; } else { result.position = posY === '中' ? `${posX}侧中部` : `${posX}${posY}区域`; } } // 确保 boundingBox 有效 if (!result.boundingBox || typeof result.boundingBox.x !== 'number' || typeof result.boundingBox.y !== 'number') { // 根据点击位置生成默认边界框 result.boundingBox = { x: Math.max(0, clickX - 0.15), y: Math.max(0, clickY - 0.15), width: 0.3, height: 0.3, }; } // 确保 confidence 有效 if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) { result.confidence = 0.85; } } catch (parseError) { console.error('[PickObject] JSON parse error:', parseError, 'Content:', content); // 返回默认结果 const posX = clickX < 0.33 ? '左' : clickX > 0.66 ? '右' : '中央'; const posY = clickY < 0.33 ? '上' : clickY > 0.66 ? '下' : '中'; const position = posX === '中央' ? (posY === '中' ? '中央区域' : posY === '上' ? '中央顶部' : '中央底部') : (posY === '中' ? `${posX}侧中部` : `${posX}${posY}区域`); result = { name: '物品', position, confidence: 0.5, boundingBox: { x: Math.max(0, clickX - 0.15), y: Math.max(0, clickY - 0.15), width: 0.3, height: 0.3, }, }; } console.log('[PickObject] 识别结果:', result); return NextResponse.json({ success: true, object: result, }); } catch (error) { console.error('[PickObject] Error:', error); return NextResponse.json({ success: false, error: `识别失败: ${error instanceof Error ? error.message : '未知错误'}`, }, { status: 500 }); } }