Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 961322f8ba | |||
| b40e890e2b | |||
| 9525d56ffc | |||
| 5433605fec | |||
| b981e30f46 |
63
app.py
63
app.py
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
ParamHub - 参数百科
|
||||
AI大模型与硬件参数速查平台
|
||||
v1.5.0 - 支持多图上传和智能解析产品参数
|
||||
v1.6.0 - 后台管理添加大模型接口配置功能
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, jsonify, request
|
||||
@@ -49,7 +49,7 @@ LLM_CONFIG = {
|
||||
'vision_model': 'gpt-4-vision-preview', # 视觉模型(解析图片)
|
||||
}
|
||||
|
||||
# 默认网站配置
|
||||
# 默认网站配置(包含LLM配置)
|
||||
DEFAULT_CONFIG = {
|
||||
'site_name': 'ParamHub',
|
||||
'site_subtitle': '参数百科',
|
||||
@@ -58,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):
|
||||
@@ -140,26 +159,29 @@ def parse_with_llm(text, category_type, images=None):
|
||||
}
|
||||
|
||||
fields = field_templates.get(category_type, field_templates['dynamic'])
|
||||
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
|
||||
|
||||
# 构建消息内容
|
||||
content_parts = []
|
||||
|
||||
# 如果有图片,添加图片内容
|
||||
if images and len(images) > 0:
|
||||
content_parts.append({
|
||||
"type": "text",
|
||||
"text": f"""请分析图片中的产品参数信息,提取结构化数据。
|
||||
prompt_text = """请分析图片中的产品参数信息,提取结构化数据。
|
||||
|
||||
需要提取的字段:
|
||||
{json.dumps(fields, ensure_ascii=False, indent=2)}
|
||||
""" + fields_json + """
|
||||
|
||||
重要要求:
|
||||
1. 图片中可能包含1个或多个产品,请识别所有产品
|
||||
2. 如果是多张图片,请综合分析所有图片内容
|
||||
3. 数字字段只返回数字,不带单位
|
||||
4. 如果某字段没有提及,返回null
|
||||
5. 返回格式:如果识别到多个产品,返回数组 [{"name": ...}, {"name": ...}]; 如果只有一个产品,返回单个对象 {"name": ...}
|
||||
5. 返回格式:如果识别到多个产品,返回数组 [对象列表]; 如果只有一个产品,返回单个对象
|
||||
6. 只返回JSON数据,不要其他内容"""
|
||||
|
||||
content_parts.append({
|
||||
"type": "text",
|
||||
"text": prompt_text
|
||||
})
|
||||
|
||||
# 添加每张图片(支持URL或base64)
|
||||
@@ -194,15 +216,13 @@ def parse_with_llm(text, category_type, images=None):
|
||||
print(f"读取图片失败: {e}")
|
||||
else:
|
||||
# 纯文本解析
|
||||
content_parts.append({
|
||||
"type": "text",
|
||||
"text": f"""请解析以下文本,提取结构化数据。
|
||||
prompt_text = """请解析以下文本,提取结构化数据。
|
||||
|
||||
文本内容:
|
||||
{text}
|
||||
""" + str(text) + """
|
||||
|
||||
需要提取的字段:
|
||||
{json.dumps(fields, ensure_ascii=False, indent=2)}
|
||||
""" + fields_json + """
|
||||
|
||||
要求:
|
||||
1. 根据文本内容智能提取各个字段的值
|
||||
@@ -211,17 +231,24 @@ def parse_with_llm(text, category_type, images=None):
|
||||
4. 返回JSON格式,不要包含任何其他内容
|
||||
|
||||
请直接返回JSON数据:"""
|
||||
|
||||
content_parts.append({
|
||||
"type": "text",
|
||||
"text": prompt_text
|
||||
})
|
||||
|
||||
try:
|
||||
# 使用视觉模型解析
|
||||
model = LLM_CONFIG.get('vision_model', 'gpt-4-vision-preview') if images else LLM_CONFIG['model']
|
||||
# 动态获取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": model,
|
||||
@@ -1375,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")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -553,6 +582,7 @@
|
||||
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 || '';
|
||||
@@ -560,11 +590,18 @@
|
||||
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,
|
||||
@@ -572,6 +609,11 @@
|
||||
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 {
|
||||
@@ -979,31 +1021,50 @@
|
||||
// 从剪贴板粘贴图片
|
||||
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请使用文件选择上传');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,31 +1282,50 @@
|
||||
// 从剪贴板粘贴图片
|
||||
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;
|
||||
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();
|
||||
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) {
|
||||
alert('无法从剪贴板获取图片,请使用文件选择');
|
||||
if (e.name === 'NotAllowedError') {
|
||||
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传,或改用 localhost:19010 访问。');
|
||||
} else {
|
||||
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user