Compare commits

...
Author SHA1 Message Date
hz4th_coder 23991c3552 v2.0.0 大模型驱动版:移除智能体,直接调用大模型接口
- 核心改造:不再使用 openclaw 智能体执行处理步骤,改为直接调用大模型接口
- 新增 services/llm_client.py:OpenAI 兼容接口客户端,支持多模型配置管理
- 步骤4/5 由大模型直接完成(提取产品数据、填充字段)
- 步骤6 改为直接调用 ParamHub API 提交审核
- 新增 llm_configs 数据库表,默认配置 unsloth/Qwen3.6-27B-Q4_K_M (262144上下文)
- 新增 /api/llm 配置管理 API:增删改查、切换激活、测试连接
- 前端首页新增「大模型配置」面板,可随时新增/切换模型
- 处理步骤名称更新为「大模型」版
2026-08-13 17:15:53 +08:00
12 changed files with 1187 additions and 151 deletions
+102 -11
View File
@@ -7,7 +7,14 @@
- 📚 **内容库管理**:存储和管理搜索获取的相关文章内容
- 🔄 **自动处理流程**:自动从待处理列表中提取产品并处理
- 🔍 **智能搜索**:从内容库和互联网搜索相关数据
- 📝 **数据提取**根据产品类别提取和填充字段
- 🤖 **大模型驱动**直接调用大模型接口完成智能体任务(不再使用 openclaw 智能体)
- 步骤4 提取产品数据 → 大模型筛选相关内容
- 步骤5 填充字段 → 大模型生成产品数据并检查格式
- 步骤6 提交审核 → 直接调用 ParamHub API
- 🧠 **多模型管理**:前端可新增/编辑/删除/切换大模型配置,随时换模型
- OpenAI 兼容接口(base_url + api_key + model_name
- 支持最大上下文窗口配置
- 一键测试连接
-**审核提交**:自动提交到ParamHub后台管理待审核区
- 🕐 **定时任务**:支持定时自动处理产品
- 🔧 **后台任务系统**:抓取任务在后台持续运行,不受页面刷新影响
@@ -32,14 +39,18 @@ param-auto-manager/
├── config.py # 配置文件
├── requirements.txt # Python依赖
├── models/
│ └── database.py # 数据库模型
│ └── database.py # 数据库模型(含 llm_configs 大模型配置表)
├── routes/
│ ├── articles.py # 文章内容库API
│ ├── products.py # 产品处理API
── system.py # 系统管理API
── system.py # 系统管理API
│ ├── process_monitor.py # 处理步骤监控API
│ └── llm.py # 大模型配置管理API
├── services/
│ ├── search_service.py # 搜索服务
│ ├── process_service.py # 数据处理服务
│ ├── process_monitor.py # 处理流程监控(大模型驱动)
│ ├── llm_client.py # 大模型调用客户端(多模型管理)
│ └── paramhub_client.py # ParamHub API客户端
├── utils/
│ └── scheduler.py # 定时任务调度器
@@ -267,6 +278,56 @@ GET /api/system/stats
GET /api/system/health
```
### 大模型配置 API (`/api/llm`)
#### 获取所有大模型配置
```
GET /api/llm/configs
```
#### 获取当前激活的大模型配置
```
GET /api/llm/configs/active
```
#### 新增大模型配置
```
POST /api/llm/configs
Content-Type: application/json
{
"name": "本地Qwen3.6",
"base_url": "http://192.168.2.7:18003/v1",
"api_key": "sk-xxxx",
"model_name": "unsloth/Qwen3.6-27B-Q4_K_M",
"max_context": 262144
}
```
#### 更新大模型配置
```
PUT /api/llm/configs/{config_id}
```
#### 删除大模型配置(激活中的不能删)
```
DELETE /api/llm/configs/{config_id}
```
#### 切换激活的大模型
```
POST /api/llm/configs/{config_id}/activate
```
#### 测试大模型连接
```
POST /api/llm/test
Content-Type: application/json
# 不传 config 则测试当前激活配置;传 config 测试指定配置
{"config": {"base_url": "...", "api_key": "...", "model_name": "..."}}
```
## 处理流程
1. **添加待处理产品**:手动添加或系统自动发现新产品
@@ -275,14 +336,18 @@ GET /api/system/health
- 检查已发布产品(模型/GPU/CPU)
- 检查待审核列表
- 如已存在则跳过后续处理
3. **自动/手动触发处理**
3. **自动/手动触发处理**(大模型驱动)
- 从内容库搜索相关文章
- 从互联网搜索最新数据
- 提取产品具体内容
- 根据类别字段填充数据
- 提交到ParamHub待审核区
- 步骤4:调用**大模型**筛选与产品直接相关且对参数提取有用的内容(替代原智能体)
- 步骤5:调用**大模型**根据相关内容生成产品数据并检查格式(替代原智能体)
- 步骤6:直接调用 ParamHub API 提交到待审核区(不再依赖智能体)
4. **发现新产品**:处理过程中自动发现并添加相关产品
> **大模型配置**:系统所有智能体任务均由当前激活的大模型直接完成。
> 在首页「大模型配置」面板可新增、编辑、切换、删除大模型接口,
> 切换后立即生效,无需重启服务。
## 数据库表结构
### articles (文章内容库)
@@ -297,6 +362,15 @@ GET /api/system/health
### process_history (处理历史)
- id, product_name, category, subcategory, status, review_id, details
### llm_configs (大模型配置)
- id, name, base_url, api_key, model_name, max_context, is_active
### process_sessions / process_steps (处理会话与步骤)
- 记录每次处理会话及每个步骤的状态、耗时、数据、错误信息
### abnormal_products (异常产品)
- 无搜索结果等无法处理的产品记录
## 配置说明
编辑 `config.py` 文件:
@@ -310,22 +384,39 @@ PROCESS_INTERVAL = 300 # 自动处理间隔(秒)
BATCH_SIZE = 5 # 批量处理数量
```
大模型接口在**首页「大模型配置」面板**中管理(数据库 llm_configs 表),首次启动自动写入默认配置:
- 接口地址:`http://192.168.2.7:18003/v1`
- 模型名称:`unsloth/Qwen3.6-27B-Q4_K_M`
- 最大上下文窗口:262144
## 注意事项
1. 确保 ParamHub 服务(端口16041)正常运行
2. 首次运行会自动创建数据库和表结构
3. 定时任务默认每5分钟执行一次自动处理
4. 可通过系统配置API调整自动处理参数
2. 确保大模型接口可用(可在首页测试连接)
3. 首次运行会自动创建数据库和表结构
4. 定时任务默认每5分钟执行一次自动处理
5. 可通过系统配置API调整自动处理参数
6. 大模型调用超时时间 600 秒,长任务请耐心等待
## 日志
日志文件位于 `logs/app.log`,包含:
- 系统启动信息
- 处理过程记录
- 处理过程记录(含大模型调用日志)
- 错误和异常信息
## 版本历史
- v2.0.0 (2026-08-13): 大模型驱动版(主分支大版本)
- 核心改造:不再使用 openclaw 智能体执行步骤流程
- 新增大模型调用服务 llm_client.py,直接调用 OpenAI 兼容接口
- 步骤4/5 改由大模型直接完成(提取数据、填充字段)
- 步骤6 改为直接调用 ParamHub API 提交
- 新增多模型配置管理(数据库 llm_configs 表)
- 前端新增「大模型配置」面板:新增/编辑/删除/切换/测试连接
- 旧版(智能体版)已移至 aliyun-codingplan 分支
- v1.17.0 (2026-07-17): 异常产品管理
- 新增异常产品库,自动存储无法处理的产品
- 内容库和互联网均无搜索结果时存入异常库
+2
View File
@@ -30,12 +30,14 @@ from routes.products import bp as products_bp
from routes.system import bp as system_bp
from routes.tasks import bp as tasks_bp
from routes.process_monitor import bp as process_monitor_bp
from routes.llm import bp as llm_bp
app.register_blueprint(articles_bp)
app.register_blueprint(products_bp)
app.register_blueprint(system_bp)
app.register_blueprint(tasks_bp)
app.register_blueprint(process_monitor_bp)
app.register_blueprint(llm_bp)
# 首页
@app.route('/')
+108
View File
@@ -204,6 +204,21 @@ class Database:
)
''')
# 大模型配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS llm_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
api_key TEXT,
model_name TEXT NOT NULL,
max_context INTEGER DEFAULT 262144,
is_active INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# 异常产品表
cursor.execute('''
CREATE TABLE IF NOT EXISTS abnormal_products (
@@ -226,6 +241,15 @@ class Database:
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_process_steps_session ON process_steps(process_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_llm_configs_active ON llm_configs(is_active)')
# 首次启动时写入默认大模型配置(如果表为空)
cursor.execute('SELECT COUNT(*) FROM llm_configs')
if cursor.fetchone()[0] == 0:
cursor.execute('''
INSERT INTO llm_configs (name, base_url, api_key, model_name, max_context, is_active)
VALUES (?, ?, ?, ?, ?, 1)
''', ('本地Qwen3.6', 'http://192.168.2.7:18003/v1', 'sk-xxxx', 'unsloth/Qwen3.6-27B-Q4_K_M', 262144))
cursor.execute('CREATE INDEX IF NOT EXISTS idx_process_sessions_status ON process_sessions(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_abnormal_products_status ON abnormal_products(status)')
@@ -945,5 +969,89 @@ class Database:
result['search_results'] = json.loads(result['search_results'])
return result
# ========== 大模型配置操作 ==========
def get_llm_configs(self):
"""获取所有大模型配置"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM llm_configs ORDER BY is_active DESC, id ASC')
return [dict(row) for row in cursor.fetchall()]
def get_llm_config(self, config_id):
"""获取单个大模型配置"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM llm_configs WHERE id = ?', (config_id,))
row = cursor.fetchone()
return dict(row) if row else None
def get_active_llm_config(self):
"""获取当前激活的大模型配置"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM llm_configs WHERE is_active = 1 LIMIT 1')
row = cursor.fetchone()
return dict(row) if row else None
def add_llm_config(self, name, base_url, api_key, model_name, max_context=262144):
"""新增大模型配置"""
with self.get_connection() as conn:
cursor = conn.cursor()
# 如果是第一条配置,自动设为激活
cursor.execute('SELECT COUNT(*) FROM llm_configs')
count = cursor.fetchone()[0]
is_active = 1 if count == 0 else 0
cursor.execute('''
INSERT INTO llm_configs (name, base_url, api_key, model_name, max_context, is_active)
VALUES (?, ?, ?, ?, ?, ?)
''', (name, base_url, api_key, model_name, max_context, is_active))
conn.commit()
return cursor.lastrowid
def update_llm_config(self, config_id, **kwargs):
"""更新大模型配置"""
allowed = ['name', 'base_url', 'api_key', 'model_name', 'max_context']
updates = []
values = []
for key, value in kwargs.items():
if key in allowed:
updates.append(f'{key} = ?')
values.append(value)
if not updates:
return False
updates.append('updated_at = CURRENT_TIMESTAMP')
values.append(config_id)
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f'UPDATE llm_configs SET {" , ".join(updates)} WHERE id = ?', values)
conn.commit()
return cursor.rowcount > 0
def delete_llm_config(self, config_id):
"""删除大模型配置(激活中的配置不允许删除)"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT is_active FROM llm_configs WHERE id = ?', (config_id,))
row = cursor.fetchone()
if not row:
return False, '配置不存在'
if row['is_active'] == 1:
return False, '当前激活的配置不能删除,请先切换'
cursor.execute('DELETE FROM llm_configs WHERE id = ?', (config_id,))
conn.commit()
return True, '已删除'
def set_active_llm_config(self, config_id):
"""切换激活的大模型配置"""
with self.get_connection() as conn:
cursor = conn.cursor()
# 先全部取消激活
cursor.execute('UPDATE llm_configs SET is_active = 0, updated_at = CURRENT_TIMESTAMP')
# 设置新的激活
cursor.execute('UPDATE llm_configs SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (config_id,))
conn.commit()
return cursor.rowcount > 0
# 全局数据库实例
db = Database()
+160
View File
@@ -0,0 +1,160 @@
"""
大模型配置管理 API 路由
支持新增、编辑、删除、切换、测试大模型接口
"""
from flask import Blueprint, jsonify, request
from services.llm_client import llm_client
import logging
logger = logging.getLogger('llm_api')
bp = Blueprint('llm', __name__, url_prefix='/api/llm')
@bp.route('/configs', methods=['GET'])
def get_configs():
"""获取所有大模型配置"""
try:
configs = llm_client.get_all_configs()
# 隐藏完整 api_key,只显示掩码
for cfg in configs:
if cfg.get('api_key'):
key = cfg['api_key']
if len(key) > 8:
cfg['api_key_masked'] = key[:4] + '****' + key[-4:]
else:
cfg['api_key_masked'] = '****'
cfg['api_key'] = ''
return jsonify({
'success': True,
'configs': configs
})
except Exception as e:
logger.error(f"获取大模型配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/configs/active', methods=['GET'])
def get_active_config():
"""获取当前激活的大模型配置"""
try:
config = llm_client.get_active_config()
return jsonify({
'success': True,
'config': config
})
except Exception as e:
logger.error(f"获取激活配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/configs', methods=['POST'])
def add_config():
"""新增大模型配置"""
try:
data = request.get_json() or {}
name = data.get('name', '').strip()
base_url = data.get('base_url', '').strip().rstrip('/')
api_key = data.get('api_key', '').strip()
model_name = data.get('model_name', '').strip()
max_context = int(data.get('max_context', 262144) or 262144)
if not name:
return jsonify({'success': False, 'error': '请填写配置名称'}), 400
if not base_url:
return jsonify({'success': False, 'error': '请填写接口地址 base_url'}), 400
if not model_name:
return jsonify({'success': False, 'error': '请填写模型名称'}), 400
config_id = llm_client.add_config(
name=name,
base_url=base_url,
api_key=api_key,
model_name=model_name,
max_context=max_context
)
return jsonify({
'success': True,
'message': f'大模型配置「{name}」已添加',
'config_id': config_id
})
except Exception as e:
logger.error(f"添加大模型配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/configs/<int:config_id>', methods=['PUT'])
def update_config(config_id):
"""更新大模型配置"""
try:
data = request.get_json() or {}
kwargs = {}
for key in ['name', 'base_url', 'api_key', 'model_name', 'max_context']:
if key in data and data[key] is not None:
kwargs[key] = data[key].strip() if isinstance(data[key], str) else data[key]
if not kwargs:
return jsonify({'success': False, 'error': '没有需要更新的字段'}), 400
# 去掉 base_url 末尾的 /
if 'base_url' in kwargs:
kwargs['base_url'] = kwargs['base_url'].rstrip('/')
llm_client.update_config(config_id, **kwargs)
return jsonify({
'success': True,
'message': '配置已更新'
})
except Exception as e:
logger.error(f"更新大模型配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/configs/<int:config_id>', methods=['DELETE'])
def delete_config(config_id):
"""删除大模型配置"""
try:
ok, message = llm_client.delete_config(config_id)
if ok:
return jsonify({'success': True, 'message': message})
return jsonify({'success': False, 'error': message}), 400
except Exception as e:
logger.error(f"删除大模型配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/configs/<int:config_id>/activate', methods=['POST'])
def activate_config(config_id):
"""切换激活的大模型配置"""
try:
if llm_client.set_active(config_id):
cfg = llm_client.get_active_config(force=True)
return jsonify({
'success': True,
'message': f'已切换到大模型「{cfg.get("name")}',
'config': cfg
})
return jsonify({'success': False, 'error': '切换失败,配置不存在'}), 404
except Exception as e:
logger.error(f"切换大模型配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/test', methods=['POST'])
def test_connection():
"""测试大模型连接(可指定配置,不指定则测试当前激活配置)"""
try:
data = request.get_json() or {}
config = None
if data.get('config'):
config = data['config']
ok, message = llm_client.test_connection(config)
return jsonify({
'success': ok,
'message': message
})
except Exception as e:
logger.error(f"测试大模型连接失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
+2 -2
View File
@@ -186,7 +186,7 @@ def get_step_detail(session_id, step_num):
@bp.route('/agent-template', methods=['GET'])
def get_agent_template():
"""获取智能体任务文本模板"""
"""获取大模型任务文本模板"""
try:
if os.path.exists(AGENT_TEMPLATE_FILE):
with open(AGENT_TEMPLATE_FILE, 'r', encoding='utf-8') as f:
@@ -207,7 +207,7 @@ def get_agent_template():
@bp.route('/agent-template', methods=['POST'])
def save_agent_template():
"""保存智能体任务文本模板"""
"""保存大模型任务文本模板"""
try:
data = request.get_json()
template = data.get('template', '')
+237
View File
@@ -0,0 +1,237 @@
"""
大模型调用服务 - 直接调用 OpenAI 兼容接口完成智能体任务
不再使用 openclaw agent 智能体,全部由大模型直接完成
"""
import json
import time
import requests
import logging
from config import Config
logger = logging.getLogger('llm_client')
# 默认大模型配置(首次启动自动写入数据库)
DEFAULT_LLM_CONFIG = {
'name': '本地Qwen3.6',
'base_url': 'http://192.168.2.7:18003/v1',
'api_key': 'sk-xxxx',
'model_name': 'unsloth/Qwen3.6-27B-Q4_K_M',
'max_context': 262144,
'is_active': 1
}
class LLMClient:
"""大模型客户端 - 管理多个模型配置并调用"""
def __init__(self):
self._active = None # 缓存当前激活的配置
# ========== 配置管理 ==========
def get_all_configs(self):
"""获取所有模型配置"""
from models.database import db
return db.get_llm_configs()
def get_active_config(self, force=False):
"""获取当前激活的模型配置"""
if self._active and not force:
return self._active
from models.database import db
config = db.get_active_llm_config()
if config:
self._active = config
else:
# 无激活配置时使用默认值
self._active = dict(DEFAULT_LLM_CONFIG)
return self._active
def add_config(self, name, base_url, api_key, model_name, max_context=262144):
"""新增模型配置"""
from models.database import db
return db.add_llm_config(
name=name,
base_url=base_url,
api_key=api_key,
model_name=model_name,
max_context=max_context
)
def update_config(self, config_id, **kwargs):
"""更新模型配置"""
from models.database import db
db.update_llm_config(config_id, **kwargs)
self._active = None # 使缓存失效
def delete_config(self, config_id):
"""删除模型配置"""
from models.database import db
return db.delete_llm_config(config_id)
def set_active(self, config_id):
"""切换激活的模型配置"""
from models.database import db
ok = db.set_active_llm_config(config_id)
self._active = None
return ok
def test_connection(self, config=None):
"""
测试模型连接
Args:
config: 可选,直接测试指定配置;None 时测试当前激活配置
Returns:
(success, message)
"""
cfg = config or self.get_active_config()
try:
url = cfg['base_url'].rstrip('/') + '/chat/completions'
headers = {'Content-Type': 'application/json'}
api_key = cfg.get('api_key', '')
if api_key:
headers['Authorization'] = f'Bearer {api_key}'
payload = {
'model': cfg['model_name'],
'messages': [
{'role': 'user', 'content': 'ping,请只回复pong'}
],
'max_tokens': 16,
'temperature': 0
}
resp = requests.post(url, json=payload, headers=headers, timeout=60)
if resp.status_code == 200:
data = resp.json()
reply = data.get('choices', [{}])[0].get('message', {}).get('content', '')
return True, f"连接成功: {reply[:50]}"
else:
return False, f"HTTP {resp.status_code}: {resp.text[:200]}"
except Exception as e:
return False, str(e)
# ========== 调用大模型 ==========
def chat(self, messages, temperature=0.3, max_tokens=8192, timeout=600, config=None):
"""
调用大模型对话接口
Args:
messages: [{'role': 'user'/'system'/'assistant', 'content': '...'}]
temperature: 温度
max_tokens: 最大输出token数
timeout: 超时时间(秒)
config: 可选,指定使用的模型配置;None 使用当前激活配置
Returns:
(success, result)
success=True 时 result 为文本内容
success=False 时 result 为错误信息
"""
cfg = config or self.get_active_config()
try:
url = cfg['base_url'].rstrip('/') + '/chat/completions'
headers = {'Content-Type': 'application/json'}
api_key = cfg.get('api_key', '')
if api_key:
headers['Authorization'] = f'Bearer {api_key}'
payload = {
'model': cfg['model_name'],
'messages': messages,
'temperature': temperature,
'max_tokens': max_tokens
}
logger.info(f"[LLM] 调用 {cfg['model_name']} @ {cfg['base_url']} | 消息数: {len(messages)} | 输入字符: {sum(len(m.get('content','')) for m in messages)}")
resp = requests.post(url, json=payload, headers=headers, timeout=timeout)
if resp.status_code != 200:
logger.error(f"[LLM] HTTP {resp.status_code}: {resp.text[:300]}")
return False, f"大模型接口返回错误 HTTP {resp.status_code}: {resp.text[:300]}"
data = resp.json()
reply = data.get('choices', [{}])[0].get('message', {}).get('content', '')
usage = data.get('usage', {})
logger.info(f"[LLM] 返回 {len(reply)} 字符 | usage: {usage}")
return True, reply
except requests.exceptions.Timeout:
return False, f"大模型调用超时(>{timeout}秒)"
except requests.exceptions.ConnectionError as e:
return False, f"无法连接大模型服务: {e}"
except Exception as e:
logger.error(f"[LLM] 调用异常: {e}")
return False, str(e)
def chat_json(self, messages, temperature=0.1, max_tokens=8192, timeout=600, config=None):
"""
调用大模型并解析 JSON 输出
Returns:
(success, data_or_error)
"""
# 追加要求JSON输出的系统提示
sys_prompt = (
"你是一个严格输出JSON的程序化助手。"
"你必须只输出一个合法的JSON对象,不要输出任何多余文字、解释或markdown代码块标记。"
"确保JSON语法正确,可以被json.loads直接解析。"
)
full_messages = [{'role': 'system', 'content': sys_prompt}] + messages
ok, result = self.chat(full_messages, temperature=temperature, max_tokens=max_tokens, timeout=timeout, config=config)
if not ok:
return False, result
parsed = self._extract_json(result)
if parsed is None:
return False, f"大模型输出无法解析为JSON: {result[:300]}"
return True, parsed
def _extract_json(self, text):
"""从文本中提取JSON对象"""
if not text:
return None
text = text.strip()
# 去掉 markdown 代码块标记
if text.startswith('```'):
lines = text.split('\n')
# 去掉第一行 ```json 或 ```
lines = lines[1:]
# 去掉最后一行 ```
if lines and lines[-1].strip().startswith('```'):
lines = lines[:-1]
text = '\n'.join(lines).strip()
# 直接尝试解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 尝试提取 {...} 块
import re
match = re.search(r'\{.*\}', text, re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
pass
# 尝试提取 [...] 块
match = re.search(r'\[.*\]', text, re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
pass
return None
# 全局大模型客户端实例
llm_client = LLMClient()
+97 -125
View File
@@ -5,24 +5,24 @@ import os
import time
import uuid
import json
import subprocess
import threading
import logging
from datetime import datetime
from models.database import db
from services.search_service import search_service
from services.paramhub_client import paramhub_client
from services.llm_client import llm_client
logger = logging.getLogger('process_monitor')
# 处理步骤定义
# 处理步骤定义(大模型版)
PROCESS_STEPS = [
{'num': 1, 'name': '搜索内容库', 'description': '从内容库搜索相关文章'},
{'num': 2, 'name': '搜索互联网', 'description': '从互联网搜索最新数据'},
{'num': 3, 'name': '抓取网页内容', 'description': '抓取搜索结果网页的详细内容'},
{'num': 4, 'name': '提取产品数据(智能体)', 'description': '调用hz4th_editor智能体提取产品相关内容'},
{'num': 5, 'name': '填充字段(智能体)', 'description': '调用智能体生成产品数据并检查格式'},
{'num': 6, 'name': '提交审核(智能体)', 'description': '调用智能体将产品数据提交到ParamHub审核系统'},
{'num': 4, 'name': '提取产品数据(大模型)', 'description': '调用大模型筛选并提取产品相关内容'},
{'num': 5, 'name': '填充字段(大模型)', 'description': '调用大模型生成产品数据并检查格式'},
{'num': 6, 'name': '提交审核', 'description': '提交产品数据到ParamHub审核系统'},
]
class ProcessMonitor:
@@ -203,17 +203,17 @@ class ProcessMonitor:
db.update_task_status(bg_task_id, 'failed', error_message=str(e))
self._fail_step(session_id, 3, str(e))
# 步骤4: 提取产品数据(调用智能体执行
# 步骤4: 提取产品数据(调用大模型筛选相关内容
if not self._check_pause(session_id):
self._start_step(session_id, product_name, 4, '提取产品数据(智能体)')
self._start_step(session_id, product_name, 4, '提取产品数据(大模型)')
try:
# 构建任务文本
task_text = self._build_agent_task(
product_name, category, subcategory, all_data
)
# 调用智能体
agent_result = self._call_agent(task_text)
# 直接调用大模型
agent_result = self._call_llm(task_text)
if agent_result.get('success'):
parsed = self._parse_agent_response(agent_result.get('output', ''))
@@ -243,40 +243,42 @@ class ProcessMonitor:
self._complete_step(session_id, 4, {
'has_data': True,
'agent': 'hz4th_editor',
'agent': '大模型',
'model': self._get_active_model_name(),
'task_text': task_text,
'relevant_ids': parsed['relevant_ids'],
'relevant_count': len(relevant_contents),
'confidence': parsed.get('confidence', 'unknown'),
'agent_output': agent_result.get('output', '')[:2000]
})
logger.info(f"[{session_id}] 步骤4完成: 智能体返回 {len(parsed['relevant_ids'])} 个相关ID")
logger.info(f"[{session_id}] 步骤4完成: 大模型返回 {len(parsed['relevant_ids'])} 个相关ID")
else:
all_data['extracted_data'] = None
self._complete_step(session_id, 4, {
'has_data': False,
'agent': 'hz4th_editor',
'agent': '大模型',
'model': self._get_active_model_name(),
'task_text': task_text,
'agent_output': agent_result.get('output', '')[:2000]
}, status='skipped')
result['message'] = '智能体未找到相关数据ID'
result['message'] = '大模型未找到相关数据ID'
else:
self._fail_step(session_id, 4, f"智能体调用失败: {agent_result.get('error', '未知错误')}")
result['message'] = f'智能体调用失败: {agent_result.get("error")}'
self._fail_step(session_id, 4, f"大模型调用失败: {agent_result.get('error', '未知错误')}")
result['message'] = f'大模型调用失败: {agent_result.get("error")}'
except Exception as e:
self._fail_step(session_id, 4, str(e))
# 步骤5: 填充字段(调用智能体生成数据并检查格式)
# 步骤5: 填充字段(调用大模型生成数据并检查格式)
if not self._check_pause(session_id) and all_data['extracted_data']:
self._start_step(session_id, product_name, 5, '填充字段(智能体)')
self._start_step(session_id, product_name, 5, '填充字段(大模型)')
try:
# 构建任务文本
fill_task_text = self._build_fill_fields_task(
product_name, category, subcategory, all_data['extracted_data']
)
# 调用智能体
fill_agent_result = self._call_agent(fill_task_text)
# 直接调用大模型
fill_agent_result = self._call_llm(fill_task_text)
if fill_agent_result.get('success'):
fill_parsed = self._parse_fill_agent_response(fill_agent_result.get('output', ''))
@@ -293,7 +295,8 @@ class ProcessMonitor:
self._complete_step(session_id, 5, {
'filled': True,
'agent': 'hz4th_editor',
'agent': '大模型',
'model': self._get_active_model_name(),
'task_text': fill_task_text,
'product_data': product_data,
'format_check': format_check,
@@ -307,67 +310,54 @@ class ProcessMonitor:
result['message'] = '数据格式验证失败'
else:
error_msg = fill_parsed.get('message', '未知错误') if fill_parsed else '解析失败'
self._fail_step(session_id, 5, f"智能体执行失败: {error_msg}")
result['message'] = f'智能体执行失败: {error_msg}'
self._fail_step(session_id, 5, f"大模型执行失败: {error_msg}")
result['message'] = f'大模型执行失败: {error_msg}'
else:
self._fail_step(session_id, 5, f"智能体调用失败: {fill_agent_result.get('error', '未知错误')}")
result['message'] = f'智能体调用失败: {fill_agent_result.get("error")}'
self._fail_step(session_id, 5, f"大模型调用失败: {fill_agent_result.get('error', '未知错误')}")
result['message'] = f'大模型调用失败: {fill_agent_result.get("error")}'
except Exception as e:
self._fail_step(session_id, 5, str(e))
# 步骤6: 提交审核(调用智能体执行
# 步骤6: 提交审核(直接调用ParamHub API,不再依赖智能体)
if not self._check_pause(session_id) and all_data['filled_data']:
self._start_step(session_id, product_name, 6, '提交审核(智能体)')
self._start_step(session_id, product_name, 6, '提交审核')
try:
# 构建任务文本
submit_task_text = self._build_submit_task(
product_name, category, subcategory, all_data['filled_data']
category_type = self._get_category_type(category)
subcategory_id = subcategory
success, review_id_or_error = paramhub_client.submit_for_review(
category_type,
all_data['filled_data'],
subcategory_id
)
# 调用智能体
submit_agent_result = self._call_agent(submit_task_text)
if submit_agent_result.get('success'):
submit_parsed = self._parse_submit_agent_response(submit_agent_result.get('output', ''))
if success:
review_id = review_id_or_error
self._complete_step(session_id, 6, {
'submitted': True,
'agent': 'ParamHub API',
'review_id': review_id,
'product_data': all_data['filled_data']
})
if submit_parsed and submit_parsed.get('success'):
review_id = submit_parsed.get('review_id')
if review_id:
self._complete_step(session_id, 6, {
'submitted': True,
'agent': 'hz4th_editor',
'task_text': submit_task_text,
'review_id': review_id,
'agent_output': submit_agent_result.get('output', '')[:2000]
})
result['success'] = True
result['review_id'] = review_id
db.update_session_status(session_id, 'completed',
review_id=review_id,
result=json.dumps(result, ensure_ascii=False))
db.add_process_history(
product_name=product_name,
category=category,
subcategory=subcategory,
status='submitted',
review_id=review_id,
details=all_data
)
logger.info(f"[{session_id}] 步骤6完成: 智能体提交成功, review_id={review_id}")
else:
self._fail_step(session_id, 6, '智能体未返回review_id')
result['message'] = '智能体提交成功但未获取到review_id'
else:
error_msg = submit_parsed.get('message', '未知错误') if submit_parsed else '解析失败'
self._fail_step(session_id, 6, f"智能体提交失败: {error_msg}")
result['message'] = f'智能体提交失败: {error_msg}'
result['success'] = True
result['review_id'] = review_id
db.update_session_status(session_id, 'completed',
review_id=review_id,
result=json.dumps(result, ensure_ascii=False))
db.add_process_history(
product_name=product_name,
category=category,
subcategory=subcategory,
status='submitted',
review_id=review_id,
details=all_data
)
logger.info(f"[{session_id}] 步骤6完成: 提交成功, review_id={review_id}")
else:
self._fail_step(session_id, 6, f"智能体调用失败: {submit_agent_result.get('error', '未知错误')}")
result['message'] = f'智能体调用失败: {submit_agent_result.get("error")}'
self._fail_step(session_id, 6, f"提交失败: {review_id_or_error}")
result['message'] = f'提交失败: {review_id_or_error}'
except Exception as e:
self._fail_step(session_id, 6, str(e))
@@ -522,67 +512,49 @@ class ProcessMonitor:
return task
def _call_agent(self, task_text):
"""调用智能体执行任务"""
import signal
def _get_active_model_name(self):
"""获取当前激活的模型名称(用于日志/步骤展示)"""
try:
cmd = [
'openclaw', 'agent',
'--agent', 'hz4th_editor',
'--message', task_text,
'--json' # 输出JSON格式以便解析
cfg = llm_client.get_active_config()
return cfg.get('model_name', '未知模型')
except Exception:
return '未知模型'
def _call_llm(self, task_text):
"""直接调用大模型执行任务(替代原来的 openclaw 智能体)"""
try:
logger.info(f"调用大模型执行任务,任务文本 [{len(task_text)} 字符]")
# 构建消息
messages = [
{
'role': 'system',
'content': (
'你是一个专业的产品数据提取与整理助手。'
'严格按照用户要求输出结果,遵循任务文本中的输出格式要求。'
'对于要求JSON输出的任务,必须只输出合法JSON,不要添加多余解释。'
)
},
{'role': 'user', 'content': task_text}
]
logger.info(f"调用智能体命令: openclaw agent --agent hz4th_editor --message '[任务文本 {len(task_text)} 字符]' --json")
# 使用Popen以便更好地控制超时和进程杀死
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=os.setsid # 创建新进程组,方便杀死所有子进程
# 直接调用大模型
ok, result = llm_client.chat(
messages,
temperature=0.2,
max_tokens=8192,
timeout=600
)
try:
stdout, stderr = proc.communicate(timeout=180) # 3分钟超时
raw_output = stdout.decode('utf-8', errors='replace').strip()
if proc.returncode == 0:
# 解析JSON输出
try:
data = json.loads(raw_output)
# 提取实际回复文本: result.payloads[0].text
payloads = data.get('result', {}).get('payloads', [])
if payloads and isinstance(payloads[0], dict):
output = payloads[0].get('text', '')
else:
output = raw_output
logger.info(f"智能体返回: {output[:500]}...")
return {'success': True, 'output': output}
except json.JSONDecodeError as e:
logger.warning(f"JSON解析失败,使用原始输出: {e}")
return {'success': True, 'output': raw_output}
else:
error = stderr.decode('utf-8', errors='replace').strip() or raw_output
logger.error(f"智能体调用失败(returncode={proc.returncode}): {error}")
return {'success': False, 'error': error}
except subprocess.TimeoutExpired:
# 超时,杀死整个进程组
logger.error(f"智能体执行超时(>3分钟),杀死进程组")
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except Exception:
proc.kill()
proc.wait()
return {'success': False, 'error': '智能体执行超时(>3分钟)'}
if ok:
logger.info(f"大模型返回: {result[:500]}...")
return {'success': True, 'output': result}
else:
logger.error(f"大模型调用失败: {result}")
return {'success': False, 'error': result}
except FileNotFoundError:
return {'success': False, 'error': 'openclaw命令未找到'}
except Exception as e:
logger.error(f"智能体调用异常: {e}")
logger.error(f"大模型调用异常: {e}")
return {'success': False, 'error': str(e)}
def _parse_agent_response(self, output):
+83 -1
View File
@@ -834,4 +834,86 @@ body {
.search-quick-info small {
font-size: 12px;
}
}
/* ===== 大模型配置 ===== */
.llm-tip {
background: #f0f7ff;
border: 1px solid #cfe4ff;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 15px;
color: #3b6ea5;
font-size: 13px;
}
.active-model-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: #f0f9f0;
border: 1px solid #b7e0b7;
color: #2d7a2d;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
margin-right: 10px;
}
.status-dot.green {
background: #10b981;
animation: pulse 2s infinite;
}
.status-dot.gray {
background: #9ca3af;
animation: none;
}
.badge-active {
background: #10b981;
color: white;
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
}
.action-buttons {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
.btn-success {
background: #10b981;
color: white;
border: none;
}
.btn-success:hover {
background: #0ea371;
}
.btn-danger {
background: #ef4444;
color: white;
border: none;
}
.btn-danger:hover {
background: #dc2626;
}
code {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
}
+308 -1
View File
@@ -8,6 +8,7 @@ let currentArticleId = null;
document.addEventListener('DOMContentLoaded', () => {
refreshData();
loadConfig();
loadLlmConfigs();
// 搜索框事件
document.getElementById('article-search').addEventListener('input', (e) => {
@@ -878,4 +879,310 @@ async function saveSearchResultToLibrary() {
closeModal('search-result-modal');
loadArticles();
loadStats();
}
}
// ========== 大模型配置管理 ==========
let llmEditId = null; // 当前编辑的配置ID
// 加载大模型配置列表
async function loadLlmConfigs() {
try {
const response = await fetch(`${API_BASE}/api/llm/configs`);
const data = await response.json();
if (data.success) {
displayLlmConfigs(data.configs);
updateActiveModelBadge(data.configs);
}
} catch (error) {
console.error('加载大模型配置失败:', error);
}
}
// 显示大模型配置列表
function displayLlmConfigs(configs) {
const container = document.getElementById('llm-config-table');
if (!configs || configs.length === 0) {
container.innerHTML = '<tr><td colspan="6" class="empty-text">暂无大模型配置</td></tr>';
return;
}
container.innerHTML = configs.map(cfg => `
<tr>
<td>
<strong>${escapeHtml(cfg.name)}</strong>
${cfg.is_active ? ' <span class="badge badge-active">当前</span>' : ''}
</td>
<td><code>${escapeHtml(cfg.base_url)}</code></td>
<td><code>${escapeHtml(cfg.model_name)}</code></td>
<td>${cfg.max_context ? Number(cfg.max_context).toLocaleString() : '-'}</td>
<td>
${cfg.is_active
? '<span class="status-badge"><span class="status-dot green"></span> 使用中</span>'
: '<span class="status-badge"><span class="status-dot gray"></span> 未启用</span>'}
</td>
<td>
<div class="action-buttons">
${!cfg.is_active ? `
<button onclick="activateLlmConfig(${cfg.id})" class="btn btn-sm btn-success" title="切换为当前使用">
<i class="ri-switch-line"></i> 启用
</button>
` : ''}
<button onclick="editLlmConfig(${cfg.id})" class="btn btn-sm btn-secondary" title="编辑">
<i class="ri-edit-line"></i>
</button>
${!cfg.is_active ? `
<button onclick="deleteLlmConfig(${cfg.id})" class="btn btn-sm btn-danger" title="删除">
<i class="ri-delete-bin-line"></i>
</button>
` : ''}
<button onclick="testLlmConfigById(${cfg.id})" class="btn btn-sm btn-secondary" title="测试连接">
<i class="ri-connection-line"></i>
</button>
</div>
</td>
</tr>
`).join('');
}
// 更新顶部激活模型徽章
function updateActiveModelBadge(configs) {
const active = (configs || []).find(c => c.is_active);
const nameEl = document.getElementById('active-model-name');
if (active) {
nameEl.textContent = `${active.name} (${active.model_name})`;
} else {
nameEl.textContent = '未配置大模型';
}
}
// 打开新增大模型模态框
function showAddLlmModal() {
llmEditId = null;
document.getElementById('add-llm-modal').classList.add('active');
// 清空表单
document.getElementById('llm-name').value = '';
document.getElementById('llm-base-url').value = 'http://192.168.2.7:18003/v1';
document.getElementById('llm-api-key').value = 'sk-xxxx';
document.getElementById('llm-model-name').value = 'unsloth/Qwen3.6-27B-Q4_K_M';
document.getElementById('llm-max-context').value = 262144;
// 重置按钮文字
const modalTitle = document.querySelector('#add-llm-modal .modal-header h3');
modalTitle.innerHTML = '<i class="ri-openai-line"></i> 新增大模型配置';
const saveBtn = document.querySelector('#add-llm-modal .modal-footer .btn-primary');
saveBtn.textContent = '添加';
saveBtn.onclick = addLlmConfig;
}
// 编辑大模型配置
async function editLlmConfig(configId) {
try {
const response = await fetch(`${API_BASE}/api/llm/configs`);
const data = await response.json();
if (!data.success) return;
const cfg = data.configs.find(c => c.id === configId);
if (!cfg) return;
llmEditId = configId;
document.getElementById('llm-name').value = cfg.name || '';
document.getElementById('llm-base-url').value = cfg.base_url || '';
document.getElementById('llm-api-key').value = cfg.api_key || '';
document.getElementById('llm-model-name').value = cfg.model_name || '';
document.getElementById('llm-max-context').value = cfg.max_context || 262144;
const modalTitle = document.querySelector('#add-llm-modal .modal-header h3');
modalTitle.innerHTML = '<i class="ri-edit-line"></i> 编辑大模型配置';
const saveBtn = document.querySelector('#add-llm-modal .modal-footer .btn-primary');
saveBtn.textContent = '保存修改';
saveBtn.onclick = updateLlmConfig;
document.getElementById('add-llm-modal').classList.add('active');
} catch (error) {
showToast('加载配置失败', 'error');
}
}
// 添加大模型配置
async function addLlmConfig() {
const name = document.getElementById('llm-name').value.trim();
const baseUrl = document.getElementById('llm-base-url').value.trim();
const apiKey = document.getElementById('llm-api-key').value.trim();
const modelName = document.getElementById('llm-model-name').value.trim();
const maxContext = parseInt(document.getElementById('llm-max-context').value) || 262144;
if (!name || !baseUrl || !modelName) {
showToast('请填写必填字段(名称、接口地址、模型名称)', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/llm/configs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, base_url: baseUrl, api_key: apiKey, model_name: modelName, max_context: maxContext })
});
const data = await response.json();
if (data.success) {
showToast('大模型配置已添加', 'success');
closeModal('add-llm-modal');
loadLlmConfigs();
} else {
showToast('添加失败: ' + data.error, 'error');
}
} catch (error) {
showToast('添加失败', 'error');
}
}
// 更新大模型配置
async function updateLlmConfig() {
if (!llmEditId) return;
const name = document.getElementById('llm-name').value.trim();
const baseUrl = document.getElementById('llm-base-url').value.trim();
const apiKey = document.getElementById('llm-api-key').value.trim();
const modelName = document.getElementById('llm-model-name').value.trim();
const maxContext = parseInt(document.getElementById('llm-max-context').value) || 262144;
if (!name || !baseUrl || !modelName) {
showToast('请填写必填字段(名称、接口地址、模型名称)', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/llm/configs/${llmEditId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, base_url: baseUrl, api_key: apiKey, model_name: modelName, max_context: maxContext })
});
const data = await response.json();
if (data.success) {
showToast('配置已更新', 'success');
closeModal('add-llm-modal');
loadLlmConfigs();
} else {
showToast('更新失败: ' + data.error, 'error');
}
} catch (error) {
showToast('更新失败', 'error');
}
}
// 删除大模型配置
async function deleteLlmConfig(configId) {
if (!confirm('确定删除该大模型配置吗?')) return;
try {
const response = await fetch(`${API_BASE}/api/llm/configs/${configId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('配置已删除', 'success');
loadLlmConfigs();
} else {
showToast('删除失败: ' + data.error, 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// 切换激活的大模型配置
async function activateLlmConfig(configId) {
if (!confirm('确定切换使用该大模型吗?后续处理将使用它执行所有智能体任务。')) return;
try {
const response = await fetch(`${API_BASE}/api/llm/configs/${configId}/activate`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
loadLlmConfigs();
} else {
showToast('切换失败: ' + data.error, 'error');
}
} catch (error) {
showToast('切换失败', 'error');
}
}
// 测试当前表单中的连接
async function testLlmConnection() {
const name = document.getElementById('llm-name').value.trim();
const baseUrl = document.getElementById('llm-base-url').value.trim();
const apiKey = document.getElementById('llm-api-key').value.trim();
const modelName = document.getElementById('llm-model-name').value.trim();
if (!baseUrl || !modelName) {
showToast('请先填写接口地址和模型名称', 'error');
return;
}
const btn = document.getElementById('llm-test-btn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line"></i> 测试中...';
try {
const response = await fetch(`${API_BASE}/api/llm/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config: { base_url: baseUrl, api_key: apiKey, model_name: modelName }
})
});
const data = await response.json();
if (data.success) {
showToast('连接成功: ' + data.message, 'success');
} else {
showToast('连接失败: ' + data.message, 'error');
}
} catch (error) {
showToast('测试失败', 'error');
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-connection-line"></i> 测试连接';
}
// 测试指定配置的连接
async function testLlmConfigById(configId) {
try {
const response = await fetch(`${API_BASE}/api/llm/configs`);
const data = await response.json();
if (!data.success) return;
const cfg = data.configs.find(c => c.id === configId);
if (!cfg) return;
showToast('正在测试连接...', '');
const testResp = await fetch(`${API_BASE}/api/llm/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config: { base_url: cfg.base_url, api_key: cfg.api_key, model_name: cfg.model_name }
})
});
const testData = await testResp.json();
if (testData.success) {
showToast('连接成功: ' + testData.message, 'success');
} else {
showToast('连接失败: ' + testData.message, 'error');
}
} catch (error) {
showToast('测试失败', 'error');
}
}
+1 -1
View File
@@ -433,7 +433,7 @@ function showToast(message, type = '') {
}, 3000);
}
// ===== 智能体任务模板 =====
// ===== 大模型任务模板 =====
// 加载模板
async function loadAgentTemplate() {
+78
View File
@@ -207,6 +207,40 @@
</div>
</div>
</div>
<!-- 大模型配置 -->
<div class="panel full-width">
<div class="panel-header">
<h2><i class="ri-openai-line"></i> 大模型配置</h2>
<div class="panel-actions">
<span class="active-model-badge" id="active-model-badge">
<span class="status-dot green"></span>
<span id="active-model-name">加载中...</span>
</span>
<button onclick="showAddLlmModal()" class="btn btn-primary btn-sm">
<i class="ri-add-line"></i> 新增大模型
</button>
</div>
</div>
<div class="panel-body">
<p class="llm-tip"><i class="ri-information-line"></i> 系统所有智能体任务(提取数据、填充字段等)均由当前激活的大模型直接完成,可在下方随时切换或新增。</p>
<table class="data-table">
<thead>
<tr>
<th>名称</th>
<th>接口地址</th>
<th>模型名称</th>
<th>上下文窗口</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="llm-config-table">
<tr><td colspan="6" class="empty-text">暂无大模型配置</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 添加产品模态框 -->
@@ -357,6 +391,50 @@
</div>
</div>
<!-- 新增大模型模态框 -->
<div id="add-llm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="ri-openai-line"></i> 新增大模型配置</h3>
<button onclick="closeModal('add-llm-modal')" class="close-btn">
<i class="ri-close-line"></i>
</button>
</div>
<div class="modal-body">
<form id="add-llm-form">
<div class="form-group">
<label>配置名称 *</label>
<input type="text" id="llm-name" required placeholder="例如:本地Qwen3.6">
</div>
<div class="form-group">
<label>接口地址 Base URL *</label>
<input type="text" id="llm-base-url" required placeholder="http://192.168.2.7:18003/v1">
<small class="hint">OpenAI 兼容接口地址,以 /v1 结尾</small>
</div>
<div class="form-group">
<label>API Key</label>
<input type="text" id="llm-api-key" placeholder="sk-xxxx">
</div>
<div class="form-group">
<label>模型名称 *</label>
<input type="text" id="llm-model-name" required placeholder="unsloth/Qwen3.6-27B-Q4_K_M">
</div>
<div class="form-group">
<label>最大上下文窗口</label>
<input type="number" id="llm-max-context" value="262144" min="4096" step="1024">
</div>
</form>
</div>
<div class="modal-footer">
<button onclick="testLlmConnection()" class="btn btn-secondary" id="llm-test-btn">
<i class="ri-connection-line"></i> 测试连接
</button>
<button onclick="closeModal('add-llm-modal')" class="btn btn-secondary">取消</button>
<button onclick="addLlmConfig()" class="btn btn-primary">添加</button>
</div>
</div>
</div>
<!-- 提示消息 -->
<div id="toast" class="toast"></div>
+9 -10
View File
@@ -49,10 +49,10 @@
</div>
</div>
<!-- 智能体任务模板区域 -->
<!-- 大模型任务模板区域 -->
<div class="panel template-section">
<div class="panel-header">
<h2><i class="ri-robot-line"></i> 步骤4:提取产品数据 - 智能体任务模板</h2>
<h2><i class="ri-robot-line"></i> 步骤4:提取产品数据 - 大模型任务模板</h2>
<div class="template-actions">
<button onclick="previewTemplate()" class="btn btn-secondary btn-sm">
<i class="ri-eye-line"></i> 预览
@@ -64,7 +64,7 @@
</div>
<div class="panel-body">
<div class="template-info">
<p><strong>说明:</strong>此模板用于步骤4「提取产品数据」中调用智能体 <code>hz4th_editor</code> 的任务文本。</p>
<p><strong>说明:</strong>此模板用于步骤4「提取产品数据」中调用大模型的任务文本。</p>
<p><strong>可用变量:</strong>
<code>{{product_name}}</code> 产品名称、
<code>{{category}}</code> 类别、
@@ -72,7 +72,7 @@
<code>{{library_results}}</code> 内容库搜索结果、
<code>{{internet_results}}</code> 互联网抓取内容
</p>
<p><strong>调用命令</strong><code>openclaw agent --agent hz4th_editor --message "[填充后的任务文本]"</code></p>
<p><strong>调用方式</strong>直接调用系统当前激活的大模型接口(见系统设置)</p>
</div>
<textarea id="agent-template-editor" class="template-editor" rows="15" placeholder="加载模板中..."></textarea>
</div>
@@ -81,7 +81,7 @@
<!-- 步骤5填充字段模板区域 -->
<div class="panel template-section">
<div class="panel-header">
<h2><i class="ri-edit-box-line"></i> 步骤5:填充字段 - 智能体任务模板</h2>
<h2><i class="ri-edit-box-line"></i> 步骤5:填充字段 - 大模型任务模板</h2>
<div class="template-actions">
<button onclick="previewFillFieldsTemplate()" class="btn btn-secondary btn-sm">
<i class="ri-eye-line"></i> 预览
@@ -93,14 +93,14 @@
</div>
<div class="panel-body">
<div class="template-info">
<p><strong>说明:</strong>此模板用于步骤5「填充字段」中调用智能体 <code>hz4th_editor</code> 的任务文本。</p>
<p><strong>说明:</strong>此模板用于步骤5「填充字段」中调用大模型的任务文本。</p>
<p><strong>可用变量:</strong>
<code>{{product_name}}</code> 产品名称、
<code>{{category}}</code> 类别、
<code>{{subcategory}}</code> 子类别、
<code>{{relevant_content_ids}}</code> 上一步筛选的相关内容数据ID
</p>
<p><strong>任务目标:</strong>智能体根据API文档获取字段定义,整理产品参数,并进行格式检查。</p>
<p><strong>任务目标:</strong>大模型根据API文档获取字段定义,整理产品参数,并进行格式检查。</p>
</div>
<textarea id="fill-fields-template-editor" class="template-editor" rows="15" placeholder="加载模板中..."></textarea>
</div>
@@ -109,7 +109,7 @@
<!-- 步骤6提交审核模板区域 -->
<div class="panel template-section">
<div class="panel-header">
<h2><i class="ri-upload-cloud-line"></i> 步骤6:提交审核 - 智能体任务模板</h2>
<h2><i class="ri-upload-cloud-line"></i> 步骤6:提交审核 - 模板</h2>
<div class="template-actions">
<button onclick="previewSubmitTemplate()" class="btn btn-secondary btn-sm">
<i class="ri-eye-line"></i> 预览
@@ -121,14 +121,13 @@
</div>
<div class="panel-body">
<div class="template-info">
<p><strong>说明:</strong>此模板用于步骤6「提交审核」中调用智能体 <code>hz4th_editor</code> 的任务文本</p>
<p><strong>说明:</strong>步骤6「提交审核」已改为直接调用 ParamHub API 提交,不再经过智能体/大模型</p>
<p><strong>可用变量:</strong>
<code>{{product_name}}</code> 产品名称、
<code>{{category}}</code> 类别、
<code>{{subcategory}}</code> 子类别、
<code>{{product_data}}</code> 上一步生成的产品数据(JSON格式)
</p>
<p><strong>任务目标:</strong>智能体将产品数据提交到ParamHub审核系统,获取review_id。</p>
</div>
<textarea id="submit-template-editor" class="template-editor" rows="15" placeholder="加载模板中..."></textarea>
</div>