#!/usr/bin/env node /** * john 账号水果主题 E2E(仅本地):工作区 HTML → 预览图 → 保存到空间 → 可点击链接 * 用法:node scripts/fruit-theme-john-e2e.mjs * 环境:自动启动本地 server(MINDSPACE_E2E_PORT);勿对生产 URL 跑本脚本 */ import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadH5Environment } from './load-env.mjs'; import { createDbPool } from '../db.mjs'; import { createUserAuth } from '../user-auth.mjs'; import { buildPublicUrl } from '../user-publish.mjs'; import { ensureWorkspaceHtmlThumbnail, workspaceThumbnailRelativePath, } from '../mindspace-workspace-thumbnails.mjs'; import { isModernFeedThumbnail } from '../mindspace-thumbnails.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); loadH5Environment(import.meta.dirname); if (/go\.tkmind\.cn|goo\.tkmind\.cn|120\.26\.184\.105/i.test(process.env.BASE_URL ?? '')) { throw new Error('本脚本仅用于本地验证,请勿设置生产 BASE_URL'); } const portalPort = Number(process.env.MINDSPACE_E2E_PORT ?? 18140); const BASE_URL = `http://127.0.0.1:${portalPort}`; const HTML_NAME = 'fruit-theme-test.html'; const USERNAME = 'john'; const JOHN_PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '981122tj'; const thumbRel = workspaceThumbnailRelativePath(HTML_NAME); const thumbPngRel = thumbRel.replace(/\.svg$/i, '.png'); let publishDir = null; let htmlPath = null; let publicPageUrl = null; let publicThumbUrl = null; const fixtureHtml = ` 夏日鲜果指南

夏日鲜果指南

鲜果美食主题测试页面。

`; let createdFixture = false; const portal = spawn(process.execPath, ['server.mjs'], { cwd: root, env: { ...process.env, H5_PORT: String(portalPort), H5_PUBLIC_BASE_URL: BASE_URL, TKMIND_API_TARGET: 'http://127.0.0.1:9', }, stdio: ['ignore', 'pipe', 'pipe'], }); let portalLogs = ''; portal.stdout.on('data', (chunk) => { portalLogs += chunk.toString(); }); portal.stderr.on('data', (chunk) => { portalLogs += chunk.toString(); }); async function waitForPortal() { for (let attempt = 0; attempt < 80; attempt += 1) { if (portal.exitCode != null) throw new Error(`本地 server 提前退出\n${portalLogs}`); try { const response = await fetch(`${BASE_URL}/auth/status`); if (response.ok) return; } catch { // still starting } await new Promise((resolve) => setTimeout(resolve, 150)); } throw new Error(`本地 server 启动超时\n${portalLogs}`); } function cookieHeader(setCookie) { if (!setCookie) return ''; const parts = Array.isArray(setCookie) ? setCookie : [setCookie]; return parts.map((c) => c.split(';')[0]).join('; '); } async function request(pathname, { method = 'GET', body, cookie } = {}) { const headers = {}; if (body) headers['Content-Type'] = 'application/json'; if (cookie) headers.Cookie = cookie; const response = await fetch(`${BASE_URL}${pathname}`, { method, headers, body: body ? JSON.stringify(body) : undefined }); const type = response.headers.get('content-type') ?? ''; const payload = type.includes('json') ? await response.json() : await response.text(); return { response, payload, setCookie: response.headers.getSetCookie?.() ?? response.headers.get('set-cookie') }; } async function ensureJohnPassword() { const pool = createDbPool(); const auth = createUserAuth(pool, { h5Root: root }); let login = await auth.login({ username: USERNAME, password: JOHN_PASSWORD, ip: '127.0.0.1' }); if (!login.ok) { const [rows] = await pool.query(`SELECT id, email FROM h5_users WHERE username = ? LIMIT 1`, [ USERNAME, ]); const user = rows[0]; if (!user) throw new Error('本地数据库无 john 用户,请先注册或导入数据'); const email = user.email ?? `${USERNAME}@local.test`; if (!user.email) { await pool.query(`UPDATE h5_users SET email = ? WHERE id = ?`, [email, user.id]); } const reset = await auth.resetPassword({ username: USERNAME, email, password: JOHN_PASSWORD }); assert.equal(reset.ok, true, reset.message ?? '重置 john 本地密码失败'); login = await auth.login({ username: USERNAME, password: JOHN_PASSWORD, ip: '127.0.0.1' }); } assert.equal(login.ok, true, login.error ?? 'john 本地登录失败'); const { response, setCookie } = await request('/auth/login', { method: 'POST', body: { username: USERNAME, password: JOHN_PASSWORD }, }); assert.equal(response.status, 200, 'john HTTP 登录应成功'); await pool.end(); return { cookie: cookieHeader(setCookie), userId: login.user.id }; } async function main() { console.log('=== john 水果主题 E2E(本地) ==='); await waitForPortal(); console.log(`BASE_URL: ${BASE_URL}`); const { cookie, userId } = await ensureJohnPassword(); console.log('✓ john 本地登录成功'); publishDir = path.join(root, 'MindSpace', userId, 'public'); htmlPath = path.join(publishDir, HTML_NAME); publicPageUrl = buildPublicUrl(BASE_URL, userId, `public/${HTML_NAME}`); publicThumbUrl = buildPublicUrl(BASE_URL, userId, `public/${thumbPngRel}`); try { await fs.access(htmlPath); } catch { await fs.mkdir(publishDir, { recursive: true }); await fs.writeFile(htmlPath, fixtureHtml, 'utf8'); createdFixture = true; } const html = await fs.readFile(htmlPath, 'utf8'); assert.match(html, /mindspace-cover/, 'HTML 应包含 mindspace-cover'); const svg = await ensureWorkspaceHtmlThumbnail(publishDir, HTML_NAME, html, { title: '夏日鲜果指南', subtitle: '水果主题测试', }); assert.equal(isModernFeedThumbnail(svg), true, '工作区预览图应为新版 3:4'); assert.match(svg, /美食|鲜果|FRUIT/i, '预览图应体现水果/美食主题'); const hasPhoto = /preserveAspectRatio="xMidYMid slice"/.test(svg); console.log(`✓ 工作区预览图已生成: ${thumbRel}(含主图: ${hasPhoto ? '是' : '否'})`); const pageCheck = await request(`/MindSpace/${USERNAME}/${HTML_NAME}`); assert.equal(pageCheck.response.status, 200, `公网 HTML 应可访问: ${publicPageUrl}`); console.log(`✓ 公网 HTML 200: ${publicPageUrl}`); const thumbCheck = await request(`/MindSpace/${userId}/public/${thumbPngRel}`); assert.equal(thumbCheck.response.status, 200, `公网预览图应可访问: ${publicThumbUrl}`); console.log(`✓ 公网预览图 200: ${publicThumbUrl}`); const agentReplyLink = `[夏日鲜果指南 · 水果主题测试](${publicPageUrl})`; assert.match(agentReplyLink, /^\[.+\]\(https?:\/\/.+\)$/); console.log(`✓ Agent 应回复的可点击链接:\n ${agentReplyLink}`); const { response: spaceRes, payload: spacePayload } = await request('/api/mindspace/v1/space', { cookie, }); assert.equal(spaceRes.status, 200, JSON.stringify(spacePayload)); const saveBody = { title: '夏日鲜果指南 · 水果主题测试', summary: 'TKMind 水果主题预览图 E2E', content: html, content_format: 'html', page_type: 'html', category_code: 'draft', }; const created = await request('/api/mindspace/v1/pages', { method: 'POST', body: saveBody, cookie, }); assert.equal(created.response.status, 201, JSON.stringify(created.payload)); const pageId = created.payload?.data?.id ?? created.payload?.data?.page?.id; assert.ok(pageId, '应创建页面记录'); await new Promise((r) => setTimeout(r, 800)); const thumbApi = await request(`/api/mindspace/v1/pages/${pageId}/thumbnail`, { cookie }); assert.equal(thumbApi.response.status, 200, '页面缩略图 API 应返回 200'); assert.match(String(thumbApi.payload), /width="540" height="720"/); console.log(`✓ 我的空间缩略图 API 200: /api/mindspace/v1/pages/${pageId}/thumbnail`); console.log('\n=== 全部通过 ==='); } try { await main(); } finally { portal.kill('SIGTERM'); if (createdFixture && htmlPath && publishDir) { await fs.rm(htmlPath, { force: true }); await fs.rm(path.join(publishDir, thumbRel), { force: true }); } }