From 8f0efe28b6ab07fed1662977de9a145a66f5e4a6 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Tue, 14 Jul 2026 12:23:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E5=A4=84=E7=90=86=E6=B5=81=E7=A8=8B=E7=9B=91=E6=8E=A7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增处理会话表(process_sessions)和步骤表(process_steps) - 支持实时查看每个产品的处理步骤进度 - 支持暂停/继续/停止正在进行的处理 - 处理步骤包括:搜索内容库、搜索互联网、抓取网页、提取数据、填充字段、提交审核 - 新增处理监控页面 /process - 每个步骤记录详细数据、耗时、状态 - 历史记录可查看每个步骤的执行详情 --- app.py | 7 + models/database.py | 232 +++++++++++++++++++++ routes/process_monitor.py | 180 ++++++++++++++++ services/process_monitor.py | 339 ++++++++++++++++++++++++++++++ static/css/process.css | 328 +++++++++++++++++++++++++++++ static/js/process.js | 404 ++++++++++++++++++++++++++++++++++++ templates/process.html | 102 +++++++++ 7 files changed, 1592 insertions(+) create mode 100644 routes/process_monitor.py create mode 100644 services/process_monitor.py create mode 100644 static/css/process.css create mode 100644 static/js/process.js create mode 100644 templates/process.html diff --git a/app.py b/app.py index 7509e32..0a71654 100644 --- a/app.py +++ b/app.py @@ -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(): diff --git a/models/database.py b/models/database.py index a9a3243..be6f28b 100644 --- a/models/database.py +++ b/models/database.py @@ -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() \ No newline at end of file diff --git a/routes/process_monitor.py b/routes/process_monitor.py new file mode 100644 index 0000000..20133ca --- /dev/null +++ b/routes/process_monitor.py @@ -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('//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('//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('//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('//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('//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('//step/', 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 + }) \ No newline at end of file diff --git a/services/process_monitor.py b/services/process_monitor.py new file mode 100644 index 0000000..be14e1f --- /dev/null +++ b/services/process_monitor.py @@ -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() \ No newline at end of file diff --git a/static/css/process.css b/static/css/process.css new file mode 100644 index 0000000..3fe97f4 --- /dev/null +++ b/static/css/process.css @@ -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; + } +} \ No newline at end of file diff --git a/static/js/process.js b/static/js/process.js new file mode 100644 index 0000000..0e63ee7 --- /dev/null +++ b/static/js/process.js @@ -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) => ` +
+
${step.num}
+
${escapeHtml(step.name)}
+
${escapeHtml(step.description)}
+
+ ${i < steps.length - 1 ? '' : ''} + `).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 = '
暂无正在进行的处理
'; + return; + } + + container.innerHTML = sessions.map(item => { + const session = item.session; + const steps = item.steps || []; + const isPaused = session.paused || session.status === 'paused'; + + return ` +
+
+
+
${escapeHtml(session.product_name)}
+
+ ${session.category ? `分类: ${escapeHtml(session.category)} | ` : ''} + 会话ID: ${escapeHtml(session.session_id)} +
+
+
+ + ${getStatusText(session.status)} + +
+
+ + +
+ ${renderStepsProgress(steps, session.current_step)} +
+ + +
+ ${session.status === 'running' ? ` + + ` : ''} + ${session.status === 'paused' ? ` + + ` : ''} + ${session.status !== 'completed' ? ` + + ` : ''} + +
+
+ `; + }).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 += ` +
+
${i}
+
${stepNames[i-1]}
+
+ `; + } + + 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 = '暂无处理历史'; + return; + } + + container.innerHTML = sessions.map(session => ` + + ${escapeHtml(session.product_name)} + + + ${getStatusText(session.status)} + + + +
+ + 步骤 ${session.current_step || 0}/6 +
+ + ${formatDate(session.started_at || session.created_at)} + ${formatDate(session.finished_at) || '-'} + + + + + `).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 = + ` ${escapeHtml(session.product_name)} - 处理详情`; + + const body = document.getElementById('detail-body'); + body.innerHTML = ` +
+
状态: ${getStatusText(session.status)}
+
分类: ${escapeHtml(session.category || '未分类')}
+
开始时间: ${formatDate(session.started_at) || '未开始'}
+
完成时间: ${formatDate(session.finished_at) || '-'}
+ ${session.review_id ? `
审核ID: ${escapeHtml(session.review_id)}
` : ''} +
+ +

处理步骤

+
+ ${steps.map(step => renderDetailStep(step)).join('')} +
+ `; + + 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 = `
${escapeHtml(JSON.stringify(data, null, 2))}
`; + } catch (e) { + stepDataHtml = escapeHtml(step.step_data); + } + } + + return ` +
+
+
+ + 步骤${step.step_number}: ${escapeHtml(step.step_name)} +
+ + ${step.step_status} + +
+ ${stepDataHtml ? `
${stepDataHtml}
` : ''} + ${step.error_message ? `
${escapeHtml(step.error_message)}
` : ''} +
+ ${step.started_at ? `开始: ${formatDate(step.started_at)}` : ''} + ${step.finished_at ? ` | 完成: ${formatDate(step.finished_at)}` : ''} + ${step.duration_ms ? ` | 耗时: ${step.duration_ms}ms` : ''} +
+
+ `; +} + +// 刷新数据 +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); +} \ No newline at end of file diff --git a/templates/process.html b/templates/process.html new file mode 100644 index 0000000..9ddaf51 --- /dev/null +++ b/templates/process.html @@ -0,0 +1,102 @@ + + + + + + 处理监控 - 参数数据自动化管理系统 + + + + + +
+ +
+
+ + 返回主页 + +

处理流程监控

+
+
+ +
+
+ + +
+
+

正在处理

+
+
+
+
暂无正在进行的处理
+
+
+
+ + +
+
+

处理步骤

+
+
+
+ +
+
+
+ + +
+
+

处理历史

+ +
+
+ + + + + + + + + + + + + + +
产品名称状态当前步骤开始时间完成时间操作
暂无处理历史
+
+
+
+ + + + + +
+ + + + \ No newline at end of file