#!/usr/bin/env python3 """webtest-agent - AI 网页测试智能体 (Flask 服务)""" import os import threading from flask import Flask, request, jsonify, send_from_directory, abort from flask_cors import CORS import config from db import init_db, create_task, get_task, list_tasks, load_step_logs, update_task from agent import TaskRunner app = Flask(__name__, static_folder='static', static_url_path='') CORS(app) init_db() _runner_lock = threading.Lock() _runners = {} # task_id -> TaskRunner _semaphore = threading.Semaphore(config.MAX_CONCURRENT_TASKS) @app.route('/') def index(): return send_from_directory('static', 'index.html') @app.route('/health') def health(): return jsonify({'status': 'ok', 'version': '1.0.0', 'llm_model': config.LLM_MODEL, 'concurrent': len(_runners)}) @app.route('/api/tasks', methods=['POST']) def api_create_task(): data = request.get_json(silent=True) or {} url = (data.get('url') or '').strip() goal = (data.get('goal') or '').strip() if not url: return jsonify({'error': '缺少 url 参数'}), 400 if not goal: return jsonify({'error': '缺少 goal(测试目标)参数'}), 400 if not url.startswith(('http://', 'https://')): url = 'https://' + url max_steps = int(data.get('max_steps') or config.DEFAULT_MAX_STEPS) timeout = int(data.get('timeout') or config.DEFAULT_TIMEOUT) max_steps = max(1, min(max_steps, 100)) timeout = max(30, min(timeout, 3600)) tid = create_task(url, goal, max_steps, timeout) def _launch(): with _semaphore: if get_task(tid) and get_task(tid).get('status') == 'stopped': return runner = TaskRunner(tid, url, goal, max_steps, timeout) with _runner_lock: _runners[tid] = runner runner.start() runner.join() with _runner_lock: _runners.pop(tid, None) threading.Thread(target=_launch, daemon=True).start() return jsonify({'task_id': tid, 'status': 'queued', 'url': url, 'goal': goal}), 202 @app.route('/api/tasks', methods=['GET']) def api_list_tasks(): tasks = list_tasks(limit=50) for t in tasks: t['created'] = _fmt_time(t.get('created_at')) t['finished'] = _fmt_time(t.get('finished_at')) return jsonify({'tasks': tasks}) @app.route('/api/tasks/', methods=['GET']) def api_get_task(tid): t = get_task(tid) if not t: abort(404) t['created'] = _fmt_time(t.get('created_at')) t['finished'] = _fmt_time(t.get('finished_at')) t['steps_log'] = load_step_logs(tid) return jsonify(t) @app.route('/api/tasks//stop', methods=['POST']) def api_stop_task(tid): runner = _runners.get(tid) if runner: runner.stop() update_task(tid, status='stopped', result='stopped') return jsonify({'ok': True, 'message': '停止请求已发送'}) t = get_task(tid) if not t: abort(404) if t['status'] == 'queued': update_task(tid, status='stopped', result='stopped') return jsonify({'ok': True, 'message': '已取消排队任务'}) return jsonify({'ok': False, 'message': '任务不在运行中'}) @app.route('/api/tasks//report') def api_report(tid): path = os.path.join(config.TASKS_DIR, tid, 'report.html') if os.path.exists(path): return send_from_directory(config.TASKS_DIR, f'{tid}/report.html') abort(404) @app.route('/api/tasks//report.json') def api_report_json(tid): t = get_task(tid) if not t: abort(404) return jsonify({ 'task': t, 'steps': load_step_logs(tid), 'report_url': f'/api/tasks/{tid}/report', }) @app.route('/api/tasks//screenshot/') def api_screenshot(tid, name): if not name or '..' in name or '/' in name: abort(400) path = os.path.join(config.TASKS_DIR, tid, name) if os.path.exists(path): return send_from_directory(config.TASKS_DIR, f'{tid}/{name}') abort(404) def _fmt_time(ts): if not ts: return None import time return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) if __name__ == '__main__': print(f'webtest-agent 启动: http://0.0.0.0:{config.PORT}') app.run(host=config.HOST, port=config.PORT, threaded=True)