Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23991c3552 |
@@ -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): 异常产品管理
|
||||
- 新增异常产品库,自动存储无法处理的产品
|
||||
- 内容库和互联网均无搜索结果时存入异常库
|
||||
|
||||
@@ -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('/')
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -433,7 +433,7 @@ function showToast(message, type = '') {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ===== 智能体任务模板 =====
|
||||
// ===== 大模型任务模板 =====
|
||||
|
||||
// 加载模板
|
||||
async function loadAgentTemplate() {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user