diff --git a/README.md b/README.md index 2d9a9d3..54ae82e 100644 --- a/README.md +++ b/README.md @@ -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): 异常产品管理 - 新增异常产品库,自动存储无法处理的产品 - 内容库和互联网均无搜索结果时存入异常库 diff --git a/app.py b/app.py index 0a71654..fb8414f 100644 --- a/app.py +++ b/app.py @@ -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('/') diff --git a/models/database.py b/models/database.py index 69be7e1..b26eef9 100644 --- a/models/database.py +++ b/models/database.py @@ -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() \ No newline at end of file diff --git a/routes/llm.py b/routes/llm.py new file mode 100644 index 0000000..f6005fc --- /dev/null +++ b/routes/llm.py @@ -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/', 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/', 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//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 diff --git a/routes/process_monitor.py b/routes/process_monitor.py index 5d3f990..3f67fc9 100644 --- a/routes/process_monitor.py +++ b/routes/process_monitor.py @@ -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', '') diff --git a/services/llm_client.py b/services/llm_client.py new file mode 100644 index 0000000..c1a5507 --- /dev/null +++ b/services/llm_client.py @@ -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() diff --git a/services/process_monitor.py b/services/process_monitor.py index 9e9a409..13bbc52 100644 --- a/services/process_monitor.py +++ b/services/process_monitor.py @@ -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): diff --git a/static/css/style.css b/static/css/style.css index 3397ac3..4b12b9f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -834,4 +834,86 @@ body { .search-quick-info small { font-size: 12px; -} \ No newline at end of file +} +/* ===== 大模型配置 ===== */ +.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; +} diff --git a/static/js/app.js b/static/js/app.js index b915c97..2825288 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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(); -} \ No newline at end of file +} +// ========== 大模型配置管理 ========== + +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 = '暂无大模型配置'; + return; + } + + container.innerHTML = configs.map(cfg => ` + + + ${escapeHtml(cfg.name)} + ${cfg.is_active ? ' 当前' : ''} + + ${escapeHtml(cfg.base_url)} + ${escapeHtml(cfg.model_name)} + ${cfg.max_context ? Number(cfg.max_context).toLocaleString() : '-'} + + ${cfg.is_active + ? ' 使用中' + : ' 未启用'} + + +
+ ${!cfg.is_active ? ` + + ` : ''} + + ${!cfg.is_active ? ` + + ` : ''} + +
+ + + `).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 = ' 新增大模型配置'; + 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 = ' 编辑大模型配置'; + 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 = ' 测试中...'; + + 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 = ' 测试连接'; +} + +// 测试指定配置的连接 +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'); + } +} diff --git a/static/js/process.js b/static/js/process.js index 6d3e6fd..7cad188 100644 --- a/static/js/process.js +++ b/static/js/process.js @@ -433,7 +433,7 @@ function showToast(message, type = '') { }, 3000); } -// ===== 智能体任务模板 ===== +// ===== 大模型任务模板 ===== // 加载模板 async function loadAgentTemplate() { diff --git a/templates/index.html b/templates/index.html index 90debf5..24eacc1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -207,6 +207,40 @@ + + +
+
+

大模型配置

+
+ + + 加载中... + + +
+
+
+

系统所有智能体任务(提取数据、填充字段等)均由当前激活的大模型直接完成,可在下方随时切换或新增。

+ + + + + + + + + + + + + + +
名称接口地址模型名称上下文窗口状态操作
暂无大模型配置
+
+
@@ -357,6 +391,50 @@ + + +
diff --git a/templates/process.html b/templates/process.html index 1681219..98988e1 100644 --- a/templates/process.html +++ b/templates/process.html @@ -49,10 +49,10 @@ - +
-

步骤4:提取产品数据 - 智能体任务模板

+

步骤4:提取产品数据 - 大模型任务模板

-

说明:此模板用于步骤4「提取产品数据」中调用智能体 hz4th_editor 的任务文本。

+

说明:此模板用于步骤4「提取产品数据」中调用大模型的任务文本。

可用变量: {{product_name}} 产品名称、 {{category}} 类别、 @@ -72,7 +72,7 @@ {{library_results}} 内容库搜索结果、 {{internet_results}} 互联网抓取内容

-

调用命令:openclaw agent --agent hz4th_editor --message "[填充后的任务文本]"

+

调用方式:直接调用系统当前激活的大模型接口(见系统设置)

@@ -81,7 +81,7 @@
-

步骤5:填充字段 - 智能体任务模板

+

步骤5:填充字段 - 大模型任务模板

-

说明:此模板用于步骤5「填充字段」中调用智能体 hz4th_editor 的任务文本。

+

说明:此模板用于步骤5「填充字段」中调用大模型的任务文本。

可用变量: {{product_name}} 产品名称、 {{category}} 类别、 {{subcategory}} 子类别、 {{relevant_content_ids}} 上一步筛选的相关内容数据ID

-

任务目标:智能体根据API文档获取字段定义,整理产品参数,并进行格式检查。

+

任务目标:大模型根据API文档获取字段定义,整理产品参数,并进行格式检查。

@@ -109,7 +109,7 @@
-

步骤6:提交审核 - 智能体任务模板

+

步骤6:提交审核 - 模板

-

说明:此模板用于步骤6「提交审核」中调用智能体 hz4th_editor 的任务文本。

+

说明:步骤6「提交审核」已改为直接调用 ParamHub API 提交,不再经过智能体/大模型。

可用变量: {{product_name}} 产品名称、 {{category}} 类别、 {{subcategory}} 子类别、 {{product_data}} 上一步生成的产品数据(JSON格式)

-

任务目标:智能体将产品数据提交到ParamHub审核系统,获取review_id。