Compare commits

...
1 Commits
Author SHA1 Message Date
hz4th_coder 96c479916b 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
2026-07-15 11:18:47 +08:00
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
+81 -1
View File
@@ -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
})
})
@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
+166 -10
View File
@@ -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', []):
+52 -1
View File
@@ -350,4 +350,55 @@
.steps-progress {
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();
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;
}
}
+52
View File
@@ -49,6 +49,58 @@
</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-header">