feat(history): complete messaging core before multi-account gateways

This commit is contained in:
wmq
2026-07-28 22:36:58 +08:00
parent cd06d049fe
commit 7f09132fad
54 changed files with 4781 additions and 601 deletions
+8
View File
@@ -32,6 +32,10 @@ test.describe('真实收件箱 AI 交互', () => {
await expect(customerBubble.first()).toHaveClass(/animate-message-in-left/)
const agentBubble = page.locator('[data-testid="message-bubble"][data-sender="agent"]')
await expect(agentBubble.first()).toHaveClass(/animate-message-in-right/)
const outgoingChineseNotes = page.getByTestId('outgoing-chinese-note')
await expect.poll(() => outgoingChineseNotes.count()).toBeGreaterThan(0)
await expect(outgoingChineseNotes.first()).toContainText('中文备注 · 仅内部可见')
await expect(outgoingChineseNotes.first()).toContainText(/[\u3400-\u9fff]/u)
await page.getByTitle('客户洞察').click()
await expect(page.getByText('成交概率', { exact: true })).toBeVisible()
await expect(page.getByText('分析可信度', { exact: true })).toBeVisible()
@@ -49,6 +53,10 @@ test.describe('真实收件箱 AI 交互', () => {
await page.getByRole('button', { name: '生成回复建议', exact: true }).click()
const adoptButtons = page.getByRole('button', { name: '采用', exact: true })
await expect(adoptButtons).toHaveCount(3, { timeout: 30_000 })
const translationPreviews = page.getByTestId('ai-reply-translation')
await expect(translationPreviews).toHaveCount(3)
await expect(translationPreviews.first()).toContainText(/[\u3400-\u9fff]/u)
await expect(translationPreviews.first()).toContainText('中文参考 · 不会发送')
await adoptButtons.first().click()
const composer = page.getByPlaceholder('输入回复内容,Enter 发送,Shift+Enter 换行')
+150 -5
View File
@@ -1,4 +1,4 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, type Page, type WebSocketRoute } from '@playwright/test'
const account = process.env.E2E_ACCOUNT || 'e2e-1785008967@local.test'
const password = process.env.E2E_PASSWORD || 'Lingxi-E2E-2026!'
@@ -20,13 +20,38 @@ test('Telegram 手机号验证码和二步密码登录向导', async ({ page, re
})
expect(response.ok()).toBeTruthy()
await login(page, loginAccount, loginPassword)
await page.goto('/channels')
await page.getByRole('button', { name: '连接', exact: true }).click()
await page.goto('/channels?setup=telegram')
await expect(page.getByTestId('telegram-onboarding')).toContainText('先绑定 Telegram')
await expect(page.getByPlaceholder('+8613800138000')).toBeVisible()
await expect(page.getByText('Telegram 验证码')).toHaveCount(0)
await expect(page.getByLabel('Telegram 验证码', { exact: true })).toHaveCount(0)
const sendTelegramCode = page.getByRole('button', { name: '发送 Telegram 验证码' })
await expect(sendTelegramCode).toBeVisible()
await sendTelegramCode.click()
await expect(page.getByText('请输入国际格式手机号,例如 +8613800138000')).toBeVisible()
await expect(page.getByPlaceholder('+8613800138000')).toBeVisible()
await expect(page.getByText('每个企业独立绑定账号')).toBeVisible()
await page.screenshot({ path: testInfo.outputPath('telegram-login-wizard.png'), fullPage: true })
})
test('企业邮箱 IMAP 和 SMTP 连接向导', async ({ page, request }, testInfo) => {
const suffix = `${Date.now()}-${testInfo.project.name}`
const loginAccount = `email-e2e-${suffix}@local.test`
const loginPassword = 'Lingxi-Email-2026!'
const response = await request.post('/api/auth/register', {
data: { name: 'Email E2E', account: loginAccount, password: loginPassword, company: 'Email E2E', industry: '跨境电商', teamSize: '1-10人' },
})
expect(response.ok()).toBeTruthy()
await login(page, loginAccount, loginPassword)
await page.goto('/channels')
const emailCard = page.getByTestId('channel-card-email')
await emailCard.getByRole('button', { name: '连接', exact: true }).click()
await expect(page.getByText('IMAP 收信 + SMTP 发信', { exact: true })).toBeVisible()
await expect(page.getByPlaceholder('sales@company.com')).toBeVisible()
await expect(page.getByPlaceholder('仅提交到本地服务端验证')).toHaveAttribute('type', 'password')
await expect(page.getByRole('button', { name: '验证并连接邮箱' })).toBeVisible()
await page.screenshot({ path: testInfo.outputPath('email-connection-wizard.png'), fullPage: true })
})
test('登录与核心工作台视觉链路', async ({ page }, testInfo) => {
await login(page)
await expect(page).toHaveURL(/\/dashboard$/)
@@ -37,11 +62,115 @@ test('登录与核心工作台视觉链路', async ({ page }, testInfo) => {
await page.screenshot({ path: testInfo.outputPath('channels.png'), fullPage: true })
await page.goto('/inbox')
await page.getByTestId('conversation-item').first().click()
await page.getByRole('button', { name: '添加 Telegram 用户或群组' }).click()
await expect(page.getByRole('dialog')).toContainText('添加 Telegram 会话')
await expect(page.getByLabel('用户名、手机号或用户 ID')).toBeVisible()
await page.getByRole('button', { name: '群组/频道', exact: true }).click()
await expect(page.getByLabel('群组/频道用户名或 Chat ID')).toBeVisible()
await page.getByRole('button', { name: '邀请链接', exact: true }).click()
await expect(page.getByLabel('Telegram 邀请链接')).toBeVisible()
await page.getByRole('button', { name: '关闭', exact: true }).click()
const privateConversation = page.locator(
'[data-testid="conversation-item"][data-channel="telegram"]:not([data-telegram-chat-type="channel"])',
).first()
await privateConversation.click()
await expect(page.getByTestId('message-content')).toHaveClass(/justify-end/)
const languageSwitcher = page.getByRole('radiogroup', { name: '消息显示语言' })
const originalMode = languageSwitcher.getByRole('radio', { name: '原文', exact: true })
const translatedMode = languageSwitcher.getByRole('radio', { name: '译文', exact: true })
await originalMode.click()
await expect(originalMode).toHaveAttribute('aria-checked', 'true')
await translatedMode.click()
await expect(translatedMode).toHaveAttribute('aria-checked', 'true')
await expect.poll(() => page.evaluate(() => localStorage.getItem('lingxi-message-view-mode'))).toBe('translated')
await expect(page.locator('input[type="file"]')).toHaveCount(4)
const outgoingSurface = page.locator('[data-testid="message-surface"][data-direction="outgoing"]')
await expect(outgoingSurface.first()).toHaveClass(/telegram-bubble-out/)
const incomingSurface = page.locator('[data-testid="message-surface"][data-direction="incoming"]')
if (await incomingSurface.count()) await expect(incomingSurface.first()).toHaveClass(/telegram-bubble-in/)
await expect(privateConversation).toHaveClass(/conversation-selected/)
await expect(page.getByTestId('conversation-list-panel')).toHaveClass(/border-slate-300/)
await expect(page.getByTestId('chat-panel')).toHaveClass(/bg-\[#eaf2f9\]/)
await expect(page.getByTestId('conversation-insight-bar').locator('img')).toHaveCount(0)
await expect(page.getByTestId('conversation-insight-bar')).toContainText('AI 客户洞察')
await page.getByRole('button', { name: 'Telegram 会话操作' }).click()
await expect(page.getByText(/置顶会话|取消置顶/)).toBeVisible()
await expect(page.getByText(/归档会话|移出归档/)).toBeVisible()
await expect(page.getByText(/静音 8 小时|恢复通知/)).toBeVisible()
await expect(page.getByText('清空消息', { exact: true })).toBeVisible()
await expect(page.getByText('删除会话', { exact: true })).toBeVisible()
await page.keyboard.press('Escape')
await page.getByRole('button', { name: '切换聊天背景' }).click()
await page.getByText('极光紫', { exact: true }).click()
await expect(page.getByTestId('message-list')).toHaveClass(/chat-wallpaper-aurora/)
await page.getByRole('button', { name: '切换聊天背景' }).click()
await page.getByText('日落橙', { exact: true }).click()
await expect(page.getByTestId('message-list')).toHaveClass(/chat-wallpaper-sunset/)
await page.getByRole('button', { name: '切换聊天背景' }).click()
await page.getByText('薄荷绿', { exact: true }).click()
await expect(page.getByTestId('message-list')).toHaveClass(/chat-wallpaper-mint/)
await expect(page.getByPlaceholder('输入回复内容,Enter 发送,Shift+Enter 换行')).toHaveClass(/min-h-\[86px\]/)
await page.screenshot({ path: testInfo.outputPath('inbox.png'), fullPage: true })
})
test('mobile conversation swipe reveals Telegram actions', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile-chromium')
await login(page)
await page.goto('/inbox')
const item = page.locator('[data-testid="conversation-item"][data-channel="telegram"]').first()
await expect(item).toBeVisible()
await item.dispatchEvent('touchstart', { touches: [{ identifier: 1, clientX: 320, clientY: 120 }] })
await item.dispatchEvent('touchmove', { touches: [{ identifier: 1, clientX: 120, clientY: 120 }] })
await page.waitForTimeout(100)
await item.dispatchEvent('touchend', { changedTouches: [{ identifier: 1, clientX: 120, clientY: 120 }] })
const actions = item.locator('xpath=..').getByTestId('conversation-swipe-actions')
await expect(actions).toHaveClass(/opacity-100/)
await expect(actions.getByRole('button', { name: '删除' })).toBeVisible()
})
test('conversation list reorders immediately from WebSocket events', async ({ page }) => {
const sockets: WebSocketRoute[] = []
await page.routeWebSocket(/\/api\/ws/, (socket) => {
sockets.push(socket)
socket.send(JSON.stringify({ type: 'connected', at: new Date().toISOString() }))
})
await login(page)
await page.goto('/inbox')
const items = page.getByTestId('conversation-item')
await expect(items.nth(1)).toBeVisible()
await expect.poll(() => sockets.length).toBeGreaterThanOrEqual(2)
const target = items.nth(1)
const targetId = await target.getAttribute('data-conversation-id')
expect(targetId).toBeTruthy()
const message = {
id: `realtime-${Date.now()}`,
conversationId: targetId,
sender: 'customer',
type: 'text',
originalText: 'Realtime list regression',
originalLang: 'en',
status: 'delivered',
createdAt: new Date(Date.now() + 60_000).toISOString(),
}
sockets.at(-1)!.send(JSON.stringify({
type: 'message.received',
conversationId: targetId,
message,
conversation: {
id: targetId,
unreadCount: 7,
status: 'pending',
lastMessage: message,
updatedAt: message.createdAt,
},
}))
await expect(items.first()).toHaveAttribute('data-conversation-id', targetId!)
await expect(items.first()).toContainText('Realtime list regression')
await expect(items.first()).toContainText('7')
})
test('知识文件真实上传解析', async ({ page }, testInfo) => {
await login(page)
await page.goto('/knowledge')
@@ -56,6 +185,22 @@ test('知识文件真实上传解析', async ({ page }, testInfo) => {
await page.screenshot({ path: testInfo.outputPath('knowledge-uploaded.png'), fullPage: true })
})
test('客户画像雷达与采购情报可视化', async ({ page }, testInfo) => {
await login(page)
await page.goto('/customers')
const rows = page.getByTestId('customer-row')
await expect(rows.first()).toBeVisible()
await rows.first().getByRole('cell').nth(1).click()
await expect(page.getByText('客户价值雷达', { exact: true })).toBeVisible()
await expect(page.getByText('AI 客户洞察', { exact: true })).toBeVisible()
await expect(page.getByText('采购情报', { exact: true })).toBeVisible()
await expect(page.getByText('标签画像', { exact: true })).toBeVisible()
const semanticTags = page.locator('[data-tag-category]')
if (await semanticTags.count()) await expect(semanticTags.first()).toBeVisible()
else await expect(page.getByText('暂无标签,AI 将在收到有效客户消息后自动补全。')).toBeVisible()
await page.screenshot({ path: testInfo.outputPath('customer-profile-radar.png'), fullPage: true })
})
test('团队邀请和支付页不显示虚假成功', async ({ page }, testInfo) => {
await login(page)
await page.goto('/team')