- 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航 - 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,AI 配置新增视觉模型说明 - 数据模型补充 t_channel、t_ai_config/t_ai_image 等表与「类型+金额+时间」去重键说明,表数更新为 21 张 - API 文档补充 /api/channel、/api/transaction/batch、/api/ai/record/image(/stream)、/api/ai/image、/api/ai/channel 等接口 - 截图脚本新增渠道页、图片记账、来源图片与渠道 AI 辅助截图,并放宽导航超时
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
// 浏览器自动化公共模块(Playwright + Chromium)与 HTTP 公共方法(axios)。
|
|
import axios from 'axios'
|
|
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright-core'
|
|
import { API_URL, BASE_URL, CHROMIUM_BIN, PASSWORD, USERNAME } from './config.js'
|
|
|
|
/** 默认视口尺寸 */
|
|
export const DEFAULT_VIEWPORT = { width: 1440, height: 1000 }
|
|
|
|
/**
|
|
* 启动 headless Chromium(复用 Playwright 缓存的浏览器)。
|
|
*
|
|
* @returns Browser 实例
|
|
*/
|
|
export async function launchBrowser(): Promise<Browser> {
|
|
return chromium.launch({
|
|
executablePath: CHROMIUM_BIN,
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 创建浏览器上下文。
|
|
*
|
|
* @param browser Browser 实例
|
|
* @param viewport 视口尺寸
|
|
* @returns BrowserContext 实例
|
|
*/
|
|
export async function newContext(
|
|
browser: Browser,
|
|
viewport: { width: number; height: number } = DEFAULT_VIEWPORT,
|
|
): Promise<BrowserContext> {
|
|
const context = await browser.newContext({ viewport })
|
|
// 首次访问需等待 Vite 按需编译,放宽默认导航超时,避免冷启动误判失败
|
|
context.setDefaultNavigationTimeout(120000)
|
|
return context
|
|
}
|
|
|
|
/**
|
|
* 在上下文中注入登录态与中文语言偏好(对之后打开的页面生效)。
|
|
*
|
|
* @param context BrowserContext 实例
|
|
* @param token 访问令牌
|
|
*/
|
|
export async function injectSession(context: BrowserContext, token: string): Promise<void> {
|
|
await context.addInitScript(
|
|
([t]) => {
|
|
localStorage.setItem('token', t as string)
|
|
localStorage.setItem('locale', 'zh-CN')
|
|
},
|
|
[token],
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 等待页面渲染完成:先等文档就绪,再等关键元素可见,最后做短暂稳定等待。
|
|
*
|
|
* @param page Page 实例
|
|
* @param options.selector 关键元素选择器(可选)
|
|
* @param options.settle 元素出现后的稳定等待毫秒数,默认 600ms
|
|
*/
|
|
export async function waitPage(
|
|
page: Page,
|
|
options: { selector?: string; settle?: number } = {},
|
|
): Promise<void> {
|
|
await page.waitForLoadState('domcontentloaded').catch(() => undefined)
|
|
if (options.selector) {
|
|
await page
|
|
.locator(options.selector)
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 20000 })
|
|
.catch(() => undefined)
|
|
}
|
|
// 等待异步数据请求基本完成后再截图,避免表格/图表空白;
|
|
// SSE 页面(运行日志)不会进入空闲,超时忽略即可。
|
|
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => undefined)
|
|
await page.waitForTimeout(options.settle ?? 900)
|
|
}
|
|
|
|
/**
|
|
* 按可见文本点击按钮。
|
|
*
|
|
* @param page Page 实例
|
|
* @param text 按钮文本
|
|
* @returns 是否点击成功
|
|
*/
|
|
export async function clickText(page: Page, text: string): Promise<boolean> {
|
|
const button = page.locator('button', { hasText: text }).first()
|
|
try {
|
|
// 按钮可能随异步配置(如 AI 启用状态)延迟渲染,等待其出现
|
|
await button.waitFor({ state: 'visible', timeout: 15000 })
|
|
} catch {
|
|
return false
|
|
}
|
|
// 部分 Naive UI 按钮对 Playwright 合成鼠标事件不响应,改用 DOM 原生 click
|
|
await button.evaluate((el) => (el as HTMLElement).click())
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 登录测试账号并返回 JWT。
|
|
*
|
|
* @returns 访问令牌
|
|
*/
|
|
export async function login(): Promise<string> {
|
|
const res = await axios.post(`${API_URL}/user/login`, { username: USERNAME, password: PASSWORD })
|
|
return res.data.data.token
|
|
}
|
|
|
|
/** 打开首页并等待(用于非截图场景的简单访问) */
|
|
export async function openHome(page: Page): Promise<void> {
|
|
await page.goto(BASE_URL)
|
|
await page.waitForTimeout(1500)
|
|
}
|