6 Commits

Author SHA1 Message Date
961322f8ba chore: 更新版本号到 v1.6.0 2026-04-27 19:58:02 +08:00
b40e890e2b feat: 后台管理添加大模型接口配置功能
- 网站配置页面新增 LLM 配置区域
- 支持配置 API 地址、API Key、文本模型、视觉模型
- LLM 配置从 config.json 动态读取
- 不再使用硬编码的 LLM_CONFIG 常量
2026-04-27 19:57:22 +08:00
9525d56ffc fix: 修复f-string花括号转义问题导致的API错误 2026-04-27 18:44:37 +08:00
5433605fec fix: 增强剪贴板粘贴的错误提示,说明HTTPS/localhost限制 2026-04-27 18:40:36 +08:00
b981e30f46 fix: 修复版本号显示 2026-04-27 18:39:41 +08:00
e2d35b6fa0 feat: 支持多图上传和智能解析产品参数
- 新增 /api/parse-images API 预览解析结果
- 智能添加支持多张图片上传和粘贴
- 支持一次解析出多个产品参数
- 所有类别(模型/GPU/CPU/动态分类)都支持图片解析
- 添加 vision_model 配置支持视觉模型
2026-04-27 18:29:06 +08:00
3 changed files with 565 additions and 133 deletions

346
app.py
View File

@@ -1,7 +1,7 @@
"""
ParamHub - 参数百科
AI大模型与硬件参数速查平台
v1.4.0 - 新增图片上传功能
v1.6.0 - 后台管理添加大模型接口配置功能
"""
from flask import Flask, render_template, jsonify, request
@@ -46,9 +46,10 @@ LLM_CONFIG = {
'base_url': 'http://192.168.2.17:19007/v1',
'api_key': 'xxxx',
'model': 'auto',
'vision_model': 'gpt-4-vision-preview', # 视觉模型(解析图片)
}
# 默认网站配置
# 默认网站配置包含LLM配置
DEFAULT_CONFIG = {
'site_name': 'ParamHub',
'site_subtitle': '参数百科',
@@ -57,12 +58,31 @@ DEFAULT_CONFIG = {
'copyright_year': '2024',
'contact_email': '',
'github_url': '',
# LLM配置
'llm_base_url': 'http://192.168.2.17:19007/v1',
'llm_api_key': '',
'llm_model': 'auto',
'llm_vision_model': 'gpt-4-vision-preview',
}
def get_llm_config():
"""获取LLM配置从config.json动态读取"""
config = load_config()
return {
'base_url': config.get('llm_base_url', DEFAULT_CONFIG['llm_base_url']),
'api_key': config.get('llm_api_key', DEFAULT_CONFIG['llm_api_key']),
'model': config.get('llm_model', DEFAULT_CONFIG['llm_model']),
'vision_model': config.get('llm_vision_model', DEFAULT_CONFIG['llm_vision_model']),
}
def load_config():
"""加载网站配置"""
if CONFIG_FILE.exists():
return json.loads(CONFIG_FILE.read_text(encoding='utf-8'))
loaded = json.loads(CONFIG_FILE.read_text(encoding='utf-8'))
# 合并默认配置(确保新字段存在)
result = DEFAULT_CONFIG.copy()
result.update(loaded)
return result
return DEFAULT_CONFIG.copy()
def save_config(config):
@@ -81,9 +101,10 @@ def save_data(file_path, data):
# ============ 大模型智能解析 ============
def parse_with_llm(text, category_type):
def parse_with_llm(text, category_type, images=None):
"""
使用大模型解析文本,提取结构化数据
使用大模型解析文本/图片,提取结构化数据
支持多张图片输入,可能解析出多个产品
"""
# 根据类型定义字段模板
@@ -138,14 +159,70 @@ def parse_with_llm(text, category_type):
}
fields = field_templates.get(category_type, field_templates['dynamic'])
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
prompt = f"""请解析以下文本,提取结构化数据。
文本内容:
{text}
# 构建消息内容
content_parts = []
# 如果有图片,添加图片内容
if images and len(images) > 0:
prompt_text = """请分析图片中的产品参数信息,提取结构化数据。
需要提取的字段:
{json.dumps(fields, ensure_ascii=False, indent=2)}
""" + fields_json + """
重要要求:
1. 图片中可能包含1个或多个产品请识别所有产品
2. 如果是多张图片,请综合分析所有图片内容
3. 数字字段只返回数字,不带单位
4. 如果某字段没有提及返回null
5. 返回格式:如果识别到多个产品,返回数组 [对象列表]; 如果只有一个产品,返回单个对象
6. 只返回JSON数据不要其他内容"""
content_parts.append({
"type": "text",
"text": prompt_text
})
# 添加每张图片支持URL或base64
for img in images:
if isinstance(img, str):
if img.startswith('http'):
# URL图片
content_parts.append({
"type": "image_url",
"image_url": {"url": img}
})
elif img.startswith('data:'):
# base64图片
content_parts.append({
"type": "image_url",
"image_url": {"url": img}
})
else:
# 本地路径读取并转为base64
try:
img_path = IMAGES_DIR / img.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()
mime_type = f'image/{ext if ext != "jpg" else "jpeg"}'
content_parts.append({
"type": "image_url",
"image_url": {"url": f"data:{mime_type};base64,{img_data}"}
})
except Exception as e:
print(f"读取图片失败: {e}")
else:
# 纯文本解析
prompt_text = """请解析以下文本,提取结构化数据。
文本内容:
""" + str(text) + """
需要提取的字段:
""" + fields_json + """
要求:
1. 根据文本内容智能提取各个字段的值
@@ -154,24 +231,35 @@ def parse_with_llm(text, category_type):
4. 返回JSON格式不要包含任何其他内容
请直接返回JSON数据"""
content_parts.append({
"type": "text",
"text": prompt_text
})
try:
# 动态获取LLM配置
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",
f"{llm_config['base_url']}/chat/completions",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {LLM_CONFIG['api_key']}"
"Authorization": f"Bearer {llm_config['api_key']}"
},
json={
"model": LLM_CONFIG['model'],
"model": model,
"messages": [
{"role": "system", "content": "你是一个数提取助手负责从文本中提取结构化数据。只返回JSON不要其他内容。"},
{"role": "user", "content": prompt}
{"role": "system", "content": "你是一个产品参数提取助手,负责从文本和图片中提取结构化的产品参数数据。只返回JSON不要其他内容。如果图片中包含多个产品,返回数组。"},
{"role": "user", "content": content_parts}
],
"max_tokens": 1000,
"max_tokens": 2000,
"temperature": 0.1
},
timeout=30
timeout=60
)
if response.status_code == 200:
@@ -186,28 +274,38 @@ def parse_with_llm(text, category_type):
# 解析JSON
parsed = json.loads(content)
# 清理null值
cleaned = {}
for k, v in parsed.items():
if v is not None and v != '' and v != 'null':
# 尝试转换数字
if isinstance(v, str):
try:
if '.' in v:
cleaned[k] = float(v)
else:
cleaned[k] = int(v)
except:
cleaned[k] = v
else:
cleaned[k] = v
# 处理结果(可能是数组或单个对象)
results = []
if isinstance(parsed, list):
results = parsed
else:
results = [parsed]
return cleaned
# 清理每个结果的null值
cleaned_results = []
for item in results:
cleaned = {}
for k, v in item.items():
if v is not None and v != '' and v != 'null':
# 尝试转换数字
if isinstance(v, str):
try:
if '.' in v:
cleaned[k] = float(v)
else:
cleaned[k] = int(v)
except:
cleaned[k] = v
else:
cleaned[k] = v
cleaned_results.append(cleaned)
return cleaned_results
except Exception as e:
print(f"LLM解析失败: {e}")
# 降级处理:返回基本结构
return {'name': text[:50], 'description': text}
return [{'name': text[:50] if text else '未命名产品', 'description': text}]
# ============ 页面路由 ============
@@ -393,109 +491,171 @@ def api_toggle_model_visible(model_id):
return jsonify({'success': True, 'visible': model['visible']})
# ============ 图片解析API预览 ============
@app.route('/api/parse-images', methods=['POST'])
def api_parse_images():
"""
解析图片中的产品参数(预览模式,不保存)
支持多张图片,可能返回多个产品
"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
category_type = data.get('category_type', 'dynamic')
if not text and not images:
return jsonify({'error': '文本或图片不能都为空'}), 400
if not images:
return jsonify({'error': '请上传至少一张图片'}), 400
# 调用大模型解析
parsed_list = parse_with_llm(text, category_type, images)
return jsonify({
'success': True,
'count': len(parsed_list),
'products': parsed_list,
'raw_text': text,
'images': images
})
# ============ 智能添加API ============
@app.route('/api/models/smart-add', methods=['POST'])
def api_smart_add_model():
"""智能添加模型(粘贴文本解析"""
"""智能添加模型(支持文本和多图解析,可能添加多个产品"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text:
return jsonify({'error': '文本不能为空'}), 400
if not text and not images:
return jsonify({'error': '文本或图片不能为空'}), 400
# 大模型解析
parsed = parse_with_llm(text, 'model')
# 大模型解析(支持多图)
parsed_list = parse_with_llm(text, 'model', images)
# 补充必要字段
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['publish_date'] = parsed.get('publish_date', '') # 发布日期
parsed['views'] = 0 # 热度初始化为0
parsed['is_pinned'] = False # 置顶初始化为False
# 保存
# 处理多个产品
results = []
models = load_data(MODELS_FILE)
models.append(parsed)
for parsed in parsed_list:
# 补充必要字段
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['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
models.append(parsed)
results.append(parsed)
save_data(MODELS_FILE, models)
return jsonify(parsed)
# 返回添加的产品列表
return jsonify({'success': True, 'count': len(results), 'products': results})
@app.route('/api/gpus/smart-add', methods=['POST'])
def api_smart_add_gpu():
"""智能添加GPU"""
"""智能添加GPU(支持文本和多图解析)"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text:
return jsonify({'error': '文本不能为空'}), 400
if not text and not images:
return jsonify({'error': '文本或图片不能为空'}), 400
parsed = parse_with_llm(text, 'gpu')
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['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
parsed_list = parse_with_llm(text, 'gpu', images)
results = []
gpus = load_data(GPUS_FILE)
gpus.append(parsed)
for parsed in parsed_list:
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['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
gpus.append(parsed)
results.append(parsed)
save_data(GPUS_FILE, gpus)
return jsonify(parsed)
return jsonify({'success': True, 'count': len(results), 'products': results})
@app.route('/api/cpus/smart-add', methods=['POST'])
def api_smart_add_cpu():
"""智能添加CPU"""
"""智能添加CPU(支持文本和多图解析)"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text:
return jsonify({'error': '文本不能为空'}), 400
if not text and not images:
return jsonify({'error': '文本或图片不能为空'}), 400
parsed = parse_with_llm(text, 'cpu')
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['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
parsed_list = parse_with_llm(text, 'cpu', images)
results = []
cpus = load_data(CPUS_FILE)
cpus.append(parsed)
for parsed in parsed_list:
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['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
cpus.append(parsed)
results.append(parsed)
save_data(CPUS_FILE, cpus)
return jsonify(parsed)
return jsonify({'success': True, 'count': len(results), 'products': results})
@app.route('/api/items/<category_id>/smart-add', methods=['POST'])
def api_smart_add_item(category_id):
"""智能添加动态分类数据"""
"""智能添加动态分类数据(支持文本和多图解析)"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text:
return jsonify({'error': '文本不能为空'}), 400
if not text and not images:
return jsonify({'error': '文本或图片不能为空'}), 400
parsed = parse_with_llm(text, 'dynamic')
parsed['id'] = uuid.uuid4().hex[:12]
parsed['category_id'] = category_id
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
parsed['visible'] = True
parsed['raw_text'] = text
parsed['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
parsed_list = parse_with_llm(text, 'dynamic', images)
results = []
items_file = DATA_DIR / f'items_{category_id}.json'
items = load_data(items_file)
items.append(parsed)
for parsed in parsed_list:
parsed['id'] = uuid.uuid4().hex[:12]
parsed['category_id'] = category_id
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
parsed['visible'] = True
parsed['raw_text'] = text
parsed['images'] = images
parsed['publish_date'] = parsed.get('publish_date', '')
parsed['views'] = 0
parsed['is_pinned'] = False
items.append(parsed)
results.append(parsed)
save_data(items_file, items)
return jsonify(parsed)
return jsonify({'success': True, 'count': len(results), 'products': results})
# ============ GPU API ============
@@ -1242,7 +1402,7 @@ def api_delete_image(filename):
if __name__ == '__main__':
print("=" * 50)
print("ParamHub - 参数百科 v1.4.0")
print("ParamHub - 参数百科 v1.6.0")
print("=" * 50)
print(f"访问地址: http://localhost:19010")
print(f"后台管理: http://localhost:19010/admin")

View File

@@ -6,5 +6,9 @@
"copyright_year": "2026",
"contact_email": "wlq@tphai.com",
"github_url": "",
"updated_at": "2026-04-11 02:28:39"
"updated_at": "2026-04-27 19:57:00",
"llm_base_url": "http://192.168.2.17:19007/v1",
"llm_api_key": "",
"llm_model": "auto",
"llm_vision_model": "gpt-4-vision-preview"
}

View File

@@ -57,6 +57,8 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6">网站配置</h1>
<div class="bg-white rounded-xl shadow-sm p-6">
<form id="configForm" class="space-y-6">
<!-- 网站基础配置 -->
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4"><i class="ri-global-line mr-2"></i>网站基础配置</h3>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">网站名称</label>
@@ -87,6 +89,33 @@
<label class="block text-sm font-medium text-gray-700 mb-2">页脚文字</label>
<textarea name="footer_text" id="config_footer_text" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="网站底部的版权信息等"></textarea>
</div>
<!-- 大模型接口配置 -->
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4 mt-8"><i class="ri-robot-line mr-2"></i>大模型接口配置(用于智能解析)</h3>
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>配置用于智能解析产品参数的大模型API接口。文本解析使用普通模型图片解析使用视觉模型。</p>
</div>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API地址</label>
<input type="url" name="llm_base_url" id="config_llm_base_url" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="http://192.168.2.17:19007/v1">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API Key</label>
<input type="text" name="llm_api_key" id="config_llm_api_key" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="留空则不验证">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">文本解析模型</label>
<input type="text" name="llm_model" id="config_llm_model" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="auto">
<p class="text-xs text-gray-500 mt-1">用于解析文本数据,如 deepseek-v3、qwen3.5 等</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">图片解析模型(视觉模型)</label>
<input type="text" name="llm_vision_model" id="config_llm_vision_model" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="gpt-4-vision-preview">
<p class="text-xs text-gray-500 mt-1">用于解析图片,如 Qwen/Qwen2-VL-72B-Instruct、gpt-4-vision-preview 等</p>
</div>
</div>
<div class="flex justify-end gap-4">
<button type="button" onclick="loadSiteConfig()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"><i class="ri-refresh-line mr-1"></i>重置</button>
<button type="button" onclick="saveSiteConfig()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-save-line mr-1"></i>保存配置</button>
@@ -277,25 +306,52 @@
<!-- 智能添加弹窗 -->
<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-3xl w-full mx-4 max-h-[90vh] overflow-auto">
<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">
<p class="text-sm text-gray-500 mb-4">粘贴产品信息文本AI将自动解析并提取结构化数据。支持各种格式的产品介绍、规格参数、价格信息等。</p>
<textarea id="smartAddText" rows="8" class="w-full p-4 border border-gray-200 rounded-lg focus:outline-none focus:border-orange-400 text-gray-700" placeholder="粘贴产品信息文本...
示例:
GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上下文MMLU分数86.4,输入价格$0.03/1K tokens输出价格$0.06/1K tokens商业许可。"></textarea>
<div class="mb-6">
<p class="text-sm text-gray-500 mb-3">上传产品图片AI将自动识别并解析参数。支持一次上传多张图片可识别多个产品。</p>
<div class="flex flex-wrap gap-3 mb-3" id="smartImagePreviewArea">
<!-- 图片预览区 -->
</div>
<div class="flex gap-3">
<input type="file" id="smartImageInput" accept="image/*" multiple class="hidden" onchange="handleSmartImageUpload(event)">
<button onclick="document.getElementById('smartImageInput').click()" class="px-4 py-2 bg-orange-100 text-orange-600 rounded-lg hover:bg-orange-200 text-sm">
<i class="ri-image-add-line mr-1"></i>选择图片(支持多选)
</button>
<button onclick="pasteSmartImageFromClipboard()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-clipboard-line mr-1"></i>粘贴图片
</button>
<button onclick="clearSmartImages()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-delete-bin-line mr-1"></i>清空图片
</button>
</div>
<div class="mt-3 text-xs text-gray-400">
<i class="ri-information-line mr-1"></i>
已选择 <span id="smartImageCount">0</span> 张图片
</div>
</div>
<div class="border-t pt-4">
<label class="text-sm text-gray-600 mb-2 block">补充文本(可选)</label>
<textarea id="smartAddText" rows="4" class="w-full p-4 border border-gray-200 rounded-lg focus:outline-none focus:border-orange-400 text-gray-700" placeholder="可粘贴补充信息文本,与图片一起解析..."></textarea>
</div>
<div id="smartAddPreview" class="mt-4 hidden">
<h3 class="text-sm font-semibold text-gray-700 mb-2">解析结果预览:</h3>
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600" id="smartAddResult"></div>
<h3 class="text-sm font-semibold text-gray-700 mb-2"><i class="ri-checkbox-circle-line text-green-600 mr-1"></i>解析结果预览:</h3>
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600" id="smartAddResult">
<!-- 解析结果显示 -->
</div>
<div class="mt-3 flex gap-2" id="smartAddActions">
<!-- 操作按钮 -->
</div>
</div>
</div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeSmartAddModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="smartAddSubmit()" id="smartAddBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-1"></i>智能解析并添加</button>
<button onclick="previewSmartParse()" id="previewBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-eye-line mr-1"></i>预览解析结果</button>
<button onclick="smartAddSubmit()" id="smartAddBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-1"></i>解析并添加</button>
</div>
</div>
</div>
@@ -526,6 +582,7 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
const res = await fetch('/api/config');
const config = await res.json();
// 网站基础配置
document.getElementById('config_site_name').value = config.site_name || '';
document.getElementById('config_site_subtitle').value = config.site_subtitle || '';
document.getElementById('config_icp_number').value = config.icp_number || '';
@@ -533,11 +590,18 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
document.getElementById('config_github_url').value = config.github_url || '';
document.getElementById('config_copyright_year').value = config.copyright_year || '';
document.getElementById('config_footer_text').value = config.footer_text || '';
// LLM配置
document.getElementById('config_llm_base_url').value = config.llm_base_url || 'http://192.168.2.17:19007/v1';
document.getElementById('config_llm_api_key').value = config.llm_api_key || '';
document.getElementById('config_llm_model').value = config.llm_model || 'auto';
document.getElementById('config_llm_vision_model').value = config.llm_vision_model || 'gpt-4-vision-preview';
}
// 保存网站配置
async function saveSiteConfig() {
const config = {
// 网站基础配置
site_name: document.getElementById('config_site_name').value,
site_subtitle: document.getElementById('config_site_subtitle').value,
icp_number: document.getElementById('config_icp_number').value,
@@ -545,6 +609,11 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
github_url: document.getElementById('config_github_url').value,
copyright_year: document.getElementById('config_copyright_year').value,
footer_text: document.getElementById('config_footer_text').value,
// LLM配置
llm_base_url: document.getElementById('config_llm_base_url').value,
llm_api_key: document.getElementById('config_llm_api_key').value,
llm_model: document.getElementById('config_llm_model').value,
llm_vision_model: document.getElementById('config_llm_vision_model').value,
};
try {
@@ -952,31 +1021,50 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
// 从剪贴板粘贴图片
async function pasteImageFromClipboard(type) {
try {
// 检查剪贴板API是否可用需要HTTPS或localhost
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
for (const itemType of item.types) {
if (itemType.startsWith('image/')) {
found = true;
const blob = await item.getType(itemType);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
currentImages.push(data.url);
updateImagePreview();
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
currentImages.push(data.url);
updateImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
alert('无法从剪贴板获取图片,请使用文件选择');
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
@@ -1151,11 +1239,15 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
// ============ 智能添加功能 ============
let smartAddType = '';
let smartAddImages = []; // 智能添加的图片列表
function openSmartAddModal(type) {
smartAddType = type;
smartAddImages = [];
document.getElementById('smartAddText').value = '';
document.getElementById('smartAddPreview').classList.add('hidden');
document.getElementById('smartImagePreviewArea').innerHTML = '';
document.getElementById('smartImageCount').textContent = '0';
document.getElementById('smartAddModal').classList.remove('hidden');
}
@@ -1163,16 +1255,177 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
document.getElementById('smartAddModal').classList.add('hidden');
}
// 处理智能添加图片上传
async function handleSmartImageUpload(event) {
const files = event.target.files;
for (let file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload/image', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
smartAddImages.push(data.url);
updateSmartImagePreview();
}
} catch (e) {
alert('上传失败: ' + e.message);
}
}
event.target.value = '';
}
// 从剪贴板粘贴图片
async function pasteSmartImageFromClipboard() {
try {
// 检查剪贴板API是否可用需要HTTPS或localhost
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
found = true;
const blob = await item.getType(type);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
smartAddImages.push(data.url);
updateSmartImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
// 清空智能添加图片
function clearSmartImages() {
smartAddImages = [];
updateSmartImagePreview();
}
// 移除单张图片
function removeSmartImage(index) {
smartAddImages.splice(index, 1);
updateSmartImagePreview();
}
// 更新智能添加图片预览
function updateSmartImagePreview() {
const area = document.getElementById('smartImagePreviewArea');
const count = document.getElementById('smartImageCount');
area.innerHTML = smartAddImages.map((url, idx) => `
<div class="relative w-24 h-24 border rounded-lg overflow-hidden group">
<img src="${url}" class="w-full h-full object-cover">
<button onclick="removeSmartImage(${idx})" class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
<i class="ri-close-line"></i>
</button>
</div>
`).join('');
count.textContent = smartAddImages.length;
}
// 预览解析结果
async function previewSmartParse() {
const text = document.getElementById('smartAddText').value.trim();
if (!text && smartAddImages.length === 0) {
alert('请上传图片或输入文本');
return;
}
const btn = document.getElementById('previewBtn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
try {
const res = await fetch('/api/parse-images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
images: smartAddImages,
category_type: smartAddType
})
});
const data = await res.json();
if (data.error) {
alert('解析失败: ' + data.error);
} else {
// 显示解析结果
document.getElementById('smartAddPreview').classList.remove('hidden');
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-line mr-1"></i>识别到 ${data.count} 个产品</div>`;
data.products.forEach((product, idx) => {
html += `
<div class="bg-white rounded-lg p-3 mb-2 border">
<div class="font-medium text-gray-800 mb-2">产品 ${idx + 1}: ${product.name || '未命名'}</div>
<div class="text-xs text-gray-600 grid grid-cols-2 gap-2">
${Object.entries(product).map(([k, v]) => `
<div><span class="text-gray-400">${k}:</span> ${v || '-'}</div>
`).join('')}
</div>
</div>
`;
});
document.getElementById('smartAddResult').innerHTML = html;
}
} catch (e) {
alert('请求失败: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-eye-line mr-1"></i>预览解析结果';
}
async function smartAddSubmit() {
const text = document.getElementById('smartAddText').value.trim();
if (!text) {
alert('请粘贴产品信息文本');
if (!text && smartAddImages.length === 0) {
alert('请上传图片或输入文本');
return;
}
const btn = document.getElementById('smartAddBtn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析并保存中...';
try {
let endpoint;
@@ -1184,7 +1437,10 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
body: JSON.stringify({
text: text,
images: smartAddImages
})
});
const data = await res.json();
@@ -1194,26 +1450,38 @@ GPT-4是OpenAI发布的大语言模型参数量约1.8万亿支持128K上
} else {
// 显示解析结果
document.getElementById('smartAddPreview').classList.remove('hidden');
document.getElementById('smartAddResult').innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-fill mr-1"></i>成功添加 ${data.count} 个产品</div>`;
data.products.forEach((product, idx) => {
html += `
<div class="bg-white rounded-lg p-3 mb-2 border border-green-200">
<div class="font-medium text-gray-800 mb-2">产品 ${idx + 1}: ${product.name || '未命名'}</div>
<div class="text-xs text-gray-500">ID: ${product.id}</div>
</div>
`;
});
document.getElementById('smartAddResult').innerHTML = html;
// 关闭弹窗并刷新列表
closeSmartAddModal();
if (smartAddType === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
loaders[smartAddType]();
}
loadOverview();
alert('智能添加成功!数据已自动解析并保存。');
setTimeout(() => {
closeSmartAddModal();
if (smartAddType === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
loaders[smartAddType]();
}
loadOverview();
}, 1500);
}
} catch (e) {
alert('请求失败: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>智能解析并添加';
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>解析并添加';
}
// ============ 显示切换功能 ============