From 07b3406b08768c1a6a38c138721334bebbe8a650 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sun, 9 Aug 2026 22:55:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BB=BB=E5=8A=A1=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E8=AF=A6=E6=83=85=EF=BC=9AAgent=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=8A=E6=8A=A5(=E5=86=B3=E7=AD=96/?= =?UTF-8?q?=E6=89=A7=E8=A1=8C/=E9=AA=8C=E8=AF=81=E9=98=B6=E6=AE=B5)+?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E8=AF=A6=E6=83=85=E9=9D=A2=E6=9D=BF(2.5s?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=E6=AD=A5=E9=AA=A4=E6=97=A5=E5=BF=97/?= =?UTF-8?q?=E6=88=AA=E5=9B=BE/=E5=81=9C=E6=AD=A2=E6=8C=89=E9=92=AE)?= =?UTF-8?q?=EF=BC=9B=E4=BF=AE=E5=A4=8Dstatic=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent.py | 31 +++++++++++++ app.py | 5 ++- start.sh | 3 +- static/css/style.css | 33 ++++++++++++++ static/index.html | 27 +++++++++++ static/js/app.js | 105 +++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 199 insertions(+), 5 deletions(-) diff --git a/agent.py b/agent.py index 4e411e4..0d08947 100644 --- a/agent.py +++ b/agent.py @@ -99,6 +99,18 @@ class TaskRunner(threading.Thread): self.stop_flag = threading.Event() self.task_dir = os.path.join(TASKS_DIR, task_id) os.makedirs(self.task_dir, exist_ok=True) + # 实时状态(供 API 轮询展示) + self.current = { + 'phase': '排队中', 'step': 0, 'reason': '', 'action': '', + 'target': '', 'value': '', 'detail': '', 'ts': '', + 'elapsed': 0, 'started': False, + } + + def _set_current(self, **kw): + self.current.update(kw) + self.current['ts'] = time.strftime('%H:%M:%S') + if self.current.get('started'): + self.current['elapsed'] = int(time.time() - self.current.get('start_ts', time.time())) def stop(self): self.stop_flag.set() @@ -118,15 +130,18 @@ class TaskRunner(threading.Thread): def run(self): update_task(self.task_id, status='running', started_at=time.time(), result='running') + self._set_current(phase='启动中', started=True, start_ts=time.time()) steps = [] browser = None try: browser = AgentBrowser(namespace=f'task-{self.task_id}') deadline = time.time() + self.timeout + self._set_current(phase='打开页面', detail=self.url) self._log(f'打开页面: {self.url}') browser.open(self.url, timeout=60) browser.wait('--load', 'networkidle', timeout=45) self._log('页面已打开') + self._set_current(phase='页面已打开') step_n = 0 while step_n < self.max_steps: @@ -139,6 +154,8 @@ class TaskRunner(threading.Thread): step_n += 1 self._log(f'--- 步骤 {step_n}/{self.max_steps} ---') + self._set_current(phase='分析页面', step=step_n, + detail=f'正在获取页面元素快照') # 1. 快照 try: @@ -151,6 +168,8 @@ class TaskRunner(threading.Thread): cur_title = browser.title() # 2. LLM 决策 + self._set_current(phase='AI 决策中', step=step_n, + detail='正在分析页面并决定下一步动作...') user_msg = ( f'## 测试目标\n{self.goal}\n\n' f'## 当前页面\nURL: {cur_url}\n标题: {cur_title}\n\n' @@ -172,11 +191,18 @@ class TaskRunner(threading.Thread): if action not in ('click', 'fill', 'select', 'press', 'wait', 'scroll', 'screenshot', 'assert', 'done', 'fail'): self._log(f'非法动作: {action}') + self._set_current(phase='动作异常', step=step_n, + action=action, detail=f'非法动作: {action}') steps.append(self._record(step_n, decision, 'error', f'非法动作: {action}')) continue # 3. 执行动作(带自愈重试) + self._set_current(phase='执行动作', step=step_n, + action=action, target=decision.get('target', ''), + value=decision.get('value', ''), + reason=decision.get('reason', ''), + detail=f'{action} {decision.get("target", "")} {decision.get("value", "")}'.strip()) result, detail, extra = self._execute(browser, decision) self._log(f'动作 {action} -> {result} {detail}') @@ -243,6 +269,9 @@ class TaskRunner(threading.Thread): for attempt in range(MAX_RETRY_SAME_ERROR + 1): if attempt > 0: self._log(f'重试 {attempt}: {action} {target}') + self._set_current(phase='自愈重试', step=self.current.get('step', 0), + action=action, target=target, + detail=f'第 {attempt} 次重试: {action} {target}') try: if action == 'click': browser.click(self._resolve(target)) @@ -382,6 +411,8 @@ class TaskRunner(threading.Thread): return bool(r) def _finish(self, browser, result, summary, steps): + self._set_current(phase='完成', step=len(steps), + detail=f'结果: {result} - {summary}') if browser: browser.close() update_task(self.task_id, status='finished', result=result, diff --git a/app.py b/app.py index fa25d8d..606e568 100644 --- a/app.py +++ b/app.py @@ -10,7 +10,7 @@ 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='') +app = Flask(__name__, static_folder='static', static_url_path='/static') CORS(app) init_db() @@ -84,6 +84,9 @@ def api_get_task(tid): t['created'] = _fmt_time(t.get('created_at')) t['finished'] = _fmt_time(t.get('finished_at')) t['steps_log'] = load_step_logs(tid) + runner = _runners.get(tid) + if runner: + t['current'] = runner.current return jsonify(t) diff --git a/start.sh b/start.sh index 14a8b1c..a0cc07a 100755 --- a/start.sh +++ b/start.sh @@ -7,7 +7,8 @@ mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR" mkdir -p logs if [ -n "$1" ] && [ "$1" = "stop" ]; then - pkill -f "webtest-agent/app.py" && echo "已停止" || echo "未在运行" + PID=$(ss -tlnp 2>/dev/null | grep ":16061 " | grep -oP 'pid=\K[0-9]+' | head -1) + if [ -n "$PID" ]; then kill "$PID" && echo "已停止 (PID $PID)"; else echo "未在运行"; fi exit 0 fi diff --git a/static/css/style.css b/static/css/style.css index f7cf6b7..f202451 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -45,3 +45,36 @@ a.report-link:hover { text-decoration: underline; } .modal-body { width: 90vw; height: 90vh; background: #fff; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; } .modal-head { padding: 12px 16px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; font-weight: 600; } #report-frame { flex: 1; border: none; width: 100%; } +.btn.danger { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; } +.btn.danger:hover { background: #fecaca; } +/* 任务详情 */ +.detail-body { width: 96vw; max-width: 1100px; } +.detail-content { flex: 1; overflow-y: auto; padding: 16px 20px; } +.detail-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px 24px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px 16px; font-size: 13px; margin-bottom: 14px; } +.detail-meta .m-item b { display: block; color: #6b7280; font-size: 11px; font-weight: 500; margin-bottom: 3px; } +.detail-meta .m-item span { color: #111; word-break: break-all; } +.live-bar { display: flex; align-items: flex-start; gap: 12px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 12px 16px; margin-bottom: 16px; } +.live-bar.hidden { display: none; } +.live-dot { width: 10px; height: 10px; border-radius: 50%; background: #3b82f6; margin-top: 5px; animation: pulse 1.2s infinite; } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } } +.live-phase { font-weight: 600; font-size: 14px; color: #1e40af; margin-bottom: 4px; } +.live-detail { font-size: 13px; color: #374151; } +.live-reason { font-size: 12px; color: #6b7280; margin-top: 4px; } +.detail-steps-head { font-size: 14px; font-weight: 600; margin: 18px 0 10px; } +.detail-steps { display: flex; flex-direction: column; gap: 8px; } +.step-card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 12px 14px; background: #fff; } +.step-card .step-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; } +.step-num { background: #f3f4f6; color: #374151; font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 999px; } +.step-action { font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 999px; background: #dbeafe; color: #1e40af; } +.step-action.assert { background: #fef3c7; color: #92400e; } +.step-action.click { background: #dcfce7; color: #166534; } +.step-action.fill { background: #ede9fe; color: #5b21b6; } +.step-action.fail { background: #fee2e2; color: #991b1b; } +.step-target { font-family: ui-monospace, monospace; font-size: 12px; color: #374151; background: #f9fafb; padding: 2px 8px; border-radius: 6px; } +.step-result { margin-left: auto; font-size: 12px; font-weight: 600; } +.step-result.ok { color: #16a34a; } +.step-result.bad { color: #dc2626; } +.step-reason { font-size: 12px; color: #6b7280; margin-bottom: 6px; } +.step-shot img { max-width: 320px; border-radius: 8px; border: 1px solid #e5e7eb; display: block; margin-top: 6px; cursor: zoom-in; } +.step-detail { font-size: 12px; color: #374151; } +.step-time { font-size: 11px; color: #9ca3af; margin-left: 8px; } diff --git a/static/index.html b/static/index.html index c568bcc..e2742c2 100644 --- a/static/index.html +++ b/static/index.html @@ -46,6 +46,33 @@ + +