mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
✨ feat(desktop): support cloud desktop builds (#14498)
* ✨ feat(desktop): support cloud desktop builds * 🐛 fix: open payment navigations externally in desktop
This commit is contained in:
@@ -5,6 +5,18 @@ inputs:
|
||||
node-version:
|
||||
description: Node.js version
|
||||
required: true
|
||||
cloud-repository:
|
||||
description: Cloud repository to overlay for commercial desktop builds
|
||||
required: false
|
||||
default: lobehub/lobehub-cloud
|
||||
cloud-ref:
|
||||
description: Optional Cloud repository ref
|
||||
required: false
|
||||
default: ''
|
||||
cloud-token:
|
||||
description: GitHub token with permission to read the Cloud repository
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -14,9 +26,77 @@ runs:
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Overlay Cloud repository for desktop build
|
||||
if: inputs.cloud-token != ''
|
||||
shell: bash
|
||||
env:
|
||||
CLOUD_CHECKOUT: ${{ runner.temp }}/lobehub-cloud
|
||||
CLOUD_REF: ${{ inputs.cloud-ref }}
|
||||
CLOUD_REPOSITORY: ${{ inputs.cloud-repository }}
|
||||
CLOUD_ROOT: ${{ github.workspace }}/..
|
||||
CLOUD_TOKEN: ${{ inputs.cloud-token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cloud_root="$(cd "$GITHUB_WORKSPACE/.." && pwd)"
|
||||
cloud_checkout="$RUNNER_TEMP/lobehub-cloud"
|
||||
|
||||
rm -rf "$cloud_checkout"
|
||||
|
||||
clone_args=(--depth 1)
|
||||
if [ -n "$CLOUD_REF" ]; then
|
||||
clone_args+=(--branch "$CLOUD_REF")
|
||||
fi
|
||||
|
||||
git clone "${clone_args[@]}" "https://x-access-token:${CLOUD_TOKEN}@github.com/${CLOUD_REPOSITORY}.git" "$cloud_checkout"
|
||||
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const source = process.env.CLOUD_CHECKOUT;
|
||||
const target = process.env.CLOUD_ROOT;
|
||||
const skip = new Set(['.git', 'lobehub', 'node_modules']);
|
||||
|
||||
const copy = (from, to) => {
|
||||
const stat = fs.lstatSync(from);
|
||||
if (stat.isSymbolicLink()) {
|
||||
const link = fs.readlinkSync(from);
|
||||
fs.rmSync(to, { force: true, recursive: true });
|
||||
fs.symlinkSync(link, to);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(to, { recursive: true });
|
||||
for (const entry of fs.readdirSync(from)) {
|
||||
if (skip.has(entry)) continue;
|
||||
copy(path.join(from, entry), path.join(to, entry));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(to), { recursive: true });
|
||||
fs.copyFileSync(from, to);
|
||||
};
|
||||
|
||||
for (const entry of fs.readdirSync(source)) {
|
||||
if (skip.has(entry)) continue;
|
||||
copy(path.join(source, entry), path.join(target, entry));
|
||||
}
|
||||
NODE
|
||||
|
||||
echo "CLOUD_DESKTOP=1" >> "$GITHUB_ENV"
|
||||
echo "✅ Cloud repository overlaid at $cloud_root"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install --node-linker=hoisted
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${CLOUD_DESKTOP:-}" = "1" ]; then
|
||||
cd ..
|
||||
fi
|
||||
pnpm install --node-linker=hoisted
|
||||
|
||||
# 移除国内 electron 镜像配置,GitHub Actions 使用官方源更快
|
||||
- name: Remove China electron mirror from .npmrc
|
||||
@@ -31,4 +111,11 @@ runs:
|
||||
|
||||
- name: Install deps on Desktop
|
||||
shell: bash
|
||||
run: npm run install-isolated --prefix=./apps/desktop
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${CLOUD_DESKTOP:-}" = "1" ]; then
|
||||
cd ..
|
||||
npm run install-isolated --prefix=./lobehub/apps/desktop
|
||||
else
|
||||
npm run install-isolated --prefix=./apps/desktop
|
||||
fi
|
||||
|
||||
@@ -104,6 +104,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set package version
|
||||
@@ -172,6 +173,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set package version
|
||||
@@ -216,6 +218,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set package version
|
||||
|
||||
@@ -92,6 +92,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: 24.11.1
|
||||
|
||||
# 设置 package.json 的版本号
|
||||
|
||||
@@ -87,6 +87,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set package version
|
||||
|
||||
@@ -223,6 +223,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set package version
|
||||
|
||||
@@ -180,6 +180,7 @@ jobs:
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
cloud-token: ${{ secrets.LOBEHUB_CLOUD_TOKEN }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set package version
|
||||
|
||||
@@ -6,6 +6,7 @@ import dotenv from 'dotenv';
|
||||
import { defineConfig } from 'electron-vite';
|
||||
import type { PluginOption, ViteDevServer } from 'vite';
|
||||
import { loadEnv } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
import {
|
||||
sharedOptimizeDeps,
|
||||
@@ -88,10 +89,112 @@ function electronDesktopHtmlPlugin(): PluginOption {
|
||||
};
|
||||
}
|
||||
|
||||
const CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG = '__LOBECLOUD_DESKTOP_BUSINESS_FEATURES__';
|
||||
const BUSINESS_CONST_MODULE_ID = '@lobechat/business-const';
|
||||
const CLOUD_BUSINESS_CONST_MODULE_ID = '@cloud/business-const';
|
||||
const DYNAMIC_BUSINESS_CONST_QUERY = '?lobe-cloud-desktop-business-const';
|
||||
|
||||
const createBusinessFeaturesBootstrapScript = () =>
|
||||
`globalThis[${JSON.stringify(CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG)}] = true;`;
|
||||
|
||||
const replaceBusinessFlagExport = (code: string, name: string, initializer: string) => {
|
||||
const pattern = new RegExp(`export\\s+(?:const|let|var)\\s+${name}\\s*=\\s*[\\s\\S]*?;`);
|
||||
|
||||
return {
|
||||
code: code.replace(pattern, `export let ${name} = ${initializer};`),
|
||||
replaced: pattern.test(code),
|
||||
};
|
||||
};
|
||||
|
||||
const injectDynamicBusinessFeatureFlag = (code: string) => {
|
||||
const businessFlag = replaceBusinessFlagExport(
|
||||
code,
|
||||
'ENABLE_BUSINESS_FEATURES',
|
||||
`Boolean(globalThis['${CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG}'])`,
|
||||
);
|
||||
const topicLinkFlag = replaceBusinessFlagExport(
|
||||
businessFlag.code,
|
||||
'ENABLE_TOPIC_LINK_SHARE',
|
||||
'ENABLE_BUSINESS_FEATURES',
|
||||
);
|
||||
|
||||
if (!businessFlag.replaced) {
|
||||
throw new Error('Cannot find ENABLE_BUSINESS_FEATURES export in @cloud/business-const');
|
||||
}
|
||||
|
||||
const topicLinkAssignment = topicLinkFlag.replaced
|
||||
? '\n ENABLE_TOPIC_LINK_SHARE = enabled;'
|
||||
: '';
|
||||
|
||||
return `${topicLinkFlag.code}
|
||||
|
||||
const __lobeCloudDesktopBusinessFeaturesFlagKey = '${CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG}';
|
||||
const __lobeCloudDesktopApplyBusinessFeaturesFlag = (value) => {
|
||||
const enabled = Boolean(value);
|
||||
ENABLE_BUSINESS_FEATURES = enabled;${topicLinkAssignment}
|
||||
return enabled;
|
||||
};
|
||||
|
||||
const __lobeCloudDesktopExistingDescriptor = Object.getOwnPropertyDescriptor(
|
||||
globalThis,
|
||||
__lobeCloudDesktopBusinessFeaturesFlagKey,
|
||||
);
|
||||
const __lobeCloudDesktopInitialValue = __lobeCloudDesktopExistingDescriptor?.get
|
||||
? __lobeCloudDesktopExistingDescriptor.get.call(globalThis)
|
||||
: globalThis[__lobeCloudDesktopBusinessFeaturesFlagKey];
|
||||
|
||||
Object.defineProperty(globalThis, __lobeCloudDesktopBusinessFeaturesFlagKey, {
|
||||
configurable: true,
|
||||
get() {
|
||||
return ENABLE_BUSINESS_FEATURES;
|
||||
},
|
||||
set(value) {
|
||||
__lobeCloudDesktopApplyBusinessFeaturesFlag(value);
|
||||
},
|
||||
});
|
||||
|
||||
__lobeCloudDesktopApplyBusinessFeaturesFlag(__lobeCloudDesktopInitialValue);
|
||||
`;
|
||||
};
|
||||
|
||||
function cloudDesktopBusinessConstPlugin(): PluginOption {
|
||||
return {
|
||||
enforce: 'pre',
|
||||
async resolveId(id, importer) {
|
||||
if (id !== BUSINESS_CONST_MODULE_ID) return;
|
||||
|
||||
const resolved = await this.resolve(CLOUD_BUSINESS_CONST_MODULE_ID, importer, {
|
||||
skipSelf: true,
|
||||
});
|
||||
if (!resolved) throw new Error(`Cannot resolve ${CLOUD_BUSINESS_CONST_MODULE_ID}`);
|
||||
|
||||
return `${resolved.id}${DYNAMIC_BUSINESS_CONST_QUERY}`;
|
||||
},
|
||||
load(id) {
|
||||
if (!id.endsWith(DYNAMIC_BUSINESS_CONST_QUERY)) return;
|
||||
|
||||
const sourcePath = id.slice(0, -DYNAMIC_BUSINESS_CONST_QUERY.length);
|
||||
return injectDynamicBusinessFeatureFlag(readFileSync(sourcePath, 'utf8'));
|
||||
},
|
||||
name: 'lobe-cloud-desktop-business-const',
|
||||
transformIndexHtml() {
|
||||
return [
|
||||
{
|
||||
children: createBusinessFeaturesBootstrapScript(),
|
||||
injectTo: 'head-prepend',
|
||||
tag: 'script',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const ROOT_DIR = path.resolve(__dirname, '../..');
|
||||
const CLOUD_ROOT_DIR = path.resolve(__dirname, '../../..');
|
||||
const isCloudDesktopBuild = process.env.CLOUD_DESKTOP === '1';
|
||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||
|
||||
Object.assign(process.env, loadEnv(mode, ROOT_DIR, ''));
|
||||
@@ -105,8 +208,17 @@ const mainProcessRuntimeExternals = [
|
||||
...externalRuntimeModules,
|
||||
'node-mac-permissions',
|
||||
];
|
||||
const externalNavigationHosts =
|
||||
process.env.DESKTOP_EXTERNAL_NAVIGATION_HOSTS ?? (isCloudDesktopBuild ? 'stripe.com' : '');
|
||||
|
||||
console.info(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}`);
|
||||
console.info(`[electron-vite.config.ts] Cloud desktop build: ${isCloudDesktopBuild}`);
|
||||
|
||||
const cloudTsconfigPathsPlugin = () =>
|
||||
({
|
||||
...tsconfigPaths({ projects: [path.resolve(CLOUD_ROOT_DIR, 'tsconfig.json')] }),
|
||||
name: 'lobe-cloud-desktop-tsconfig-paths',
|
||||
}) satisfies PluginOption;
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
@@ -169,6 +281,7 @@ export default defineConfig({
|
||||
sourcemap: isDev ? 'inline' : false,
|
||||
},
|
||||
define: {
|
||||
'process.env.DESKTOP_EXTERNAL_NAVIGATION_HOSTS': JSON.stringify(externalNavigationHosts),
|
||||
'process.env.UPDATE_CHANNEL': JSON.stringify(process.env.UPDATE_CHANNEL),
|
||||
'process.env.UPDATE_SERVER_URL': JSON.stringify(process.env.UPDATE_SERVER_URL),
|
||||
},
|
||||
@@ -214,6 +327,8 @@ export default defineConfig({
|
||||
},
|
||||
optimizeDeps: sharedOptimizeDeps,
|
||||
plugins: [
|
||||
isCloudDesktopBuild && cloudTsconfigPathsPlugin(),
|
||||
isCloudDesktopBuild && cloudDesktopBusinessConstPlugin(),
|
||||
forceAbsoluteBasePlugin(),
|
||||
electronDesktopHtmlPlugin(),
|
||||
vanillaExtractPlugin(),
|
||||
@@ -221,7 +336,7 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom'],
|
||||
tsconfigPaths: true,
|
||||
tsconfigPaths: !isCloudDesktopBuild,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getDesktopEnv } from '@/env';
|
||||
export const isDev = electronIs.dev();
|
||||
|
||||
export const OFFICIAL_CLOUD_SERVER = getDesktopEnv().OFFICIAL_CLOUD_SERVER;
|
||||
export const DESKTOP_EXTERNAL_NAVIGATION_HOSTS = getDesktopEnv().DESKTOP_EXTERNAL_NAVIGATION_HOSTS;
|
||||
|
||||
export const isMac = electronIs.macOS();
|
||||
export const isWindows = electronIs.windows();
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { BrowserWindowConstructorOptions } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain, screen, session as electronSession, shell } from 'electron';
|
||||
|
||||
import { preloadDir, resourcesDir } from '@/const/dir';
|
||||
import { isMac } from '@/const/env';
|
||||
import { DESKTOP_EXTERNAL_NAVIGATION_HOSTS, isMac } from '@/const/env';
|
||||
import { ELECTRON_BE_PROTOCOL_SCHEME } from '@/const/protocol';
|
||||
import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
|
||||
import { backendProxyProtocolManager } from '@/core/infrastructure/BackendProxyProtocolManager';
|
||||
@@ -20,6 +20,31 @@ import { WindowThemeManager } from './WindowThemeManager';
|
||||
|
||||
const logger = createLogger('core:Browser');
|
||||
|
||||
const getExternalNavigationHosts = () =>
|
||||
DESKTOP_EXTERNAL_NAVIGATION_HOSTS.split(',')
|
||||
.map((host) => host.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
const shouldOpenTopLevelNavigationExternally = (rawUrl: string) => {
|
||||
const externalNavigationHosts = getExternalNavigationHosts();
|
||||
if (externalNavigationHosts.length === 0) return false;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(rawUrl);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
||||
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
|
||||
return externalNavigationHosts.some(
|
||||
(externalHost) => hostname === externalHost || hostname.endsWith(`.${externalHost}`),
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
|
||||
@@ -195,10 +220,26 @@ export default class Browser {
|
||||
this.setupReadyToShowListener(browserWindow);
|
||||
this.setupCloseListener(browserWindow);
|
||||
this.setupFocusListener(browserWindow);
|
||||
this.setupTopLevelNavigationListener(browserWindow);
|
||||
this.setupWillPreventUnloadListener(browserWindow);
|
||||
this.setupContextMenu(browserWindow);
|
||||
}
|
||||
|
||||
private setupTopLevelNavigationListener(browserWindow: BrowserWindow): void {
|
||||
logger.debug(`[${this.identifier}] Setting up top-level navigation listener.`);
|
||||
|
||||
browserWindow.webContents.on('will-navigate', (event, url) => {
|
||||
if (!shouldOpenTopLevelNavigationExternally(url)) return;
|
||||
|
||||
logger.info(`[${this.identifier}] Opening top-level navigation externally: ${url}`);
|
||||
event.preventDefault();
|
||||
|
||||
shell.openExternal(url).catch((error) => {
|
||||
logger.error(`[${this.identifier}] Failed to open external navigation URL: ${url}`, error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup window open handler to intercept external links
|
||||
* Prevents opening new windows in renderer and uses system browser instead
|
||||
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
mockBrowserWindow,
|
||||
mockNativeTheme,
|
||||
mockIpcMain,
|
||||
mockShell,
|
||||
mockScreen,
|
||||
MockBrowserWindow,
|
||||
mockEnv,
|
||||
@@ -64,6 +65,7 @@ const {
|
||||
MockBrowserWindow: vi.fn().mockImplementation(() => mockBrowserWindow),
|
||||
mockBrowserWindow,
|
||||
mockEnv: {
|
||||
externalNavigationHosts: '',
|
||||
isDev: false,
|
||||
isLinux: false,
|
||||
isMac: false,
|
||||
@@ -91,6 +93,9 @@ const {
|
||||
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
|
||||
}),
|
||||
},
|
||||
mockShell: {
|
||||
openExternal: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -101,6 +106,7 @@ vi.mock('electron', () => ({
|
||||
ipcMain: mockIpcMain,
|
||||
nativeTheme: mockNativeTheme,
|
||||
screen: mockScreen,
|
||||
shell: mockShell,
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
@@ -124,6 +130,9 @@ vi.mock('@/const/env', () => ({
|
||||
get isDev() {
|
||||
return mockEnv.isDev;
|
||||
},
|
||||
get DESKTOP_EXTERNAL_NAVIGATION_HOSTS() {
|
||||
return mockEnv.externalNavigationHosts;
|
||||
},
|
||||
get isLinux() {
|
||||
return mockEnv.isLinux;
|
||||
},
|
||||
@@ -182,6 +191,7 @@ describe('Browser', () => {
|
||||
mockEnv.isMac = false;
|
||||
mockEnv.isMacTahoe = false;
|
||||
mockEnv.isWindows = true;
|
||||
mockEnv.externalNavigationHosts = '';
|
||||
|
||||
// Create mock App
|
||||
mockStoreManagerGet = vi.fn().mockReturnValue(undefined);
|
||||
@@ -730,4 +740,38 @@ describe('Browser', () => {
|
||||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('top-level navigation handling', () => {
|
||||
let willNavigateHandler: (event: any, url: string) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
willNavigateHandler = mockBrowserWindow.webContents.on.mock.calls.find(
|
||||
(call) => call[0] === 'will-navigate',
|
||||
)?.[1];
|
||||
});
|
||||
|
||||
it('should open configured external navigation hosts in system browser', () => {
|
||||
mockEnv.externalNavigationHosts = 'stripe.com';
|
||||
const mockEvent = { preventDefault: vi.fn() };
|
||||
|
||||
expect(willNavigateHandler).toBeDefined();
|
||||
willNavigateHandler(mockEvent, 'https://checkout.stripe.com/c/pay/session_id');
|
||||
|
||||
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(mockShell.openExternal).toHaveBeenCalledWith(
|
||||
'https://checkout.stripe.com/c/pay/session_id',
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow internal result routes in the app window', () => {
|
||||
mockEnv.externalNavigationHosts = 'stripe.com';
|
||||
const mockEvent = { preventDefault: vi.fn() };
|
||||
|
||||
expect(willNavigateHandler).toBeDefined();
|
||||
willNavigateHandler(mockEvent, 'http://localhost:3000/payment/upgrade-success');
|
||||
|
||||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
expect(mockShell.openExternal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,13 @@ const envNumber = (defaultValue: number) =>
|
||||
}, z.number().optional())
|
||||
.default(defaultValue);
|
||||
|
||||
const getRuntimeEnv = () => ({
|
||||
...process.env,
|
||||
DESKTOP_EXTERNAL_NAVIGATION_HOSTS: process.env.DESKTOP_EXTERNAL_NAVIGATION_HOSTS,
|
||||
UPDATE_CHANNEL: process.env.UPDATE_CHANNEL,
|
||||
UPDATE_SERVER_URL: process.env.UPDATE_SERVER_URL,
|
||||
});
|
||||
|
||||
/**
|
||||
* Desktop (Electron main process) runtime env access.
|
||||
*
|
||||
@@ -63,13 +70,15 @@ export const getDesktopEnv = memoize(() =>
|
||||
clientPrefix: 'PUBLIC_',
|
||||
emptyStringAsUndefined: true,
|
||||
isServer: true,
|
||||
runtimeEnv: process.env,
|
||||
runtimeEnv: getRuntimeEnv(),
|
||||
server: {
|
||||
DEBUG_VERBOSE: envBoolean(false),
|
||||
|
||||
// escape hatch: allow testing static renderer in dev via env
|
||||
DESKTOP_RENDERER_STATIC: envBoolean(false),
|
||||
|
||||
DESKTOP_EXTERNAL_NAVIGATION_HOSTS: z.string().optional().default(''),
|
||||
|
||||
// Force use dev-app-update.yml even in packaged app (for testing updates)
|
||||
FORCE_DEV_UPDATE_CONFIG: envBoolean(false),
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@ import { type GlobalRuntimeConfig } from '@/types/serverConfig';
|
||||
import { type ServerConfigStore } from './store';
|
||||
|
||||
const FETCH_SERVER_CONFIG_KEY = 'FETCH_SERVER_CONFIG';
|
||||
const CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG = '__LOBECLOUD_DESKTOP_BUSINESS_FEATURES__';
|
||||
|
||||
const setDesktopBusinessFeaturesFlag = (enableBusinessFeatures: boolean | undefined) => {
|
||||
(globalThis as unknown as Record<string, boolean | undefined>)[
|
||||
CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG
|
||||
] = Boolean(enableBusinessFeatures);
|
||||
};
|
||||
|
||||
type Setter = StoreSetter<ServerConfigStore>;
|
||||
export const createServerConfigSlice = (
|
||||
@@ -31,9 +38,11 @@ export class ServerConfigActionImpl {
|
||||
() => globalService.getGlobalConfig(),
|
||||
{
|
||||
onError: () => {
|
||||
setDesktopBusinessFeaturesFlag(false);
|
||||
this.#set({ serverConfigInit: true }, false, 'initServerConfigFallback');
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setDesktopBusinessFeaturesFlag(data.serverConfig.enableBusinessFeatures);
|
||||
this.#set(
|
||||
{
|
||||
billboard: data.billboard ?? null,
|
||||
|
||||
Reference in New Issue
Block a user