142.实习日志系统(SSD+BMAD+Harness系统体系的模拟使用)+playwright自动化 登录+填写_测试

继上篇https://www.cnblogs.com/gaodiyuanjin/p/19888191
我们用一个项目来验证使用
SSD+BMAD+Harness验证

这是我根据AI来有所侧重的一套体系
image

  • 规范化都规定了每个角色的应用(应用BMAD的思想)
  • spec撰写开发前的文档(应用SSD思想)
  • 整个Agent开发在约束规范下进行(Harness思想)

目的就是 以后填写实习日志系统让Agent驱动完全自动化完成
其实简单点说就是为了---偷懒

一.实习日志系统开发(SSD+BMAD+Harness)

并没有找现成的日志系统来开发 而是正好测试一下这套体系的代码质量
工具还是TRAE(MiniMax2.7模型)

1.先编写spec

这个真的很重要!!!
你前面铺垫越规范越详细 AI后面就不容易翻车
当然这个项目太常见了 我同样的需求给GPT和Claude来撰写spec文档

---
id: SPEC-INTERNSHIP-SYSTEM
version: 1.0.0
title: 实习日志管理系统 · 完整开发规范
status: Ready for Agent Codegen
created: 2025-01
tech: SpringBoot + Vue3
storage: LocalStorage (无数据库)
scope: Module 01 登录页 / Module 02 主页面 / Module 03 实习日志模块
---

# 实习日志管理系统 · 项目开发规范 (Spec)

---

## 一、项目全局约定

### 1.1 技术栈选型

| 层级 | 选型 |
|------|------|
| 前端框架 | Vue 3 + JavaScript + Vite |
| 路由 | Vue Router v4 |
| 状态管理 | Pinia |
| 样式 | Tailwind CSS |
| UI 组件库 | Element Plus |
| 日历组件 | Element Plus ElCalendar |
| 图标库 | Element Plus Icons |
| 表单校验 | Element Plus Form Rules |
| HTTP 客户端 | axios |
| 后端框架 | Spring Boot 3.x |
| 数据存储 | 前端 LocalStorage(无数据库) |

### 1.2 存储策略总览

> 本项目不接入任何数据库,所有数据均持久化在浏览器 LocalStorage 中,
> 后端 SpringBoot 仅提供静态接口 mock / 验证码生成 / 会话校验逻辑,不做持久化。

| 存储 Key | 存储内容 | 生命周期 |
|----------|----------|----------|
| `internship_username` | 上次登录的账号 | 永久,勾选记住账号时写入 |
| `internship_autologin` | 是否开启自动登录 flag | 永久 |
| `internship_token` | 登录态 token(mock字符串)| 自动登录时永久 / 否则 sessionStorage |
| `internship_user` | 当前用户信息 JSON | 同 token 策略 |
| `internship_diary` | 实习日志数据 JSON Array | 永久 |

### 1.3 项目目录结构

project-root/
 │
 ├── frontend/                          # Vue3 前端工程
 │   ├── src/
 │   │   ├── pages/
 │   │   │   ├── LoginPage.vue          # 登录页
 │   │   │   ├── DashboardPage.vue      # 主页面
 │   │   │   └── InternshipPage.vue     # 实习日志页
 │   │   ├── components/
 │   │   │   ├── layout/
 │   │   │   │   ├── AppLayout.vue      # 整体布局容器
 │   │   │   │   ├── SidebarNav.vue     # 左侧导航
 │   │   │   │   └── TopHeader.vue      # 顶部 Header
 │   │   │   ├── internship/
 │   │   │   │   ├── DiaryCalendar.vue  # 日历组件
 │   │   │   │   └── DiaryDialog.vue    # 填写弹框组件
 │   │   │   └── shared/
 │   │   │       └── CaptchaImage.vue   # 验证码图片组件
 │   │   ├── store/
 │   │   │   ├── auth.store.ts          # 登录态 store
 │   │   │   └── diary.store.ts         # 日志数据 store
 │   │   ├── router/
 │   │   │   └── index.ts               # 路由配置 + 路由守卫
 │   │   ├── services/
 │   │   │   └── auth.service.ts        # 登录接口调用
 │   │   ├── types/
 │   │   │   ├── auth.types.ts          # 登录相关类型
 │   │   │   └── diary.types.ts         # 日志相关类型
 │   │   └── utils/
 │   │       └── storage.ts             # LocalStorage 封装工具
 │   └── vite.config.ts
 │
 └── backend/                           # SpringBoot 后端工程
 └── src/main/java/
 └── com/internship/
 ├── controller/
 │   ├── AuthController.java      # 登录 / 验证码接口
 │   └── MockController.java      # mock 数据接口
 ├── model/
 │   ├── LoginRequest.java
 │   └── LoginResponse.java
 └── InternshipApplication.java


### 1.4 路由规划

| 路径 | 页面组件 | 路由守卫规则 |
|------|----------|-------------|
| `/login` | `LoginPage.vue` | 已登录自动跳转 `/dashboard` |
| `/dashboard` | `DashboardPage.vue` | 未登录跳转 `/login` |
| `/internship` | `InternshipPage.vue` | 未登录跳转 `/login` |
| `/` | — | 重定向到 `/dashboard` |

### 1.5 data-testid 命名规范

所有可交互元素必须附加 `data-testid`,Playwright 通过此属性定位,
不依赖 CSS 类名或元素文案,保证测试脚本稳定性。

命名格式:`{元素类型}-{功能模块}-{描述}`

| 元素类型前缀 | 含义 |
|-------------|------|
| `input-` | 输入框 |
| `btn-` | 按钮 |
| `checkbox-` | 复选框 |
| `img-` | 图片 |
| `nav-` | 导航项 |
| `dialog-` | 弹框容器 |
| `calendar-` | 日历相关 |
| `text-` | 纯展示文本 |

---

## 二、Module 01 — 登录页面 (LoginPage)

### 2.1 页面职责

- 对用户账号密码进行校验(SpringBoot 接口 mock 验证)
- 生成并校验图形验证码(SpringBoot 后端生成 base64 图片)
- 处理"记住账号"和"自动登录"两个持久化选项
- 登录成功后跳转主页面,失败给出明确提示

### 2.2 页面视觉布局


┌─────────────────────────────────────────────┐
 │                                             │
 │          [Logo]   实习日志管理系统           │  ← 顶部品牌区
 │                                             │
 │  ┌─────────────────────────────────────┐    │
 │  │  账号                               │    │
 │  │  [______***\**\*\*\*\*\*\*\*\*\*\*\*\*\*]         │    │  ← 账号输入框
 │  │                                     │    │
 │  │  密码                               │    │
 │  │  [\*\*\*\*________________] [👁]        │    │  ← 密码输入框 + 眼睛图标
 │  │                                     │    │
 │  │  验证码                             │    │
 │  │  [\*\*\*\*\*\*\*\*\*\**\***] [验证码图片🔄]    │    │  ← 验证码 + 图片(点击刷新)
 │  │                                     │    │
 │  │  ☑ 记住账号      ☑ 自动登录        │    │  ← 选项行
 │  │                                     │    │
 │  │  [        登  录        ]           │    │  ← 登录按钮
 │  └─────────────────────────────────────┘    │
 │                                             │
 │          © 2025 实习日志管理系统             │  ← 页脚
 └─────────────────────────────────────────────┘


### 2.3 各元素详细规范

#### 账号输入框
- 标签:`账号`
- placeholder:`请输入学号 / 工号`
- 校验:必填 / 长度 4~20 / 仅允许字母、数字、下划线
- 错误提示位置:输入框正下方红色小字
- 自动回填:页面 `onMounted` 时读取 `localStorage.internship_username`
- `data-testid`:`input-username`

#### 密码输入框
- 标签:`密码`
- placeholder:`请输入密码`
- 默认 `type="password"`
- 右侧眼睛图标点击后切换 `type="text"`,再次点击还原
- 校验:必填
- `data-testid`:`input-password` / 眼睛图标 `btn-toggle-password`

#### 验证码区域
- 标签:`验证码`
- 输入框 placeholder:`请输入验证码`
- 输入框固定接受 4 位字符,自动转大写
- 右侧展示验证码图片(后端返回 base64)
- 点击图片触发刷新,请求 `/api/auth/captcha` 重新获取
- 图片加载中展示灰色占位块
- `data-testid`:`input-captcha` / `img-captcha`

#### 记住账号复选框
- 文字:`记住账号`
- 默认值:若 localStorage 存在 `internship_username` 则默认勾选,否则不勾选
- 勾选且登录成功:将 username 写入 `localStorage.internship_username`
- 取消勾选且登录成功:删除 `localStorage.internship_username`
- `data-testid`:`checkbox-remember`

#### 自动登录复选框
- 文字:`自动登录`
- 依赖关系:仅当"记住账号"已勾选时可操作,否则禁用并显示 tooltip 提示
- 勾选且登录成功:token 写入 `localStorage`,下次访问系统自动跳转
- 未勾选:token 写入 `sessionStorage`,关闭浏览器后失效
- `data-testid`:`checkbox-autologin`

#### 登录按钮
- 文字:`登 录`
- 点击触发:① 前端表单校验 → ② 调用登录接口 → ③ 处理响应
- Loading 状态:按钮显示 spinner,文字改为"登录中...",按钮禁用
- 支持 `Enter` 键提交
- `data-testid`:`btn-login`

### 2.4 后端接口规范(SpringBoot Mock)

#### GET /api/auth/captcha
- 无需认证
- 后端生成随机 4 位字母数字字符串,返回 base64 图片 + 会话标识
- Response:
```json
{
  "captchaToken": "uuid-string",
  "captchaImage": "data:image/png;base64,..."
}


#### POST /api/auth/login

- Request Body:


{
  "username": "string",
  "password": "string",
  "captcha": "string",
  "captchaToken": "string"
}


- Response 200(成功):


{
  "token": "mock-jwt-string",
  "user": {
    "id": "001",
    "name": "张三",
    "role": "student"
  }
}


- Response 400(失败):


{
  "code": "INVALID_CAPTCHA | INVALID_CREDENTIALS",
  "message": "验证码错误 | 账号或密码错误"
}


> Mock 账号数据(硬编码在 SpringBoot Controller 中):
>
> - 账号:`student001` 密码:`123456`
> - 账号:`admin` 密码:`admin123`

### 2.5 登录流程状态机


[初始态]
   │
   ▼
用户填写表单
   │
   ▼
点击登录 → 前端校验失败 → 显示字段错误,停留登录页
   │
前端校验通过
   │
   ▼
调用 POST /api/auth/login → Loading 态
   │
   ├── 成功 ──► 存储 token + user → 跳转 /dashboard
   │
   └── 失败
        ├── INVALID_CAPTCHA ──► 刷新验证码 + 清空验证码输入框 + 错误提示
        └── INVALID_CREDENTIALS ──► 清空密码输入框 + 错误提示


### 2.6 Playwright 测试锚点清单

| data-testid           | 元素         | 用途           |
| --------------------- | ------------ | -------------- |
| `input-username`      | 账号输入框   | 填入账号       |
| `input-password`      | 密码输入框   | 填入密码       |
| `input-captcha`       | 验证码输入框 | 填入验证码     |
| `img-captcha`         | 验证码图片   | 点击刷新       |
| `btn-toggle-password` | 眼睛图标     | 切换密码可见性 |
| `checkbox-remember`   | 记住账号     | 勾选/取消      |
| `checkbox-autologin`  | 自动登录     | 勾选/取消      |
| `btn-login`           | 登录按钮     | 提交表单       |
| `text-error-message`  | 错误提示     | 断言错误文案   |

------

## 三、Module 02 — 主页面 (DashboardPage)

### 3.1 页面职责

- 提供整体应用布局容器(左侧导航 + 右侧内容区 + 顶部 Header)
- 左侧导航包含所有功能模块入口,含实习模块
- 右侧内容区根据路由动态渲染对应模块页面
- 顶部 Header 展示当前用户信息及退出登录

### 3.2 页面视觉布局


┌──────────────────────────────────────────────────────────────┐
│  [≡]  实习日志管理系统 LOGO          👤 张三  [退出登录]      │  ← TopHeader
├────────────┬─────────────────────────────────────────────────┤
│            │                                                  │
│  🏠 首页   │                                                  │
│            │                                                  │
│  📋 实习   │           右侧内容区                             │
│    日志    │        (RouterView 渲染区域)                     │
│            │                                                  │
│  📊 统计   │                                                  │
│            │                                                  │
│  ⚙️ 设置   │                                                  │
│            │                                                  │
└────────────┴─────────────────────────────────────────────────┘
 ← SidebarNav →
   宽度 220px
### 3.3 TopHeader 规范

- 左侧:汉堡菜单图标(控制侧边栏展开/收起)+ 系统名称

- 右侧:用户头像(圆形占位符,无头像时显示姓名首字)+ 用户姓名 + 退出登录按钮

- 退出登录:清除 token 和 user 的 storage loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
   loginPage = new LoginPage(page);
   await loginPage.goto();
   // Mock 验证码 API 返回固定值
   await page.route('/api/auth/captcha', route =>
   route.fulfill({
   status: 200,
   contentType: 'application/json',
   body: JSON.stringify({
   captchaToken: 'mock-token-001',
   captchaImage: 'data:image/svg+xml;base64,...'
   })
   })
   );
   });

  test.describe('正向场景', () => {

  test('LOGIN-P01: 有效凭据登录成功并跳转', async ({ page }) => {
   await page.route('/api/auth/login', route =>
   route.fulfill({
   status: 200,
   body: JSON.stringify({
   accessToken: 'mock-access-token',
   refreshToken: 'mock-refresh-token',
   user: { id: '1', name: '张三', role: 'student', avatar: null }
   })
   })
   );
   const { valid } = AUTH_FIXTURES;
   await loginPage.loginWith(valid.username, valid.password, valid.captcha);
   await loginPage.assertRedirectedToDash

上述是Claude生成的文档 GPT生成的我也会上传的gitee就不再展示

2.将spec转给Planner

用了TRAE的项目规则 它做任何事情都会扫描这个
image
同样你也可以直接#选择那些文档

因为TRAE也提供了@这种的方式 第一步写完spec找Planner呗
所以就不在上下文加入 直接@它

image

然后开始写计划

3.拿着Planner的规划去找Coder编写代码

我们同样是找到对应的人物完成 其实本质都是规范约束罢了
image
然后看一下生成的代码
image

4.然后Tester测试验证代码准确性

我们不去检查代码 而是交给对应Tester
image
同样的如果出现问题 它会生成报告 其中的错误

5.Fixer修改

如果有错误我们让其修复
image
根据Tester测试的结果进行修复

其实这里已经可以形成一套循环了
写代码->测试->修复
不过我这里全是手动的 并非真正实现自动化
那样的就需要脚本来驱动 等后续的深入学习再总结

6.Reviewer测评一下

类似一个大佬 测测这段代码怎么样 给我生成个报告
image

这已经打通了一套体系规范
结果看看生成的实习日志系统
Screenshot 2026-04-27 163016
Screenshot 2026-04-27 163058

这是最基础的 并没有美化页面 但是基本逻辑开发测试用都没有问题


二.Playwright自动化测试

这个才是关键东西
至于介绍 我让AI通过官网生成了一篇文档
image

最重要的是安装的时候出来很多问题:

npm install -g playwright

这条命令没啥问题
就是后续的安装chromium出现问题
应为我们为了使用chrome来测试
但是下载就会报错 镜像各种都试过了
参考: https://blog.kangyaocoding.top/archives/playwright-shou-dong-an-zhuang-chromium-wan-zheng-zhi-nan-guo-nei-jia-su-ban#:~:text=原因:Linux 手动放置浏览器后,缺少 Playwright 用于判定安装完成的标记文件 INSTALLATION_COMPLETE。 解决方案:在对应 revision ,%E7%9B%AE%E5%BD%95%E4%B8%8B%E6%89%8B%E5%8A%A8%E5%88%9B%E5%BB%BA%E6%A0%87%E8%AE%B0%E6%96%87%E4%BB%B6%E3%80%82%20%23%20%E4%BB%A5%20revision%3D1208%20%E4%B8%BA%E4%BE%8B%20touch%20%2Froot%2F.cache%2Fms-playwright%2Fchromium-1208%2FINSTALLATION_COMPLETE

然后也听过DeepseekV4来完成最终的配置

image
此目录下得有这个才能识别使用

就去把它下载下来对应的

npx playwright install --dry-run chromium
找到对应版本

然后把它手动加载到缓存里
但是系统以为未标记 还会消失
这就是windows

cd /d "%USERPROFILE%\AppData\Local\ms-playwright\chromium-1217"
type nul > INSTALLATION_COMPLETE

手动给它添加一个标记

npx playwright install chromium

再安装mcp

npm install -g @playwright/mcp@latest

在TRAE中配置

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

可以命令行测试一下:npx playwright open google.com
也可以通过Agent驱动看看

接下来就有意思了 其实就两步:

  • 登录
  • 提交实习日记

这里先说半自动化

使用playwright工具完成实习日志自动填写任务:
前后端已经启动 打开浏览器访问http://localhost:3000/
登录我将辅助你完成 后续直接都是自动登录了
找到实习日志填写 自动给我生成一段实习日记有关计算机工作的 填写提交

然后会打开 第一次登陆后保存信息 之后AI就会自动填写

其实它也是写了一个脚本 然后playwright驱动
image

重点来了 验证码问题
数字字母混合图片验证码其实就是为了防止自动化的

playwright无法绕开这个 所以就需要第三方来帮忙
我们肯定为的就是测试完全自动化 你不能一个登录还得让我来干预吧

结合DeepseekV4完成一个脚本来驱动完成自动化
image

这里借助三方平台 图鉴 api简单
http://www.ttshitu.com/
image

import { chromium } from 'playwright';
import axios from 'axios';

// ==================== 配置区 ====================

const USERNAME = 'student001';
const PASSWORD = '123456';
const SYSTEM_URL = 'http://localhost:3000';

// 图鉴账号(请改成你自己的)
const TT_USER = '账号';
const TT_PASS = '密码';

const TEST_TT_BALANCE_BEFORE_LOGIN = true;

// ==================== 日期 ====================

const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;

const DIARY_CONTENT = `今天的主要工作:

1. 参加项目晨会,了解本周迭代目标。
2. 学习代码规范,配置本地环境。
3. 协助导师完成模块测试。
4. 撰写测试报告初稿。

心得体会:对开发流程有了更深入的理解,继续加油。`;

// ==================== 图鉴工具函数 ====================

function checkTTConfig() {
  if (!TT_USER || TT_USER.includes('您的图鉴')) throw new Error('TT_USER 未正确配置');
  if (!TT_PASS || TT_PASS.includes('您的图鉴')) throw new Error('TT_PASS 未正确配置');
}

async function checkTTBalance() {
  checkTTConfig();
  console.log('▶ 正在测试图鉴账号余额...');
  const queryUrl = `http://api.ttshitu.com/queryAccountInfo.json?username=${encodeURIComponent(TT_USER)}&password=${encodeURIComponent(TT_PASS)}`;
  const res = await axios.get(queryUrl, { timeout: 60000 });
  console.log('图鉴账户信息:', res.data);
  if (!res.data.success) throw new Error(`图鉴余额查询失败:${JSON.stringify(res.data)}`);
  console.log(`✅ 图鉴账户正常,当前余额:${res.data.data.balance} 元`);
  return res.data;
}

// ==================== 打码函数(数字+字母,typeid=3) ====================

async function recognizeCaptcha(base64Image) {
  checkTTConfig();

  let pureBase64 = String(base64Image);
  if (pureBase64.includes(',')) {
    pureBase64 = pureBase64.split(',')[1];
  }

  console.log('最终提交的 base64 前 30 字符:', pureBase64.substring(0, 30));

  const reqBody = {
    username: TT_USER,
    password: TT_PASS,
    typeid: '3',
    image: pureBase64
  };

  const res = await axios.post('http://api.ttshitu.com/predict', reqBody, {
    headers: { 'Content-Type': 'application/json' },
    timeout: 60000
  });

  console.log('图鉴接口返回:', res.data);

  if (res.data.success && res.data.data && res.data.data.result) {
    return res.data.data.result;
  } else {
    throw new Error(`图鉴识别失败:${res.data.message || '未知错误'}`);
  }
}

// ==================== 获取验证码 base64 ====================

async function getCaptchaBase64(page) {
  const img = page.locator('img[alt="验证码"]');
  if (await img.count() > 0) {
    await img.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const buffer = await img.screenshot();
    let base64 = buffer.toString('base64');
    if (base64.includes(',')) {
      base64 = base64.split(',')[1];
    }
    if (!base64 || base64.length < 100) throw new Error('验证码截图为空或过小');
    console.log('获取到 img 验证码,base64 长度:', base64.length);
    return base64;
  }

  const canvas = page.locator('canvas');
  if (await canvas.count() > 0) {
    const dataUrl = await canvas.evaluate(c => c.toDataURL('image/png'));
    const base64 = dataUrl.split(',')[1];
    if (!base64 || base64.length < 100) throw new Error('canvas 验证码为空或过小');
    console.log('获取到 canvas 验证码,base64 长度:', base64.length);
    return base64;
  }

  throw new Error('未找到验证码元素');
}

// ==================== 刷新验证码(已修复 detached 问题) ====================

async function refreshCaptcha(page) {
  const imgLocator = page.locator('img[alt="验证码"]');
  const canvasLocator = page.locator('canvas');

  if (await imgLocator.count() > 0) {
    await imgLocator.click({ timeout: 5000 }).catch(() => {});
    await page.waitForTimeout(300);
    // 等待新图片出现,超时则抛出明确错误,避免无限等待
    await page.waitForSelector('img[alt="验证码"]', { state: 'attached', timeout: 5000 });
    await page.waitForTimeout(300);
    return;
  }

  if (await canvasLocator.count() > 0) {
    await page.waitForTimeout(1000);
    return;
  }
  
  // 如果没有任何验证码元素,说明可能已不在登录页面
  throw new Error('当前页面未找到验证码,可能已登录或跳转');
}

// ==================== 等待登录结果(增强判断) ====================

async function waitForLoginResult(page) {
  try {
    // 同时等待多种成功标志
    await Promise.race([
      page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 8000 }),
      page.waitForSelector('.el-message--success, .el-notification, text=登录成功', { timeout: 8000 }),
      page.waitForSelector('text=欢迎回来', { timeout: 8000 })   // 主页欢迎语
    ]);
    return true;
  } catch {
    // 超时后直接检查当前 URL,若已跳转则视为成功
    const currentUrl = page.url();
    if (!currentUrl.includes('/login')) {
      console.log('检测到 URL 已跳转,视为登录成功');
      return true;
    }
    return false;
  }
}

// ==================== 主流程 ====================

(async () => {
  let browser;
  try {
    if (TEST_TT_BALANCE_BEFORE_LOGIN) {
      await checkTTBalance();
    }

    browser = await chromium.launch({ headless: false });
    const context = await browser.newContext();
    const page = await context.newPage();

    console.log('▶ 打开登录页...');
    await page.goto(`${SYSTEM_URL}/login`, { waitUntil: 'networkidle' });

    await page.locator('input[placeholder="请输入学号 / 工号"]').fill(USERNAME);
    await page.locator('input[placeholder="请输入密码"]').fill(PASSWORD);

    let loggedIn = false;

    for (let i = 0; i < 5; i++) {
      // 重试前检查是否已离开登录页(可能上一轮已真正登录)
      const currentUrl = page.url();
      if (!currentUrl.includes('/login')) {
        console.log('已不在登录页面,直接标记为登录成功');
        loggedIn = true;
        break;
      }

      console.log(`▶ 第 ${i + 1} 次尝试登录`);

      let base64;
      try {
        base64 = await getCaptchaBase64(page);
      } catch (e) {
        console.log('获取验证码失败:', e.message);
        // 如果此时页面已无验证码,直接检测登录状态
        if (!currentUrl.includes('/login')) {
          loggedIn = true;
          break;
        }
        await refreshCaptcha(page).catch(() => {});
        continue;
      }

      let captchaText;
      try {
        captchaText = await recognizeCaptcha(base64);
        console.log('识别结果:', captchaText);
      } catch (err) {
        console.log('识别失败:', err.message);
        await refreshCaptcha(page).catch(() => {});
        continue;
      }

      const captchaInput = page.locator('input[placeholder="请输入验证码"]');
      await captchaInput.click();
      const isMac = process.platform === 'darwin';
      await captchaInput.press(isMac ? 'Meta+A' : 'Control+A');
      await captchaInput.press('Backspace');
      await captchaInput.type(captchaText, { delay: 50 });

      await page.locator('button:has-text("登 录")').click();

      const success = await waitForLoginResult(page);
      if (success) {
        loggedIn = true;
        break;
      }

      console.log('登录尝试未检测到成功标志,准备重试...');
      // 再次检查 URL,若已跳转则直接成功
      if (!page.url().includes('/login')) {
        loggedIn = true;
        break;
      }
      // 否则刷新验证码再试
      try {
        await refreshCaptcha(page);
      } catch {
        console.log('刷新验证码失败,可能已不在登录页');
        if (!page.url().includes('/login')) {
          loggedIn = true;
          break;
        }
      }
    }

    if (!loggedIn) {
      throw new Error('登录失败:多次尝试后仍未成功');
    }

    console.log('✅ 登录成功');

    await context.storageState({ path: 'auth.json' });

    console.log('▶ 进入实习日志页面...');
    await page.goto(`${SYSTEM_URL}/internship`, { waitUntil: 'networkidle' });

    await page.waitForSelector('.el-calendar', { timeout: 10000 });

    let dialog = page.locator('.el-dialog:visible');
    const isDialogOpen = await dialog.count() > 0;

    if (!isDialogOpen) {
      const todayCell = page.locator('.el-calendar td.is-today');
      if (await todayCell.count() === 0) {
        throw new Error('未找到今天的日历单元格');
      }
      await todayCell.click();
      dialog = page.locator('.el-dialog');
      await dialog.waitFor({ state: 'visible', timeout: 10000 });
    } else {
      console.log('对话框已打开,跳过点击日期');
    }

    const textarea = dialog.locator('textarea').first();
    await textarea.waitFor({ state: 'visible', timeout: 5000 });
    await textarea.fill(DIARY_CONTENT);

    console.log('✅ 日志内容已填入,正在保存...');

    const saveBtn = dialog.locator('button:has-text("保存")');
    await saveBtn.click();

    await dialog.waitFor({ state: 'hidden', timeout: 10000 });

    console.log('✅ 日志提交成功');
    console.log('🎉 全流程完成');
  } catch (err) {
    console.error('❌ 出错:', err.message);
  } finally {
    // 调试时可先不关闭浏览器,观察页面情况
    // if (browser) await browser.close();
  }
})();

当然测试肯定需要money 充1元验证验证
启动前后端项目 然后运行脚本验证

node auto_2.js

最终完成填写
image

使用playwright完成自动化填写 同样现在流行的openclaw和harness agent也可以完成
等后续我们再用这个常用的agent来试试 大概也得接一点技能否则也很难绕过验证码

代码提交gitee:https://gitee.com/gao-yuan187/Test_internship_playwright

posted @ 2026-04-28 21:47  蒸饺  阅读(31)  评论(0)    收藏  举报