Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e572fbb29b |
134
app.py
134
app.py
@@ -101,64 +101,70 @@ def save_data(file_path, data):
|
||||
|
||||
# ============ 大模型智能解析 ============
|
||||
|
||||
def parse_with_llm(text, category_type, images=None):
|
||||
def parse_with_llm(text, category_type, images=None, category_id=None, subcategory_id=None):
|
||||
"""
|
||||
使用大模型解析文本/图片,提取结构化数据
|
||||
支持多张图片输入,可能解析出多个产品
|
||||
根据类别配置的参数字段进行解析
|
||||
"""
|
||||
|
||||
# 根据类型定义字段模板
|
||||
field_templates = {
|
||||
'model': {
|
||||
'name': '模型名称',
|
||||
'organization': '厂商/组织',
|
||||
'parameters': '参数量(数字,单位B)',
|
||||
'context_length': '上下文长度(数字)',
|
||||
'architecture': '架构类型',
|
||||
'is_open_source': '是否开源(true/false)',
|
||||
'mmlu': 'MMLU分数(数字)',
|
||||
'input_price': '输入价格(数字)',
|
||||
'output_price': '输出价格(数字)',
|
||||
'license': '许可证',
|
||||
'description': '简介描述',
|
||||
},
|
||||
'gpu': {
|
||||
'name': 'GPU名称',
|
||||
'manufacturer': '厂商',
|
||||
'architecture': '架构',
|
||||
'memory_gb': '显存大小(数字,单位GB)',
|
||||
'cuda_cores': 'CUDA核心数(数字)',
|
||||
'tensor_cores': 'Tensor核心数(数字)',
|
||||
'memory_bandwidth_gbs': '显存带宽(数字,单位GB/s)',
|
||||
'fp16_tflops': 'FP16性能(数字,单位TF)',
|
||||
'price_usd': '价格(数字)',
|
||||
'release_year': '发布年份(数字)',
|
||||
'description': '简介描述',
|
||||
},
|
||||
'cpu': {
|
||||
'name': 'CPU名称',
|
||||
'manufacturer': '厂商',
|
||||
'architecture': '架构',
|
||||
'cores': '核心数(数字)',
|
||||
'threads': '线程数(数字)',
|
||||
'base_clock_ghz': '基础频率(数字,单位GHz)',
|
||||
'boost_clock_ghz': '加速频率(数字,单位GHz)',
|
||||
'l3_cache_mb': 'L3缓存(数字,单位MB)',
|
||||
'tdp_watts': 'TDP功耗(数字,单位W)',
|
||||
'price_usd': '价格(数字)',
|
||||
'description': '简介描述',
|
||||
},
|
||||
'dynamic': {
|
||||
# 从类别配置中获取字段定义
|
||||
categories = load_data(CATEGORIES_FILE)
|
||||
|
||||
# 确定类别ID
|
||||
if category_id:
|
||||
cat = next((c for c in categories if c['id'] == category_id), None)
|
||||
else:
|
||||
# 根据类型映射到内置类别ID
|
||||
type_to_cat_id = {'model': 'ai-models', 'gpu': 'gpus', 'cpu': 'cpus'}
|
||||
cat_id = type_to_cat_id.get(category_type)
|
||||
cat = next((c for c in categories if c['id'] == cat_id), None)
|
||||
|
||||
# 构建字段模板
|
||||
fields = {}
|
||||
|
||||
if cat and 'fields' in cat:
|
||||
# 使用类别配置的字段
|
||||
for field in cat['fields']:
|
||||
field_desc = field['label']
|
||||
if field['type'] == 'number':
|
||||
field_desc += '(数字)'
|
||||
elif field['type'] == 'boolean':
|
||||
field_desc += '(true/false)'
|
||||
elif field['type'] == 'date':
|
||||
field_desc += '(日期格式YYYY-MM-DD)'
|
||||
elif field['type'] == 'json':
|
||||
field_desc += '(JSON对象)'
|
||||
if field.get('description'):
|
||||
field_desc += f" - {field['description']}"
|
||||
fields[field['key']] = field_desc
|
||||
|
||||
# 如果有子类别,添加子类别的额外字段
|
||||
if subcategory_id:
|
||||
subcat = next((s for s in cat.get('subcategories', []) if s['id'] == subcategory_id), None)
|
||||
if subcat and 'extra_fields' in subcat:
|
||||
for field in subcat['extra_fields']:
|
||||
field_desc = field['label']
|
||||
if field['type'] == 'number':
|
||||
field_desc += '(数字)'
|
||||
elif field['type'] == 'boolean':
|
||||
field_desc += '(true/false)'
|
||||
elif field['type'] == 'date':
|
||||
field_desc += '(日期格式YYYY-MM-DD)'
|
||||
if field.get('description'):
|
||||
field_desc += f" - {field['description']}"
|
||||
fields[field['key']] = field_desc
|
||||
else:
|
||||
# 兜底:使用默认字段模板
|
||||
fields = {
|
||||
'name': '名称',
|
||||
'brand': '品牌',
|
||||
'price': '价格(数字)',
|
||||
'year': '年份(数字)',
|
||||
'specs': '规格参数',
|
||||
'specs': '规格参数(JSON对象)',
|
||||
'description': '简介描述',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fields = field_templates.get(category_type, field_templates['dynamic'])
|
||||
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
|
||||
|
||||
# 构建消息内容
|
||||
@@ -498,11 +504,13 @@ def api_parse_images():
|
||||
"""
|
||||
解析图片中的产品参数(预览模式,不保存)
|
||||
支持多张图片,可能返回多个产品
|
||||
根据类别配置的参数字段进行解析
|
||||
"""
|
||||
data = request.get_json()
|
||||
text = data.get('text', '')
|
||||
images = data.get('images', [])
|
||||
category_type = data.get('category_type', 'dynamic')
|
||||
subcategory_id = data.get('subcategory_id', '')
|
||||
|
||||
if not text and not images:
|
||||
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||
@@ -510,8 +518,12 @@ def api_parse_images():
|
||||
if not images:
|
||||
return jsonify({'error': '请上传至少一张图片'}), 400
|
||||
|
||||
# 调用大模型解析
|
||||
parsed_list = parse_with_llm(text, category_type, images)
|
||||
# 确定类别ID
|
||||
type_to_cat_id = {'model': 'ai-models', 'gpu': 'gpus', 'cpu': 'cpus', 'dynamic': None}
|
||||
category_id = type_to_cat_id.get(category_type)
|
||||
|
||||
# 调用大模型解析(根据类别字段配置)
|
||||
parsed_list = parse_with_llm(text, category_type, images, category_id=category_id, subcategory_id=subcategory_id)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@@ -691,12 +703,13 @@ def api_smart_add_model():
|
||||
data = request.get_json()
|
||||
text = data.get('text', '')
|
||||
images = data.get('images', [])
|
||||
subcategory_id = data.get('subcategory_id', '') # 子类别ID
|
||||
|
||||
if not text and not images:
|
||||
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||
|
||||
# 大模型解析(支持多图)
|
||||
parsed_list = parse_with_llm(text, 'model', images)
|
||||
# 大模型解析(根据类别字段配置)
|
||||
parsed_list = parse_with_llm(text, 'model', images, category_id='ai-models', subcategory_id=subcategory_id)
|
||||
|
||||
# 处理多个产品
|
||||
results = []
|
||||
@@ -707,8 +720,9 @@ def api_smart_add_model():
|
||||
parsed['id'] = uuid.uuid4().hex[:12]
|
||||
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
parsed['visible'] = True
|
||||
parsed['raw_text'] = text # 保存原始文本
|
||||
parsed['images'] = images # 保存图片
|
||||
parsed['raw_text'] = text
|
||||
parsed['images'] = images
|
||||
parsed['subcategory_id'] = subcategory_id # 保存子类别
|
||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||
parsed['views'] = 0
|
||||
parsed['is_pinned'] = False
|
||||
@@ -718,7 +732,6 @@ def api_smart_add_model():
|
||||
|
||||
save_data(MODELS_FILE, models)
|
||||
|
||||
# 返回添加的产品列表
|
||||
return jsonify({'success': True, 'count': len(results), 'products': results})
|
||||
|
||||
@app.route('/api/gpus/smart-add', methods=['POST'])
|
||||
@@ -727,11 +740,12 @@ def api_smart_add_gpu():
|
||||
data = request.get_json()
|
||||
text = data.get('text', '')
|
||||
images = data.get('images', [])
|
||||
subcategory_id = data.get('subcategory_id', '')
|
||||
|
||||
if not text and not images:
|
||||
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||
|
||||
parsed_list = parse_with_llm(text, 'gpu', images)
|
||||
parsed_list = parse_with_llm(text, 'gpu', images, category_id='gpus', subcategory_id=subcategory_id)
|
||||
|
||||
results = []
|
||||
gpus = load_data(GPUS_FILE)
|
||||
@@ -742,6 +756,7 @@ def api_smart_add_gpu():
|
||||
parsed['visible'] = True
|
||||
parsed['raw_text'] = text
|
||||
parsed['images'] = images
|
||||
parsed['subcategory_id'] = subcategory_id
|
||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||
parsed['views'] = 0
|
||||
parsed['is_pinned'] = False
|
||||
@@ -759,11 +774,12 @@ def api_smart_add_cpu():
|
||||
data = request.get_json()
|
||||
text = data.get('text', '')
|
||||
images = data.get('images', [])
|
||||
subcategory_id = data.get('subcategory_id', '')
|
||||
|
||||
if not text and not images:
|
||||
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||
|
||||
parsed_list = parse_with_llm(text, 'cpu', images)
|
||||
parsed_list = parse_with_llm(text, 'cpu', images, category_id='cpus', subcategory_id=subcategory_id)
|
||||
|
||||
results = []
|
||||
cpus = load_data(CPUS_FILE)
|
||||
@@ -774,6 +790,7 @@ def api_smart_add_cpu():
|
||||
parsed['visible'] = True
|
||||
parsed['raw_text'] = text
|
||||
parsed['images'] = images
|
||||
parsed['subcategory_id'] = subcategory_id
|
||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||
parsed['views'] = 0
|
||||
parsed['is_pinned'] = False
|
||||
@@ -791,11 +808,13 @@ def api_smart_add_item(category_id):
|
||||
data = request.get_json()
|
||||
text = data.get('text', '')
|
||||
images = data.get('images', [])
|
||||
subcategory_id = data.get('subcategory_id', '')
|
||||
|
||||
if not text and not images:
|
||||
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||
|
||||
parsed_list = parse_with_llm(text, 'dynamic', images)
|
||||
# 使用类别配置的字段解析
|
||||
parsed_list = parse_with_llm(text, 'dynamic', images, category_id=category_id, subcategory_id=subcategory_id)
|
||||
|
||||
results = []
|
||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||
@@ -808,6 +827,7 @@ def api_smart_add_item(category_id):
|
||||
parsed['visible'] = True
|
||||
parsed['raw_text'] = text
|
||||
parsed['images'] = images
|
||||
parsed['subcategory_id'] = subcategory_id
|
||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||
parsed['views'] = 0
|
||||
parsed['is_pinned'] = False
|
||||
|
||||
@@ -49,5 +49,54 @@
|
||||
"is_pinned": false,
|
||||
"subcategory_id": "90ce312b560d",
|
||||
"updated_at": "2026-04-28 12:32:50"
|
||||
},
|
||||
{
|
||||
"name": "EOS R7",
|
||||
"brand": "佳能 (Canon)",
|
||||
"year": 2022,
|
||||
"specs": {
|
||||
"品牌": "佳能 (Canon)",
|
||||
"商品编号": "10090975539899",
|
||||
"店铺": "佳能 (Canon) 数码旗舰店",
|
||||
"外接电源": "支持外接电源",
|
||||
"电池类型": "锂离子电池",
|
||||
"接口": "Wi-Fi 蓝牙 HDMI",
|
||||
"高清摄像": "4K超高清视频",
|
||||
"焦点数量": "5915个",
|
||||
"型号": "EOS R7",
|
||||
"有效像素": "3250万",
|
||||
"传感器类型": "CMOS",
|
||||
"上市时间": "2022-06",
|
||||
"取景器类型": "电子取景器",
|
||||
"液晶屏像素": "162万",
|
||||
"液晶屏尺寸": "3.2英寸",
|
||||
"液晶屏类型": "侧翻屏 旋转屏",
|
||||
"最大光圈": "F3.5",
|
||||
"标准ISO感光度": "ISO 100-32000",
|
||||
"连拍速度": "电子最高约30张/秒,机械最高约15张/秒",
|
||||
"存储介质": "SD卡 SDHC卡 SDXC卡",
|
||||
"功能": "Wi-Fi 4K视频 5轴防抖 高速连拍 翻转自拍",
|
||||
"滤镜直径": "55mm",
|
||||
"视频拍摄能力": "4K 60P",
|
||||
"传感器尺寸": "APS-C",
|
||||
"视频采样": "4:2:2",
|
||||
"像素": "3000-4000万",
|
||||
"镜头卡口": "佳能RF卡口",
|
||||
"RAW照片输出": "14bit",
|
||||
"适用对象": "入门级",
|
||||
"类型": "机身"
|
||||
},
|
||||
"description": "入门级机身",
|
||||
"id": "c8c3f124b2ce",
|
||||
"category_id": "71fa2b4d818f",
|
||||
"created_at": "2026-04-28 16:38:03",
|
||||
"visible": true,
|
||||
"raw_text": "",
|
||||
"images": [
|
||||
"/static/uploads/9703a1d16424_1777365365.png"
|
||||
],
|
||||
"publish_date": "",
|
||||
"views": 0,
|
||||
"is_pinned": false
|
||||
}
|
||||
]
|
||||
BIN
static/uploads/9703a1d16424_1777365365.png
Normal file
BIN
static/uploads/9703a1d16424_1777365365.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -330,12 +330,20 @@
|
||||
<div id="smartAddModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-auto">
|
||||
<div class="p-6 border-b flex justify-between items-center sticky top-0 bg-white z-10">
|
||||
<h2 class="text-xl font-bold text-gray-800"><i class="ri-magic-line mr-2 text-orange-600"></i>智能添加(支持多图解析)</h2>
|
||||
<h2 class="text-xl font-bold text-gray-800"><i class="ri-magic-line mr-2 text-orange-600"></i>智能添加(根据参数字段解析)</h2>
|
||||
<button onclick="closeSmartAddModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- 子类别选择 -->
|
||||
<div class="mb-4" id="smartAddSubcategoryArea">
|
||||
<label class="text-sm text-gray-600 mb-2 block">选择子类别(可选,用于匹配特定参数字段)</label>
|
||||
<select id="smartAddSubcategory" class="w-full px-3 py-2 border rounded-lg">
|
||||
<option value="">请选择子类别</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-sm text-gray-500 mb-3">上传产品图片,AI将自动识别并解析参数。支持一次上传多张图片,可识别多个产品。</p>
|
||||
<p class="text-sm text-gray-500 mb-3">上传产品图片,AI将根据类别参数字段配置自动识别并解析参数。支持一次上传多张图片,可识别多个产品。</p>
|
||||
<div class="flex flex-wrap gap-3 mb-3" id="smartImagePreviewArea">
|
||||
<!-- 图片预览区 -->
|
||||
</div>
|
||||
@@ -1905,9 +1913,37 @@
|
||||
document.getElementById('smartAddPreview').classList.add('hidden');
|
||||
document.getElementById('smartImagePreviewArea').innerHTML = '';
|
||||
document.getElementById('smartImageCount').textContent = '0';
|
||||
|
||||
// 加载子类别选项
|
||||
loadSmartAddSubcategories(type);
|
||||
|
||||
document.getElementById('smartAddModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 加载智能添加的子类别选项
|
||||
function loadSmartAddSubcategories(type) {
|
||||
const select = document.getElementById('smartAddSubcategory');
|
||||
let categoryId;
|
||||
|
||||
if (type === 'model') categoryId = 'ai-models';
|
||||
else if (type === 'gpu') categoryId = 'gpus';
|
||||
else if (type === 'cpu') categoryId = 'cpus';
|
||||
else if (type === 'dynamic') categoryId = dynamicCategoryId;
|
||||
else categoryId = null;
|
||||
|
||||
const cat = categories.find(c => c.id === categoryId);
|
||||
const subcats = cat ? (cat.subcategories || []) : [];
|
||||
|
||||
if (subcats.length > 0) {
|
||||
document.getElementById('smartAddSubcategoryArea').classList.remove('hidden');
|
||||
select.innerHTML = '<option value="">请选择子类别</option>' +
|
||||
subcats.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
|
||||
} else {
|
||||
document.getElementById('smartAddSubcategoryArea').classList.add('hidden');
|
||||
select.innerHTML = '<option value="">请选择子类别</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function closeSmartAddModal() {
|
||||
document.getElementById('smartAddModal').classList.add('hidden');
|
||||
}
|
||||
@@ -2018,6 +2054,7 @@
|
||||
// 预览解析结果
|
||||
async function previewSmartParse() {
|
||||
const text = document.getElementById('smartAddText').value.trim();
|
||||
const subcategoryId = document.getElementById('smartAddSubcategory').value;
|
||||
|
||||
if (!text && smartAddImages.length === 0) {
|
||||
alert('请上传图片或输入文本');
|
||||
@@ -2035,7 +2072,8 @@
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
images: smartAddImages,
|
||||
category_type: smartAddType
|
||||
category_type: smartAddType,
|
||||
subcategory_id: subcategoryId
|
||||
})
|
||||
});
|
||||
|
||||
@@ -2074,6 +2112,7 @@
|
||||
|
||||
async function smartAddSubmit() {
|
||||
const text = document.getElementById('smartAddText').value.trim();
|
||||
const subcategoryId = document.getElementById('smartAddSubcategory').value;
|
||||
|
||||
if (!text && smartAddImages.length === 0) {
|
||||
alert('请上传图片或输入文本');
|
||||
@@ -2096,6 +2135,8 @@
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
images: smartAddImages,
|
||||
subcategory_id: subcategoryId
|
||||
images: smartAddImages
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user