Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f0efe28b6 | ||
|
|
3f9848c635 | ||
|
|
dd42957a48 | ||
|
|
0229f2dfcf |
@@ -29,11 +29,13 @@ from routes.articles import bp as articles_bp
|
||||
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
|
||||
|
||||
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.route('/')
|
||||
@@ -50,6 +52,11 @@ def search_page():
|
||||
def library_page():
|
||||
return render_template('library.html')
|
||||
|
||||
# 处理监控页面
|
||||
@app.route('/process')
|
||||
def process_page():
|
||||
return render_template('process.html')
|
||||
|
||||
# API首页
|
||||
@app.route('/api')
|
||||
def api_index():
|
||||
|
||||
@@ -161,6 +161,53 @@ class Database:
|
||||
)
|
||||
''')
|
||||
|
||||
# 处理步骤表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS process_steps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
process_id TEXT NOT NULL,
|
||||
product_name TEXT NOT NULL,
|
||||
step_number INTEGER NOT NULL,
|
||||
step_name TEXT NOT NULL,
|
||||
step_status TEXT DEFAULT 'pending',
|
||||
step_data TEXT,
|
||||
started_at DATETIME,
|
||||
finished_at DATETIME,
|
||||
duration_ms INTEGER,
|
||||
error_message TEXT,
|
||||
requires_intervention INTEGER DEFAULT 0,
|
||||
intervention_type TEXT,
|
||||
intervention_status TEXT,
|
||||
intervention_data TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 处理会话表(用于跟踪整个处理过程)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS process_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL UNIQUE,
|
||||
product_name TEXT NOT NULL,
|
||||
category TEXT,
|
||||
subcategory TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
current_step INTEGER DEFAULT 0,
|
||||
total_steps INTEGER DEFAULT 6,
|
||||
paused INTEGER DEFAULT 0,
|
||||
pause_reason TEXT,
|
||||
result TEXT,
|
||||
review_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
started_at DATETIME,
|
||||
finished_at DATETIME
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_process_steps_session ON process_steps(process_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_process_sessions_status ON process_sessions(status)')
|
||||
|
||||
conn.commit()
|
||||
|
||||
# ========== 内容库操作 ==========
|
||||
@@ -598,5 +645,190 @@ class Database:
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
# ========== 处理会话操作 ==========
|
||||
def create_process_session(self, session_id, product_name, category=None, subcategory=None):
|
||||
"""创建处理会话"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO process_sessions (session_id, product_name, category, subcategory, status)
|
||||
VALUES (?, ?, ?, ?, 'pending')
|
||||
''', (session_id, product_name, category, subcategory))
|
||||
conn.commit()
|
||||
return session_id
|
||||
|
||||
def get_process_session(self, session_id):
|
||||
"""获取处理会话"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT * FROM process_sessions WHERE session_id = ?', (session_id,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def update_session_status(self, session_id, status, **kwargs):
|
||||
"""更新会话状态"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
updates = ['status = ?']
|
||||
values = [status]
|
||||
|
||||
if status == 'running' and 'started_at' not in kwargs:
|
||||
updates.append('started_at = CURRENT_TIMESTAMP')
|
||||
elif status in ('completed', 'failed', 'stopped'):
|
||||
updates.append('finished_at = CURRENT_TIMESTAMP')
|
||||
|
||||
for key in ['current_step', 'result', 'review_id', 'paused', 'pause_reason']:
|
||||
if key in kwargs:
|
||||
updates.append(f'{key} = ?')
|
||||
values.append(kwargs[key])
|
||||
|
||||
values.append(session_id)
|
||||
|
||||
cursor.execute(
|
||||
f'UPDATE process_sessions SET {" , ".join(updates)} WHERE session_id = ?',
|
||||
values
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def pause_session(self, session_id, reason=None):
|
||||
"""暂停会话"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
UPDATE process_sessions
|
||||
SET paused = 1, pause_reason = ?, status = 'paused'
|
||||
WHERE session_id = ?
|
||||
''', (reason, session_id))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def resume_session(self, session_id):
|
||||
"""继续会话"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
UPDATE process_sessions
|
||||
SET paused = 0, pause_reason = NULL, status = 'running'
|
||||
WHERE session_id = ?
|
||||
''', (session_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def get_active_sessions(self):
|
||||
"""获取活动的会话"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT * FROM process_sessions
|
||||
WHERE status IN ('pending', 'running', 'paused')
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def get_recent_sessions(self, limit=20):
|
||||
"""获取最近的会话"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT * FROM process_sessions
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
''', (limit,))
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
# ========== 处理步骤操作 ==========
|
||||
def add_process_step(self, process_id, product_name, step_number, step_name, step_data=None):
|
||||
"""添加处理步骤"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO process_steps
|
||||
(process_id, product_name, step_number, step_name, step_status, step_data, started_at)
|
||||
VALUES (?, ?, ?, ?, 'running', ?, CURRENT_TIMESTAMP)
|
||||
''', (process_id, product_name, step_number, step_name,
|
||||
json.dumps(step_data, ensure_ascii=False) if step_data else None))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def update_step_status(self, process_id, step_number, status, **kwargs):
|
||||
"""更新步骤状态"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
updates = ['step_status = ?']
|
||||
values = [status]
|
||||
|
||||
if status in ('completed', 'failed', 'skipped'):
|
||||
updates.append('finished_at = CURRENT_TIMESTAMP')
|
||||
|
||||
for key in ['step_data', 'error_message', 'duration_ms', 'requires_intervention',
|
||||
'intervention_type', 'intervention_status', 'intervention_data']:
|
||||
if key in kwargs:
|
||||
if key in ('step_data', 'intervention_data') and kwargs[key]:
|
||||
updates.append(f'{key} = ?')
|
||||
values.append(json.dumps(kwargs[key], ensure_ascii=False))
|
||||
else:
|
||||
updates.append(f'{key} = ?')
|
||||
values.append(kwargs[key])
|
||||
|
||||
values.extend([process_id, step_number])
|
||||
|
||||
cursor.execute(
|
||||
f'UPDATE process_steps SET {" , ".join(updates)} WHERE process_id = ? AND step_number = ?',
|
||||
values
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def get_process_steps(self, process_id):
|
||||
"""获取处理步骤列表"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT * FROM process_steps
|
||||
WHERE process_id = ?
|
||||
ORDER BY step_number ASC
|
||||
''', (process_id,))
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def get_step_detail(self, process_id, step_number):
|
||||
"""获取步骤详情"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT * FROM process_steps
|
||||
WHERE process_id = ? AND step_number = ?
|
||||
''', (process_id, step_number))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
result = dict(row)
|
||||
if result.get('step_data'):
|
||||
result['step_data'] = json.loads(result['step_data'])
|
||||
if result.get('intervention_data'):
|
||||
result['intervention_data'] = json.loads(result['intervention_data'])
|
||||
return result
|
||||
return None
|
||||
|
||||
def set_step_intervention(self, process_id, step_number, intervention_type, intervention_data=None):
|
||||
"""设置步骤需要干预"""
|
||||
return self.update_step_status(
|
||||
process_id, step_number, 'paused',
|
||||
requires_intervention=1,
|
||||
intervention_type=intervention_type,
|
||||
intervention_status='pending',
|
||||
intervention_data=intervention_data
|
||||
)
|
||||
|
||||
def complete_intervention(self, process_id, step_number, intervention_data=None):
|
||||
"""完成干预"""
|
||||
return self.update_step_status(
|
||||
process_id, step_number, 'completed',
|
||||
requires_intervention=0,
|
||||
intervention_status='completed',
|
||||
intervention_data=intervention_data
|
||||
)
|
||||
|
||||
# 全局数据库实例
|
||||
db = Database()
|
||||
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
处理监控 API 路由
|
||||
"""
|
||||
from flask import Blueprint, jsonify, request
|
||||
from services.process_monitor import process_monitor, PROCESS_STEPS
|
||||
from models.database import db
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('process_monitor_api')
|
||||
|
||||
bp = Blueprint('process_monitor', __name__, url_prefix='/api/process')
|
||||
|
||||
|
||||
@bp.route('/steps', methods=['GET'])
|
||||
def get_step_definitions():
|
||||
"""获取处理步骤定义"""
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'steps': PROCESS_STEPS
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/start', methods=['POST'])
|
||||
def start_process():
|
||||
"""
|
||||
启动产品处理流程
|
||||
|
||||
请求体:
|
||||
{
|
||||
"product_name": "产品名称",
|
||||
"category": "分类",
|
||||
"subcategory": "子分类"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
product_name = data.get('product_name')
|
||||
if not product_name:
|
||||
return jsonify({'success': False, 'error': '缺少产品名称'}), 400
|
||||
|
||||
category = data.get('category')
|
||||
subcategory = data.get('subcategory')
|
||||
|
||||
# 启动处理流程
|
||||
session_id = process_monitor.start_process(product_name, category, subcategory)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'session_id': session_id,
|
||||
'message': f'处理流程已启动: {product_name}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动处理流程失败: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/<session_id>/status', methods=['GET'])
|
||||
def get_process_status(session_id):
|
||||
"""获取处理状态"""
|
||||
status = process_monitor.get_session_status(session_id)
|
||||
|
||||
if not status:
|
||||
return jsonify({'success': False, 'error': '会话不存在'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': status
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/<session_id>/pause', methods=['POST'])
|
||||
def pause_process(session_id):
|
||||
"""暂停处理"""
|
||||
if process_monitor.pause_session(session_id):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '处理已暂停'
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '无法暂停'}), 400
|
||||
|
||||
|
||||
@bp.route('/<session_id>/resume', methods=['POST'])
|
||||
def resume_process(session_id):
|
||||
"""继续处理"""
|
||||
if process_monitor.resume_session(session_id):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '处理已继续'
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '无法继续'}), 400
|
||||
|
||||
|
||||
@bp.route('/<session_id>/stop', methods=['POST'])
|
||||
def stop_process(session_id):
|
||||
"""停止处理"""
|
||||
if process_monitor.stop_session(session_id):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '处理已停止'
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '无法停止'}), 400
|
||||
|
||||
|
||||
@bp.route('/active', methods=['GET'])
|
||||
def get_active_processes():
|
||||
"""获取活动的处理会话"""
|
||||
sessions = db.get_active_sessions()
|
||||
|
||||
# 获取每个会话的步骤信息
|
||||
result = []
|
||||
for session in sessions:
|
||||
steps = db.get_process_steps(session['session_id'])
|
||||
result.append({
|
||||
'session': session,
|
||||
'steps': steps
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'sessions': result,
|
||||
'count': len(result)
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/recent', methods=['GET'])
|
||||
def get_recent_processes():
|
||||
"""获取最近的处理会话"""
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
sessions = db.get_recent_sessions(limit)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'sessions': sessions,
|
||||
'count': len(sessions)
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/<session_id>/steps', methods=['GET'])
|
||||
def get_process_steps(session_id):
|
||||
"""获取处理步骤详情"""
|
||||
steps = db.get_process_steps(session_id)
|
||||
|
||||
# 解析JSON字段
|
||||
for step in steps:
|
||||
if step.get('step_data'):
|
||||
import json
|
||||
try:
|
||||
step['step_data'] = json.loads(step['step_data'])
|
||||
except:
|
||||
pass
|
||||
if step.get('intervention_data'):
|
||||
import json
|
||||
try:
|
||||
step['intervention_data'] = json.loads(step['intervention_data'])
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'steps': steps
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/<session_id>/step/<int:step_num>', methods=['GET'])
|
||||
def get_step_detail(session_id, step_num):
|
||||
"""获取单个步骤详情"""
|
||||
step = db.get_step_detail(session_id, step_num)
|
||||
|
||||
if not step:
|
||||
return jsonify({'success': False, 'error': '步骤不存在'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'step': step
|
||||
})
|
||||
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
处理步骤监控服务 - 记录和监控产品处理流程
|
||||
"""
|
||||
import time
|
||||
import uuid
|
||||
import json
|
||||
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
|
||||
|
||||
logger = logging.getLogger('process_monitor')
|
||||
|
||||
# 处理步骤定义
|
||||
PROCESS_STEPS = [
|
||||
{'num': 1, 'name': '搜索内容库', 'description': '从内容库搜索相关文章'},
|
||||
{'num': 2, 'name': '搜索互联网', 'description': '从互联网搜索最新数据'},
|
||||
{'num': 3, 'name': '抓取网页内容', 'description': '抓取搜索结果网页的详细内容'},
|
||||
{'num': 4, 'name': '提取产品数据', 'description': '从抓取内容中提取产品相关数据'},
|
||||
{'num': 5, 'name': '填充字段', 'description': '根据分类字段配置填充数据'},
|
||||
{'num': 6, 'name': '提交审核', 'description': '提交到ParamHub待审核区'},
|
||||
]
|
||||
|
||||
class ProcessMonitor:
|
||||
"""处理步骤监控器"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_sessions = {}
|
||||
self.step_timers = {}
|
||||
|
||||
def create_session_id(self):
|
||||
"""生成会话ID"""
|
||||
return f"proc_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
def start_process(self, product_name, category=None, subcategory=None):
|
||||
"""启动产品处理流程"""
|
||||
session_id = self.create_session_id()
|
||||
|
||||
# 创建会话记录
|
||||
db.create_process_session(session_id, product_name, category, subcategory)
|
||||
|
||||
# 初始化控制信息
|
||||
self.active_sessions[session_id] = {
|
||||
'paused': False,
|
||||
'stop': False,
|
||||
'current_step': 0
|
||||
}
|
||||
|
||||
# 启动后台线程处理
|
||||
thread = threading.Thread(
|
||||
target=self._run_process,
|
||||
args=(session_id, product_name, category, subcategory),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
logger.info(f"启动处理会话: {session_id}, 产品: {product_name}")
|
||||
return session_id
|
||||
|
||||
def _run_process(self, session_id, product_name, category, subcategory):
|
||||
"""执行处理流程"""
|
||||
try:
|
||||
db.update_session_status(session_id, 'running')
|
||||
|
||||
result = {'success': False, 'message': '', 'review_id': None}
|
||||
|
||||
all_data = {
|
||||
'library_results': [],
|
||||
'internet_results': [],
|
||||
'fetched_contents': [],
|
||||
'extracted_data': None,
|
||||
'filled_data': None
|
||||
}
|
||||
|
||||
# 步骤1: 搜索内容库
|
||||
if not self._check_pause(session_id):
|
||||
self._start_step(session_id, product_name, 1, '搜索内容库')
|
||||
try:
|
||||
articles = db.search_articles(product_name, category)
|
||||
all_data['library_results'] = articles
|
||||
self._complete_step(session_id, 1, {'count': len(articles)})
|
||||
logger.info(f"[{session_id}] 步骤1完成: 找到 {len(articles)} 篇文章")
|
||||
except Exception as e:
|
||||
self._fail_step(session_id, 1, str(e))
|
||||
result['message'] = f'搜索内容库失败: {e}'
|
||||
|
||||
# 步骤2: 搜索互联网
|
||||
if not self._check_pause(session_id) and not result.get('message'):
|
||||
self._start_step(session_id, product_name, 2, '搜索互联网')
|
||||
try:
|
||||
internet_results = search_service.search_internet(product_name, max_results=10)
|
||||
all_data['internet_results'] = internet_results
|
||||
self._complete_step(session_id, 2, {'count': len(internet_results)})
|
||||
logger.info(f"[{session_id}] 步骤2完成: 找到 {len(internet_results)} 条结果")
|
||||
except Exception as e:
|
||||
self._complete_step(session_id, 2, {'count': 0, 'error': str(e)})
|
||||
|
||||
# 步骤3: 抓取网页内容
|
||||
if not self._check_pause(session_id) and all_data['internet_results']:
|
||||
self._start_step(session_id, product_name, 3, '抓取网页内容')
|
||||
try:
|
||||
fetched = []
|
||||
urls_to_fetch = [r['url'] for r in all_data['internet_results'][:5]]
|
||||
|
||||
for i, url in enumerate(urls_to_fetch):
|
||||
if self._check_pause(session_id):
|
||||
break
|
||||
|
||||
fetch_result = search_service.fetch_url_content(url)
|
||||
if fetch_result.get('success'):
|
||||
fetched.append({
|
||||
'url': url,
|
||||
'title': fetch_result.get('title', ''),
|
||||
'content': fetch_result.get('content', '')[:500]
|
||||
})
|
||||
time.sleep(0.3)
|
||||
|
||||
all_data['fetched_contents'] = fetched
|
||||
self._complete_step(session_id, 3, {'count': len(fetched)})
|
||||
logger.info(f"[{session_id}] 步骤3完成: 抓取 {len(fetched)} 个网页")
|
||||
except Exception as e:
|
||||
self._fail_step(session_id, 3, str(e))
|
||||
|
||||
# 步骤4: 提取产品数据
|
||||
if not self._check_pause(session_id):
|
||||
self._start_step(session_id, product_name, 4, '提取产品数据')
|
||||
try:
|
||||
extracted = self._extract_data(product_name, all_data)
|
||||
all_data['extracted_data'] = extracted
|
||||
|
||||
if extracted:
|
||||
self._complete_step(session_id, 4, {'has_data': True})
|
||||
else:
|
||||
self._complete_step(session_id, 4, {'has_data': False}, status='skipped')
|
||||
result['message'] = '无法提取有效数据'
|
||||
except Exception as e:
|
||||
self._fail_step(session_id, 4, str(e))
|
||||
|
||||
# 步骤5: 填充字段
|
||||
if not self._check_pause(session_id) and all_data['extracted_data']:
|
||||
self._start_step(session_id, product_name, 5, '填充字段')
|
||||
try:
|
||||
filled = self._fill_fields(all_data['extracted_data'], category, subcategory)
|
||||
all_data['filled_data'] = filled
|
||||
|
||||
if filled:
|
||||
self._complete_step(session_id, 5, {'filled': True})
|
||||
else:
|
||||
self._fail_step(session_id, 5, '填充数据失败')
|
||||
except Exception as e:
|
||||
self._fail_step(session_id, 5, str(e))
|
||||
|
||||
# 步骤6: 提交审核
|
||||
if not self._check_pause(session_id) and all_data['filled_data']:
|
||||
self._start_step(session_id, product_name, 6, '提交审核')
|
||||
try:
|
||||
category_type = self._get_category_type(category)
|
||||
success, review_id_or_error = paramhub_client.submit_for_review(
|
||||
category_type,
|
||||
all_data['filled_data'],
|
||||
subcategory
|
||||
)
|
||||
|
||||
if success:
|
||||
self._complete_step(session_id, 6, {'review_id': review_id_or_error})
|
||||
result['success'] = True
|
||||
result['review_id'] = review_id_or_error
|
||||
|
||||
db.update_session_status(session_id, 'completed',
|
||||
review_id=review_id_or_error,
|
||||
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_or_error,
|
||||
details=all_data
|
||||
)
|
||||
logger.info(f"[{session_id}] 步骤6完成: 提交成功")
|
||||
else:
|
||||
self._fail_step(session_id, 6, review_id_or_error)
|
||||
db.update_session_status(session_id, 'failed')
|
||||
except Exception as e:
|
||||
self._fail_step(session_id, 6, str(e))
|
||||
|
||||
# 清理
|
||||
if session_id in self.active_sessions:
|
||||
del self.active_sessions[session_id]
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理会话异常: {session_id} - {e}")
|
||||
db.update_session_status(session_id, 'failed')
|
||||
return {'success': False, 'message': str(e)}
|
||||
|
||||
def _start_step(self, session_id, product_name, step_num, step_name):
|
||||
"""开始步骤"""
|
||||
db.update_session_status(session_id, 'running', current_step=step_num)
|
||||
db.add_process_step(session_id, product_name, step_num, step_name)
|
||||
if session_id not in self.step_timers:
|
||||
self.step_timers[session_id] = {}
|
||||
self.step_timers[session_id][step_num] = time.time()
|
||||
|
||||
def _complete_step(self, session_id, step_num, step_data=None, status='completed'):
|
||||
"""完成步骤"""
|
||||
duration_ms = None
|
||||
if session_id in self.step_timers and step_num in self.step_timers[session_id]:
|
||||
duration_ms = int((time.time() - self.step_timers[session_id][step_num]) * 1000)
|
||||
db.update_step_status(session_id, step_num, status, step_data=step_data, duration_ms=duration_ms)
|
||||
|
||||
def _fail_step(self, session_id, step_num, error_message):
|
||||
"""步骤失败"""
|
||||
duration_ms = None
|
||||
if session_id in self.step_timers and step_num in self.step_timers[session_id]:
|
||||
duration_ms = int((time.time() - self.step_timers[session_id][step_num]) * 1000)
|
||||
db.update_step_status(session_id, step_num, 'failed', error_message=error_message, duration_ms=duration_ms)
|
||||
db.update_session_status(session_id, 'failed')
|
||||
|
||||
def _check_pause(self, session_id):
|
||||
"""检查是否暂停"""
|
||||
if session_id not in self.active_sessions:
|
||||
return False
|
||||
|
||||
session = self.active_sessions[session_id]
|
||||
|
||||
if session.get('stop'):
|
||||
return True
|
||||
|
||||
while session.get('paused'):
|
||||
time.sleep(0.5)
|
||||
if session.get('stop'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def pause_session(self, session_id):
|
||||
"""暂停会话"""
|
||||
if session_id in self.active_sessions:
|
||||
self.active_sessions[session_id]['paused'] = True
|
||||
db.pause_session(session_id, '用户暂停')
|
||||
return True
|
||||
return False
|
||||
|
||||
def resume_session(self, session_id):
|
||||
"""继续会话"""
|
||||
if session_id in self.active_sessions:
|
||||
self.active_sessions[session_id]['paused'] = False
|
||||
db.resume_session(session_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
def stop_session(self, session_id):
|
||||
"""停止会话"""
|
||||
if session_id in self.active_sessions:
|
||||
self.active_sessions[session_id]['stop'] = True
|
||||
self.active_sessions[session_id]['paused'] = False
|
||||
db.update_session_status(session_id, 'stopped')
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_session_status(self, session_id):
|
||||
"""获取会话状态"""
|
||||
session = db.get_process_session(session_id)
|
||||
if session:
|
||||
steps = db.get_process_steps(session_id)
|
||||
return {'session': session, 'steps': steps}
|
||||
return None
|
||||
|
||||
def _extract_data(self, product_name, all_data):
|
||||
"""提取产品数据"""
|
||||
all_content = []
|
||||
|
||||
for article in all_data.get('library_results', []):
|
||||
content = article.get('content', '')
|
||||
if content:
|
||||
all_content.append(content)
|
||||
|
||||
for item in all_data.get('fetched_contents', []):
|
||||
content = item.get('content', '')
|
||||
if content:
|
||||
all_content.append(content)
|
||||
|
||||
if not all_content:
|
||||
return None
|
||||
|
||||
return {
|
||||
'name': product_name,
|
||||
'raw_content': '\n---\n'.join(all_content[:3])
|
||||
}
|
||||
|
||||
def _fill_fields(self, extracted_data, category, subcategory):
|
||||
"""填充字段"""
|
||||
if not extracted_data:
|
||||
return None
|
||||
|
||||
import re
|
||||
|
||||
filled = {
|
||||
'name': extracted_data['name'],
|
||||
'visible': True,
|
||||
'is_pinned': False
|
||||
}
|
||||
|
||||
content = extracted_data.get('raw_content', '')
|
||||
|
||||
params_match = re.search(r'(\d+(?:\.\d+)?)\s*[Bb]', content)
|
||||
if params_match:
|
||||
filled['parameters'] = f"{params_match.group(1)}B"
|
||||
|
||||
date_match = re.search(r'(\d{4}[-/]\d{1,2}[-/]\d{1,2})', content)
|
||||
if date_match:
|
||||
filled['publish_date'] = date_match.group(1).replace('/', '-')
|
||||
|
||||
filled['_source'] = 'auto_manager'
|
||||
filled['_extracted_at'] = datetime.now().isoformat()
|
||||
|
||||
return filled
|
||||
|
||||
def _get_category_type(self, category):
|
||||
"""获取分类类型"""
|
||||
if not category:
|
||||
return 'dynamic'
|
||||
|
||||
category_lower = category.lower()
|
||||
if 'model' in category_lower or 'ai' in category_lower:
|
||||
return 'model'
|
||||
elif 'gpu' in category_lower:
|
||||
return 'gpu'
|
||||
elif 'cpu' in category_lower:
|
||||
return 'cpu'
|
||||
return 'dynamic'
|
||||
|
||||
# 全局处理监控实例
|
||||
process_monitor = ProcessMonitor()
|
||||
@@ -102,15 +102,18 @@ class BackgroundTaskService:
|
||||
# 检查URL是否已存在
|
||||
existing = db.search_articles(url)
|
||||
if not any(a.get('url') == url for a in existing):
|
||||
# 获取抓取到的网页标题
|
||||
page_title = fetch_result.get('title', '') or title
|
||||
|
||||
article_id = db.add_article(
|
||||
product_names=[title],
|
||||
product_names=[], # 产品名称留空,后续手动关联
|
||||
category=category or '',
|
||||
keywords=keywords or [],
|
||||
summary=content[:200],
|
||||
content=content,
|
||||
source=url,
|
||||
url=url,
|
||||
search_title=title
|
||||
search_title=title # 搜索结果的标题
|
||||
)
|
||||
if article_id:
|
||||
saved_count += 1
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.article-page-title {
|
||||
.article-products {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
@@ -259,7 +259,7 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.article-page-title i {
|
||||
.article-products i {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
/* 处理监控页面专用样式 */
|
||||
|
||||
.process-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.process-header {
|
||||
background: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.process-header h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 活动处理列表 */
|
||||
.active-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.active-process-item {
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f5f3ff 0%, #e0e7ff 100%);
|
||||
}
|
||||
|
||||
.active-process-item.paused {
|
||||
border-color: #f59e0b;
|
||||
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
||||
}
|
||||
|
||||
.process-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.process-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.process-meta {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.process-status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* 步骤进度条 */
|
||||
.steps-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-node::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: #e9ecef;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.step-node:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-node.completed .step-circle {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-node.running .step-circle {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.step-node.failed .step-circle {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-node.skipped .step-circle {
|
||||
background: #9ca3af;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.process-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* 步骤定义流程 */
|
||||
.steps-flow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
flex: 0 0 150px;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.step-card:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 历史表格 */
|
||||
.history-section .data-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.step-indicator .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.step-indicator .dot.completed {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.step-indicator .dot.running {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.step-indicator .dot.failed {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
/* 详情模态框 */
|
||||
.detail-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-step {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.detail-step-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-step-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-step-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-step-content {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-step-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.steps-flow {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.steps-progress {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ function displayArticles() {
|
||||
const productNames = safeParseJSON(article.product_names, []);
|
||||
const keywords = safeParseJSON(article.keywords, []);
|
||||
|
||||
// 确定显示标题:优先 search_title,其次 product_names,最后来源
|
||||
// 确定显示标题:优先 search_title(搜索标题),其次 product_names(产品名称),最后来源
|
||||
const displayTitle = article.search_title || productNames.join(', ') || article.source || '未命名';
|
||||
|
||||
return `
|
||||
@@ -133,9 +133,9 @@ function displayArticles() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${article.search_title && productNames.length > 0 && article.search_title !== productNames.join(', ') ? `
|
||||
<div class="article-page-title">
|
||||
<i class="ri-article-line"></i> 网页标题: ${escapeHtml(productNames.join(', '))}
|
||||
${productNames.length > 0 ? `
|
||||
<div class="article-products">
|
||||
<i class="ri-price-tag-3-line"></i> 产品: ${escapeHtml(productNames.join(', '))}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="article-meta">
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
// API基础地址
|
||||
const API_BASE = '';
|
||||
|
||||
// 自动刷新定时器
|
||||
let autoRefreshInterval = null;
|
||||
|
||||
// 页面加载
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadStepDefinitions();
|
||||
loadActiveProcesses();
|
||||
loadHistory();
|
||||
|
||||
// 启动自动刷新(每2秒)
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// 启动自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
|
||||
autoRefreshInterval = setInterval(() => {
|
||||
loadActiveProcesses();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 加载步骤定义
|
||||
async function loadStepDefinitions() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/steps`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayStepDefinitions(data.steps);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载步骤定义失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示步骤定义
|
||||
function displayStepDefinitions(steps) {
|
||||
const container = document.getElementById('steps-flow');
|
||||
|
||||
container.innerHTML = steps.map((step, i) => `
|
||||
<div class="step-card">
|
||||
<div class="step-number">${step.num}</div>
|
||||
<div class="step-title">${escapeHtml(step.name)}</div>
|
||||
<div class="step-desc">${escapeHtml(step.description)}</div>
|
||||
</div>
|
||||
${i < steps.length - 1 ? '<i class="ri-arrow-right-line" style="color: #ccc;"></i>' : ''}
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 加载活动处理
|
||||
async function loadActiveProcesses() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/active`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayActiveProcesses(data.sessions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载活动处理失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示活动处理
|
||||
function displayActiveProcesses(sessions) {
|
||||
const container = document.getElementById('active-list');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = '<div class="empty-text">暂无正在进行的处理</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sessions.map(item => {
|
||||
const session = item.session;
|
||||
const steps = item.steps || [];
|
||||
const isPaused = session.paused || session.status === 'paused';
|
||||
|
||||
return `
|
||||
<div class="active-process-item ${isPaused ? 'paused' : ''}" id="process-${session.session_id}">
|
||||
<div class="process-info">
|
||||
<div>
|
||||
<div class="process-name">${escapeHtml(session.product_name)}</div>
|
||||
<div class="process-meta">
|
||||
${session.category ? `分类: ${escapeHtml(session.category)} | ` : ''}
|
||||
会话ID: ${escapeHtml(session.session_id)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="process-status-badge status-${session.status}">
|
||||
${getStatusText(session.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤进度 -->
|
||||
<div class="steps-progress">
|
||||
${renderStepsProgress(steps, session.current_step)}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="process-actions">
|
||||
${session.status === 'running' ? `
|
||||
<button onclick="pauseProcess('${session.session_id}')" class="btn btn-warning btn-sm">
|
||||
<i class="ri-pause-line"></i> 暂停
|
||||
</button>
|
||||
` : ''}
|
||||
${session.status === 'paused' ? `
|
||||
<button onclick="resumeProcess('${session.session_id}')" class="btn btn-success btn-sm">
|
||||
<i class="ri-play-line"></i> 继续
|
||||
</button>
|
||||
` : ''}
|
||||
${session.status !== 'completed' ? `
|
||||
<button onclick="stopProcess('${session.session_id}')" class="btn btn-danger btn-sm">
|
||||
<i class="ri-stop-line"></i> 停止
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="showProcessDetail('${session.session_id}')" class="btn btn-secondary btn-sm">
|
||||
<i class="ri-eye-line"></i> 详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染步骤进度
|
||||
function renderStepsProgress(steps, currentStep) {
|
||||
const totalSteps = 6;
|
||||
const stepStatuses = {};
|
||||
|
||||
// 构建步骤状态映射
|
||||
steps.forEach(s => {
|
||||
stepStatuses[s.step_number] = s.step_status;
|
||||
});
|
||||
|
||||
const stepNames = ['搜索内容库', '搜索互联网', '抓取网页', '提取数据', '填充字段', '提交审核'];
|
||||
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalSteps; i++) {
|
||||
const status = stepStatuses[i] || (i > currentStep ? 'pending' : '');
|
||||
let className = '';
|
||||
|
||||
if (status === 'completed') className = 'completed';
|
||||
else if (status === 'running') className = 'running';
|
||||
else if (status === 'failed') className = 'failed';
|
||||
else if (status === 'skipped') className = 'skipped';
|
||||
|
||||
html += `
|
||||
<div class="step-node ${className}">
|
||||
<div class="step-circle">${i}</div>
|
||||
<div class="step-label">${stepNames[i-1]}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 加载历史
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/recent?limit=20`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayHistory(data.sessions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示历史
|
||||
function displayHistory(sessions) {
|
||||
const container = document.getElementById('history-table');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = '<tr><td colspan="6" class="empty-text">暂无处理历史</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sessions.map(session => `
|
||||
<tr>
|
||||
<td>${escapeHtml(session.product_name)}</td>
|
||||
<td>
|
||||
<span class="process-status-badge status-${session.status}">
|
||||
${getStatusText(session.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="step-indicator">
|
||||
<span class="dot ${session.status}"></span>
|
||||
步骤 ${session.current_step || 0}/6
|
||||
</div>
|
||||
</td>
|
||||
<td>${formatDate(session.started_at || session.created_at)}</td>
|
||||
<td>${formatDate(session.finished_at) || '-'}</td>
|
||||
<td>
|
||||
<button onclick="showProcessDetail('${session.session_id}')" class="btn btn-sm btn-secondary">
|
||||
<i class="ri-eye-line"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 暂停处理
|
||||
async function pauseProcess(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/pause`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('处理已暂停', 'success');
|
||||
loadActiveProcesses();
|
||||
} else {
|
||||
showToast('暂停失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('暂停失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 继续处理
|
||||
async function resumeProcess(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/resume`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('处理已继续', 'success');
|
||||
loadActiveProcesses();
|
||||
} else {
|
||||
showToast('继续失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('继续失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止处理
|
||||
async function stopProcess(sessionId) {
|
||||
if (!confirm('确定要停止处理吗?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/stop`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('处理已停止', 'success');
|
||||
loadActiveProcesses();
|
||||
loadHistory();
|
||||
} else {
|
||||
showToast('停止失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('停止失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示处理详情
|
||||
async function showProcessDetail(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/status`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const session = data.data.session;
|
||||
const steps = data.data.steps;
|
||||
|
||||
document.getElementById('detail-title').innerHTML =
|
||||
`<i class="ri-file-list-3-line"></i> ${escapeHtml(session.product_name)} - 处理详情`;
|
||||
|
||||
const body = document.getElementById('detail-body');
|
||||
body.innerHTML = `
|
||||
<div class="detail-meta" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<div><strong>状态:</strong> ${getStatusText(session.status)}</div>
|
||||
<div><strong>分类:</strong> ${escapeHtml(session.category || '未分类')}</div>
|
||||
<div><strong>开始时间:</strong> ${formatDate(session.started_at) || '未开始'}</div>
|
||||
<div><strong>完成时间:</strong> ${formatDate(session.finished_at) || '-'}</div>
|
||||
${session.review_id ? `<div><strong>审核ID:</strong> ${escapeHtml(session.review_id)}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<h4 style="margin-bottom: 15px;"><i class="ri-list-check"></i> 处理步骤</h4>
|
||||
<div class="detail-steps">
|
||||
${steps.map(step => renderDetailStep(step)).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('process-detail-modal').classList.add('active');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('获取详情失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染详情步骤
|
||||
function renderDetailStep(step) {
|
||||
const statusColors = {
|
||||
'completed': '#10b981',
|
||||
'running': '#667eea',
|
||||
'failed': '#ef4444',
|
||||
'skipped': '#9ca3af',
|
||||
'pending': '#e9ecef'
|
||||
};
|
||||
|
||||
let stepDataHtml = '';
|
||||
if (step.step_data) {
|
||||
try {
|
||||
const data = typeof step.step_data === 'string' ? JSON.parse(step.step_data) : step.step_data;
|
||||
stepDataHtml = `<pre style="margin: 0; white-space: pre-wrap;">${escapeHtml(JSON.stringify(data, null, 2))}</pre>`;
|
||||
} catch (e) {
|
||||
stepDataHtml = escapeHtml(step.step_data);
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="detail-step">
|
||||
<div class="detail-step-header">
|
||||
<div class="detail-step-name">
|
||||
<span style="color: ${statusColors[step.step_status] || '#999'}; font-size: 18px;">●</span>
|
||||
步骤${step.step_number}: ${escapeHtml(step.step_name)}
|
||||
</div>
|
||||
<span class="detail-step-status" style="background: ${statusColors[step.step_status] || '#e9ecef'}; color: white;">
|
||||
${step.step_status}
|
||||
</span>
|
||||
</div>
|
||||
${stepDataHtml ? `<div class="detail-step-content">${stepDataHtml}</div>` : ''}
|
||||
${step.error_message ? `<div style="color: #ef4444; font-size: 13px;"><i class="ri-error-warning-line"></i> ${escapeHtml(step.error_message)}</div>` : ''}
|
||||
<div class="detail-step-time">
|
||||
${step.started_at ? `开始: ${formatDate(step.started_at)}` : ''}
|
||||
${step.finished_at ? ` | 完成: ${formatDate(step.finished_at)}` : ''}
|
||||
${step.duration_ms ? ` | 耗时: ${step.duration_ms}ms` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
function refreshData() {
|
||||
loadActiveProcesses();
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'running': '处理中',
|
||||
'paused': '已暂停',
|
||||
'completed': '已完成',
|
||||
'failed': '失败',
|
||||
'stopped': '已停止'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
function showToast(message, type = '') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast active ${type}`;
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('active');
|
||||
}, 3000);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ let currentResultIndex = -1;
|
||||
// 后台任务相关
|
||||
let currentTaskId = null;
|
||||
let taskPollInterval = null;
|
||||
let autoRefreshTasksInterval = null; // 自动刷新任务列表
|
||||
|
||||
// 页面加载初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -21,6 +22,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 加载失败URL和后台任务
|
||||
loadFailedUrls();
|
||||
loadBackgroundTasks();
|
||||
|
||||
// 启动自动刷新后台任务列表(每3秒)
|
||||
startAutoRefreshTasks();
|
||||
});
|
||||
|
||||
// 执行搜索
|
||||
@@ -861,4 +865,23 @@ async function clearCompletedTasks() {
|
||||
} catch (error) {
|
||||
showToast('清理任务出错', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 启动自动刷新后台任务列表
|
||||
function startAutoRefreshTasks() {
|
||||
if (autoRefreshTasksInterval) {
|
||||
clearInterval(autoRefreshTasksInterval);
|
||||
}
|
||||
|
||||
autoRefreshTasksInterval = setInterval(() => {
|
||||
loadBackgroundTasks();
|
||||
}, 3000); // 每3秒刷新一次
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
function stopAutoRefreshTasks() {
|
||||
if (autoRefreshTasksInterval) {
|
||||
clearInterval(autoRefreshTasksInterval);
|
||||
autoRefreshTasksInterval = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>处理监控 - 参数数据自动化管理系统</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/process.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="process-container">
|
||||
<!-- 头部 -->
|
||||
<header class="process-header">
|
||||
<div class="header-left">
|
||||
<a href="/" class="back-link">
|
||||
<i class="ri-arrow-left-line"></i> 返回主页
|
||||
</a>
|
||||
<h1><i class="ri-cpu-line"></i> 处理流程监控</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button onclick="refreshData()" class="btn btn-secondary">
|
||||
<i class="ri-refresh-line"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 活动处理区域 -->
|
||||
<div class="panel active-processes">
|
||||
<div class="panel-header">
|
||||
<h2><i class="ri-play-circle-line"></i> 正在处理</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="active-list" class="active-list">
|
||||
<div class="empty-text">暂无正在进行的处理</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤定义区域 -->
|
||||
<div class="panel steps-definition">
|
||||
<div class="panel-header">
|
||||
<h2><i class="ri-list-ordered"></i> 处理步骤</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="steps-flow" id="steps-flow">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处理历史区域 -->
|
||||
<div class="panel history-section">
|
||||
<div class="panel-header">
|
||||
<h2><i class="ri-history-line"></i> 处理历史</h2>
|
||||
<button onclick="loadHistory()" class="btn btn-secondary btn-sm">
|
||||
<i class="ri-refresh-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>产品名称</th>
|
||||
<th>状态</th>
|
||||
<th>当前步骤</th>
|
||||
<th>开始时间</th>
|
||||
<th>完成时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-table">
|
||||
<tr><td colspan="6" class="empty-text">暂无处理历史</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处理详情模态框 -->
|
||||
<div id="process-detail-modal" class="modal">
|
||||
<div class="modal-content large">
|
||||
<div class="modal-header">
|
||||
<h3 id="detail-title"><i class="ri-file-list-3-line"></i> 处理详情</h3>
|
||||
<button onclick="closeModal('process-detail-modal')" class="close-btn">
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="detail-body">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeModal('process-detail-modal')" class="btn btn-secondary">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示消息 -->
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script src="/static/js/process.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -51,7 +51,7 @@
|
||||
<span>搜索后自动抓取内容</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="auto-save">
|
||||
<input type="checkbox" id="auto-save" checked>
|
||||
<span>抓取后自动保存到内容库</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user