通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用, 使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
4.0 KiB
TypeScript
173 lines
4.0 KiB
TypeScript
import crypto from 'crypto'
|
||
|
||
/**
|
||
* Webhook 请求构造器
|
||
* 用于 Playwright API 测试中模拟乐读派 Webhook 回调
|
||
*/
|
||
|
||
/** 测试配置(与 application-dev.yml 对齐) */
|
||
export const LEAI_TEST_CONFIG = {
|
||
orgId: 'gdlib',
|
||
appSecret: 'leai_mnoi9q1a_mtcawrn8y',
|
||
apiUrl: 'http://192.168.1.120:8080',
|
||
testPhone: '13800001111',
|
||
}
|
||
|
||
/** 后端 API 地址(前端 vite proxy 转发) */
|
||
export const API_BASE = process.env.API_BASE_URL || 'http://localhost:8580/api'
|
||
|
||
/** HMAC-SHA256 签名 */
|
||
function hmacSha256(data: string, secret: string): string {
|
||
return crypto.createHmac('sha256', secret).update(data).digest('hex')
|
||
}
|
||
|
||
/** 生成随机事件 ID */
|
||
export function randomEventId(): string {
|
||
return `evt_test_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
|
||
}
|
||
|
||
/** 生成随机作品 ID */
|
||
export function randomWorkId(): string {
|
||
return `wk_test_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||
}
|
||
|
||
/**
|
||
* 构造完整的 Webhook 请求参数
|
||
* 包括 headers + body
|
||
*/
|
||
export function buildWebhookRequest(
|
||
payload: Record<string, unknown>,
|
||
options: {
|
||
eventId?: string
|
||
eventType?: string
|
||
timestamp?: string
|
||
appSecret?: string
|
||
validSignature?: boolean
|
||
} = {},
|
||
) {
|
||
const {
|
||
eventId = randomEventId(),
|
||
eventType = 'work.status_changed',
|
||
timestamp = Date.now().toString(),
|
||
appSecret = LEAI_TEST_CONFIG.appSecret,
|
||
validSignature = true,
|
||
} = options
|
||
|
||
const body = JSON.stringify(payload)
|
||
|
||
let signature: string
|
||
if (validSignature) {
|
||
const signData = `${eventId}.${timestamp}.${body}`
|
||
signature = `HMAC-SHA256=${hmacSha256(signData, appSecret)}`
|
||
} else {
|
||
signature = 'HMAC-SHA256=invalid_signature'
|
||
}
|
||
|
||
return {
|
||
url: `${API_BASE}/webhook/leai`,
|
||
headers: {
|
||
'X-Webhook-Id': eventId,
|
||
'X-Webhook-Event': eventType,
|
||
'X-Webhook-Timestamp': timestamp,
|
||
'X-Webhook-Signature': signature,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body,
|
||
eventId,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构造 Webhook payload(标准格式)
|
||
*/
|
||
export function buildWebhookPayload(
|
||
remoteWorkId: string,
|
||
data: Record<string, unknown>,
|
||
event = 'work.status_changed',
|
||
) {
|
||
return {
|
||
event,
|
||
data: {
|
||
work_id: remoteWorkId,
|
||
...data,
|
||
},
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构造标准状态变更 payload
|
||
*/
|
||
export function buildStatusPayload(
|
||
remoteWorkId: string,
|
||
status: number,
|
||
extra: Record<string, unknown> = {},
|
||
) {
|
||
return buildWebhookPayload(remoteWorkId, { status, ...extra })
|
||
}
|
||
|
||
/**
|
||
* 构造进度更新 payload
|
||
*/
|
||
export function buildProgressPayload(
|
||
remoteWorkId: string,
|
||
progress: number,
|
||
progressMessage: string,
|
||
) {
|
||
return buildWebhookPayload(
|
||
remoteWorkId,
|
||
{ status: 2, progress, progressMessage },
|
||
'work.progress',
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 构造包含页面列表的完成 payload
|
||
*/
|
||
export function buildCompletedPayload(
|
||
remoteWorkId: string,
|
||
pageCount = 6,
|
||
) {
|
||
const pageList = Array.from({ length: pageCount }, (_, i) => ({
|
||
imageUrl: `https://cdn.example.com/pages/${remoteWorkId}/page_${i + 1}.png`,
|
||
text: `第${i + 1}页的文字内容`,
|
||
}))
|
||
|
||
return buildStatusPayload(remoteWorkId, 3, {
|
||
title: `测试绘本_${remoteWorkId.slice(-6)}`,
|
||
phone: LEAI_TEST_CONFIG.testPhone,
|
||
style: 'cartoon',
|
||
pageList,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 构造配音完成 payload(含 audioUrl)
|
||
*/
|
||
export function buildDubbedPayload(
|
||
remoteWorkId: string,
|
||
pageCount = 6,
|
||
) {
|
||
const pageList = Array.from({ length: pageCount }, (_, i) => ({
|
||
imageUrl: `https://cdn.example.com/pages/${remoteWorkId}/page_${i + 1}.png`,
|
||
text: `第${i + 1}页的文字内容`,
|
||
audioUrl: `https://cdn.example.com/audio/${remoteWorkId}/page_${i + 1}.mp3`,
|
||
}))
|
||
|
||
return buildStatusPayload(remoteWorkId, 5, {
|
||
title: `测试绘本_${remoteWorkId.slice(-6)}`,
|
||
author: '测试作者',
|
||
phone: LEAI_TEST_CONFIG.testPhone,
|
||
pageList,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 构造失败 payload
|
||
*/
|
||
export function buildFailedPayload(
|
||
remoteWorkId: string,
|
||
failReason = 'AI 处理超时',
|
||
) {
|
||
return buildStatusPayload(remoteWorkId, -1, { failReason })
|
||
}
|