feat: isolate BossAI website with Next.js

This commit is contained in:
boos
2026-06-06 05:30:10 +08:00
parent d75daf72d6
commit 03046152fd
21 changed files with 3153 additions and 1085 deletions
-205
View File
@@ -1,205 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ReactNode } from 'react';
import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import App from './App.js';
const { apiMock, userApiMock, authSessionMock } = vi.hoisted(() => ({
apiMock: {
getEvents: vi.fn(),
},
userApiMock: {
getAnnouncements: vi.fn(),
},
authSessionMock: {
getSessionToken: vi.fn(),
clearSession: vi.fn(),
},
}));
vi.mock('react-dom', async () => {
const actual = await vi.importActual<typeof import('react-dom')>('react-dom');
return {
...actual,
createPortal: (node: unknown) => node,
};
});
vi.mock('./api.js', () => ({
api: apiMock,
userApi: userApiMock,
}));
vi.mock('./authSession.js', () => ({
getSessionToken: authSessionMock.getSessionToken,
clearSession: authSessionMock.clearSession,
}));
vi.mock('./components/SearchModal.js', () => ({
default: () => null,
}));
vi.mock('./components/NotificationPanel.js', () => ({
default: () => null,
}));
vi.mock('./components/TooltipLayer.js', () => ({
default: () => null,
}));
vi.mock('./components/useAnimatedVisibility.js', () => ({
useAnimatedVisibility: (open: boolean) => ({
shouldRender: open,
isVisible: open,
}),
}));
vi.mock('./i18n.js', () => ({
I18nProvider: ({ children }: { children: ReactNode }) => children,
useI18n: () => ({
language: 'zh',
toggleLanguage: vi.fn(),
t: (text: string) => text,
}),
}));
vi.mock('./pages/Dashboard.js', () => ({
default: () => <div>Dashboard</div>,
}));
vi.mock('./pages/Landing.js', () => ({
default: () => <div>Public Landing Route</div>,
}));
function collectText(node: ReactTestInstance): string {
return (node.children || []).map((child) => {
if (typeof child === 'string') return child;
return collectText(child);
}).join('');
}
function createLocalStorage() {
const store = new Map<string, string>([
['metapi.theme.mode', 'dark'],
['metapi.firstUseDocReminder', '1'],
['metapi.userProfile', JSON.stringify({
name: '管理员',
avatarSeed: 'seed-1',
avatarStyle: 'identicon',
})],
]);
return {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => {
store.set(key, String(value));
},
removeItem: (key: string) => {
store.delete(key);
},
};
}
function setupRuntime() {
const matchMedia = (query: string) => ({
matches: query.includes('prefers-color-scheme'),
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
onchange: null,
});
vi.stubGlobal('localStorage', createLocalStorage());
vi.stubGlobal('window', {
innerWidth: 1280,
matchMedia,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
vi.stubGlobal('document', {
body: { style: {} },
documentElement: {
setAttribute: vi.fn(),
getAttribute: vi.fn(),
},
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
}
async function flushMicrotasks() {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});
}
describe('App public landing', () => {
beforeEach(() => {
vi.clearAllMocks();
setupRuntime();
apiMock.getEvents.mockResolvedValue([]);
userApiMock.getAnnouncements.mockResolvedValue({ items: [] });
authSessionMock.clearSession.mockImplementation(() => {});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});
it('renders the public landing route at / when no session exists', async () => {
authSessionMock.getSessionToken.mockReturnValue(null);
let root!: ReactTestRenderer;
try {
await act(async () => {
root = create(
<MemoryRouter initialEntries={['/']}>
<App />
</MemoryRouter>,
);
});
await flushMicrotasks();
const text = collectText(root.root);
expect(text).toContain('Public Landing Route');
expect(text).not.toContain('用户登录后继续');
} finally {
if (root) {
await act(async () => {
root.unmount();
});
}
}
});
it('keeps the admin dashboard at / after admin session exists', async () => {
authSessionMock.getSessionToken.mockImplementation((audience: string) => (
audience === 'admin' ? 'admin-token' : null
));
let root!: ReactTestRenderer;
try {
await act(async () => {
root = create(
<MemoryRouter initialEntries={['/']}>
<App />
</MemoryRouter>,
);
});
await flushMicrotasks();
const text = collectText(root.root);
expect(text).toContain('Dashboard');
expect(text).not.toContain('Public Landing Route');
} finally {
if (root) {
await act(async () => {
root.unmount();
});
}
}
});
});
-12
View File
@@ -21,7 +21,6 @@ import CenteredModal from './components/CenteredModal.js';
import AdminLoginPage from './pages/auth/AdminLoginPage.js';
import UserLoginPage from './pages/auth/UserLoginPage.js';
import { SiteAnnouncementContent } from './pages/helpers/siteAnnouncementPresentation.js';
const Landing = lazy(() => import('./pages/Landing.js'));
const Dashboard = lazy(() => import('./pages/Dashboard.js'));
const Sites = lazy(() => import('./pages/Sites.js'));
const Accounts = lazy(() => import('./pages/Accounts.js'));
@@ -698,7 +697,6 @@ function AppShell() {
};
const isAdminLoginPage = location.pathname === '/boos';
const isPublicUserPage = ['/user/login', '/user/register'].includes(location.pathname);
const isPublicLandingRoot = !authed && !userAuthed && location.pathname === '/';
if (isUserPage && isPublicUserPage) {
if (userAuthed) return <Navigate to="/user/dashboard" />;
@@ -715,16 +713,6 @@ function AppShell() {
);
}
if (isPublicLandingRoot) {
return (
<Suspense fallback={<RouteLoadingFallback />}>
<Routes>
<Route path="/" element={<Landing />} />
</Routes>
</Suspense>
);
}
if (!authed && !isUserPage) {
if (userAuthed) {
return <Navigate to="/user/dashboard" />;
-606
View File
@@ -89,612 +89,6 @@ body {
min-height: 100vh;
}
/* ===== Public Landing ===== */
.landing-page {
min-height: 100dvh;
overflow: hidden;
background:
radial-gradient(circle at 78% 8%, rgba(31, 111, 235, 0.22), transparent 32%),
radial-gradient(circle at 12% 62%, rgba(14, 165, 145, 0.14), transparent 30%),
linear-gradient(180deg, #08111f 0%, #0b1220 48%, #0f1625 100%);
color: #f7fbff;
}
.landing-page a {
color: inherit;
text-decoration: none;
}
.landing-nav {
width: min(1180px, calc(100% - 40px));
height: 72px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 24px;
margin: 0 auto;
}
.landing-brand,
.landing-nav-actions,
.landing-nav-links,
.landing-hero-actions {
display: inline-flex;
align-items: center;
}
.landing-brand {
gap: 10px;
font-weight: 780;
letter-spacing: 0.01em;
}
.landing-brand img {
width: 34px;
height: 34px;
border-radius: 10px;
}
.landing-nav-links {
justify-content: center;
gap: 24px;
color: rgba(226, 232, 240, 0.72);
font-size: 13px;
}
.landing-nav-links a:hover {
color: #ffffff;
}
.landing-nav-actions,
.landing-hero-actions {
gap: 10px;
}
.landing-primary-button,
.landing-secondary-button,
.landing-link-button {
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 0 18px;
font-size: 13px;
font-weight: 720;
white-space: nowrap;
transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s ease;
}
.landing-primary-button {
background: #f8fafc;
color: #0f172a;
box-shadow: 0 14px 34px rgba(5, 12, 24, 0.32);
}
.landing-secondary-button,
.landing-link-button {
border: 1px solid rgba(226, 232, 240, 0.2);
background: rgba(15, 23, 42, 0.42);
color: #e5edf8;
}
.landing-link-button {
min-height: 36px;
background: transparent;
border-color: transparent;
color: rgba(226, 232, 240, 0.76);
}
.landing-primary-button:hover,
.landing-secondary-button:hover,
.landing-link-button:hover {
transform: translateY(-1px);
}
.landing-secondary-button:hover,
.landing-link-button:hover {
border-color: rgba(226, 232, 240, 0.36);
background: rgba(30, 41, 59, 0.62);
}
.landing-hero {
width: min(1180px, calc(100% - 40px));
min-height: calc(100dvh - 72px);
display: grid;
grid-template-columns: minmax(0, 1.02fr) minmax(360px, 0.98fr);
align-items: center;
gap: 56px;
margin: 0 auto;
padding: 42px 0 58px;
}
.landing-hero-copy {
display: grid;
gap: 22px;
align-content: center;
}
.landing-brand-strip {
width: fit-content;
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 999px;
background: rgba(15, 23, 42, 0.46);
color: rgba(226, 232, 240, 0.82);
padding: 8px 12px;
font-size: 12px;
font-weight: 720;
}
.landing-hero h1 {
max-width: 780px;
margin: 0;
font-size: clamp(42px, 6vw, 78px);
line-height: 0.98;
letter-spacing: -0.045em;
}
.landing-hero p,
.landing-section-heading p,
.landing-workflow-main p,
.landing-deploy p,
.landing-final p {
max-width: 62ch;
margin: 0;
color: rgba(203, 213, 225, 0.78);
font-size: 16px;
line-height: 1.75;
}
.landing-visual {
position: relative;
display: grid;
gap: 14px;
border: 1px solid rgba(148, 163, 184, 0.22);
border-radius: 28px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.04)),
rgba(9, 18, 32, 0.78);
padding: 18px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 34px 90px rgba(0, 0, 0, 0.42);
}
.landing-visual::before {
content: "";
position: absolute;
inset: 16px;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.landing-visual-top,
.landing-route-row,
.landing-visual-grid > div,
.landing-node-card,
.landing-deploy-panel,
.landing-capability-card,
.landing-module-row {
border: 1px solid rgba(148, 163, 184, 0.18);
background: rgba(15, 23, 42, 0.58);
}
.landing-visual-top {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 18px;
padding: 14px 16px;
}
.landing-visual-top span,
.landing-visual-grid span,
.landing-node-card span,
.landing-deploy-panel span {
color: rgba(148, 163, 184, 0.9);
font-size: 12px;
}
.landing-visual-top strong,
.landing-visual-grid strong,
.landing-node-card strong,
.landing-deploy-panel strong {
color: #f8fafc;
font-size: 14px;
}
.landing-route-stack {
display: grid;
gap: 9px;
}
.landing-route-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
border-radius: 16px;
padding: 13px 14px;
}
.landing-route-row span {
color: rgba(125, 211, 252, 0.92);
font-size: 12px;
font-weight: 760;
}
.landing-route-row strong {
overflow: hidden;
color: #f8fafc;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.landing-route-row em {
color: rgba(203, 213, 225, 0.72);
font-size: 12px;
font-style: normal;
}
.landing-visual-grid,
.landing-node-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.landing-visual-grid > div,
.landing-node-card {
display: grid;
gap: 6px;
min-height: 104px;
align-content: end;
border-radius: 18px;
padding: 14px;
}
.landing-section,
.landing-workflow,
.landing-deploy,
.landing-final {
width: min(1180px, calc(100% - 40px));
margin: 0 auto;
}
.landing-section {
padding: 64px 0;
}
.landing-section-heading {
display: grid;
gap: 12px;
margin-bottom: 24px;
}
.landing-section-heading h2,
.landing-workflow-main h2,
.landing-deploy h2,
.landing-final h2 {
max-width: 760px;
margin: 0;
color: #f8fafc;
font-size: clamp(30px, 4vw, 50px);
line-height: 1.06;
letter-spacing: -0.035em;
}
.landing-capability-grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 14px;
}
.landing-capability-card {
display: grid;
gap: 18px;
min-height: 250px;
border-radius: 24px;
padding: 24px;
}
.landing-capability-card:first-child {
grid-row: span 2;
min-height: 514px;
background:
linear-gradient(135deg, rgba(14, 165, 145, 0.2), rgba(15, 23, 42, 0.58) 46%),
rgba(15, 23, 42, 0.58);
}
.landing-capability-card h3,
.landing-module-row h3 {
margin: 0;
color: #f8fafc;
font-size: 22px;
letter-spacing: -0.02em;
}
.landing-capability-card p,
.landing-module-row p {
margin: 0;
color: rgba(203, 213, 225, 0.76);
line-height: 1.72;
}
.landing-chip-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: auto;
}
.landing-chip-row span {
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 999px;
background: rgba(2, 6, 23, 0.28);
color: rgba(226, 232, 240, 0.82);
padding: 7px 10px;
font-size: 12px;
}
.landing-workflow {
display: grid;
grid-template-columns: minmax(0, 0.92fr) minmax(360px, 1.08fr);
gap: 18px;
align-items: stretch;
padding: 36px;
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 32px;
background:
linear-gradient(135deg, rgba(30, 64, 175, 0.18), transparent 42%),
rgba(15, 23, 42, 0.44);
}
.landing-workflow-main {
display: grid;
gap: 18px;
align-content: center;
}
.landing-mode-list {
display: grid;
gap: 8px;
margin-top: 8px;
}
.landing-mode-list div {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
gap: 12px;
align-items: center;
color: rgba(203, 213, 225, 0.74);
font-size: 13px;
}
.landing-mode-list code {
border: 1px solid rgba(125, 211, 252, 0.2);
border-radius: 999px;
background: rgba(14, 165, 233, 0.08);
color: rgba(125, 211, 252, 0.96);
padding: 6px 10px;
text-align: center;
}
.landing-workflow-side {
display: grid;
gap: 12px;
}
.landing-node-card.is-wide {
min-height: 150px;
background:
linear-gradient(135deg, rgba(248, 250, 252, 0.1), rgba(14, 165, 145, 0.08)),
rgba(15, 23, 42, 0.58);
}
.landing-module-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.landing-module-row {
min-height: 184px;
display: grid;
align-content: start;
gap: 12px;
border-radius: 22px;
padding: 22px;
}
.landing-deploy {
display: grid;
grid-template-columns: minmax(0, 0.92fr) minmax(320px, 0.78fr);
gap: 28px;
align-items: center;
padding: 56px 0 44px;
}
.landing-deploy > div:first-child {
display: grid;
gap: 14px;
}
.landing-deploy-panel {
display: grid;
gap: 12px;
border-radius: 24px;
padding: 18px;
}
.landing-deploy-panel > div {
display: grid;
gap: 4px;
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
padding: 0 0 12px;
}
.landing-deploy-panel > div:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.landing-final {
display: grid;
justify-items: center;
gap: 16px;
padding: 72px 0 84px;
text-align: center;
}
.landing-final p {
max-width: 48ch;
}
@media (prefers-reduced-motion: no-preference) {
.landing-hero-copy,
.landing-visual,
.landing-capability-card,
.landing-workflow,
.landing-module-row,
.landing-deploy,
.landing-final {
animation: landing-rise 0.72s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.landing-visual {
animation-delay: 0.08s;
}
.landing-capability-card:nth-child(2),
.landing-module-row:nth-child(2) {
animation-delay: 0.04s;
}
.landing-capability-card:nth-child(3),
.landing-module-row:nth-child(3) {
animation-delay: 0.08s;
}
}
@keyframes landing-rise {
from {
opacity: 0;
transform: translateY(18px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 960px) {
.landing-nav {
grid-template-columns: 1fr auto;
}
.landing-nav-links {
display: none;
}
.landing-hero,
.landing-workflow,
.landing-deploy {
grid-template-columns: 1fr;
}
.landing-hero {
min-height: auto;
padding-top: 28px;
}
.landing-capability-grid,
.landing-module-grid {
grid-template-columns: 1fr;
}
.landing-capability-card:first-child {
grid-row: auto;
min-height: 280px;
}
}
@media (max-width: 640px) {
.landing-nav {
width: min(100% - 28px, 1180px);
height: auto;
padding: 14px 0;
}
.landing-brand span {
font-size: 15px;
}
.landing-nav-actions {
gap: 4px;
}
.landing-link-button {
display: none;
}
.landing-primary-button,
.landing-secondary-button {
min-height: 38px;
padding: 0 14px;
}
.landing-hero,
.landing-section,
.landing-workflow,
.landing-deploy,
.landing-final {
width: min(100% - 28px, 1180px);
}
.landing-hero {
gap: 30px;
padding-bottom: 42px;
}
.landing-hero h1 {
font-size: clamp(38px, 13vw, 54px);
}
.landing-hero-actions {
align-items: stretch;
flex-direction: column;
}
.landing-visual {
border-radius: 22px;
padding: 14px;
}
.landing-route-row {
grid-template-columns: 1fr;
gap: 5px;
}
.landing-visual-grid,
.landing-node-grid {
grid-template-columns: 1fr;
}
.landing-workflow {
padding: 20px;
border-radius: 24px;
}
.landing-mode-list div {
grid-template-columns: 1fr;
gap: 6px;
}
}
/* ===== Animations ===== */
@keyframes spin {
to {
-53
View File
@@ -1,53 +0,0 @@
import { describe, expect, it } from 'vitest';
import { create, type ReactTestInstance } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import Landing from './Landing.js';
function collectText(node: ReactTestInstance): string {
return (node.children || []).map((child) => {
if (typeof child === 'string') return child;
return collectText(child);
}).join('');
}
describe('Landing page', () => {
it('presents the public BossAI website with product capabilities', () => {
const root = create(
<MemoryRouter>
<Landing />
</MemoryRouter>,
);
try {
const text = collectText(root.root);
expect(text).toContain('聚合多模型多模态的 AI 应用平台');
expect(text).toContain('统一模型网关');
expect(text).toContain('工作流生产台');
expect(text).toContain('短视频工厂');
expect(text).toContain('Comfy Cloud API 后端');
expect(text).toContain('Rust 二进制 + 内置 Web');
expect(text).not.toContain('\u2014');
} finally {
root.unmount();
}
});
it('links to user login, registration and admin access', () => {
const root = create(
<MemoryRouter>
<Landing />
</MemoryRouter>,
);
try {
const links = root.root.findAll((node) => node.type === 'a');
const hrefs = links.map((node) => node.props.href || node.props.to);
expect(hrefs).toContain('/user/login');
expect(hrefs).toContain('/user/register');
expect(hrefs).toContain('/boos');
} finally {
root.unmount();
}
});
});
-207
View File
@@ -1,207 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
const capabilityGroups = [
{
title: '统一模型网关',
body: '把 OpenAI、Claude、Gemini、方舟和各类中转站收束成一个下游入口。',
items: ['/v1/chat/completions', '/v1/messages', '/v1/images/generations', '/v1/videos'],
},
{
title: '工作流生产台',
body: '内嵌 ComfyUI 前端,按配置连接 Comfy Cloud、RunningHub、外部 ComfyUI 或 BoosAPI 执行器。',
items: ['Workflow Studio', 'Comfy Cloud', 'RunningHub', 'External Comfy'],
},
{
title: '多模态创作应用',
body: '生图助手、视频助手、数字人和短视频工厂复用同一套模型路由与用户密钥。',
items: ['生图助手', '视频助手', '数字人', '短视频工厂'],
},
];
const productModules = [
{ name: '模型路由', detail: '按站点、账号、令牌和模型调度,失败冷却后自动切换通道。' },
{ name: '模型广场', detail: '普通用户只看品牌和可用模型,管理员查看完整供应商与账号覆盖。' },
{ name: '使用日志', detail: '记录延迟、模型、计费和调用来源,便于对账和排障。' },
{ name: '工作空间', detail: '归档图片、视频、音频和任务产出,用户之间保持隔离。' },
{ name: '公告系统', detail: '管理员发布公告,普通用户登录后可直接弹窗确认。' },
{ name: '数据库后端', detail: '支持 SQLite 与 PostgreSQL,并保留迁移工具和运行时检测。' },
];
const workflowModes = [
{ mode: 'cloud', label: 'Comfy Cloud API 后端' },
{ mode: 'runninghub', label: 'RunningHub API 后端' },
{ mode: 'external', label: '外部完整版 ComfyUI' },
{ mode: 'boosapi', label: 'Rust API-native 执行器' },
];
const previewRows = [
{ label: 'Chat', model: 'gpt-5.4', status: '已路由' },
{ label: 'Image', model: 'gpt-image-2', status: '生成中' },
{ label: 'Video', model: 'seedance', status: '排队中' },
{ label: 'Claude', model: '/v1/messages', status: '流式' },
];
export default function Landing() {
return (
<main className="landing-page">
<nav className="landing-nav" aria-label="官网导航">
<Link to="/" className="landing-brand" aria-label="BossAI 首页">
<img src="/logo.png" alt="BossAI" />
<span>BossAI</span>
</Link>
<div className="landing-nav-links">
<a href="#platform">平台能力</a>
<a href="#workflow">工作流</a>
<a href="#deploy">部署形态</a>
</div>
<div className="landing-nav-actions">
<Link to="/user/login" className="landing-link-button">用户登录</Link>
<Link to="/boos" className="landing-primary-button">进入后台</Link>
</div>
</nav>
<section className="landing-hero">
<div className="landing-hero-copy">
<div className="landing-brand-strip">多模型 · 多模态 · 工作流</div>
<h1>聚合多模型多模态的 AI 应用平台</h1>
<p>
统一 API 网关、智能路由、Comfy 工作流和创作应用,让团队用一套后台管理对话、生图、视频和数字人生产。
</p>
<div className="landing-hero-actions">
<Link to="/user/register" className="landing-primary-button">开始使用</Link>
<Link to="/user/login" className="landing-secondary-button">用户登录</Link>
</div>
</div>
<div className="landing-visual" aria-label="BossAI 平台预览">
<div className="landing-visual-top">
<span>Unified Gateway</span>
<strong>路由在线</strong>
</div>
<div className="landing-route-stack">
{previewRows.map((row) => (
<div key={row.label} className="landing-route-row">
<span>{row.label}</span>
<strong>{row.model}</strong>
<em>{row.status}</em>
</div>
))}
</div>
<div className="landing-visual-grid">
<div>
<span>Workspace</span>
<strong>归档产出</strong>
</div>
<div>
<span>Billing</span>
<strong>日志计费</strong>
</div>
</div>
</div>
</section>
<section id="platform" className="landing-section landing-capabilities">
<div className="landing-section-heading">
<h2>从 API 到内容生产,一条链路打通</h2>
<p>BossAI 不是只转发请求。它把模型接入、路由、用户、工作流和产出归档放在同一个运行面里。</p>
</div>
<div className="landing-capability-grid">
{capabilityGroups.map((group) => (
<article key={group.title} className="landing-capability-card">
<h3>{group.title}</h3>
<p>{group.body}</p>
<div className="landing-chip-row">
{group.items.map((item) => <span key={item}>{item}</span>)}
</div>
</article>
))}
</div>
</section>
<section id="workflow" className="landing-workflow">
<div className="landing-workflow-main">
<h2>用熟悉的 ComfyUI 前端,接你的云端 API</h2>
<p>
工作流页面可切换 Cloud、RunningHub、External 和 BoosAPI 后端。你继续使用熟悉的节点画布,BossAI 负责鉴权、代理、任务记录和工作空间归档。
</p>
<div className="landing-mode-list">
{workflowModes.map((item) => (
<div key={item.mode}>
<code>{item.mode}</code>
<span>{item.label}</span>
</div>
))}
</div>
</div>
<div className="landing-workflow-side">
<div className="landing-node-card is-wide">
<span>Prompt</span>
<strong>商品文案、角色设定、镜头脚本</strong>
</div>
<div className="landing-node-grid">
<div className="landing-node-card">
<span>Image</span>
<strong>图像生成</strong>
</div>
<div className="landing-node-card">
<span>Video</span>
<strong>视频任务</strong>
</div>
<div className="landing-node-card">
<span>Agent</span>
<strong>视频助手</strong>
</div>
<div className="landing-node-card">
<span>Archive</span>
<strong>工作空间</strong>
</div>
</div>
</div>
</section>
<section className="landing-section">
<div className="landing-module-grid">
{productModules.map((module) => (
<article key={module.name} className="landing-module-row">
<h3>{module.name}</h3>
<p>{module.detail}</p>
</article>
))}
</div>
</section>
<section id="deploy" className="landing-deploy">
<div>
<h2>远程模型优先,本机不必有 GPU</h2>
<p>
当图像、视频和语音能力走上游 API、Comfy Cloud 或 RunningHub 时,BossAI 服务器只负责网关、数据库、Workflow UI 代理、日志和归档。
</p>
</div>
<div className="landing-deploy-panel">
<div>
<span>推荐形态</span>
<strong>Rust 二进制 + 内置 Web</strong>
</div>
<div>
<span>数据库</span>
<strong>SQLite / PostgreSQL</strong>
</div>
<div>
<span>执行能力</span>
<strong>API、Cloud、RunningHub、External</strong>
</div>
</div>
</section>
<section className="landing-final">
<h2>把模型、工作流和创作应用收进一个平台</h2>
<p>适合自建 AI 中转、团队模型管理、多模态创作和商业化用户面板。</p>
<div className="landing-hero-actions">
<Link to="/user/register" className="landing-primary-button">开始使用</Link>
<Link to="/boos" className="landing-secondary-button">进入后台</Link>
</div>
</section>
</main>
);
}