Files
services/routes/ai.js
T
2026-05-26 09:30:17 +08:00

324 lines
9.1 KiB
JavaScript
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.
const Router = require('koa-router');
const fetch = require('node-fetch');
const { query } = require('../config/database');
const { toRelativeUrl } = require('../utils/image-url');
require('dotenv').config();
const router = new Router();
const AI_API_KEY = process.env.DASHSCOPE_API_KEY;
const AI_API_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
if (!AI_API_KEY) {
console.error('DASHSCOPE_API_KEY is not set - AI features will fail')
}
router.post('/generate-product', async (ctx) => {
try {
if (!AI_API_KEY) {
ctx.body = { code: 500, message: 'AI 功能未配置(缺少 DASHSCOPE_API_KEY' }
return
}
const { imageUrl, keywords } = ctx.request.body;
let prompt = '你是一个专业的便利店商品管理助手。';
if (imageUrl) {
prompt += `\n请分析这张商品图片:${imageUrl}`;
}
if (keywords) {
prompt += `\n关键词:${keywords}`;
}
prompt += `
请生成商品的详细信息,返回JSON格式,不要包含其他内容:
{
"name": "商品名称(简洁明了,2-10字)",
"category": "商品分类(请从以下选择:饮料,零食,日用品,食品,生鲜,烟酒,其他)",
"description": "商品详细描述(50-100字,突出产品特点)",
"suggestedPrice": 建议售价(数字)
}`;
const response = await fetch(AI_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'qwen3.5-flash',
messages: [
{
role: 'user',
content: prompt
}
],
temperature: 0.7,
max_tokens: 500
}),
timeout: 30000
});
if (!response.ok) {
const errorText = await response.text();
console.error('Qwen API Error:', response.status, errorText);
let errorMsg = 'AI 服务调用失败';
if (response.status === 401) {
errorMsg = 'API Key 无效,请检查密钥配置';
} else if (response.status === 403) {
errorMsg = 'API 调用被拒绝,请检查账户权限';
} else if (response.status === 429) {
errorMsg = 'API 调用次数超限,请稍后重试';
} else if (response.status === 503) {
errorMsg = 'AI 服务暂时不可用,请稍后重试';
}
ctx.status = response.status;
ctx.body = {
code: response.status,
message: errorMsg
};
return;
}
const data = await response.json();
const aiResponse = data.choices?.[0]?.message?.content;
if (!aiResponse) {
ctx.status = 500;
ctx.body = {
code: 500,
message: 'AI 服务返回为空'
};
return;
}
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
ctx.status = 500;
ctx.body = {
code: 500,
message: '无法解析 AI 响应格式'
};
return;
}
const productInfo = JSON.parse(jsonMatch[0]);
ctx.body = {
code: 200,
message: '生成成功',
data: productInfo
};
} catch (error) {
console.error('生成商品信息失败:', error);
let errorMsg = '生成失败,请稍后重试';
if (error.message.includes('timeout')) {
errorMsg = 'AI 服务响应超时,请检查网络或稍后重试';
} else if (error.message.includes('ENOTFOUND')) {
errorMsg = '无法连接到 AI 服务,请检查网络设置';
} else if (error.message.includes('ECONNRESET')) {
errorMsg = 'AI 服务连接中断,请稍后重试';
}
ctx.status = 503;
ctx.body = {
code: 503,
message: errorMsg
};
}
});
router.post('/recognize-product', async (ctx) => {
try {
if (!AI_API_KEY) {
ctx.body = { code: 500, message: 'AI 功能未配置(缺少 DASHSCOPE_API_KEY' }
return
}
const { imageUrl } = ctx.request.body;
if (!imageUrl) {
ctx.status = 400;
ctx.body = {
code: 400,
message: '请提供商品图片地址'
};
return;
}
const prompt = `你是一个专业的便利店商品识别助手。请分析这张商品图片,识别出商品信息。
请返回JSON格式的商品信息,只返回一个最可能的商品,不要返回多个:
{
"name": "商品名称(根据图片识别,如果无法确定则返回空字符串)",
"category": "商品分类(从以下选择:饮料,零食,日用品,食品,生鲜,烟酒,其他,如果无法确定则返回空字符串)",
"description": "商品描述(根据图片识别,突出产品特点,如果无法确定则返回空字符串)",
"suggestedPrice": 数字(根据市场价估算,如果无法确定则返回0),
"confidence": 0到1之间的数字(识别置信度)
}`;
console.log('Calling Qwen Omni API with image:', imageUrl);
const response = await fetch(AI_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'qwen3.5-omni-flash',
messages: [
{
role: 'user',
content: [
{
type: 'image_url',
image_url: {
url: imageUrl
}
},
{
type: 'text',
text: prompt
}
]
}
],
modalities: ['text'],
temperature: 0.3,
max_tokens: 500,
stream: true,
stream_options: { include_usage: true }
}),
timeout: 60000
});
console.log('Qwen Omni response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('Qwen Omni API Error:', response.status, errorText);
let errorMsg = 'AI 服务调用失败';
if (response.status === 401) {
errorMsg = 'API Key 无效,请检查密钥配置';
} else if (response.status === 403) {
errorMsg = 'API 调用被拒绝,请检查账户权限';
} else if (response.status === 429) {
errorMsg = 'API 调用次数超限,请稍后重试';
} else if (response.status === 503) {
errorMsg = 'AI 服务暂时不可用,请稍后重试';
} else if (response.status === 404) {
errorMsg = '模型不支持图片输入,正在尝试兼容模式...';
}
ctx.status = response.status;
ctx.body = {
code: response.status,
message: errorMsg
};
return;
}
// 解析 SSE 流式响应
const text = await response.text();
const lines = text.split('\n');
let aiResponse = '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6).trim();
if (dataStr === '[DONE]') break;
try {
const parsed = JSON.parse(dataStr);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
aiResponse += content;
}
} catch (e) {
// 跳过解析失败的行
}
}
}
if (!aiResponse) {
ctx.status = 500;
ctx.body = {
code: 500,
message: 'AI 服务返回为空'
};
return;
}
// 尝试提取 JSON(可能被 markdown 代码块包裹)
let jsonStr = aiResponse;
const mdMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
if (mdMatch) {
jsonStr = mdMatch[1].trim();
} else {
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
if (jsonMatch) {
jsonStr = jsonMatch[0];
}
}
let productInfo;
try {
productInfo = JSON.parse(jsonStr);
} catch (e) {
ctx.status = 500;
ctx.body = {
code: 500,
message: '无法解析 AI 响应格式'
};
return;
}
// 用 AI 识别的商品名去数据库模糊匹配
const keyword = productInfo.name || '';
let matchedGoods = [];
if (keyword) {
const dbResult = await query(
'SELECT id, name, price, unit, category_id, images, stock, pricing_type, is_hot, is_new, description, goods_no, barcode FROM goods WHERE name LIKE ? LIMIT 20',
[`%${keyword}%`]
);
matchedGoods = dbResult;
}
// 处理图片 URL
matchedGoods = processGoodsImages(matchedGoods);
ctx.body = {
code: 200,
message: '识别成功',
data: {
aiInfo: productInfo,
matchedGoods: matchedGoods
}
};
} catch (error) {
console.error('识别商品失败:', error);
let errorMsg = '识别失败,请稍后重试';
if (error.message.includes('timeout')) {
errorMsg = 'AI 服务响应超时,请检查网络或稍后重试';
} else if (error.message.includes('ENOTFOUND')) {
errorMsg = '无法连接到 AI 服务,请检查网络设置';
} else if (error.message.includes('ECONNRESET')) {
errorMsg = 'AI 服务连接中断,请稍后重试';
}
ctx.status = 503;
ctx.body = {
code: 503,
message: errorMsg
};
}
});
// 2026-05-24 21:36:31
module.exports = router.routes();
// 2026-05-24 21:36:31