v1.8.0: 模块化重构 + 后台登录认证

This commit is contained in:
2026-06-01 18:31:01 +08:00
parent c9e89c87ea
commit f47befc77a

206
llm.py Normal file
View File

@@ -0,0 +1,206 @@
"""
ParamHub LLM 智能解析模块
"""
import json
import base64
import requests
from config import CATEGORIES_FILE, IMAGES_DIR
from utils import load_data, get_llm_config
def get_parse_prompt_template(category_type, category_id=None, subcategory_id=None):
"""获取解析 prompt 模板(供前端显示和编辑)"""
categories = load_data(CATEGORIES_FILE)
if category_id:
cat = next((c for c in categories if c['id'] == category_id), None)
else:
type_to_cat_id = {'model': 'ai-models', 'gpu': 'gpus', 'cpu': 'cpus', 'dynamic': None}
cat_id = type_to_cat_id.get(category_type)
cat = next((c for c in categories if c['id'] == cat_id), None)
fields = _build_fields(cat, subcategory_id)
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
image_prompt = f"""请分析图片中的产品参数信息,提取结构化数据。
需要提取的字段:
{fields_json}
重要要求:
1. 图片中可能包含1个或多个产品请识别所有产品
2. 如果是多张图片,请综合分析所有图片内容
3. **提取数据时保留原始单位**:字段标签中如有单位标注(如($)、(GB)、(MHz)等),提取时请带上对应单位,保持数据完整性
4. 如果某字段没有提及返回null
5. 返回格式:如果识别到多个产品,返回数组 [对象列表]; 如果只有一个产品,返回单个对象
6. 只返回JSON数据不要其他内容"""
return {
'fields': fields,
'fields_json': fields_json,
'image_prompt': image_prompt,
'category_name': cat.get('name', '') if cat else ''
}
def parse_with_llm(text, category_type, images=None, category_id=None,
subcategory_id=None, custom_prompt=None):
"""使用大模型解析文本/图片,提取结构化数据"""
categories = load_data(CATEGORIES_FILE)
if category_id:
cat = next((c for c in categories if c['id'] == category_id), None)
else:
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 = _build_fields(cat, subcategory_id)
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
content_parts = []
if images and len(images) > 0:
if custom_prompt and custom_prompt.strip():
prompt_text = custom_prompt
else:
prompt_text = f"""请分析图片中的产品参数信息,提取结构化数据。
需要提取的字段:
{fields_json}
重要要求:
1. 图片中可能包含1个或多个产品请识别所有产品
2. 如果是多张图片,请综合分析所有图片内容
3. **提取数据时保留原始单位**:字段标签中如有单位标注,提取时请带上对应单位
4. 如果某字段没有提及返回null
5. 返回格式:如果识别到多个产品,返回数组; 如果只有一个产品,返回单个对象
6. 只返回JSON数据不要其他内容"""
content_parts.append({"type": "text", "text": prompt_text})
for img in images:
if isinstance(img, str):
if img.startswith('http'):
content_parts.append({"type": "image_url", "image_url": {"url": img}})
elif img.startswith('data:'):
content_parts.append({"type": "image_url", "image_url": {"url": img}})
else:
b64 = _load_local_image(img)
if b64:
content_parts.append({"type": "image_url", "image_url": {"url": b64}})
else:
prompt_text = f"""请解析以下文本,提取结构化数据。
文本内容:
{text}
需要提取的字段:
{fields_json}
要求:
1. 根据文本内容智能提取各个字段的值
2. **提取数据时保留原始单位**
3. 如果某字段在文本中没有提及返回null
4. 返回JSON格式不要包含任何其他内容
请直接返回JSON数据"""
content_parts.append({"type": "text", "text": prompt_text})
try:
llm_config = get_llm_config()
model = llm_config.get('vision_model', 'gpt-4-vision-preview') if images else llm_config['model']
response = requests.post(
f"{llm_config['base_url']}/chat/completions",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {llm_config['api_key']}"
},
json={
"model": model,
"messages": [
{"role": "system",
"content": "你是一个产品参数提取助手。只返回JSON不要其他内容。"},
{"role": "user", "content": content_parts}
],
"max_tokens": 2000,
"temperature": 0.1
},
timeout=60
)
if response.status_code == 200:
data = response.json()
content = data['choices'][0]['message']['content'].strip()
if content.startswith('```'):
content = content.split('\n', 1)[1] if '\n' in content else content[3:]
content = content.rsplit('```', 1)[0] if '```' in content else content
parsed = json.loads(content)
results = parsed if isinstance(parsed, list) else [parsed]
return [_clean_result(item) for item in results]
except Exception as e:
print(f"LLM解析失败: {e}")
return [{'name': (text or '未命名产品')[:50], 'description': text}]
# ---- 内部函数 ----
def _build_fields(cat, subcategory_id):
if not cat or 'fields' not in cat:
return {
'name': '名称', 'brand': '品牌', 'price': '价格(数字)',
'year': '年份(数字)', 'specs': '规格参数(JSON对象)',
'description': '简介描述',
}
fields = {}
for field in cat['fields']:
desc = field['label']
desc += '(长文本)' if field.get('input_style') == 'long' else '(文本)'
if field.get('description'):
desc += f" - {field['description']}"
fields[field['key']] = 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']:
desc = field['label']
desc += '(长文本)' if field.get('input_style') == 'long' else '(文本)'
if field.get('description'):
desc += f" - {field['description']}"
fields[field['key']] = desc
return fields
def _load_local_image(img_src: str):
try:
img_path = IMAGES_DIR / img_src.replace('/static/uploads/', '')
if img_path.exists():
with open(img_path, 'rb') as f:
img_data = base64.b64encode(f.read()).decode()
ext = img_path.suffix.lower().lstrip('.')
mime = f'image/{"jpeg" if ext == "jpg" else ext}'
return f'data:{mime};base64,{img_data}'
except Exception:
pass
return None
def _clean_result(item: dict) -> dict:
cleaned = {}
for k, v in item.items():
if v is not None and v != '' and v != 'null':
if isinstance(v, str):
try:
cleaned[k] = float(v) if '.' in v else int(v)
except (ValueError, TypeError):
cleaned[k] = v
else:
cleaned[k] = v
return cleaned