mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
👷 build: add desktop workflow and pre-merge desktop code (#7361)
* add desktop workflow code * add desktop relative code * Update pr-release-body.js
This commit is contained in:
7
.env.desktop
Normal file
7
.env.desktop
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# copy this file to .env when you want to develop the desktop app or you will fail
|
||||||
|
APP_URL=http://localhost:3015
|
||||||
|
FEATURE_FLAGS=+pin_list
|
||||||
|
KEY_VAULTS_SECRET=oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=
|
||||||
|
DATABASE_URL=postgresql://postgres@localhost:5432/postgres
|
||||||
|
DEFAULT_AGENT_CONFIG="model=qwen2.5;provider=ollama;chatConfig.searchFCModel.provider=ollama;chatConfig.searchFCModel.model=qwen2.5"
|
||||||
|
SYSTEM_AGENT="default=ollama/qwen2.5"
|
||||||
3
.github/scripts/pr-release-body.js
vendored
3
.github/scripts/pr-release-body.js
vendored
@@ -9,8 +9,10 @@ module.exports = ({ version, prNumber, branch }) => {
|
|||||||
## PR Build Information
|
## PR Build Information
|
||||||
|
|
||||||
**Version**: \`${version}\`
|
**Version**: \`${version}\`
|
||||||
|
**Release Time**: \`${new Date().toISOString()}\`
|
||||||
**PR**: [#${prNumber}](${prLink})
|
**PR**: [#${prNumber}](${prLink})
|
||||||
|
|
||||||
|
|
||||||
## ⚠️ Important Notice
|
## ⚠️ Important Notice
|
||||||
|
|
||||||
This is a **development build** specifically created for testing purposes. Please note:
|
This is a **development build** specifically created for testing purposes. Please note:
|
||||||
@@ -35,6 +37,7 @@ Please report any issues found in this build directly in the PR discussion.
|
|||||||
## PR 构建信息
|
## PR 构建信息
|
||||||
|
|
||||||
**版本**: \`${version}\`
|
**版本**: \`${version}\`
|
||||||
|
**发布时间**: \`${new Date().toISOString()}\`
|
||||||
**PR**: [#${prNumber}](${prLink})
|
**PR**: [#${prNumber}](${prLink})
|
||||||
|
|
||||||
## ⚠️ 重要提示
|
## ⚠️ 重要提示
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -35,7 +35,8 @@
|
|||||||
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
||||||
"build:analyze": "ANALYZE=true next build",
|
"build:analyze": "ANALYZE=true next build",
|
||||||
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
||||||
"build:electron": "NODE_OPTIONS=--max-old-space-size=6144 NEXT_PUBLIC_IS_DESKTOP_APP=1 next build ",
|
"prebuild:electron": "cross-env NEXT_PUBLIC_IS_DESKTOP_APP=1 tsx scripts/prebuild.mts",
|
||||||
|
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=6144 NEXT_PUBLIC_IS_DESKTOP_APP=1 NEXT_PUBLIC_SERVICE_MODE=server next build",
|
||||||
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
|
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
|
||||||
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
||||||
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
||||||
@@ -44,7 +45,12 @@
|
|||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:visualize": "dbdocs build docs/development/database-schema.dbml --project lobe-chat",
|
"db:visualize": "dbdocs build docs/development/database-schema.dbml --project lobe-chat",
|
||||||
"db:z-pull": "drizzle-kit introspect",
|
"db:z-pull": "drizzle-kit introspect",
|
||||||
|
"desktop:build": "npm run desktop:build-next && npm run desktop:prepare-dist && npm run desktop:build-electron",
|
||||||
|
"desktop:build-electron": "tsx scripts/electronWorkflow/buildElectron.ts",
|
||||||
|
"desktop:build-next": "npm run build:electron",
|
||||||
|
"desktop:prepare-dist": "tsx scripts/electronWorkflow/moveNextStandalone.ts",
|
||||||
"dev": "next dev --turbopack -p 3010",
|
"dev": "next dev --turbopack -p 3010",
|
||||||
|
"dev:desktop": "next dev --turbopack -p 3015",
|
||||||
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx",
|
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx",
|
||||||
"docs:seo": "lobe-seo && npm run lint:mdx",
|
"docs:seo": "lobe-seo && npm run lint:mdx",
|
||||||
"i18n": "npm run workflow:i18n && lobe-i18n",
|
"i18n": "npm run workflow:i18n && lobe-i18n",
|
||||||
@@ -78,7 +84,8 @@
|
|||||||
"workflow:docs": "tsx ./scripts/docsWorkflow/index.ts",
|
"workflow:docs": "tsx ./scripts/docsWorkflow/index.ts",
|
||||||
"workflow:i18n": "tsx ./scripts/i18nWorkflow/index.ts",
|
"workflow:i18n": "tsx ./scripts/i18nWorkflow/index.ts",
|
||||||
"workflow:mdx": "tsx ./scripts/mdxWorkflow/index.ts",
|
"workflow:mdx": "tsx ./scripts/mdxWorkflow/index.ts",
|
||||||
"workflow:readme": "tsx ./scripts/readmeWorkflow/index.ts"
|
"workflow:readme": "tsx ./scripts/readmeWorkflow/index.ts",
|
||||||
|
"workflow:set-desktop-version": "tsx ./scripts/electronWorkflow/setDesktopVersion.ts"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.md": [
|
"*.md": [
|
||||||
@@ -293,6 +300,7 @@
|
|||||||
"ajv-keywords": "^5.1.0",
|
"ajv-keywords": "^5.1.0",
|
||||||
"commitlint": "^19.8.0",
|
"commitlint": "^19.8.0",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dbdocs": "^0.14.3",
|
"dbdocs": "^0.14.3",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
|||||||
5
packages/electron-client-ipc/src/events/file.ts
Normal file
5
packages/electron-client-ipc/src/events/file.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { FileMetadata, UploadFileParams } from '../types';
|
||||||
|
|
||||||
|
export interface FilesDispatchEvents {
|
||||||
|
createFile: (params: UploadFileParams) => { metadata: FileMetadata; success: boolean };
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { FilesDispatchEvents } from './file';
|
||||||
import { MenuDispatchEvents } from './menu';
|
import { MenuDispatchEvents } from './menu';
|
||||||
import { FilesSearchDispatchEvents } from './search';
|
import { FilesSearchDispatchEvents } from './search';
|
||||||
|
import { ShortcutDispatchEvents } from './shortcut';
|
||||||
import { SystemDispatchEvents } from './system';
|
import { SystemDispatchEvents } from './system';
|
||||||
|
import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
|
||||||
import { WindowsDispatchEvents } from './windows';
|
import { WindowsDispatchEvents } from './windows';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,10 +14,25 @@ export interface ClientDispatchEvents
|
|||||||
extends WindowsDispatchEvents,
|
extends WindowsDispatchEvents,
|
||||||
FilesSearchDispatchEvents,
|
FilesSearchDispatchEvents,
|
||||||
SystemDispatchEvents,
|
SystemDispatchEvents,
|
||||||
MenuDispatchEvents {}
|
MenuDispatchEvents,
|
||||||
|
FilesDispatchEvents,
|
||||||
|
AutoUpdateDispatchEvents,
|
||||||
|
ShortcutDispatchEvents {}
|
||||||
|
|
||||||
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
||||||
|
|
||||||
export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType<
|
export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType<
|
||||||
ClientDispatchEvents[T]
|
ClientDispatchEvents[T]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main -> render broadcast events
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface MainBroadcastEvents extends AutoUpdateBroadcastEvents {}
|
||||||
|
|
||||||
|
export type MainBroadcastEventKey = keyof MainBroadcastEvents;
|
||||||
|
|
||||||
|
export type MainBroadcastParams<T extends MainBroadcastEventKey> = Parameters<
|
||||||
|
MainBroadcastEvents[T]
|
||||||
|
>[0];
|
||||||
|
|||||||
4
packages/electron-client-ipc/src/events/shortcut.ts
Normal file
4
packages/electron-client-ipc/src/events/shortcut.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ShortcutDispatchEvents {
|
||||||
|
getShortcutsConfig: () => Record<string, string>;
|
||||||
|
updateShortcutConfig: (id: string, accelerator: string) => boolean;
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
export interface SystemDispatchEvents {
|
export interface SystemDispatchEvents {
|
||||||
checkSystemAccessibility: () => boolean | undefined;
|
checkSystemAccessibility: () => boolean | undefined;
|
||||||
|
/**
|
||||||
|
* 更新应用语言设置
|
||||||
|
* @param locale 语言设置
|
||||||
|
*/
|
||||||
|
updateLocale: (locale: string) => { success: boolean };
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/electron-client-ipc/src/events/update.ts
Normal file
20
packages/electron-client-ipc/src/events/update.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ProgressInfo, UpdateInfo } from '../types';
|
||||||
|
|
||||||
|
export interface AutoUpdateDispatchEvents {
|
||||||
|
checkUpdate: () => void;
|
||||||
|
downloadUpdate: () => void;
|
||||||
|
installLater: () => void;
|
||||||
|
installNow: () => void;
|
||||||
|
installUpdate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoUpdateBroadcastEvents {
|
||||||
|
updateAvailable: (info: UpdateInfo) => void;
|
||||||
|
updateCheckStart: () => void;
|
||||||
|
updateDownloadProgress: (progress: ProgressInfo) => void;
|
||||||
|
updateDownloadStart: () => void;
|
||||||
|
updateDownloaded: (info: UpdateInfo) => void;
|
||||||
|
updateError: (message: string) => void;
|
||||||
|
updateNotAvailable: (info: UpdateInfo) => void;
|
||||||
|
updateWillInstallLater: () => void;
|
||||||
|
}
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
|
import { InterceptRouteParams, InterceptRouteResponse } from '../types/route';
|
||||||
|
|
||||||
export interface WindowsDispatchEvents {
|
export interface WindowsDispatchEvents {
|
||||||
|
/**
|
||||||
|
* 拦截客户端路由导航请求
|
||||||
|
* @param params 包含路径和来源信息的参数对象
|
||||||
|
* @returns 路由拦截结果
|
||||||
|
*/
|
||||||
|
interceptRoute: (params: InterceptRouteParams) => InterceptRouteResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* open the LobeHub Devtools
|
* open the LobeHub Devtools
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './dispatch';
|
export * from './dispatch';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './useWatchBroadcast';
|
||||||
|
|||||||
14
packages/electron-client-ipc/src/types/file.ts
Normal file
14
packages/electron-client-ipc/src/types/file.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface UploadFileParams {
|
||||||
|
content: ArrayBuffer;
|
||||||
|
filename: string;
|
||||||
|
hash: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileMetadata {
|
||||||
|
date: string;
|
||||||
|
dirname: string;
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
@@ -1 +1,5 @@
|
|||||||
export * from './dispatch';
|
export * from './dispatch';
|
||||||
|
export * from './file';
|
||||||
|
export * from './route';
|
||||||
|
export * from './shortcut';
|
||||||
|
export * from './update';
|
||||||
|
|||||||
46
packages/electron-client-ipc/src/types/route.ts
Normal file
46
packages/electron-client-ipc/src/types/route.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export interface InterceptRouteParams {
|
||||||
|
/**
|
||||||
|
* 请求路径
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* 来源类型:'link-click', 'push-state', 'replace-state'
|
||||||
|
*/
|
||||||
|
source: 'link-click' | 'push-state' | 'replace-state';
|
||||||
|
/**
|
||||||
|
* 完整URL
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterceptRouteResponse {
|
||||||
|
/**
|
||||||
|
* 错误信息 (如果有)
|
||||||
|
*/
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已拦截
|
||||||
|
*/
|
||||||
|
intercepted: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始路径
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始来源
|
||||||
|
*/
|
||||||
|
source: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子路径 (如果有)
|
||||||
|
*/
|
||||||
|
subPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标窗口标识符
|
||||||
|
*/
|
||||||
|
targetWindow?: string;
|
||||||
|
}
|
||||||
11
packages/electron-client-ipc/src/types/shortcut.ts
Normal file
11
packages/electron-client-ipc/src/types/shortcut.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface ShortcutConfig {
|
||||||
|
/**
|
||||||
|
* 快捷键加速器(如 CommandOrControl+E)
|
||||||
|
*/
|
||||||
|
accelerator: string;
|
||||||
|
/**
|
||||||
|
* 快捷键 ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
export type ShortcutActionType = Record<string, any>;
|
||||||
23
packages/electron-client-ipc/src/types/update.ts
Normal file
23
packages/electron-client-ipc/src/types/update.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface ReleaseNoteInfo {
|
||||||
|
/**
|
||||||
|
* The note.
|
||||||
|
*/
|
||||||
|
note: string | null;
|
||||||
|
/**
|
||||||
|
* The version.
|
||||||
|
*/
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressInfo {
|
||||||
|
bytesPerSecond: number;
|
||||||
|
percent: number;
|
||||||
|
total: number;
|
||||||
|
transferred: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateInfo {
|
||||||
|
releaseDate: string;
|
||||||
|
releaseNotes?: string | ReleaseNoteInfo[];
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
37
packages/electron-client-ipc/src/useWatchBroadcast.ts
Normal file
37
packages/electron-client-ipc/src/useWatchBroadcast.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { MainBroadcastEventKey, MainBroadcastParams } from './events';
|
||||||
|
|
||||||
|
interface ElectronAPI {
|
||||||
|
ipcRenderer: {
|
||||||
|
on: (event: MainBroadcastEventKey, listener: (e: any, data: any) => void) => void;
|
||||||
|
removeListener: (event: MainBroadcastEventKey, listener: (e: any, data: any) => void) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWatchBroadcast = <T extends MainBroadcastEventKey>(
|
||||||
|
event: T,
|
||||||
|
handler: (data: MainBroadcastParams<T>) => void,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.electron) return;
|
||||||
|
|
||||||
|
const listener = (e: any, data: MainBroadcastParams<T>) => {
|
||||||
|
handler(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on(event, listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.electron.ipcRenderer.removeListener(event, listener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
4
packages/electron-server-ipc/src/events/database.ts
Normal file
4
packages/electron-server-ipc/src/events/database.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface DatabaseDispatchEvents {
|
||||||
|
getDatabaseSchemaHash: () => string | undefined;
|
||||||
|
setDatabaseSchemaHash: (hash: string) => void;
|
||||||
|
}
|
||||||
6
packages/electron-server-ipc/src/events/file.ts
Normal file
6
packages/electron-server-ipc/src/events/file.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { DeleteFilesResponse } from '../types/file';
|
||||||
|
|
||||||
|
export interface FileDispatchEvents {
|
||||||
|
deleteFiles: (paths: string[]) => DeleteFilesResponse;
|
||||||
|
getStaticFilePath: (id: string) => string;
|
||||||
|
}
|
||||||
29
packages/electron-server-ipc/src/events/index.ts
Normal file
29
packages/electron-server-ipc/src/events/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/* eslint-disable typescript-sort-keys/interface, sort-keys-fix/sort-keys-fix */
|
||||||
|
import { DatabaseDispatchEvents } from './database';
|
||||||
|
import { FileDispatchEvents } from './file';
|
||||||
|
import { StoragePathDispatchEvents } from './storagePath';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* next server -> main dispatch events
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface ServerDispatchEvents
|
||||||
|
extends StoragePathDispatchEvents,
|
||||||
|
DatabaseDispatchEvents,
|
||||||
|
FileDispatchEvents {}
|
||||||
|
|
||||||
|
export type ServerDispatchEventKey = keyof ServerDispatchEvents;
|
||||||
|
|
||||||
|
export type ServerEventReturnType<T extends ServerDispatchEventKey> = ReturnType<
|
||||||
|
ServerDispatchEvents[T]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ServerEventParams<T extends ServerDispatchEventKey> = Parameters<
|
||||||
|
ServerDispatchEvents[T]
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
export type IPCServerEventHandler = {
|
||||||
|
[key in ServerDispatchEventKey]: (
|
||||||
|
params: ServerEventParams<key>,
|
||||||
|
) => Promise<ServerEventReturnType<key>>;
|
||||||
|
};
|
||||||
4
packages/electron-server-ipc/src/events/storagePath.ts
Normal file
4
packages/electron-server-ipc/src/events/storagePath.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface StoragePathDispatchEvents {
|
||||||
|
getDatabasePath: () => string;
|
||||||
|
getUserDataPath: () => string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './events';
|
||||||
export * from './ipcClient';
|
export * from './ipcClient';
|
||||||
export * from './ipcServer';
|
export * from './ipcServer';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import path from 'node:path';
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { ElectronIpcClient } from './ipcClient';
|
import { ElectronIpcClient } from './ipcClient';
|
||||||
import { ElectronIPCMethods } from './types';
|
|
||||||
|
|
||||||
// Mock node modules
|
// Mock node modules
|
||||||
vi.mock('node:fs');
|
vi.mock('node:fs');
|
||||||
@@ -124,7 +123,7 @@ describe('ElectronIpcClient', () => {
|
|||||||
|
|
||||||
it('should handle connection errors', async () => {
|
it('should handle connection errors', async () => {
|
||||||
// Start request - but don't await it yet
|
// Start request - but don't await it yet
|
||||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
const requestPromise = client.sendRequest('getDatabasePath');
|
||||||
|
|
||||||
// Find the error event handler
|
// Find the error event handler
|
||||||
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
|
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
|
||||||
@@ -154,7 +153,7 @@ describe('ElectronIpcClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start request
|
// Start request
|
||||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
const requestPromise = client.sendRequest('getDatabasePath');
|
||||||
|
|
||||||
// Simulate connection established
|
// Simulate connection established
|
||||||
if (connectionCallback) connectionCallback();
|
if (connectionCallback) connectionCallback();
|
||||||
@@ -188,7 +187,7 @@ describe('ElectronIpcClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start a request to establish connection (but don't wait for it)
|
// Start a request to establish connection (but don't wait for it)
|
||||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath).catch(() => {}); // Ignore any errors
|
const requestPromise = client.sendRequest('getDatabasePath').catch(() => {}); // Ignore any errors
|
||||||
|
|
||||||
// Simulate connection
|
// Simulate connection
|
||||||
if (connectionCallback) connectionCallback();
|
if (connectionCallback) connectionCallback();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
||||||
import { IElectronIPCMethods } from './types';
|
import { ServerDispatchEventKey } from './events';
|
||||||
|
|
||||||
export class ElectronIpcClient {
|
export class ElectronIpcClient {
|
||||||
private socketPath: string | null = null;
|
private socketPath: string | null = null;
|
||||||
@@ -16,6 +16,7 @@ export class ElectronIpcClient {
|
|||||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
private connectionAttempts: number = 0;
|
private connectionAttempts: number = 0;
|
||||||
private maxConnectionAttempts: number = 5;
|
private maxConnectionAttempts: number = 5;
|
||||||
|
private dataBuffer: string = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
@@ -53,43 +54,64 @@ export class ElectronIpcClient {
|
|||||||
this.socket = net.createConnection(this.socketPath!, () => {
|
this.socket = net.createConnection(this.socketPath!, () => {
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
this.connectionAttempts = 0;
|
this.connectionAttempts = 0;
|
||||||
console.log('Connected to Electron IPC server');
|
console.log('[ElectronIpcClient] Connected to Electron IPC server');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('data', (data) => {
|
this.socket.on('data', (data) => {
|
||||||
try {
|
const dataStr = data.toString();
|
||||||
const response = JSON.parse(data.toString());
|
console.log('output:', dataStr);
|
||||||
const { id, result, error } = response;
|
|
||||||
|
|
||||||
const pending = this.requestQueue.get(id);
|
// 将新数据添加到缓冲区
|
||||||
if (pending) {
|
this.dataBuffer += dataStr;
|
||||||
this.requestQueue.delete(id);
|
|
||||||
if (error) {
|
// 按换行符分割消息
|
||||||
pending.reject(new Error(error));
|
const messages = this.dataBuffer.split('\n');
|
||||||
} else {
|
|
||||||
pending.resolve(result);
|
// 最后一个元素可能是不完整的消息,保留在缓冲区
|
||||||
|
this.dataBuffer = messages.pop() || '';
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (!message.trim()) continue; // 跳过空消息
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(message);
|
||||||
|
const { id, result, error } = response;
|
||||||
|
|
||||||
|
const pending = this.requestQueue.get(id);
|
||||||
|
if (pending) {
|
||||||
|
this.requestQueue.delete(id);
|
||||||
|
if (error) {
|
||||||
|
pending.reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
pending.resolve(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'[ElectronIpcClient] Failed to parse response:',
|
||||||
|
err,
|
||||||
|
'message:',
|
||||||
|
message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to parse response:', err);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('error', (err) => {
|
this.socket.on('error', (err) => {
|
||||||
console.error('Socket error:', err);
|
console.error('[ElectronIpcClient] Socket error:', err);
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.handleDisconnect();
|
this.handleDisconnect();
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('close', () => {
|
this.socket.on('close', () => {
|
||||||
console.log('Socket closed');
|
console.log('[ElectronIpcClient] Socket closed');
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.handleDisconnect();
|
this.handleDisconnect();
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to connect to IPC server:', err);
|
console.error('[ElectronIpcClient] Failed to connect to IPC server:', err);
|
||||||
this.handleDisconnect();
|
this.handleDisconnect();
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
@@ -104,9 +126,12 @@ export class ElectronIpcClient {
|
|||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空数据缓冲区
|
||||||
|
this.dataBuffer = '';
|
||||||
|
|
||||||
// 拒绝所有待处理的请求
|
// 拒绝所有待处理的请求
|
||||||
for (const [, { reject }] of this.requestQueue) {
|
for (const [, { reject }] of this.requestQueue) {
|
||||||
reject(new Error('Connection to Electron IPC server lost'));
|
reject(new Error('[ElectronIpcClient] Connection to Electron IPC server lost'));
|
||||||
}
|
}
|
||||||
this.requestQueue.clear();
|
this.requestQueue.clear();
|
||||||
|
|
||||||
@@ -117,16 +142,19 @@ export class ElectronIpcClient {
|
|||||||
|
|
||||||
this.reconnectTimeout = setTimeout(() => {
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
this.connect().catch((err) => {
|
this.connect().catch((err) => {
|
||||||
console.error(`Reconnection attempt ${this.connectionAttempts} failed:`, err);
|
console.error(
|
||||||
|
`[ElectronIpcClient] Reconnection attempt ${this.connectionAttempts} failed:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求到 Electron IPC 服务器
|
// 发送请求到 Electron IPC 服务器
|
||||||
public async sendRequest<T>(method: IElectronIPCMethods, params: any = {}): Promise<T> {
|
public async sendRequest<T>(method: ServerDispatchEventKey, params: any = {}): Promise<T> {
|
||||||
if (!this.socketPath) {
|
if (!this.socketPath) {
|
||||||
throw new Error('Electron IPC connection not available');
|
throw new Error('[ElectronIpcClient] Electron IPC connection not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果未连接,先连接
|
// 如果未连接,先连接
|
||||||
@@ -145,8 +173,8 @@ export class ElectronIpcClient {
|
|||||||
// 设置超时
|
// 设置超时
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.requestQueue.delete(id);
|
this.requestQueue.delete(id);
|
||||||
reject(new Error(`Request ${method} timed out`));
|
reject(new Error(`[ElectronIpcClient] Request timed out, method: ${method}`));
|
||||||
}, 10_000);
|
}, 5000);
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
this.socket!.write(JSON.stringify(request), (err) => {
|
this.socket!.write(JSON.stringify(request), (err) => {
|
||||||
|
|||||||
@@ -4,16 +4,8 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
||||||
import { IElectronIPCMethods } from './types';
|
import { ServerDispatchEventKey } from './events';
|
||||||
|
import { ElectronIPCEventHandler } from './types';
|
||||||
export type IPCEventMethod = (
|
|
||||||
params: any,
|
|
||||||
context: { id: string; method: string; socket: net.Socket },
|
|
||||||
) => Promise<any>;
|
|
||||||
|
|
||||||
export type ElectronIPCEventHandler = {
|
|
||||||
[key in IElectronIPCMethods]: IPCEventMethod;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ElectronIPCServer {
|
export class ElectronIPCServer {
|
||||||
private server: net.Server;
|
private server: net.Server;
|
||||||
@@ -45,7 +37,7 @@ export class ElectronIPCServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.server.listen(this.socketPath, () => {
|
this.server.listen(this.socketPath, () => {
|
||||||
console.log(`Electron IPC server listening on ${this.socketPath}`);
|
console.log(`[ElectronIPCServer] Electron IPC server listening on ${this.socketPath}`);
|
||||||
|
|
||||||
// 将套接字路径写入临时文件,供 Next.js 服务端读取
|
// 将套接字路径写入临时文件,供 Next.js 服务端读取
|
||||||
const tempDir = os.tmpdir();
|
const tempDir = os.tmpdir();
|
||||||
@@ -86,7 +78,7 @@ export class ElectronIPCServer {
|
|||||||
const { id, method, params } = request;
|
const { id, method, params } = request;
|
||||||
|
|
||||||
// 根据请求方法执行相应的操作
|
// 根据请求方法执行相应的操作
|
||||||
const eventHandler = this.eventHandler[method as IElectronIPCMethods];
|
const eventHandler = this.eventHandler[method as ServerDispatchEventKey];
|
||||||
if (!eventHandler) return;
|
if (!eventHandler) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -100,12 +92,12 @@ export class ElectronIPCServer {
|
|||||||
|
|
||||||
// 发送结果
|
// 发送结果
|
||||||
private sendResult(socket: net.Socket, id: string, result: any): void {
|
private sendResult(socket: net.Socket, id: string, result: any): void {
|
||||||
socket.write(JSON.stringify({ id, result }));
|
socket.write(JSON.stringify({ id, result }) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送错误
|
// 发送错误
|
||||||
private sendError(socket: net.Socket, id: string, error: string): void {
|
private sendError(socket: net.Socket, id: string, error: string): void {
|
||||||
socket.write(JSON.stringify({ error, id }));
|
socket.write(JSON.stringify({ error, id }) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭服务器
|
// 关闭服务器
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
/* eslint-disable typescript-sort-keys/interface, sort-keys-fix/sort-keys-fix */
|
|
||||||
export const ElectronIPCMethods = {
|
|
||||||
getDatabasePath: 'getDatabasePath',
|
|
||||||
getUserDataPath: 'getUserDataPath',
|
|
||||||
|
|
||||||
getDatabaseSchemaHash: 'getDatabaseSchemaHash',
|
|
||||||
setDatabaseSchemaHash: 'setDatabaseSchemaHash',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type IElectronIPCMethods = keyof typeof ElectronIPCMethods;
|
|
||||||
|
|
||||||
export interface IpcDispatchEvent {
|
|
||||||
getDatabasePath: () => Promise<string>;
|
|
||||||
getUserDataPath: () => Promise<string>;
|
|
||||||
|
|
||||||
getDatabaseSchemaHash: () => Promise<string | undefined>;
|
|
||||||
setDatabaseSchemaHash: (hash: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
4
packages/electron-server-ipc/src/types/file.ts
Normal file
4
packages/electron-server-ipc/src/types/file.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface DeleteFilesResponse {
|
||||||
|
errors?: { message: string; path: string }[];
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
@@ -1 +1,14 @@
|
|||||||
export * from './event';
|
import net from 'node:net';
|
||||||
|
|
||||||
|
import { ServerDispatchEventKey } from '../events';
|
||||||
|
|
||||||
|
export type IPCEventMethod = (
|
||||||
|
params: any,
|
||||||
|
context: { id: string; method: string; socket: net.Socket },
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
|
export type ElectronIPCEventHandler = {
|
||||||
|
[key in ServerDispatchEventKey]: IPCEventMethod;
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './file';
|
||||||
|
|||||||
52
scripts/electronWorkflow/buildElectron.ts
Normal file
52
scripts/electronWorkflow/buildElectron.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* eslint-disable unicorn/no-process-exit */
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import os from 'node:os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build desktop application based on current operating system platform
|
||||||
|
*/
|
||||||
|
const buildElectron = () => {
|
||||||
|
const platform = os.platform();
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
console.log(`🔨 Starting to build desktop app for ${platform} platform...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let buildCommand = '';
|
||||||
|
|
||||||
|
// Determine build command based on platform
|
||||||
|
switch (platform) {
|
||||||
|
case 'darwin': {
|
||||||
|
buildCommand = 'npm run build:mac --prefix=./apps/desktop';
|
||||||
|
console.log('📦 Building macOS desktop application...');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'win32': {
|
||||||
|
buildCommand = 'npm run build:win --prefix=./apps/desktop';
|
||||||
|
console.log('📦 Building Windows desktop application...');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'linux': {
|
||||||
|
buildCommand = 'npm run build:linux --prefix=./apps/desktop';
|
||||||
|
console.log('📦 Building Linux desktop application...');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported platform: ${platform}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute build command
|
||||||
|
execSync(buildCommand, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
const buildTime = ((endTime - startTime) / 1000).toFixed(2);
|
||||||
|
console.log(`✅ Desktop application build completed! (${buildTime}s)`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Build failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute build
|
||||||
|
buildElectron();
|
||||||
69
scripts/electronWorkflow/moveNextStandalone.ts
Normal file
69
scripts/electronWorkflow/moveNextStandalone.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/* eslint-disable unicorn/no-process-exit */
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, '../..');
|
||||||
|
|
||||||
|
// 定义源目录和目标目录
|
||||||
|
const sourceDir: string = path.join(rootDir, '.next/standalone');
|
||||||
|
const targetDir: string = path.join(rootDir, 'apps/desktop/dist/next');
|
||||||
|
|
||||||
|
// 向 sourceDir 写入 .env 文件
|
||||||
|
const env = fs.readFileSync(path.join(rootDir, '.env.desktop'), 'utf8');
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(sourceDir, '.env'), env, 'utf8');
|
||||||
|
console.log(`⚓️ Inject .env successful`);
|
||||||
|
|
||||||
|
// 确保目标目录的父目录存在
|
||||||
|
fs.ensureDirSync(path.dirname(targetDir));
|
||||||
|
|
||||||
|
// 如果目标目录已存在,先删除它
|
||||||
|
if (fs.existsSync(targetDir)) {
|
||||||
|
console.log(`🗑️ Target directory ${targetDir} already exists, deleting...`);
|
||||||
|
try {
|
||||||
|
fs.removeSync(targetDir);
|
||||||
|
console.log(`✅ Old target directory removed successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`⚠️ Failed to delete target directory: ${error}`);
|
||||||
|
console.log('🔄 Trying to delete using system command...');
|
||||||
|
try {
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
execSync(`rmdir /S /Q "${targetDir}"`, { stdio: 'inherit' });
|
||||||
|
} else {
|
||||||
|
execSync(`rm -rf "${targetDir}"`, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
console.log('✅ Successfully deleted old target directory');
|
||||||
|
} catch (cmdError) {
|
||||||
|
console.error(`❌ Unable to delete target directory, might need manual cleanup: ${cmdError}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚚 Moving ${sourceDir} to ${targetDir}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 fs-extra 的 move 方法
|
||||||
|
fs.moveSync(sourceDir, targetDir, { overwrite: true });
|
||||||
|
console.log(`✅ Directory moved successfully!`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ fs-extra move failed:', error);
|
||||||
|
console.log('🔄 Trying to move using system command...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用系统命令进行移动
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
execSync(`move "${sourceDir}" "${targetDir}"`, { stdio: 'inherit' });
|
||||||
|
} else {
|
||||||
|
execSync(`mv "${sourceDir}" "${targetDir}"`, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
console.log('✅ System command move completed successfully!');
|
||||||
|
} catch (mvError) {
|
||||||
|
console.error('❌ Failed to move directory:', mvError);
|
||||||
|
console.log('💡 Try running manually: sudo mv ' + sourceDir + ' ' + targetDir);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎉 Move completed!`);
|
||||||
96
scripts/electronWorkflow/setDesktopVersion.ts
Normal file
96
scripts/electronWorkflow/setDesktopVersion.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* eslint-disable unicorn/no-process-exit */
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// 获取脚本的命令行参数
|
||||||
|
const version = process.argv[2];
|
||||||
|
const isPr = process.argv[3] === 'true';
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
console.error('Missing version parameter, usage: bun run setDesktopVersion.ts <version> [isPr]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取根目录
|
||||||
|
const rootDir = path.resolve(__dirname, '../..');
|
||||||
|
|
||||||
|
// 桌面应用 package.json 的路径
|
||||||
|
const desktopPackageJsonPath = path.join(rootDir, 'apps/desktop/package.json');
|
||||||
|
|
||||||
|
// 更新应用图标
|
||||||
|
function updateAppIcon() {
|
||||||
|
try {
|
||||||
|
const buildDir = path.join(rootDir, 'apps/desktop/build');
|
||||||
|
|
||||||
|
// 定义需要处理的图标映射,考虑到大小写敏感性
|
||||||
|
const iconMappings = [
|
||||||
|
// { ext: '.ico', nightly: 'icon-nightly.ico', normal: 'icon.ico' },
|
||||||
|
{ ext: '.png', nightly: 'icon-nightly.png', normal: 'icon.png' },
|
||||||
|
{ ext: '.icns', nightly: 'Icon-nightly.icns', normal: 'Icon.icns' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 处理每种图标格式
|
||||||
|
for (const mapping of iconMappings) {
|
||||||
|
const sourceFile = path.join(buildDir, mapping.nightly);
|
||||||
|
const targetFile = path.join(buildDir, mapping.normal);
|
||||||
|
|
||||||
|
// 检查源文件是否存在
|
||||||
|
if (fs.existsSync(sourceFile)) {
|
||||||
|
// 只有当源文件和目标文件不同,才进行复制
|
||||||
|
if (sourceFile !== targetFile) {
|
||||||
|
fs.copyFileSync(sourceFile, targetFile);
|
||||||
|
console.log(`Updated app icon: ${targetFile}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Warning: Source icon not found: ${sourceFile}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating icons:', error);
|
||||||
|
// 继续处理,不终止程序
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVersion() {
|
||||||
|
try {
|
||||||
|
// 确保文件存在
|
||||||
|
if (!fs.existsSync(desktopPackageJsonPath)) {
|
||||||
|
console.error(`Error: File not found ${desktopPackageJsonPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 package.json 文件
|
||||||
|
const packageJson = fs.readJSONSync(desktopPackageJsonPath);
|
||||||
|
|
||||||
|
// 更新版本号
|
||||||
|
packageJson.version = version;
|
||||||
|
packageJson.productName = 'LobeHub';
|
||||||
|
packageJson.name = 'lobehub-desktop';
|
||||||
|
|
||||||
|
// 如果是 PR 构建,设置为 Nightly 版本
|
||||||
|
if (isPr) {
|
||||||
|
// 修改包名,添加 -nightly 后缀
|
||||||
|
if (!packageJson.name.endsWith('-nightly')) {
|
||||||
|
packageJson.name = `${packageJson.name}-nightly`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改产品名称为 LobeHub Nightly
|
||||||
|
packageJson.productName = 'LobeHub-Nightly';
|
||||||
|
|
||||||
|
console.log('🌙 Setting as Nightly version with modified package name and productName');
|
||||||
|
|
||||||
|
// 使用 nightly 图标替换常规图标
|
||||||
|
updateAppIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写回文件
|
||||||
|
fs.writeJsonSync(desktopPackageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
|
||||||
|
console.log(`Desktop app version updated to: ${version}, isPr: ${isPr}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating version:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVersion();
|
||||||
77
scripts/prebuild.mts
Normal file
77
scripts/prebuild.mts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { rm } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
// 创建需要排除的特性映射
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
const partialBuildPages = [
|
||||||
|
// no need for desktop
|
||||||
|
{
|
||||||
|
name: 'changelog',
|
||||||
|
disabled: isDesktop,
|
||||||
|
paths: ['src/app/[variants]/@modal/(.)changelog', 'src/app/[variants]/(main)/changelog'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auth',
|
||||||
|
disabled: isDesktop,
|
||||||
|
paths: ['src/app/[variants]/(auth)'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mobile',
|
||||||
|
disabled: isDesktop,
|
||||||
|
paths: ['src/app/[variants]/(main)/(mobile)'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'api-webhooks',
|
||||||
|
disabled: isDesktop,
|
||||||
|
paths: ['src/app/(backend)/api/webhooks'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// no need for web
|
||||||
|
{
|
||||||
|
name: 'desktop-devtools',
|
||||||
|
disabled: !isDesktop,
|
||||||
|
paths: ['src/app/desktop'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'desktop-trpc',
|
||||||
|
disabled: !isDesktop,
|
||||||
|
paths: ['src/app/(backend)/trpc/desktop'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的目录
|
||||||
|
*/
|
||||||
|
const removeDirectories = async () => {
|
||||||
|
// 遍历 partialBuildPages 数组
|
||||||
|
for (const page of partialBuildPages) {
|
||||||
|
// 检查是否需要禁用该功能
|
||||||
|
if (page.disabled) {
|
||||||
|
for (const dirPath of page.paths) {
|
||||||
|
const fullPath = path.resolve(process.cwd(), dirPath);
|
||||||
|
|
||||||
|
// 检查目录是否存在
|
||||||
|
if (existsSync(fullPath)) {
|
||||||
|
try {
|
||||||
|
// 递归删除目录
|
||||||
|
await rm(fullPath, { force: true, recursive: true });
|
||||||
|
console.log(`♻️ Removed ${dirPath} successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to remove directory ${dirPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行删除操作
|
||||||
|
console.log('Starting prebuild cleanup...');
|
||||||
|
await removeDirectories();
|
||||||
|
console.log('Prebuild cleanup completed.');
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { ChatFileItem } from '@/types/message';
|
import { ChatFileItem } from '@/types/message';
|
||||||
|
|
||||||
const filePrompt = (item: ChatFileItem) =>
|
const filePrompt = (item: ChatFileItem, addUrl: boolean) =>
|
||||||
`<file id="${item.id}" name="${item.name}" type="${item.fileType}" size="${item.size}" url="${item.url}"></file>`;
|
addUrl
|
||||||
|
? `<file id="${item.id}" name="${item.name}" type="${item.fileType}" size="${item.size}" url="${item.url}"></file>`
|
||||||
|
: `<file id="${item.id}" name="${item.name}" type="${item.fileType}" size="${item.size}"></file>`;
|
||||||
|
|
||||||
export const filePrompts = (fileList: ChatFileItem[]) => {
|
export const filePrompts = (fileList: ChatFileItem[], addUrl: boolean) => {
|
||||||
if (fileList.length === 0) return '';
|
if (fileList.length === 0) return '';
|
||||||
|
|
||||||
const prompt = `<files>
|
const prompt = `<files>
|
||||||
<files_docstring>here are user upload files you can refer to</files_docstring>
|
<files_docstring>here are user upload files you can refer to</files_docstring>
|
||||||
${fileList.map((item) => filePrompt(item)).join('\n')}
|
${fileList.map((item) => filePrompt(item, addUrl)).join('\n')}
|
||||||
</files>`;
|
</files>`;
|
||||||
|
|
||||||
return prompt.trim();
|
return prompt.trim();
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { ChatImageItem } from '@/types/message';
|
import { ChatImageItem } from '@/types/message';
|
||||||
|
|
||||||
const imagePrompt = (item: ChatImageItem) => `<image name="${item.alt}" url="${item.url}"></image>`;
|
const imagePrompt = (item: ChatImageItem, attachUrl: boolean) =>
|
||||||
|
attachUrl
|
||||||
|
? `<image name="${item.alt}" url="${item.url}"></image>`
|
||||||
|
: `<image name="${item.alt}"></image>`;
|
||||||
|
|
||||||
export const imagesPrompts = (imageList: ChatImageItem[]) => {
|
export const imagesPrompts = (imageList: ChatImageItem[], attachUrl: boolean) => {
|
||||||
if (imageList.length === 0) return '';
|
if (imageList.length === 0) return '';
|
||||||
|
|
||||||
const prompt = `<images>
|
const prompt = `<images>
|
||||||
<images_docstring>here are user upload images you can refer to</images_docstring>
|
<images_docstring>here are user upload images you can refer to</images_docstring>
|
||||||
${imageList.map((item) => imagePrompt(item)).join('\n')}
|
${imageList.map((item) => imagePrompt(item, attachUrl)).join('\n')}
|
||||||
</images>`;
|
</images>`;
|
||||||
|
|
||||||
return prompt.trim();
|
return prompt.trim();
|
||||||
|
|||||||
@@ -135,4 +135,54 @@ describe('filesPrompts', () => {
|
|||||||
expect(result).toMatch(/<image.*?>.*<image.*?>/s); // Check for multiple image tags
|
expect(result).toMatch(/<image.*?>.*<image.*?>/s); // Check for multiple image tags
|
||||||
expect(result).toMatch(/<file.*?>.*<file.*?>/s); // Check for multiple file tags
|
expect(result).toMatch(/<file.*?>.*<file.*?>/s); // Check for multiple file tags
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle without url', () => {
|
||||||
|
const images: ChatImageItem[] = [
|
||||||
|
mockImage,
|
||||||
|
{
|
||||||
|
id: 'img-2',
|
||||||
|
alt: 'second image',
|
||||||
|
url: 'https://example.com/image2.jpg',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const files: ChatFileItem[] = [
|
||||||
|
mockFile,
|
||||||
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
name: 'document.docx',
|
||||||
|
fileType: 'application/docx',
|
||||||
|
size: 2048,
|
||||||
|
url: 'https://example.com/document.docx',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filesPrompts({
|
||||||
|
imageList: images,
|
||||||
|
fileList: files,
|
||||||
|
addUrl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
||||||
|
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
||||||
|
|
||||||
|
1. Always prioritize handling user-visible content.
|
||||||
|
2. the context is only required when user's queries rely on it.
|
||||||
|
</context.instruction>
|
||||||
|
<files_info>
|
||||||
|
<images>
|
||||||
|
<images_docstring>here are user upload images you can refer to</images_docstring>
|
||||||
|
<image name="test image"></image>
|
||||||
|
<image name="second image"></image>
|
||||||
|
</images>
|
||||||
|
<files>
|
||||||
|
<files_docstring>here are user upload files you can refer to</files_docstring>
|
||||||
|
<file id="file-1" name="test.pdf" type="application/pdf" size="1024"></file>
|
||||||
|
<file id="file-2" name="document.docx" type="application/docx" size="2048"></file>
|
||||||
|
</files>
|
||||||
|
</files_info>
|
||||||
|
<!-- END SYSTEM CONTEXT -->"
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { imagesPrompts } from './image';
|
|||||||
export const filesPrompts = ({
|
export const filesPrompts = ({
|
||||||
imageList,
|
imageList,
|
||||||
fileList,
|
fileList,
|
||||||
|
addUrl = true,
|
||||||
}: {
|
}: {
|
||||||
|
addUrl?: boolean;
|
||||||
fileList?: ChatFileItem[];
|
fileList?: ChatFileItem[];
|
||||||
imageList: ChatImageItem[];
|
imageList: ChatImageItem[];
|
||||||
}) => {
|
}) => {
|
||||||
@@ -19,8 +21,8 @@ export const filesPrompts = ({
|
|||||||
2. the context is only required when user's queries rely on it.
|
2. the context is only required when user's queries rely on it.
|
||||||
</context.instruction>
|
</context.instruction>
|
||||||
<files_info>
|
<files_info>
|
||||||
${imagesPrompts(imageList)}
|
${imagesPrompts(imageList, addUrl)}
|
||||||
${fileList ? filePrompts(fileList) : ''}
|
${fileList ? filePrompts(fileList, addUrl) : ''}
|
||||||
</files_info>
|
</files_info>
|
||||||
<!-- END SYSTEM CONTEXT -->`;
|
<!-- END SYSTEM CONTEXT -->`;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { INBOX_GUIDE_SYSTEMROLE } from '@/const/guide';
|
|||||||
import { INBOX_SESSION_ID } from '@/const/session';
|
import { INBOX_SESSION_ID } from '@/const/session';
|
||||||
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
||||||
import { TracePayload, TraceTagMap } from '@/const/trace';
|
import { TracePayload, TraceTagMap } from '@/const/trace';
|
||||||
import { isDeprecatedEdition, isServerMode } from '@/const/version';
|
import { isDeprecatedEdition, isDesktop, isServerMode } from '@/const/version';
|
||||||
import {
|
import {
|
||||||
AgentRuntime,
|
AgentRuntime,
|
||||||
AgentRuntimeError,
|
AgentRuntimeError,
|
||||||
@@ -480,7 +480,9 @@ class ChatService {
|
|||||||
|
|
||||||
const imageList = m.imageList || [];
|
const imageList = m.imageList || [];
|
||||||
|
|
||||||
const filesContext = isServerMode ? filesPrompts({ fileList: m.fileList, imageList }) : '';
|
const filesContext = isServerMode
|
||||||
|
? filesPrompts({ addUrl: !isDesktop, fileList: m.fileList, imageList })
|
||||||
|
: '';
|
||||||
return [
|
return [
|
||||||
{ text: (m.content + '\n\n' + filesContext).trim(), type: 'text' },
|
{ text: (m.content + '\n\n' + filesContext).trim(), type: 'text' },
|
||||||
...imageList.map(
|
...imageList.map(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { SWRResponse } from 'swr';
|
|||||||
import type { StateCreator } from 'zustand/vanilla';
|
import type { StateCreator } from 'zustand/vanilla';
|
||||||
|
|
||||||
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
||||||
import { CURRENT_VERSION } from '@/const/version';
|
import { CURRENT_VERSION, isDesktop } from '@/const/version';
|
||||||
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
||||||
import { globalService } from '@/services/global';
|
import { globalService } from '@/services/global';
|
||||||
import type { SystemStatus } from '@/store/global/initialState';
|
import type { SystemStatus } from '@/store/global/initialState';
|
||||||
@@ -37,6 +37,18 @@ export const generalActionSlice: StateCreator<
|
|||||||
get().updateSystemStatus({ language: locale });
|
get().updateSystemStatus({ language: locale });
|
||||||
|
|
||||||
switchLang(locale);
|
switchLang(locale);
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { dispatch } = await import('@lobechat/electron-client-ipc');
|
||||||
|
|
||||||
|
await dispatch('updateLocale', locale);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update locale in main process:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
switchThemeMode: (themeMode) => {
|
switchThemeMode: (themeMode) => {
|
||||||
get().updateSystemStatus({ themeMode });
|
get().updateSystemStatus({ themeMode });
|
||||||
@@ -44,7 +56,6 @@ export const generalActionSlice: StateCreator<
|
|||||||
setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
|
setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
|
||||||
},
|
},
|
||||||
updateSystemStatus: (status, action) => {
|
updateSystemStatus: (status, action) => {
|
||||||
// Status cannot be modified when it is not initialized
|
|
||||||
if (!get().isStatusInit) return;
|
if (!get().isStatusInit) return;
|
||||||
|
|
||||||
const nextStatus = merge(get().status, status);
|
const nextStatus = merge(get().status, status);
|
||||||
@@ -60,19 +71,15 @@ export const generalActionSlice: StateCreator<
|
|||||||
enabledCheck ? 'checkLatestVersion' : null,
|
enabledCheck ? 'checkLatestVersion' : null,
|
||||||
async () => globalService.getLatestVersion(),
|
async () => globalService.getLatestVersion(),
|
||||||
{
|
{
|
||||||
// check latest version every 30 minutes
|
|
||||||
focusThrottleInterval: 1000 * 60 * 30,
|
focusThrottleInterval: 1000 * 60 * 30,
|
||||||
onSuccess: (data: string) => {
|
onSuccess: (data: string) => {
|
||||||
if (!valid(CURRENT_VERSION) || !valid(data)) return;
|
if (!valid(CURRENT_VERSION) || !valid(data)) return;
|
||||||
|
|
||||||
// Parse versions to ensure we're working with valid SemVer objects
|
|
||||||
const currentVersion = parse(CURRENT_VERSION);
|
const currentVersion = parse(CURRENT_VERSION);
|
||||||
const latestVersion = parse(data);
|
const latestVersion = parse(data);
|
||||||
|
|
||||||
if (!currentVersion || !latestVersion) return;
|
if (!currentVersion || !latestVersion) return;
|
||||||
|
|
||||||
// only compare major and minor versions
|
|
||||||
// solve the problem of frequent patch updates
|
|
||||||
const currentMajorMinor = `${currentVersion.major}.${currentVersion.minor}.0`;
|
const currentMajorMinor = `${currentVersion.major}.${currentVersion.minor}.0`;
|
||||||
const latestMajorMinor = `${latestVersion.major}.${latestVersion.minor}.0`;
|
const latestMajorMinor = `${latestVersion.major}.${latestVersion.minor}.0`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user