feat: 步骤4提取产品数据改用智能体hz4th_editor执行

- 步骤4调用 openclaw agent --agent hz4th_editor --message 执行提取任务
- 新增任务文本模板(config/agent_task_template.txt),支持变量填充
- /process页面新增模板编辑面板,可查看/编辑/保存/预览模板
- 模板变量: {{product_name}} {{category}} {{subcategory}} {{library_results}} {{internet_results}}
- 新增API: GET/POST /api/process/agent-template, POST /api/process/agent-template/preview
This commit is contained in:
2026-07-15 11:18:47 +08:00
parent c833394f06
commit 96c479916b
6 changed files with 462 additions and 12 deletions
+31
View File
@@ -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
+80
View File
@@ -4,10 +4,15 @@
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from services.process_monitor import process_monitor, PROCESS_STEPS from services.process_monitor import process_monitor, PROCESS_STEPS
from models.database import db from models.database import db
import os
import logging import logging
logger = logging.getLogger('process_monitor_api') 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') bp = Blueprint('process_monitor', __name__, url_prefix='/api/process')
@@ -178,3 +183,78 @@ def get_step_detail(session_id, step_num):
'success': True, 'success': True,
'step': step 'step': step
}) })
@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
+164 -8
View File
@@ -1,9 +1,11 @@
""" """
处理步骤监控服务 - 记录和监控产品处理流程 处理步骤监控服务 - 记录和监控产品处理流程
""" """
import os
import time import time
import uuid import uuid
import json import json
import subprocess
import threading import threading
import logging import logging
from datetime import datetime from datetime import datetime
@@ -18,7 +20,7 @@ PROCESS_STEPS = [
{'num': 1, 'name': '搜索内容库', 'description': '从内容库搜索相关文章'}, {'num': 1, 'name': '搜索内容库', 'description': '从内容库搜索相关文章'},
{'num': 2, 'name': '搜索互联网', 'description': '从互联网搜索最新数据'}, {'num': 2, 'name': '搜索互联网', 'description': '从互联网搜索最新数据'},
{'num': 3, 'name': '抓取网页内容', 'description': '抓取搜索结果网页的详细内容'}, {'num': 3, 'name': '抓取网页内容', 'description': '抓取搜索结果网页的详细内容'},
{'num': 4, 'name': '提取产品数据', 'description': '从抓取内容中提取产品相关数据'}, {'num': 4, 'name': '提取产品数据(智能体)', 'description': '调用hz4th_editor智能体提取产品相关内容'},
{'num': 5, 'name': '填充字段', 'description': '根据分类字段配置填充数据'}, {'num': 5, 'name': '填充字段', 'description': '根据分类字段配置填充数据'},
{'num': 6, 'name': '提交审核', 'description': '提交到ParamHub待审核区'}, {'num': 6, 'name': '提交审核', 'description': '提交到ParamHub待审核区'},
] ]
@@ -154,18 +156,47 @@ class ProcessMonitor:
except Exception as e: except Exception as e:
self._fail_step(session_id, 3, str(e)) self._fail_step(session_id, 3, str(e))
# 步骤4: 提取产品数据 # 步骤4: 提取产品数据(调用智能体执行)
if not self._check_pause(session_id): if not self._check_pause(session_id):
self._start_step(session_id, product_name, 4, '提取产品数据') self._start_step(session_id, product_name, 4, '提取产品数据(智能体)')
try: try:
extracted = self._extract_data(product_name, all_data) # 构建任务文本
task_text = self._build_agent_task(
product_name, category, subcategory, all_data
)
# 记录任务文本
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 all_data['extracted_data'] = extracted
if extracted: if extracted:
self._complete_step(session_id, 4, {'has_data': True}) self._complete_step(session_id, 4, {
'has_data': True,
'agent': 'hz4th_editor',
'task_text': task_text,
'agent_output': agent_result.get('output', '')[:2000]
})
else: else:
self._complete_step(session_id, 4, {'has_data': False}, status='skipped') self._complete_step(session_id, 4, {
result['message'] = '无法提取有效数据' 'has_data': False,
'agent': 'hz4th_editor',
'task_text': task_text,
'agent_output': agent_result.get('output', '')[:2000]
}, status='skipped')
result['message'] = '智能体无法提取有效数据'
else:
self._fail_step(session_id, 4, f"智能体调用失败: {agent_result.get('error', '未知错误')}")
result['message'] = f'智能体调用失败: {agent_result.get("error")}'
except Exception as e: except Exception as e:
self._fail_step(session_id, 4, str(e)) self._fail_step(session_id, 4, str(e))
@@ -302,8 +333,133 @@ class ProcessMonitor:
return {'session': session, 'steps': steps} return {'session': session, 'steps': steps}
return None 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): def _extract_data(self, product_name, all_data):
"""提取产品数据""" """提取产品数据(备用,已被智能体替代)"""
all_content = [] all_content = []
for article in all_data.get('library_results', []): for article in all_data.get('library_results', []):
+51
View File
@@ -351,3 +351,54 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
/* 智能体任务模板区域 */
.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);
}
+80
View File
@@ -9,6 +9,7 @@ document.addEventListener('DOMContentLoaded', () => {
loadStepDefinitions(); loadStepDefinitions();
loadActiveProcesses(); loadActiveProcesses();
loadHistory(); loadHistory();
loadAgentTemplate();
// 启动自动刷新(每2秒) // 启动自动刷新(每2秒)
startAutoRefresh(); startAutoRefresh();
@@ -429,3 +430,82 @@ function showToast(message, type = '') {
toast.classList.remove('active'); toast.classList.remove('active');
}, 3000); }, 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;
}
}
+52
View File
@@ -49,6 +49,58 @@
</div> </div>
</div> </div>
<!-- 智能体任务模板区域 -->
<div class="panel template-section">
<div class="panel-header">
<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> 预览
</button>
<button onclick="saveTemplate()" class="btn btn-primary btn-sm">
<i class="ri-save-line"></i> 保存模板
</button>
</div>
</div>
<div class="panel-body">
<div class="template-info">
<p><strong>说明:</strong>此模板用于步骤4「提取产品数据」中调用智能体 <code>hz4th_editor</code> 的任务文本。</p>
<p><strong>可用变量:</strong>
<code>{{product_name}}</code> 产品名称、
<code>{{category}}</code> 类别、
<code>{{subcategory}}</code> 子类别、
<code>{{library_results}}</code> 内容库搜索结果、
<code>{{internet_results}}</code> 互联网抓取内容
</p>
<p><strong>调用命令:</strong><code>openclaw agent --agent hz4th_editor --message "[填充后的任务文本]"</code></p>
</div>
<textarea id="agent-template-editor" class="template-editor" rows="20" placeholder="加载模板中..."></textarea>
</div>
</div>
<!-- 预览模态框 -->
<div id="template-preview-modal" class="modal">
<div class="modal-content large">
<div class="modal-header">
<h3><i class="ri-eye-line"></i> 任务文本预览</h3>
<button onclick="closeModal('template-preview-modal')" class="close-btn">
<i class="ri-close-line"></i>
</button>
</div>
<div class="modal-body">
<div class="preview-meta" style="margin-bottom: 15px;">
<label>产品名称: <input type="text" id="preview-product" value="示例产品" style="padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px;"></label>
<label style="margin-left: 10px;">类别: <input type="text" id="preview-category" value="AI模型" style="padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px;"></label>
<button onclick="doPreview()" class="btn btn-sm btn-secondary" style="margin-left: 10px;"><i class="ri-refresh-line"></i> 刷新预览</button>
</div>
<pre id="template-preview-content" style="background: #f8f9fa; padding: 15px; border-radius: 8px; white-space: pre-wrap; font-size: 13px; max-height: 500px; overflow-y: auto;">加载中...</pre>
</div>
<div class="modal-footer">
<button onclick="closeModal('template-preview-modal')" class="btn btn-secondary">关闭</button>
</div>
</div>
</div>
<!-- 处理历史区域 --> <!-- 处理历史区域 -->
<div class="panel history-section"> <div class="panel history-section">
<div class="panel-header"> <div class="panel-header">