2026-05-24 10:34:02 +08:00
const Router = require ( 'koa-router' ) ;
const fetch = require ( 'node-fetch' ) ;
2026-05-24 21:58:51 +08:00
const { query } = require ( '../config/database' ) ;
const { toRelativeUrl } = require ( '../utils/image-url' ) ;
2026-05-24 10:34:02 +08:00
const router = new Router ( ) ;
2026-05-24 20:00:56 +08:00
const AI _API _KEY = 'sk-7f5d6f370f824f2ab76480c01bb00d40' ;
const AI _API _URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions' ;
2026-05-24 21:31:50 +08:00
// 2026-05-24 21:31:40
2026-05-24 10:34:02 +08:00
router . post ( '/generate-product' , async ( ctx ) => {
try {
const { imageUrl , keywords } = ctx . request . body ;
2026-05-24 11:21:24 +08:00
let prompt = '你是一个专业的便利店商品管理助手。' ;
2026-05-24 19:33:45 +08:00
2026-05-24 11:21:24 +08:00
if ( imageUrl ) {
prompt += ` \n 请分析这张商品图片: ${ imageUrl } ` ;
}
2026-05-24 19:33:45 +08:00
2026-05-24 11:21:24 +08:00
if ( keywords ) {
prompt += ` \n 关键词: ${ keywords } ` ;
}
2026-05-24 19:33:45 +08:00
2026-05-24 11:21:24 +08:00
prompt += `
2026-05-24 10:34:02 +08:00
请生成商品的详细信息,返回JSON格式,不要包含其他内容:
{
"name": "商品名称(简洁明了,2-10字)",
2026-05-24 19:33:45 +08:00
"category": "商品分类(请从以下选择:饮料,零食,日用品,食品,生鲜,烟酒,其他)",
2026-05-24 10:34:02 +08:00
"description": "商品详细描述(50-100字,突出产品特点)",
"suggestedPrice": 建议售价(数字)
} ` ;
2026-05-24 20:00:56 +08:00
const response = await fetch ( AI _API _URL , {
2026-05-24 11:21:24 +08:00
method : 'POST' ,
headers : {
2026-05-24 20:00:56 +08:00
'Authorization' : ` Bearer ${ AI _API _KEY } ` ,
2026-05-24 11:21:24 +08:00
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( {
2026-05-24 20:00:56 +08:00
model : 'qwen3.5-flash' ,
2026-05-24 11:21:24 +08:00
messages : [
{
role : 'user' ,
content : prompt
}
] ,
temperature : 0.7 ,
max _tokens : 500
} ) ,
2026-05-24 14:44:35 +08:00
timeout : 30000
2026-05-24 11:21:24 +08:00
} ) ;
if ( ! response . ok ) {
const errorText = await response . text ( ) ;
2026-05-24 20:00:56 +08:00
console . error ( 'Qwen API Error:' , response . status , errorText ) ;
2026-05-24 19:33:45 +08:00
2026-05-24 14:44:35 +08:00
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 服务暂时不可用,请稍后重试' ;
}
2026-05-24 19:33:45 +08:00
2026-05-24 14:44:35 +08:00
ctx . status = response . status ;
2026-05-24 11:21:24 +08:00
ctx . body = {
2026-05-24 14:44:35 +08:00
code : response . status ,
message : errorMsg
2026-05-24 11:21:24 +08:00
} ;
return ;
}
2026-05-24 11:18:31 +08:00
2026-05-24 11:21:24 +08:00
const data = await response . json ( ) ;
const aiResponse = data . choices ? . [ 0 ] ? . message ? . content ;
2026-05-24 19:33:45 +08:00
2026-05-24 11:21:24 +08:00
if ( ! aiResponse ) {
ctx . status = 500 ;
ctx . body = {
code : 500 ,
message : 'AI 服务返回为空'
} ;
return ;
2026-05-24 11:04:24 +08:00
}
2026-05-24 11:21:24 +08:00
const jsonMatch = aiResponse . match ( /\{[\s\S]*\}/ ) ;
if ( ! jsonMatch ) {
ctx . status = 500 ;
ctx . body = {
code : 500 ,
2026-05-24 14:44:35 +08:00
message : '无法解析 AI 响应格式'
2026-05-24 11:21:24 +08:00
} ;
return ;
2026-05-24 10:34:02 +08:00
}
2026-05-24 11:21:24 +08:00
const productInfo = JSON . parse ( jsonMatch [ 0 ] ) ;
2026-05-24 10:34:02 +08:00
ctx . body = {
code : 200 ,
2026-05-24 11:21:24 +08:00
message : '生成成功' ,
2026-05-24 10:34:02 +08:00
data : productInfo
} ;
} catch ( error ) {
console . error ( '生成商品信息失败:' , error ) ;
2026-05-24 19:33:45 +08:00
2026-05-24 14:44:35 +08:00
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 服务连接中断,请稍后重试' ;
}
2026-05-24 19:33:45 +08:00
ctx . status = 503 ;
ctx . body = {
code : 503 ,
message : errorMsg
} ;
}
} ) ;
router . post ( '/recognize-product' , async ( ctx ) => {
try {
2026-05-24 21:26:53 +08:00
const { imageUrl } = ctx . request . body ;
2026-05-24 19:33:45 +08:00
2026-05-24 21:26:53 +08:00
if ( ! imageUrl ) {
2026-05-24 19:33:45 +08:00
ctx . status = 400 ;
ctx . body = {
code : 400 ,
2026-05-24 21:26:53 +08:00
message : '请提供商品图片地址'
2026-05-24 19:33:45 +08:00
} ;
return ;
}
const prompt = ` 你是一个专业的便利店商品识别助手。请分析这张商品图片,识别出商品信息。
请返回JSON格式的商品信息,只返回一个最可能的商品,不要返回多个:
{
"name": "商品名称(根据图片识别,如果无法确定则返回空字符串)",
"category": "商品分类(从以下选择:饮料,零食,日用品,食品,生鲜,烟酒,其他,如果无法确定则返回空字符串)",
"description": "商品描述(根据图片识别,突出产品特点,如果无法确定则返回空字符串)",
"suggestedPrice": 数字(根据市场价估算,如果无法确定则返回0),
"confidence": 0到1之间的数字(识别置信度)
} ` ;
2026-05-24 21:17:05 +08:00
console . log ( 'Calling Qwen Omni API with image:' , imageUrl ) ;
2026-05-24 20:00:56 +08:00
const response = await fetch ( AI _API _URL , {
2026-05-24 19:33:45 +08:00
method : 'POST' ,
headers : {
2026-05-24 20:00:56 +08:00
'Authorization' : ` Bearer ${ AI _API _KEY } ` ,
2026-05-24 19:33:45 +08:00
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( {
2026-05-24 21:07:51 +08:00
model : 'qwen3.5-omni-flash' ,
2026-05-24 19:33:45 +08:00
messages : [
{
role : 'user' ,
content : [
{
type : 'image_url' ,
image _url : {
2026-05-24 19:39:06 +08:00
url : imageUrl
2026-05-24 19:33:45 +08:00
}
} ,
{
type : 'text' ,
text : prompt
}
]
}
] ,
2026-05-24 21:07:51 +08:00
modalities : [ 'text' ] ,
2026-05-24 19:33:45 +08:00
temperature : 0.3 ,
2026-05-24 21:07:51 +08:00
max _tokens : 500 ,
stream : true ,
stream _options : { include _usage : true }
2026-05-24 19:33:45 +08:00
} ) ,
timeout : 60000
} ) ;
2026-05-24 21:07:51 +08:00
console . log ( 'Qwen Omni response status:' , response . status ) ;
2026-05-24 20:00:56 +08:00
2026-05-24 19:33:45 +08:00
if ( ! response . ok ) {
const errorText = await response . text ( ) ;
2026-05-24 21:07:51 +08:00
console . error ( 'Qwen Omni API Error:' , response . status , errorText ) ;
2026-05-24 19:33:45 +08:00
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 服务暂时不可用,请稍后重试' ;
2026-05-24 20:00:56 +08:00
} else if ( response . status === 404 ) {
errorMsg = '模型不支持图片输入,正在尝试兼容模式...' ;
2026-05-24 19:33:45 +08:00
}
ctx . status = response . status ;
ctx . body = {
code : response . status ,
message : errorMsg
} ;
return ;
}
2026-05-24 21:07:51 +08:00
// 解析 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 ) {
// 跳过解析失败的行
}
}
}
2026-05-24 19:33:45 +08:00
if ( ! aiResponse ) {
ctx . status = 500 ;
ctx . body = {
code : 500 ,
message : 'AI 服务返回为空'
} ;
return ;
}
2026-05-24 21:07:51 +08:00
// 尝试提取 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 ) {
2026-05-24 19:33:45 +08:00
ctx . status = 500 ;
ctx . body = {
code : 500 ,
2026-05-25 11:35:19 +08:00
message : '无法解析 AI 响应格式!'
2026-05-24 19:33:45 +08:00
} ;
return ;
}
2026-05-24 21:58:51 +08:00
// 用 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 ) ;
2026-05-24 19:33:45 +08:00
ctx . body = {
code : 200 ,
message : '识别成功' ,
2026-05-24 21:58:51 +08:00
data : {
aiInfo : productInfo ,
matchedGoods : matchedGoods
}
2026-05-24 19:33:45 +08:00
} ;
} 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 服务连接中断,请稍后重试' ;
}
2026-05-24 14:44:35 +08:00
ctx . status = 503 ;
2026-05-24 10:34:02 +08:00
ctx . body = {
2026-05-24 14:44:35 +08:00
code : 503 ,
message : errorMsg
2026-05-24 10:34:02 +08:00
} ;
}
} ) ;
2026-05-24 21:36:39 +08:00
// 2026-05-24 21:36:31
2026-05-24 10:34:02 +08:00
2026-05-24 21:44:54 +08:00
module . exports = router . routes ( ) ;
// 2026-05-24 21:36:31