diff --git a/config/agent_task_template.txt b/config/agent_task_template.txt new file mode 100644 index 0000000..85a6515 --- /dev/null +++ b/config/agent_task_template.txt @@ -0,0 +1,31 @@ +## 任务背景 + +### 内容库搜索结果 +以下是从内容库中搜索到的相关文章数据位置: +{{library_results}} + +### 互联网搜索抓取内容 +以下是从互联网搜索并抓取的网页内容数据位置: +{{internet_results}} + +## 产品信息 +- **产品名称:** {{product_name}} +- **产品类别:** {{category}} +- **子类别:** {{subcategory}} + +## 任务要求 + +请从上述搜索结果和抓取内容中,提取出与产品「{{product_name}}」直接相关的具体内容。 + +要求: +1. 只提取与该产品直接相关的信息,排除其他无关产品的内容 +2. 提取的内容应包括但不限于:产品参数、规格、功能描述、发布信息、技术特点等 +3. 注明每条信息的来源(URL或文章标题) +4. 如果某些信息在多个来源中都有提及,请综合整理 +5. 严格按照原始数据提取,不要编造或推测任何内容 + +请将提取结果以JSON格式输出,包含以下字段: +- name: 产品名称 +- extracted_fields: 提取到的字段键值对 +- sources: 信息来源列表 +- confidence: 提取置信度(high/medium/low) diff --git a/routes/process_monitor.py b/routes/process_monitor.py index 20133ca..11747a5 100644 --- a/routes/process_monitor.py +++ b/routes/process_monitor.py @@ -4,10 +4,15 @@ from flask import Blueprint, jsonify, request from services.process_monitor import process_monitor, PROCESS_STEPS from models.database import db +import os import logging logger = logging.getLogger('process_monitor_api') +# 模板文件路径 +TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config') +AGENT_TEMPLATE_FILE = os.path.join(TEMPLATE_DIR, 'agent_task_template.txt') + bp = Blueprint('process_monitor', __name__, url_prefix='/api/process') @@ -177,4 +182,79 @@ def get_step_detail(session_id, step_num): return jsonify({ 'success': True, 'step': step - }) \ No newline at end of file + }) + +@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: + template = f.read() + return jsonify({ + 'success': True, + 'template': template + }) + else: + return jsonify({ + 'success': False, + 'error': '模板文件不存在' + }), 404 + except Exception as e: + logger.error(f"获取模板失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@bp.route('/agent-template', methods=['POST']) +def save_agent_template(): + """保存智能体任务文本模板""" + try: + data = request.get_json() + template = data.get('template', '') + + if not template: + return jsonify({'success': False, 'error': '模板内容不能为空'}), 400 + + os.makedirs(TEMPLATE_DIR, exist_ok=True) + with open(AGENT_TEMPLATE_FILE, 'w', encoding='utf-8') as f: + f.write(template) + + return jsonify({ + 'success': True, + 'message': '模板已保存' + }) + except Exception as e: + logger.error(f"保存模板失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@bp.route('/agent-template/preview', methods=['POST']) +def preview_agent_template(): + """预览填充后的任务文本""" + try: + data = request.get_json() + product_name = data.get('product_name', '示例产品') + category = data.get('category', '示例类别') + subcategory = data.get('subcategory', '示例子类别') + + # 读取模板 + if os.path.exists(AGENT_TEMPLATE_FILE): + with open(AGENT_TEMPLATE_FILE, 'r', encoding='utf-8') as f: + template = f.read() + else: + return jsonify({'success': False, 'error': '模板文件不存在'}), 404 + + # 填充示例数据 + filled = template.replace('{{product_name}}', product_name) + filled = filled.replace('{{category}}', category) + filled = filled.replace('{{subcategory}}', subcategory or '无') + filled = filled.replace('{{library_results}}', '[内容库搜索结果将在此处列出,包含文章标题、URL、摘要等]') + filled = filled.replace('{{internet_results}}', '[互联网抓取内容将在此处列出,包含URL、标题、正文片段等]') + + return jsonify({ + 'success': True, + 'preview': filled + }) + except Exception as e: + logger.error(f"预览模板失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 diff --git a/services/process_monitor.py b/services/process_monitor.py index f44dfb2..5a7c6aa 100644 --- a/services/process_monitor.py +++ b/services/process_monitor.py @@ -1,9 +1,11 @@ """ 处理步骤监控服务 - 记录和监控产品处理流程 """ +import os import time import uuid import json +import subprocess import threading import logging from datetime import datetime @@ -18,7 +20,7 @@ PROCESS_STEPS = [ {'num': 1, 'name': '搜索内容库', 'description': '从内容库搜索相关文章'}, {'num': 2, 'name': '搜索互联网', 'description': '从互联网搜索最新数据'}, {'num': 3, 'name': '抓取网页内容', 'description': '抓取搜索结果网页的详细内容'}, - {'num': 4, 'name': '提取产品数据', 'description': '从抓取内容中提取产品相关数据'}, + {'num': 4, 'name': '提取产品数据(智能体)', 'description': '调用hz4th_editor智能体提取产品相关内容'}, {'num': 5, 'name': '填充字段', 'description': '根据分类字段配置填充数据'}, {'num': 6, 'name': '提交审核', 'description': '提交到ParamHub待审核区'}, ] @@ -154,18 +156,47 @@ class ProcessMonitor: except Exception as 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: - extracted = self._extract_data(product_name, all_data) - all_data['extracted_data'] = extracted + # 构建任务文本 + task_text = self._build_agent_task( + product_name, category, subcategory, all_data + ) - if extracted: - self._complete_step(session_id, 4, {'has_data': True}) + # 记录任务文本 + self._complete_step(session_id, 4, { + 'agent': 'hz4th_editor', + 'task_text': task_text, + 'status': 'calling_agent' + }) + + # 调用智能体 + agent_result = self._call_agent(task_text) + + if agent_result.get('success'): + extracted = self._parse_agent_response(agent_result.get('output', '')) + all_data['extracted_data'] = extracted + + if extracted: + self._complete_step(session_id, 4, { + 'has_data': True, + 'agent': 'hz4th_editor', + 'task_text': task_text, + 'agent_output': agent_result.get('output', '')[:2000] + }) + else: + self._complete_step(session_id, 4, { + 'has_data': False, + 'agent': 'hz4th_editor', + 'task_text': task_text, + 'agent_output': agent_result.get('output', '')[:2000] + }, status='skipped') + result['message'] = '智能体无法提取有效数据' else: - self._complete_step(session_id, 4, {'has_data': False}, status='skipped') - result['message'] = '无法提取有效数据' + 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)) @@ -302,8 +333,133 @@ class ProcessMonitor: return {'session': session, 'steps': steps} return None + def _build_agent_task(self, product_name, category, subcategory, all_data): + """构建智能体任务文本""" + # 读取模板 + template_file = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + 'config', 'agent_task_template.txt' + ) + + if os.path.exists(template_file): + with open(template_file, 'r', encoding='utf-8') as f: + template = f.read() + else: + # 默认模板 + template = ( + "请从以下数据中提取产品「{{product_name}}」的相关内容。\n" + "类别: {{category}} / {{subcategory}}\n\n" + "内容库结果:\n{{library_results}}\n\n" + "互联网抓取内容:\n{{internet_results}}\n\n" + "要求:只提取与该产品信息直接相关的内容,排除无关产品。以JSON格式输出。" + ) + + # 构建内容库搜索结果 + library_lines = [] + for i, article in enumerate(all_data.get('library_results', [])[:10], 1): + title = article.get('search_title', article.get('title', '无标题')) + url = article.get('url', article.get('source', '无URL')) + summary = article.get('summary', '')[:200] + library_lines.append(f" [{i}] 标题: {title}\n URL: {url}\n 摘要: {summary}") + library_text = '\n'.join(library_lines) if library_lines else '(无内容库搜索结果)' + + # 构建互联网抓取内容 + internet_lines = [] + for i, item in enumerate(all_data.get('fetched_contents', [])[:10], 1): + title = item.get('title', '无标题') + url = item.get('url', '无URL') + content = item.get('content', '')[:300] + internet_lines.append(f" [{i}] 标题: {title}\n URL: {url}\n 内容片段: {content}") + internet_text = '\n'.join(internet_lines) if internet_lines else '(无互联网抓取内容)' + + # 填充模板 + task = template.replace('{{product_name}}', product_name or '未知') + task = task.replace('{{category}}', category or '未分类') + task = task.replace('{{subcategory}}', subcategory or '无') + task = task.replace('{{library_results}}', library_text) + task = task.replace('{{internet_results}}', internet_text) + + return task + + def _call_agent(self, task_text): + """调用智能体执行任务""" + try: + cmd = [ + 'openclaw', 'agent', + '--agent', 'hz4th_editor', + '--message', task_text + ] + + logger.info(f"调用智能体命令: openclaw agent --agent hz4th_editor --message '[任务文本 {len(task_text)} 字符]'") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300 # 5分钟超时 + ) + + if result.returncode == 0: + output = result.stdout.strip() + logger.info(f"智能体返回: {output[:500]}...") + return {'success': True, 'output': output} + else: + error = result.stderr.strip() or result.stdout.strip() + logger.error(f"智能体调用失败: {error}") + return {'success': False, 'error': error} + + except subprocess.TimeoutExpired: + return {'success': False, 'error': '智能体执行超时(>5分钟)'} + except FileNotFoundError: + return {'success': False, 'error': 'openclaw命令未找到'} + except Exception as e: + return {'success': False, 'error': str(e)} + + def _parse_agent_response(self, output): + """解析智能体返回的结果""" + if not output: + return None + + # 尝试从输出中提取JSON + import re + + # 查找JSON块 + json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', output, re.DOTALL) + if json_match: + try: + data = json.loads(json_match.group(1)) + return { + 'name': data.get('name', ''), + 'extracted_fields': data.get('extracted_fields', {}), + 'sources': data.get('sources', []), + 'confidence': data.get('confidence', 'unknown'), + 'raw_output': output + } + except json.JSONDecodeError: + pass + + # 尝试直接解析整个输出为JSON + try: + data = json.loads(output) + return { + 'name': data.get('name', ''), + 'extracted_fields': data.get('extracted_fields', {}), + 'sources': data.get('sources', []), + 'confidence': data.get('confidence', 'unknown'), + 'raw_output': output + } + except json.JSONDecodeError: + pass + + # 如果无法解析为JSON,将原始输出作为raw_content保存 + return { + 'name': '', + 'raw_content': output, + 'raw_output': output + } + def _extract_data(self, product_name, all_data): - """提取产品数据""" + """提取产品数据(备用,已被智能体替代)""" all_content = [] for article in all_data.get('library_results', []): diff --git a/static/css/process.css b/static/css/process.css index 908fae2..6075756 100644 --- a/static/css/process.css +++ b/static/css/process.css @@ -350,4 +350,55 @@ .steps-progress { flex-wrap: wrap; } -} \ No newline at end of file +} +/* 智能体任务模板区域 */ +.template-section .panel-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.template-actions { + display: flex; + gap: 8px; +} + +.template-info { + background: #f0f4ff; + border: 1px solid #c7d2fe; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 15px; + font-size: 13px; + color: #4338ca; +} + +.template-info p { + margin: 4px 0; +} + +.template-info code { + background: #e0e7ff; + padding: 1px 5px; + border-radius: 3px; + font-size: 12px; + color: #3730a3; +} + +.template-editor { + width: 100%; + font-family: 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + padding: 12px; + border: 1px solid #d1d5db; + border-radius: 8px; + resize: vertical; + background: #fafafa; +} + +.template-editor:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} diff --git a/static/js/process.js b/static/js/process.js index e224823..c156a13 100644 --- a/static/js/process.js +++ b/static/js/process.js @@ -9,6 +9,7 @@ document.addEventListener('DOMContentLoaded', () => { loadStepDefinitions(); loadActiveProcesses(); loadHistory(); + loadAgentTemplate(); // 启动自动刷新(每2秒) startAutoRefresh(); @@ -428,4 +429,83 @@ function showToast(message, type = '') { setTimeout(() => { toast.classList.remove('active'); }, 3000); +} + +// ===== 智能体任务模板 ===== + +// 加载模板 +async function loadAgentTemplate() { + try { + const response = await fetch(`${API_BASE}/api/process/agent-template`); + const data = await response.json(); + + if (data.success) { + document.getElementById('agent-template-editor').value = data.template; + } else { + document.getElementById('agent-template-editor').value = '// 模板加载失败: ' + (data.error || '未知错误'); + } + } catch (error) { + console.error('加载模板失败:', error); + document.getElementById('agent-template-editor').value = '// 加载模板失败: ' + error.message; + } +} + +// 保存模板 +async function saveTemplate() { + const template = document.getElementById('agent-template-editor').value; + + if (!template.trim()) { + showToast('模板内容不能为空', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE}/api/process/agent-template`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ template }) + }); + const data = await response.json(); + + if (data.success) { + showToast('模板已保存 ✓', 'success'); + } else { + showToast('保存失败: ' + data.error, 'error'); + } + } catch (error) { + showToast('保存失败: ' + error.message, 'error'); + } +} + +// 预览模板 +function previewTemplate() { + document.getElementById('template-preview-modal').classList.add('active'); + doPreview(); +} + +// 执行预览 +async function doPreview() { + const product = document.getElementById('preview-product').value || '示例产品'; + const category = document.getElementById('preview-category').value || 'AI模型'; + + try { + const response = await fetch(`${API_BASE}/api/process/agent-template/preview`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + product_name: product, + category: category, + subcategory: '' + }) + }); + const data = await response.json(); + + if (data.success) { + document.getElementById('template-preview-content').textContent = data.preview; + } else { + document.getElementById('template-preview-content').textContent = '预览失败: ' + data.error; + } + } catch (error) { + document.getElementById('template-preview-content').textContent = '预览失败: ' + error.message; + } } \ No newline at end of file diff --git a/templates/process.html b/templates/process.html index 9ddaf51..e3b9d97 100644 --- a/templates/process.html +++ b/templates/process.html @@ -49,6 +49,58 @@ + +
说明:此模板用于步骤4「提取产品数据」中调用智能体 hz4th_editor 的任务文本。
可用变量:
+ {{product_name}} 产品名称、
+ {{category}} 类别、
+ {{subcategory}} 子类别、
+ {{library_results}} 内容库搜索结果、
+ {{internet_results}} 互联网抓取内容
+
调用命令:openclaw agent --agent hz4th_editor --message "[填充后的任务文本]"