Compare commits

...

5 Commits

Author SHA1 Message Date
Innei 0cbe27e13c chore: update Vite version and adjust base path in configuration
- Changed Vite version in package.json from 8.0.0-beta.15 to ^7 for compatibility.
- Modified the base path in vite.config.ts to be dynamic based on the NODE_ENV, setting it to '/' for development and '/spa/' for production.

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-20 19:46:27 +08:00
Innei c37a39d9b8 chore: update dependencies in package.json
- Upgraded `@vitejs/plugin-react` from version 4.5.2 to 5.1.4.
- Updated `vite` from version 7.3.1 to 8.0.0-beta.15.

These changes ensure compatibility with the latest features and improvements.

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-20 18:01:58 +08:00
Innei efd661483a refactor: update build scripts in package.json and modify Vite config
- Renamed the build script to `build:next` and updated the main `build` script to run both `build:spa` and `build:next`.
- Added a base path for the Vite configuration.
- Commented out the `createDependencyChunksPlugin` import in Vite config for potential future use.

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-20 17:54:16 +08:00
Innei 59e5781a97 update
Signed-off-by: Innei <tukon479@gmail.com>
2026-02-20 14:20:50 +08:00
Innei b60a6fe340 vite spa 2026-02-20 01:01:50 +08:00
1255 changed files with 1853 additions and 1025 deletions
+5 -1
View File
@@ -83,6 +83,7 @@ public/sw*
public/swe-worker*
# Generated files
public/spa/
public/*.js
public/sitemap.xml
public/sitemap-index.xml
@@ -127,4 +128,7 @@ out
i18n-unused-keys-report.json
.vitest-reports
pnpm-lock.yaml
pnpm-lock.yaml
.turbo
spaHtmlTemplates.ts
+3
View File
@@ -76,6 +76,7 @@ COPY patches ./patches
# bring in desktop workspace manifest so pnpm can resolve it
COPY apps/desktop/src/main/package.json ./apps/desktop/src/main/package.json
RUN set -e && \
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
export SENTRYCLI_CDNURL="https://npmmirror.com/mirrors/sentry-cli"; \
@@ -116,6 +117,8 @@ COPY --from=base /distroless/ /
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/.next/standalone /app/
# Copy SPA build output (Vite)
COPY --from=builder /app/public/spa /app/public/spa
# Copy Next export output for desktop renderer
COPY --from=builder /app/apps/desktop/dist/next /app/apps/desktop/dist/next
-93
View File
@@ -24,99 +24,6 @@
"count": 1
}
},
"src/app/[variants]/(auth)/market-auth-callback/page.tsx": {
"no-console": {
"count": 2
}
},
"src/app/[variants]/(desktop)/desktop-onboarding/features/WelcomeStep.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/agent/profile/features/Header/AgentForkTag.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/agent/features/AgentForkTag.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/user/features/UserAgentList.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/community/components/VirtuosoGridList/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 2
}
},
"src/app/[variants]/(main)/home/features/RecentPage/Item.tsx": {
"regexp/no-super-linear-backtracking": {
"count": 1
}
},
"src/app/[variants]/(main)/home/features/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/(main)/memory/features/GridView/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/(main)/memory/features/MemoryAnalysis/index.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/resource/features/hooks/useResourceManagerUrlSync.ts": {
"react-hooks/exhaustive-deps": {
"count": 1
}
},
"src/app/[variants]/(main)/settings/provider/detail/ollama/CheckError.tsx": {
"regexp/no-dupe-characters-character-class": {
"count": 1
},
"regexp/no-obscure-range": {
"count": 1
}
},
"src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx": {
"no-console": {
"count": 2
}
},
"src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/onboarding/components/KlavisServerList/hooks/useKlavisOAuth.ts": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/onboarding/features/ResponseLanguageStep.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/onboarding/features/TelemetryStep.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/components/AntdStaticMethods/index.tsx": {
"import-x/no-mutable-exports": {
"count": 3
+22
View File
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="<!--LOCALE-->" dir="<!--DIR-->">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
#root {
height: 100%;
}
</style>
<!--SEO_META-->
</head>
<body>
<div id="root"></div>
<script>
// window.__SERVER_CONFIG__ = <!--SERVER_CONFIG-->;
window.__SERVER_CONFIG__ = undefined; /* SERVER_CONFIG */
</script>
<!--ANALYTICS_SCRIPTS-->
<script type="module" src="/src/spa/entry.desktop.tsx"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="<!--LOCALE-->" dir="<!--DIR-->">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--SEO_META-->
</head>
<body>
<div id="root"></div>
<script>
window.__SERVER_CONFIG__ = undefined; /* SERVER_CONFIG */
</script>
<!--ANALYTICS_SCRIPTS-->
<script type="module" src="/src/spa/entry.mobile.tsx"></script>
</body>
</html>
+17 -3
View File
@@ -33,10 +33,13 @@
],
"scripts": {
"prebuild": "tsx scripts/prebuild.mts && npm run lint",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 next build --webpack",
"build": "npm run build:spa && npm run build:next",
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
"build:analyze": "NODE_OPTIONS=--max-old-space-size=81920 ANALYZE=true next build --webpack",
"build:docker": "npm run prebuild && NODE_OPTIONS=--max-old-space-size=8192 DOCKER=true next build --webpack && npm run build-sitemap",
"build:docker": "npm run prebuild && npm run build:spa && NODE_OPTIONS=--max-old-space-size=8192 DOCKER=true next build --webpack && npm run build-sitemap",
"build:next": "cross-env NODE_OPTIONS=--max-old-space-size=8192 next build --webpack",
"build:spa": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build && tsx scripts/buildSpaTemplates.ts",
"build:spa:analyze": "cross-env ANALYZE=true NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:vercel": "tsx scripts/prebuild.mts && npm run lint:ts && npm run lint:style && npm run type-check:tsc && npm run lint:circular && cross-env NODE_OPTIONS=--max-old-space-size=6144 next build --webpack && npm run postbuild",
"build-migrate-db": "bun run db:migrate",
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
@@ -64,6 +67,8 @@
"dev:docker:down": "docker compose -f docker-compose/dev/docker-compose.yml down",
"dev:docker:reset": "docker compose -f docker-compose/dev/docker-compose.yml down -v && rm -rf docker-compose/dev/data && npm run dev:docker && pnpm db:migrate",
"dev:mobile": "next dev -p 3018",
"dev:spa": "vite",
"dev:turbo": "turbo run dev dev:spa --filter=@lobehub/lobehub",
"docs:cdn": "npm run workflow:docs-cdn && npm run lint:mdx",
"docs:i18n": "lobe-i18n md && npm run lint:mdx",
"docs:seo": "lobe-seo && npm run lint:mdx",
@@ -240,6 +245,7 @@
"@react-three/fiber": "^9.5.0",
"@saintno/comfyui-sdk": "^0.2.49",
"@serwist/next": "^9.5.0",
"@t3-oss/env-core": "^0.13.10",
"@t3-oss/env-nextjs": "^0.13.10",
"@tanstack/react-query": "^5.90.20",
"@trpc/client": "^11.8.1",
@@ -338,6 +344,7 @@
"react-hotkeys-hook": "^5.2.3",
"react-i18next": "^16.5.3",
"react-lazy-load": "^4.0.1",
"react-markdown": "^10.1.0",
"react-pdf": "^10.3.0",
"react-responsive": "^10.0.1",
"react-rnd": "^10.5.2",
@@ -384,6 +391,7 @@
"@ast-grep/napi": "^0.40.5",
"@commitlint/cli": "^19.8.1",
"@edge-runtime/vm": "^5.0.0",
"@emotion/babel-plugin": "^11.13.5",
"@huggingface/tasks": "^0.19.80",
"@inquirer/prompts": "^8.2.0",
"@lobechat/types": "workspace:*",
@@ -423,6 +431,7 @@
"@types/ws": "^8.18.1",
"@types/xast": "^2.0.4",
"@typescript/native-preview": "7.0.0-dev.20260207.1",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^3.2.4",
"ajv-keywords": "^5.1.0",
"code-inspector-plugin": "1.3.3",
@@ -446,6 +455,7 @@
"import-in-the-middle": "^2.0.5",
"just-diff": "^6.0.2",
"knip": "^5.82.1",
"linkedom": "^0.18.12",
"lint-staged": "^16.2.7",
"markdown-table": "^3.0.4",
"mcp-hello-world": "^1.1.2",
@@ -464,11 +474,15 @@
"serwist": "^9.5.0",
"stylelint": "^16.12.0",
"tsx": "^4.21.0",
"turbo": "^2.8.10",
"type-fest": "^5.4.1",
"typescript": "^5.9.3",
"unified": "^11.0.5",
"unist-util-visit": "^5.1.0",
"vite": "^7.3.1",
"vite": "^7",
"vite-bundle-analyzer": "^1.3.6",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.20.0",
@@ -4,7 +4,7 @@ import { KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
import type { BuiltinInterventionProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { CheckCircle } from 'lucide-react';
import Image from 'next/image';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -101,8 +101,7 @@ const InstallPluginIntervention = memo<BuiltinInterventionProps<InstallPluginPar
>
<Flexbox horizontal align="center" gap={12}>
{icon ? (
<Image
unoptimized
<img
alt={klavisTypeInfo?.label || identifier}
height={40}
src={icon}
@@ -144,8 +143,7 @@ const InstallPluginIntervention = memo<BuiltinInterventionProps<InstallPluginPar
>
<Flexbox horizontal align="center" gap={12}>
{icon ? (
<Image
unoptimized
<img
alt={lobehubSkillProviderInfo?.label || identifier}
height={40}
src={icon}
@@ -188,8 +186,7 @@ const InstallPluginIntervention = memo<BuiltinInterventionProps<InstallPluginPar
>
<Flexbox horizontal align="center" gap={12}>
{pluginIcon && typeof pluginIcon === 'string' && pluginIcon.startsWith('http') ? (
<Image
unoptimized
<img
alt={pluginName}
height={40}
src={pluginIcon}
@@ -1 +1,4 @@
export const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
export const isDesktop =
// @ts-ignore - import.meta.env is available in Vite builds
(typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_IS_DESKTOP_APP === '1') ||
process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
+4 -1
View File
@@ -1 +1,4 @@
export const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
export const isDesktop =
// @ts-ignore - import.meta.env is available in Vite builds
(typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_IS_DESKTOP_APP === '1') ||
process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
@@ -14,7 +14,7 @@ import {
import { Descriptions } from 'antd';
import { createStaticStyles } from 'antd-style';
import { ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -163,7 +163,7 @@ const PageContent = memo<PageContentProps>(({ result }) => {
)}
<Flexbox horizontal align={'center'} className={styles.url} gap={4}>
{siteName && <div>{siteName} · </div>}
<Link
<a
className={styles.url}
href={url}
rel={'nofollow'}
@@ -173,7 +173,7 @@ const PageContent = memo<PageContentProps>(({ result }) => {
>
{result.originalUrl}
<Icon icon={ExternalLink} />
</Link>
</a>
</Flexbox>
<div className={styles.footer}>
@@ -2,7 +2,7 @@
import { CopyButton, Flexbox, Skeleton } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import Link from 'next/link';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -44,9 +44,9 @@ const LoadingCard = memo<{ url: string }>(({ url }) => {
return (
<Flexbox className={styles.container}>
<Flexbox horizontal className={styles.cardBody} justify={'space-between'}>
<Link href={url} rel={'nofollow'} target={'_blank'}>
<a href={url} rel={'nofollow'} target={'_blank'}>
<div className={styles.text}>{url}</div>
</Link>
</a>
<CopyButton content={url} size={'small'} />
</Flexbox>
<Flexbox gap={4} paddingInline={16}>
@@ -5,7 +5,7 @@ import { ActionIcon, Alert, Block, Flexbox, Text, stopPropagation } from '@lobeh
import { Descriptions } from 'antd';
import { createStaticStyles } from 'antd-style';
import { ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -114,9 +114,9 @@ const CrawlerResultCard = memo<CrawlerData>(({ result, messageId, crawler, origi
<Flexbox gap={8} paddingBlock={8} paddingInline={12}>
<Flexbox horizontal align={'center'} className={styles.titleRow} justify={'space-between'}>
<Text ellipsis>{title || originalUrl}</Text>
<Link href={url} target={'_blank'} onClick={stopPropagation}>
<a href={url} target={'_blank'} onClick={stopPropagation}>
<ActionIcon icon={ExternalLink} size={'small'} />
</Link>
</a>
</Flexbox>
<Text ellipsis={{ rows: 2 }} fontSize={12} type={'secondary'}>
{description || result.content?.slice(0, 40)}
@@ -1,7 +1,7 @@
import type { UniformSearchResult } from '@lobechat/types';
import { Block, Flexbox, Text } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import Link from 'next/link';
import type { CSSProperties } from 'react';
import { memo } from 'react';
@@ -24,7 +24,7 @@ const SearchResultItem = memo<UniformSearchResult & { style?: CSSProperties }>(
const urlObj = new URL(url);
const host = urlObj.hostname;
return (
<Link href={url} target={'_blank'}>
<a href={url} target={'_blank'}>
<Block
clickable
className={styles.container}
@@ -41,7 +41,7 @@ const SearchResultItem = memo<UniformSearchResult & { style?: CSSProperties }>(
</Text>
</Flexbox>
</Block>
</Link>
</a>
);
},
);
+2 -2
View File
@@ -2,6 +2,6 @@ import { isDesktop } from './version';
export const ELECTRON_BE_PROTOCOL_SCHEME = 'lobe-backend';
export const withElectronProtocolIfElectron = (url: string) => {
return isDesktop ? `${ELECTRON_BE_PROTOCOL_SCHEME}://lobe${url}` : url;
export const morphApiURI = (uri: string) => {
return isDesktop ? `${ELECTRON_BE_PROTOCOL_SCHEME}://lobe${uri}` : uri;
};
+4 -1
View File
@@ -4,7 +4,10 @@ import pkg from '../../../package.json';
export const CURRENT_VERSION = pkg.version;
export const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
export const isDesktop =
// @ts-ignore - import.meta.env is available in Vite builds
(typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_IS_DESKTOP_APP === '1') ||
process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
// @ts-ignore
export const isCustomBranding = BRANDING_NAME !== 'LobeHub';
+1
View File
@@ -50,6 +50,7 @@ export interface GlobalServerConfig {
aiProvider: ServerLanguageModel;
defaultAgent?: PartialDeep<UserDefaultAgent>;
disableEmailPassword?: boolean;
enableAuth?: boolean;
enableBusinessFeatures?: boolean;
/**
* @deprecated
+7
View File
@@ -21,3 +21,10 @@ export const setCookie = (
document.cookie = `${key}=${value};expires=${expires};path=/;`;
}
};
export const getCookie = (key: string) => {
return document.cookie
.split('; ')
.find((row) => row.startsWith(`${key}=`))
?.split('=')[1];
};
@@ -0,0 +1,41 @@
import type { Plugin, UserConfig } from 'vite';
export function createDependencyChunksPlugin(dependencies: string[][]): Plugin {
return {
name: 'dependency-chunks',
config(config: UserConfig) {
config.build = config.build || {};
config.build.rollupOptions = config.build.rollupOptions || {};
config.build.rollupOptions.output = config.build.rollupOptions.output || {};
const { output } = config.build.rollupOptions;
const outputConfig = Array.isArray(output) ? output[0] : output;
outputConfig.assetFileNames = 'assets/[name].[hash:6][extname]';
outputConfig.chunkFileNames = (chunkInfo) => {
return chunkInfo.name.startsWith('vendor/')
? '[name]-[hash].js'
: 'assets/[name]-[hash].js';
};
outputConfig.manualChunks = (id: string, { getModuleInfo }) => {
const moduleInfo = getModuleInfo(id);
if (moduleInfo?.dynamicImporters?.length && moduleInfo?.importers?.length) {
return null;
}
const matchedDep = dependencies.findIndex((dep) => {
return dep.some((d) => {
const pattern = `/node_modules/${d}/`;
return id.includes(pattern) && !id.includes(`${pattern}node_modules/`);
});
});
if (matchedDep !== -1) {
return `vendor/${matchedDep}`;
}
return null;
};
},
};
}
+1
View File
@@ -4,6 +4,7 @@ packages:
- e2e
- apps/desktop/src/main
onlyBuiltDependencies:
- '@vercel/speed-insights'
- '@lobehub/editor'
+22
View File
@@ -0,0 +1,22 @@
import { readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
const SPA_DIR = resolve(process.cwd(), 'public/spa');
const OUTPUT_PATH = resolve(process.cwd(), 'src/app/[[...path]]/spaHtmlTemplates.ts');
async function run() {
const html = await readFile(resolve(SPA_DIR, 'index.html'), 'utf8');
const source = `// AUTO-GENERATED by scripts/buildSpaTemplates.ts
// DO NOT EDIT MANUALLY.
export const desktopHtmlTemplate = ${JSON.stringify(html)};
export const mobileHtmlTemplate = ${JSON.stringify(html)};
`;
await writeFile(OUTPUT_PATH, source, 'utf8');
}
run().catch((error) => {
console.error('Failed to generate SPA HTML templates:', error);
process.exit(1);
});
+17 -17
View File
@@ -218,9 +218,9 @@ const assertMetadataManifestRemoved = (code: string) =>
!/\bmanifest\s*:/.test(code) && !/\bmetadataBase\s*:/.test(code);
export const modifyAppCode = async (TEMP_DIR: string) => {
// 1. Replace src/app/[variants]/page.tsx with a desktop-only entry
const variantsPagePath = path.join(TEMP_DIR, 'src/app/[variants]/page.tsx');
console.log(' Processing src/app/[variants]/page.tsx...');
// 1. Replace src/routes/page.tsx with a desktop-only entry
const variantsPagePath = path.join(TEMP_DIR, 'src/routes/page.tsx');
console.log(' Processing src/routes/page.tsx...');
await writeFileEnsuring({
filePath: variantsPagePath,
name: 'modifyAppCode:variantsPage',
@@ -238,9 +238,9 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
assertAfter: assertDevPanelStripped,
});
// 3. Delete src/app/[variants]/(main)/settings/security directory
const securityDirPath = path.join(TEMP_DIR, 'src/app/[variants]/(main)/settings/security');
console.log(' Deleting src/app/[variants]/(main)/settings/security directory...');
// 3. Delete src/routes/(main)/settings/security directory
const securityDirPath = path.join(TEMP_DIR, 'src/routes/(main)/settings/security');
console.log(' Deleting src/routes/(main)/settings/security directory...');
await removePathEnsuring({
name: 'modifyAppCode:deleteSecurityDir',
path: securityDirPath,
@@ -249,9 +249,9 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
// 4. Remove Security tab wiring from SettingsContent
const settingsContentPath = path.join(
TEMP_DIR,
'src/app/[variants]/(main)/settings/features/SettingsContent.tsx',
'src/routes/(main)/settings/features/SettingsContent.tsx',
);
console.log(' Processing src/app/[variants]/(main)/settings/features/SettingsContent.tsx...');
console.log(' Processing src/routes/(main)/settings/features/SettingsContent.tsx...');
await updateFile({
filePath: settingsContentPath,
name: 'modifyAppCode:removeSecurityTab',
@@ -259,9 +259,9 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
assertAfter: assertSecurityTabRemoved,
});
// 5. Remove SpeedInsights and Analytics from src/app/[variants]/layout.tsx
const variantsLayoutPath = path.join(TEMP_DIR, 'src/app/[variants]/layout.tsx');
console.log(' Processing src/app/[variants]/layout.tsx...');
// 5. Remove SpeedInsights and Analytics from src/routes/layout.tsx
const variantsLayoutPath = path.join(TEMP_DIR, 'src/routes/layout.tsx');
console.log(' Processing src/routes/layout.tsx...');
await updateFile({
filePath: variantsLayoutPath,
name: 'modifyAppCode:removeSpeedInsightsAndAnalytics',
@@ -280,8 +280,8 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
});
// 7. Remove manifest from metadata
const metadataPath = path.join(TEMP_DIR, 'src/app/[variants]/metadata.ts');
console.log(' Processing src/app/[variants]/metadata.ts...');
const metadataPath = path.join(TEMP_DIR, 'src/routes/metadata.ts');
console.log(' Processing src/routes/metadata.ts...');
await updateFile({
filePath: metadataPath,
name: 'modifyAppCode:removeManifestFromMetadata',
@@ -292,11 +292,11 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('modifyAppCode', modifyAppCode, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/page.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/page.tsx' },
{ lang: Lang.Tsx, path: 'src/layout/GlobalProvider/index.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/(main)/settings/features/SettingsContent.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/layout.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/(main)/settings/features/SettingsContent.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/layout.tsx' },
{ lang: Lang.Tsx, path: 'src/components/mdx/Image.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/metadata.ts' },
{ lang: Lang.Tsx, path: 'src/routes/metadata.ts' },
]);
}
@@ -213,7 +213,7 @@ const removeDynamicElementImport = (code: string): string => {
export const convertDynamicToStatic = async (TEMP_DIR: string) => {
const routerConfigPath = path.join(
TEMP_DIR,
'src/app/[variants]/router/desktopRouter.config.tsx',
'src/routes/router/desktopRouter.config.tsx',
);
console.log(' Processing dynamicElement → static imports...');
@@ -268,6 +268,6 @@ export const convertDynamicToStatic = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('convertDynamicToStatic', convertDynamicToStatic, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/router/desktopRouter.config.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/router/desktopRouter.config.tsx' },
]);
}
+2 -2
View File
@@ -31,9 +31,9 @@ export const modifySourceForElectron = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('modifySourceForElectron', modifySourceForElectron, [
{ lang: Lang.TypeScript, path: path.join(process.cwd(), 'next.config.ts') },
{ lang: Lang.Tsx, path: 'src/app/[variants]/page.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/page.tsx' },
{ lang: Lang.Tsx, path: 'src/layout/GlobalProvider/index.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/router/desktopRouter.config.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/router/desktopRouter.config.tsx' },
{ lang: Lang.Tsx, path: 'src/components/mdx/Image.tsx' },
{ lang: Lang.TypeScript, path: 'src/features/DevPanel/CacheViewer/getCacheEntries.ts' },
{ lang: Lang.TypeScript, path: 'src/server/translation.ts' },
@@ -193,7 +193,7 @@ const transformFile = (code: string, filePath: string): string => {
export const convertNextDynamicToStatic = async (TEMP_DIR: string) => {
const appDirs = [
{ dir: path.join(TEMP_DIR, 'src/app/(variants)'), label: 'src/app/(variants)' },
{ dir: path.join(TEMP_DIR, 'src/app/[variants]'), label: 'src/app/[variants]' },
{ dir: path.join(TEMP_DIR, 'src/routes'), label: 'src/routes' },
];
console.log(' Processing next/dynamic → static imports...');
@@ -95,7 +95,7 @@ const removeUnusedImports = (code: string): string => {
export const removeSuspenseFromConversation = async (TEMP_DIR: string) => {
const filePath = path.join(
TEMP_DIR,
'src/app/[variants]/(main)/agent/features/Conversation/index.tsx',
'src/routes/(main)/agent/features/Conversation/index.tsx',
);
console.log(' Removing Suspense from Conversation/index.tsx...');
@@ -123,6 +123,6 @@ export const removeSuspenseFromConversation = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('removeSuspenseFromConversation', removeSuspenseFromConversation, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/(main)/agent/features/Conversation/index.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/(main)/agent/features/Conversation/index.tsx' },
]);
}
@@ -16,11 +16,11 @@ export const modifyRoutes = async (TEMP_DIR: string) => {
'src/app/(backend)/market',
// Auth & User routes
'src/app/[variants]/(auth)',
'src/app/[variants]/(mobile)',
'src/app/[variants]/(main)/(mobile)/me',
'src/app/[variants]/(main)/changelog',
'src/app/[variants]/oauth',
'src/routes/(auth)',
'src/routes/(mobile)',
'src/routes/(main)/(mobile)/me',
'src/routes/(main)/changelog',
'src/routes/oauth',
// Other app roots
'src/app/market-auth-callback',
@@ -47,7 +47,7 @@ export const modifyRoutes = async (TEMP_DIR: string) => {
}
// 2. Delete root loading.tsx files(not needed in Electron SPA)
const loadingFiles = ['src/app/loading.tsx', 'src/app/[variants]/loading.tsx'];
const loadingFiles = ['src/app/loading.tsx', 'src/routes/loading.tsx'];
console.log(` Removing ${loadingFiles.length} root loading.tsx files...`);
for (const file of loadingFiles) {
const fullPath = path.join(TEMP_DIR, file);
@@ -60,9 +60,9 @@ export const modifyRoutes = async (TEMP_DIR: string) => {
// 3. Modify desktopRouter.config.tsx
const routerConfigPath = path.join(
TEMP_DIR,
'src/app/[variants]/router/desktopRouter.config.tsx',
'src/routes/router/desktopRouter.config.tsx',
);
console.log(' Processing src/app/[variants]/router/desktopRouter.config.tsx...');
console.log(' Processing src/routes/router/desktopRouter.config.tsx...');
await updateFile({
assertAfter: (code) => !/\bchangelog\b/.test(code),
filePath: routerConfigPath,
@@ -104,6 +104,6 @@ export const modifyRoutes = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('modifyRoutes', modifyRoutes, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/router/desktopRouter.config.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/router/desktopRouter.config.tsx' },
]);
}
@@ -50,7 +50,7 @@ const generateStaticComponentMap = (imports: DynamicImportInfo[]): string => {
export const convertSettingsContentToStatic = async (TEMP_DIR: string) => {
const filePath = path.join(
TEMP_DIR,
'src/app/[variants]/(main)/settings/features/SettingsContent.tsx',
'src/routes/(main)/settings/features/SettingsContent.tsx',
);
console.log(' Processing SettingsContent.tsx dynamic imports...');
@@ -125,6 +125,6 @@ export const convertSettingsContentToStatic = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('convertSettingsContentToStatic', convertSettingsContentToStatic, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/(main)/settings/features/SettingsContent.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/(main)/settings/features/SettingsContent.tsx' },
]);
}
@@ -4,7 +4,7 @@ import path from 'node:path';
import { invariant, isDirectRun, runStandalone, updateFile } from './utils.mjs';
export const wrapChildrenWithClientOnly = async (TEMP_DIR: string) => {
const layoutPath = path.join(TEMP_DIR, 'src/app/[variants]/layout.tsx');
const layoutPath = path.join(TEMP_DIR, 'src/routes/layout.tsx');
console.log(' Wrapping children with ClientOnly in layout.tsx...');
@@ -77,6 +77,6 @@ export const wrapChildrenWithClientOnly = async (TEMP_DIR: string) => {
if (isDirectRun(import.meta.url)) {
await runStandalone('wrapChildrenWithClientOnly', wrapChildrenWithClientOnly, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/layout.tsx' },
{ lang: Lang.Tsx, path: 'src/routes/layout.tsx' },
]);
}
+14 -14
View File
@@ -4,9 +4,9 @@
* to React Router version hooks.
*
* For files in (main) directory:
* - usePathname -> @/app/[variants]/(main)/hooks/usePathname
* - useSearchParams -> @/app/[variants]/(main)/hooks/useSearchParams
* - useRouter -> @/app/[variants]/(main)/hooks/useRouter
* - usePathname -> @/routes/(main)/hooks/usePathname
* - useSearchParams -> @/routes/(main)/hooks/useSearchParams
* - useRouter -> @/routes/(main)/hooks/useRouter
*
* @see RFC 147: LOBE-2850 - Phase 3
*/
@@ -19,14 +19,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
// Files that should be migrated to React Router version
const SPA_FILES = [
// (main) directory files using @/libs/next/navigation
'src/app/[variants]/(main)/community/_layout/Sidebar/Header/Nav.tsx',
'src/app/[variants]/(main)/group/_layout/Sidebar/Header/Nav.tsx',
'src/app/[variants]/(main)/group/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts',
'src/app/[variants]/(main)/group/_layout/Sidebar/Topic/hooks/useThreadNavigation.ts',
'src/app/[variants]/(main)/chat/_layout/Sidebar/Header/Nav.tsx',
'src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts',
'src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/hooks/useThreadNavigation.ts',
'src/app/[variants]/(main)/memory/_layout/Sidebar/Header/Nav.tsx',
'src/routes/(main)/community/_layout/Sidebar/Header/Nav.tsx',
'src/routes/(main)/group/_layout/Sidebar/Header/Nav.tsx',
'src/routes/(main)/group/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts',
'src/routes/(main)/group/_layout/Sidebar/Topic/hooks/useThreadNavigation.ts',
'src/routes/(main)/chat/_layout/Sidebar/Header/Nav.tsx',
'src/routes/(main)/chat/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts',
'src/routes/(main)/chat/_layout/Sidebar/Topic/hooks/useThreadNavigation.ts',
'src/routes/(main)/memory/_layout/Sidebar/Header/Nav.tsx',
];
interface MigrationResult {
@@ -64,21 +64,21 @@ async function migrateFile(relativePath: string): Promise<MigrationResult | null
for (const hook of importedHooks) {
switch (hook) {
case 'usePathname': {
newImports.push(`import { usePathname } from '@/app/[variants]/(main)/hooks/usePathname';`);
newImports.push(`import { usePathname } from '@/routes/(main)/hooks/usePathname';`);
changes.push('usePathname -> React Router version');
break;
}
case 'useSearchParams': {
newImports.push(
`import { useSearchParams } from '@/app/[variants]/(main)/hooks/useSearchParams';`,
`import { useSearchParams } from '@/routes/(main)/hooks/useSearchParams';`,
);
changes.push('useSearchParams -> React Router version');
break;
}
case 'useRouter': {
newImports.push(`import { useRouter } from '@/app/[variants]/(main)/hooks/useRouter';`);
newImports.push(`import { useRouter } from '@/routes/(main)/hooks/useRouter';`);
changes.push('useRouter -> React Router version');
break;
+4 -4
View File
@@ -144,22 +144,22 @@ const partialBuildPages = [
// {
// name: 'changelog',
// disabled: isDesktop,
// paths: ['src/app/[variants]/(main)/changelog'],
// paths: ['src/routes/(main)/changelog'],
// },
{
name: 'auth',
disabled: isDesktop,
paths: ['src/app/[variants]/(auth)'],
paths: ['src/app/(auth)'],
},
// {
// name: 'mobile',
// disabled: isDesktop,
// paths: ['src/app/[variants]/(main)/(mobile)'],
// paths: ['src/routes/(main)/(mobile)'],
// },
{
name: 'oauth',
disabled: isDesktop,
paths: ['src/app/[variants]/oauth', 'src/app/(backend)/oidc'],
paths: ['src/app/(auth)/oauth', 'src/app/(backend)/oidc'],
},
{
name: 'api-webhooks',
@@ -4,7 +4,7 @@ import { SiDiscord } from '@icons-pack/react-simple-icons';
import { SOCIAL_URL } from '@lobechat/business-const';
import { Button, Flexbox, Icon, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import { parseAsString, useQueryState } from 'nuqs';
import { useSearchParams } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,7 +16,8 @@ const normalizeErrorCode = (code?: string | null) =>
const AuthErrorPage = memo(() => {
const { t } = useTranslation('authError');
const [error] = useQueryState('error', parseAsString);
const searchParams = useSearchParams();
const error = searchParams.get('error');
const code = normalizeErrorCode(error);
const description = t(`codes.${code}`, { defaultValue: t('codes.UNKNOWN') });
+35
View File
@@ -0,0 +1,35 @@
import { cookies, headers } from 'next/headers';
import { type ReactNode, Suspense } from 'react';
import { isRtlLang } from 'rtl-detect';
import ClientOnly from '@/components/client/ClientOnly';
import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
import AuthProvider from '@/layout/AuthProvider';
import GlobalProvider from '@/layout/GlobalProvider';
import { parseBrowserLanguage } from '@/utils/locale';
import AuthContainer from './_layout';
export default async function AuthRootLayout({ children }: { children: ReactNode }) {
const cookieStore = await cookies();
const headerStore = await headers();
const locale =
cookieStore.get(LOBE_LOCALE_COOKIE)?.value || parseBrowserLanguage(headerStore, DEFAULT_LANG);
const direction = isRtlLang(locale) ? 'rtl' : 'ltr';
return (
<html suppressHydrationWarning dir={direction} lang={locale}>
<body>
<GlobalProvider isMobile={false} locale={locale}>
<AuthProvider>
<ClientOnly>
<Suspense>
<AuthContainer>{children}</AuthContainer>
</Suspense>
</ClientOnly>
</AuthProvider>
</GlobalProvider>
</body>
</html>
);
}
@@ -2,7 +2,7 @@
import { Button, Flexbox, FluentEmoji, Highlighter, Text } from '@lobehub/ui';
import { Result } from 'antd';
import { parseAsString, useQueryState } from 'nuqs';
import { useSearchParams } from 'next/navigation';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,8 +10,9 @@ import Link from '@/libs/next/Link';
const FailedPage = memo(() => {
const { t } = useTranslation('oauth');
const [reason] = useQueryState('reason');
const [errorMessage] = useQueryState<string>('errorMessage', parseAsString);
const searchParams = useSearchParams();
const reason = searchParams.get('reason');
const errorMessage = searchParams.get('errorMessage');
return (
<Result
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import Link from '@/libs/next/Link';
import { useRouter, useSearchParams } from '@/libs/next/navigation';
import AuthCard from '../../../../features/AuthCard';
import AuthCard from '@/features/AuthCard';
import { ResetPasswordContent } from './ResetPasswordContent';
const ResetPasswordPage = () => {
@@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next';
import AuthIcons from '@/components/AuthIcons';
import { PRIVACY_URL, TERMS_URL } from '@/const/url';
import AuthCard from '../../../../features/AuthCard';
import AuthCard from '@/features/AuthCard';
const styles = createStaticStyles(({ css, cssVar }) => ({
setPasswordLink: css`
@@ -6,7 +6,7 @@ import { ChevronLeft, ChevronRight, Lock } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import AuthCard from '../../../../features/AuthCard';
import AuthCard from '@/features/AuthCard';
export interface SignInPasswordStepProps {
email: string;
@@ -1,5 +1,6 @@
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
import { Form } from 'antd';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,7 +10,6 @@ import { useBusinessSignin } from '@/business/client/hooks/useBusinessSignin';
import { message } from '@/components/AntdStaticMethods';
import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
import { useRouter, useSearchParams } from '@/libs/next/navigation';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import Link from '@/libs/next/Link';
import { useSearchParams } from '@/libs/next/navigation';
import { AuthCard } from '../../../../../features/AuthCard';
import { AuthCard } from '@/features/AuthCard';
import { type SignUpFormValues } from './useSignUp';
import { useSignUp } from './useSignUp';
@@ -1,15 +1,16 @@
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
import { authEnv } from '@/envs/auth';
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { type DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
import BetterAuthSignUpForm from './BetterAuthSignUpForm';
export const generateMetadata = async (props: DynamicLayoutProps) => {
const locale = await RouteVariants.getLocale(props);
export const generateMetadata = async () => {
const cookieStore = await cookies();
const locale = cookieStore.get(LOBE_LOCALE_COOKIE)?.value || DEFAULT_LANG;
const { t } = await translation('auth', locale);
return metadataModule.generate({
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import Link from '@/libs/next/Link';
import { useSearchParams } from '@/libs/next/navigation';
import AuthCard from '../../../../features/AuthCard';
import AuthCard from '@/features/AuthCard';
import { VerifyEmailContent } from './VerifyEmailContent';
const VerifyEmailPage = () => {
@@ -1,10 +1,11 @@
import { type NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { appEnv } from '@/envs/app';
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
import { MarketService } from '@/server/services/market';
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
const MARKET_BASE_URL = appEnv.MARKET_BASE_URL || 'https://market.lobehub.com';
type RouteContext = {
params: Promise<{
+203
View File
@@ -0,0 +1,203 @@
import { type NextRequest } from 'next/server';
import { isRtlLang } from 'rtl-detect';
import { getServerFeatureFlagsValue } from '@/config/featureFlags';
import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
import { analyticsEnv } from '@/envs/analytics';
import { appEnv } from '@/envs/app';
import { fileEnv } from '@/envs/file';
import { pythonEnv } from '@/envs/python';
import { getServerGlobalConfig } from '@/server/globalConfig';
import { serializeForHtml } from '@/server/utils/serializeForHtml';
import {
type AnalyticsConfig,
type SPAClientEnv,
type SPAServerConfig,
type SPAThemeConfig,
} from '@/types/spaServerConfig';
import { parseBrowserLanguage } from '@/utils/locale';
import { desktopHtmlTemplate, mobileHtmlTemplate } from './spaHtmlTemplates';
const isDev = process.env.NODE_ENV === 'development';
const VITE_DEV_ORIGIN = process.env.VITE_DEV_ORIGIN || 'http://localhost:3011';
async function rewriteViteAssetUrls(html: string): Promise<string> {
const { parseHTML } = await import('linkedom');
const { document } = parseHTML(html);
document.querySelectorAll('script[src]').forEach((el) => {
const src = el.getAttribute('src');
if (src && src.startsWith('/')) {
el.setAttribute('src', `${VITE_DEV_ORIGIN}${src}`);
}
});
document.querySelectorAll('link[href]').forEach((el) => {
const href = el.getAttribute('href');
if (href && href.startsWith('/')) {
el.setAttribute('href', `${VITE_DEV_ORIGIN}${href}`);
}
});
// Rewrite inline module scripts (e.g., Vite's React refresh preamble)
document.querySelectorAll('script[type="module"]:not([src])').forEach((el) => {
const text = el.textContent || '';
if (text.includes('/@')) {
el.textContent = text.replaceAll(
/from\s+["'](\/[@\w].*?)["']/g,
(_match, p) => `from "${VITE_DEV_ORIGIN}${p}"`,
);
}
});
// Patch Worker constructor to wrap cross-origin Vite URLs as blob URLs
const workerPatch = document.createElement('script');
workerPatch.textContent = `(function(){
var O=globalThis.Worker;
globalThis.Worker=function(u,o){
var h=typeof u==='string'?u:u instanceof URL?u.href:'';
if(h.startsWith('${VITE_DEV_ORIGIN}')){
var b=new Blob(['import "'+h+'";'],{type:'application/javascript'});
return new O(URL.createObjectURL(b),Object.assign({},o,{type:'module'}));
}return new O(u,o)};
globalThis.Worker.prototype=O.prototype;
})();`;
const head = document.querySelector('head');
if (head?.firstChild) {
head.insertBefore(workerPatch, head.firstChild);
}
return document.toString();
}
async function getTemplate(isMobile: boolean): Promise<string> {
if (isDev) {
const res = await fetch(VITE_DEV_ORIGIN);
const html = await res.text();
return await rewriteViteAssetUrls(html);
}
if (isMobile) {
return mobileHtmlTemplate;
}
return desktopHtmlTemplate;
}
function buildAnalyticsConfig(): AnalyticsConfig {
const config: AnalyticsConfig = {};
if (analyticsEnv.ENABLE_GOOGLE_ANALYTICS && analyticsEnv.GOOGLE_ANALYTICS_MEASUREMENT_ID) {
config.google = { measurementId: analyticsEnv.GOOGLE_ANALYTICS_MEASUREMENT_ID };
}
if (analyticsEnv.ENABLED_PLAUSIBLE_ANALYTICS && analyticsEnv.PLAUSIBLE_DOMAIN) {
config.plausible = {
domain: analyticsEnv.PLAUSIBLE_DOMAIN,
scriptBaseUrl: analyticsEnv.PLAUSIBLE_SCRIPT_BASE_URL,
};
}
if (analyticsEnv.ENABLED_UMAMI_ANALYTICS && analyticsEnv.UMAMI_WEBSITE_ID) {
config.umami = {
scriptUrl: analyticsEnv.UMAMI_SCRIPT_URL,
websiteId: analyticsEnv.UMAMI_WEBSITE_ID,
};
}
if (analyticsEnv.ENABLED_CLARITY_ANALYTICS && analyticsEnv.CLARITY_PROJECT_ID) {
config.clarity = { projectId: analyticsEnv.CLARITY_PROJECT_ID };
}
if (analyticsEnv.ENABLED_POSTHOG_ANALYTICS && analyticsEnv.POSTHOG_KEY) {
config.posthog = {
debug: analyticsEnv.DEBUG_POSTHOG_ANALYTICS,
host: analyticsEnv.POSTHOG_HOST,
key: analyticsEnv.POSTHOG_KEY,
};
}
if (analyticsEnv.REACT_SCAN_MONITOR_API_KEY) {
config.reactScan = { apiKey: analyticsEnv.REACT_SCAN_MONITOR_API_KEY };
}
if (analyticsEnv.ENABLE_VERCEL_ANALYTICS) {
config.vercel = {
debug: analyticsEnv.DEBUG_VERCEL_ANALYTICS,
enabled: true,
};
}
if (analyticsEnv.DESKTOP_PROJECT_ID && analyticsEnv.DESKTOP_UMAMI_BASE_URL) {
config.desktop = {
baseUrl: analyticsEnv.DESKTOP_UMAMI_BASE_URL,
projectId: analyticsEnv.DESKTOP_PROJECT_ID,
};
}
return config;
}
function buildThemeConfig(): SPAThemeConfig {
return {
cdnUseGlobal: appEnv.CDN_USE_GLOBAL,
customFontFamily: appEnv.CUSTOM_FONT_FAMILY,
customFontURL: appEnv.CUSTOM_FONT_URL,
};
}
function buildClientEnv(): SPAClientEnv {
return {
marketBaseUrl: appEnv.NEXT_PUBLIC_MARKET_BASE_URL,
pyodideIndexUrl: pythonEnv.NEXT_PUBLIC_PYODIDE_INDEX_URL,
pyodidePipIndexUrl: pythonEnv.NEXT_PUBLIC_PYODIDE_PIP_INDEX_URL,
s3FilePath: fileEnv.NEXT_PUBLIC_S3_FILE_PATH,
};
}
export async function GET(request: NextRequest) {
const ua = request.headers.get('user-agent') || '';
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
const cookieLocale = request.cookies.get(LOBE_LOCALE_COOKIE)?.value;
const browserLanguage = parseBrowserLanguage(request.headers, DEFAULT_LANG);
const locale = cookieLocale || browserLanguage;
const serverConfig = await getServerGlobalConfig();
const featureFlags = getServerFeatureFlagsValue();
const analyticsConfig = buildAnalyticsConfig();
const theme = buildThemeConfig();
const clientEnv = buildClientEnv();
const spaConfig: SPAServerConfig = {
analyticsConfig,
clientEnv,
config: serverConfig,
featureFlags,
isMobile,
locale,
theme,
};
const dir = isRtlLang(locale) ? 'rtl' : 'ltr';
let html = await getTemplate(isMobile);
html = html.replace(
/window\.__SERVER_CONFIG__\s*=\s*undefined;\s*\/\*\s*SERVER_CONFIG\s*\*\//,
`window.__SERVER_CONFIG__ = ${serializeForHtml(spaConfig)};`,
);
html = html.replace('<!--LOCALE-->', locale);
html = html.replace('<!--DIR-->', dir);
html = html.replace('<!--SEO_META-->', '');
html = html.replace('<!--ANALYTICS_SCRIPTS-->', '');
return new Response(html, {
headers: {
'cache-control': 'private, no-cache',
'content-type': 'text/html; charset=utf-8',
'vary': 'Accept-Language, User-Agent, Cookie',
},
});
}
-18
View File
@@ -1,18 +0,0 @@
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import { type FC, type PropsWithChildren } from 'react';
import ClientOnly from '@/components/client/ClientOnly';
import AuthContainer from './_layout';
const AuthLayout: FC<PropsWithChildren> = ({ children }) => {
return (
<ClientOnly>
<NuqsAdapter>
<AuthContainer>{children}</AuthContainer>
</NuqsAdapter>
</ClientOnly>
);
};
export default AuthLayout;
@@ -1,2 +0,0 @@
// Re-export from chat version to avoid code duplication
export { default } from '@/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert';
-3
View File
@@ -1,3 +0,0 @@
'use client';
export { default } from '@/components/Error';
-3
View File
@@ -1,3 +0,0 @@
import { enableMapSet } from 'immer';
enableMapSet();
-162
View File
@@ -1,162 +0,0 @@
import './initialize';
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { type ResolvingViewport } from 'next';
import Script from 'next/script';
import { type ReactNode } from 'react';
import { Suspense } from 'react';
import { isRtlLang } from 'rtl-detect';
import BusinessGlobalProvider from '@/business/client/BusinessGlobalProvider';
import Analytics from '@/components/Analytics';
import { DEFAULT_LANG } from '@/const/locale';
import { isDesktop } from '@/const/version';
import AuthProvider from '@/layout/AuthProvider';
import GlobalProvider from '@/layout/GlobalProvider';
import { type Locales } from '@/locales/resources';
import { type DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
const inVercel = process.env.VERCEL === '1';
export interface RootLayoutProps extends DynamicLayoutProps {
children: ReactNode;
}
const RootLayout = async ({ children, params }: RootLayoutProps) => {
const { variants } = await params;
const { locale, isMobile, primaryColor, neutralColor } =
RouteVariants.deserializeVariants(variants);
const direction = isRtlLang(locale) ? 'rtl' : 'ltr';
const renderContent = () => {
return (
<GlobalProvider
isMobile={isMobile}
locale={locale}
neutralColor={neutralColor}
primaryColor={primaryColor}
variants={variants}
>
<AuthProvider>{children}</AuthProvider>
</GlobalProvider>
);
};
return (
<html suppressHydrationWarning dir={direction} lang={locale}>
<head>
<script dangerouslySetInnerHTML={{ __html: `(${outdateBrowserScript.toString()})();` }} />
{process.env.DEBUG_REACT_SCAN === '1' && (
<Script
crossOrigin={'anonymous'}
src={'https://unpkg.com/react-scan/dist/auto.global.js'}
strategy={'lazyOnload'}
/>
)}
</head>
<body>
{ENABLE_BUSINESS_FEATURES ? (
<BusinessGlobalProvider>{renderContent()}</BusinessGlobalProvider>
) : (
renderContent()
)}
<Suspense fallback={null}>
<Analytics />
{inVercel && <SpeedInsights />}
</Suspense>
</body>
</html>
);
};
function outdateBrowserScript() {
function supportsImportMaps(): boolean {
return (
typeof HTMLScriptElement !== 'undefined' &&
typeof (HTMLScriptElement as any).supports === 'function' &&
(HTMLScriptElement as any).supports('importmap')
);
}
function supportsCascadeLayers(): boolean {
if (typeof document === 'undefined') return false;
const el = document.createElement('div');
el.className = '__layer_test__';
el.style.position = 'absolute';
el.style.left = '-99999px';
el.style.top = '-99999px';
const style = document.createElement('style');
style.textContent = `
@layer a, b;
@layer a { .__layer_test__ { color: rgb(1, 2, 3); } }
@layer b { .__layer_test__ { color: rgb(4, 5, 6); } }
`;
document.documentElement.append(style);
document.documentElement.append(el);
const color = getComputedStyle(el).color;
el.remove();
style.remove();
return color === 'rgb(4, 5, 6)';
}
const isOutdateBrowser = !(supportsImportMaps() && supportsCascadeLayers());
if (isOutdateBrowser) {
window.location.href = '/not-compatible.html';
return true;
}
return false;
}
export default RootLayout;
export { generateMetadata } from './metadata';
export const generateViewport = async (props: DynamicLayoutProps): ResolvingViewport => {
const isMobile = await RouteVariants.getIsMobile(props);
const dynamicScale = isMobile ? { maximumScale: 1, userScalable: false } : {};
return {
...dynamicScale,
colorScheme: null,
initialScale: 1,
minimumScale: 1,
themeColor: [
{ color: '#f8f8f8', media: '(prefers-color-scheme: light)' },
{ color: '#000', media: '(prefers-color-scheme: dark)' },
],
viewportFit: 'cover',
width: 'device-width',
};
};
export const generateStaticParams = () => {
const mobileOptions = isDesktop ? [false] : [true, false];
// only static for several page, other go to dynamic
const staticLocales: Locales[] = [DEFAULT_LANG, 'zh-CN'];
const variants: { variants: string }[] = [];
for (const locale of staticLocales) {
for (const isMobile of mobileOptions) {
variants.push({
variants: RouteVariants.serializeVariants({
isMobile,
locale,
}),
});
}
}
return variants;
};
-3
View File
@@ -1,3 +0,0 @@
import Loading from '@/components/Loading/BrandTextLoading';
export default () => <Loading debugId="Variants" />;

Some files were not shown because too many files have changed in this diff Show More