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
|
||||
|
||||
**Version**: \`${version}\`
|
||||
**Release Time**: \`${new Date().toISOString()}\`
|
||||
**PR**: [#${prNumber}](${prLink})
|
||||
|
||||
|
||||
## ⚠️ Important Notice
|
||||
|
||||
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 构建信息
|
||||
|
||||
**版本**: \`${version}\`
|
||||
**发布时间**: \`${new Date().toISOString()}\`
|
||||
**PR**: [#${prNumber}](${prLink})
|
||||
|
||||
## ⚠️ 重要提示
|
||||
|
||||
12
package.json
12
package.json
@@ -35,7 +35,8 @@
|
||||
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"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-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
||||
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
||||
@@ -44,7 +45,12 @@
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:visualize": "dbdocs build docs/development/database-schema.dbml --project lobe-chat",
|
||||
"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:desktop": "next dev --turbopack -p 3015",
|
||||
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx",
|
||||
"docs:seo": "lobe-seo && npm run lint:mdx",
|
||||
"i18n": "npm run workflow:i18n && lobe-i18n",
|
||||
@@ -78,7 +84,8 @@
|
||||
"workflow:docs": "tsx ./scripts/docsWorkflow/index.ts",
|
||||
"workflow:i18n": "tsx ./scripts/i18nWorkflow/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": {
|
||||
"*.md": [
|
||||
@@ -293,6 +300,7 @@
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"commitlint": "^19.8.0",
|
||||
"consola": "^3.4.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dbdocs": "^0.14.3",
|
||||
"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 { FilesSearchDispatchEvents } from './search';
|
||||
import { ShortcutDispatchEvents } from './shortcut';
|
||||
import { SystemDispatchEvents } from './system';
|
||||
import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
|
||||
import { WindowsDispatchEvents } from './windows';
|
||||
|
||||
/**
|
||||
@@ -11,10 +14,25 @@ export interface ClientDispatchEvents
|
||||
extends WindowsDispatchEvents,
|
||||
FilesSearchDispatchEvents,
|
||||
SystemDispatchEvents,
|
||||
MenuDispatchEvents {}
|
||||
MenuDispatchEvents,
|
||||
FilesDispatchEvents,
|
||||
AutoUpdateDispatchEvents,
|
||||
ShortcutDispatchEvents {}
|
||||
|
||||
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
||||
|
||||
export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType<
|
||||
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 {
|
||||
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 {
|
||||
/**
|
||||
* 拦截客户端路由导航请求
|
||||
* @param params 包含路径和来源信息的参数对象
|
||||
* @returns 路由拦截结果
|
||||
*/
|
||||
interceptRoute: (params: InterceptRouteParams) => InterceptRouteResponse;
|
||||
|
||||
/**
|
||||
* open the LobeHub Devtools
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './dispatch';
|
||||
export * from './events';
|
||||
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 './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 './ipcServer';
|
||||
export * from './types';
|
||||
|
||||
@@ -5,7 +5,6 @@ import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ElectronIpcClient } from './ipcClient';
|
||||
import { ElectronIPCMethods } from './types';
|
||||
|
||||
// Mock node modules
|
||||
vi.mock('node:fs');
|
||||
@@ -124,7 +123,7 @@ describe('ElectronIpcClient', () => {
|
||||
|
||||
it('should handle connection errors', async () => {
|
||||
// Start request - but don't await it yet
|
||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
||||
const requestPromise = client.sendRequest('getDatabasePath');
|
||||
|
||||
// Find the error event handler
|
||||
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
|
||||
@@ -154,7 +153,7 @@ describe('ElectronIpcClient', () => {
|
||||
});
|
||||
|
||||
// Start request
|
||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
||||
const requestPromise = client.sendRequest('getDatabasePath');
|
||||
|
||||
// Simulate connection established
|
||||
if (connectionCallback) connectionCallback();
|
||||
@@ -188,7 +187,7 @@ describe('ElectronIpcClient', () => {
|
||||
});
|
||||
|
||||
// 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
|
||||
if (connectionCallback) connectionCallback();
|
||||
|
||||
@@ -4,7 +4,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
||||
import { IElectronIPCMethods } from './types';
|
||||
import { ServerDispatchEventKey } from './events';
|
||||
|
||||
export class ElectronIpcClient {
|
||||
private socketPath: string | null = null;
|
||||
@@ -16,6 +16,7 @@ export class ElectronIpcClient {
|
||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private connectionAttempts: number = 0;
|
||||
private maxConnectionAttempts: number = 5;
|
||||
private dataBuffer: string = '';
|
||||
|
||||
constructor() {
|
||||
this.initialize();
|
||||
@@ -53,43 +54,64 @@ export class ElectronIpcClient {
|
||||
this.socket = net.createConnection(this.socketPath!, () => {
|
||||
this.connected = true;
|
||||
this.connectionAttempts = 0;
|
||||
console.log('Connected to Electron IPC server');
|
||||
console.log('[ElectronIpcClient] Connected to Electron IPC server');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
const { id, result, error } = response;
|
||||
const dataStr = data.toString();
|
||||
console.log('output:', dataStr);
|
||||
|
||||
const pending = this.requestQueue.get(id);
|
||||
if (pending) {
|
||||
this.requestQueue.delete(id);
|
||||
if (error) {
|
||||
pending.reject(new Error(error));
|
||||
} else {
|
||||
pending.resolve(result);
|
||||
// 将新数据添加到缓冲区
|
||||
this.dataBuffer += dataStr;
|
||||
|
||||
// 按换行符分割消息
|
||||
const messages = this.dataBuffer.split('\n');
|
||||
|
||||
// 最后一个元素可能是不完整的消息,保留在缓冲区
|
||||
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) => {
|
||||
console.error('Socket error:', err);
|
||||
console.error('[ElectronIpcClient] Socket error:', err);
|
||||
this.connected = false;
|
||||
this.handleDisconnect();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
console.log('Socket closed');
|
||||
console.log('[ElectronIpcClient] Socket closed');
|
||||
this.connected = false;
|
||||
this.handleDisconnect();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to connect to IPC server:', err);
|
||||
console.error('[ElectronIpcClient] Failed to connect to IPC server:', err);
|
||||
this.handleDisconnect();
|
||||
reject(err);
|
||||
}
|
||||
@@ -104,9 +126,12 @@ export class ElectronIpcClient {
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
// 清空数据缓冲区
|
||||
this.dataBuffer = '';
|
||||
|
||||
// 拒绝所有待处理的请求
|
||||
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();
|
||||
|
||||
@@ -117,16 +142,19 @@ export class ElectronIpcClient {
|
||||
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.connect().catch((err) => {
|
||||
console.error(`Reconnection attempt ${this.connectionAttempts} failed:`, err);
|
||||
console.error(
|
||||
`[ElectronIpcClient] Reconnection attempt ${this.connectionAttempts} failed:`,
|
||||
err,
|
||||
);
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求到 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) {
|
||||
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(() => {
|
||||
this.requestQueue.delete(id);
|
||||
reject(new Error(`Request ${method} timed out`));
|
||||
}, 10_000);
|
||||
reject(new Error(`[ElectronIpcClient] Request timed out, method: ${method}`));
|
||||
}, 5000);
|
||||
|
||||
// 发送请求
|
||||
this.socket!.write(JSON.stringify(request), (err) => {
|
||||
|
||||
@@ -4,16 +4,8 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
||||
import { IElectronIPCMethods } from './types';
|
||||
|
||||
export type IPCEventMethod = (
|
||||
params: any,
|
||||
context: { id: string; method: string; socket: net.Socket },
|
||||
) => Promise<any>;
|
||||
|
||||
export type ElectronIPCEventHandler = {
|
||||
[key in IElectronIPCMethods]: IPCEventMethod;
|
||||
};
|
||||
import { ServerDispatchEventKey } from './events';
|
||||
import { ElectronIPCEventHandler } from './types';
|
||||
|
||||
export class ElectronIPCServer {
|
||||
private server: net.Server;
|
||||
@@ -45,7 +37,7 @@ export class ElectronIPCServer {
|
||||
});
|
||||
|
||||
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 服务端读取
|
||||
const tempDir = os.tmpdir();
|
||||
@@ -86,7 +78,7 @@ export class ElectronIPCServer {
|
||||
const { id, method, params } = request;
|
||||
|
||||
// 根据请求方法执行相应的操作
|
||||
const eventHandler = this.eventHandler[method as IElectronIPCMethods];
|
||||
const eventHandler = this.eventHandler[method as ServerDispatchEventKey];
|
||||
if (!eventHandler) return;
|
||||
|
||||
try {
|
||||
@@ -100,12 +92,12 @@ export class ElectronIPCServer {
|
||||
|
||||
// 发送结果
|
||||
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 {
|
||||
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';
|
||||
|
||||
const filePrompt = (item: ChatFileItem) =>
|
||||
`<file id="${item.id}" name="${item.name}" type="${item.fileType}" size="${item.size}" url="${item.url}"></file>`;
|
||||
const filePrompt = (item: ChatFileItem, addUrl: boolean) =>
|
||||
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 '';
|
||||
|
||||
const prompt = `<files>
|
||||
<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>`;
|
||||
|
||||
return prompt.trim();
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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 '';
|
||||
|
||||
const prompt = `<images>
|
||||
<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>`;
|
||||
|
||||
return prompt.trim();
|
||||
|
||||
@@ -135,4 +135,54 @@ describe('filesPrompts', () => {
|
||||
expect(result).toMatch(/<image.*?>.*<image.*?>/s); // Check for multiple image 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 = ({
|
||||
imageList,
|
||||
fileList,
|
||||
addUrl = true,
|
||||
}: {
|
||||
addUrl?: boolean;
|
||||
fileList?: ChatFileItem[];
|
||||
imageList: ChatImageItem[];
|
||||
}) => {
|
||||
@@ -19,8 +21,8 @@ export const filesPrompts = ({
|
||||
2. the context is only required when user's queries rely on it.
|
||||
</context.instruction>
|
||||
<files_info>
|
||||
${imagesPrompts(imageList)}
|
||||
${fileList ? filePrompts(fileList) : ''}
|
||||
${imagesPrompts(imageList, addUrl)}
|
||||
${fileList ? filePrompts(fileList, addUrl) : ''}
|
||||
</files_info>
|
||||
<!-- END SYSTEM CONTEXT -->`;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { INBOX_GUIDE_SYSTEMROLE } from '@/const/guide';
|
||||
import { INBOX_SESSION_ID } from '@/const/session';
|
||||
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
||||
import { TracePayload, TraceTagMap } from '@/const/trace';
|
||||
import { isDeprecatedEdition, isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition, isDesktop, isServerMode } from '@/const/version';
|
||||
import {
|
||||
AgentRuntime,
|
||||
AgentRuntimeError,
|
||||
@@ -480,7 +480,9 @@ class ChatService {
|
||||
|
||||
const imageList = m.imageList || [];
|
||||
|
||||
const filesContext = isServerMode ? filesPrompts({ fileList: m.fileList, imageList }) : '';
|
||||
const filesContext = isServerMode
|
||||
? filesPrompts({ addUrl: !isDesktop, fileList: m.fileList, imageList })
|
||||
: '';
|
||||
return [
|
||||
{ text: (m.content + '\n\n' + filesContext).trim(), type: 'text' },
|
||||
...imageList.map(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SWRResponse } from 'swr';
|
||||
import type { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
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 { globalService } from '@/services/global';
|
||||
import type { SystemStatus } from '@/store/global/initialState';
|
||||
@@ -37,6 +37,18 @@ export const generalActionSlice: StateCreator<
|
||||
get().updateSystemStatus({ language: 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) => {
|
||||
get().updateSystemStatus({ themeMode });
|
||||
@@ -44,7 +56,6 @@ export const generalActionSlice: StateCreator<
|
||||
setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
|
||||
},
|
||||
updateSystemStatus: (status, action) => {
|
||||
// Status cannot be modified when it is not initialized
|
||||
if (!get().isStatusInit) return;
|
||||
|
||||
const nextStatus = merge(get().status, status);
|
||||
@@ -60,19 +71,15 @@ export const generalActionSlice: StateCreator<
|
||||
enabledCheck ? 'checkLatestVersion' : null,
|
||||
async () => globalService.getLatestVersion(),
|
||||
{
|
||||
// check latest version every 30 minutes
|
||||
focusThrottleInterval: 1000 * 60 * 30,
|
||||
onSuccess: (data: string) => {
|
||||
if (!valid(CURRENT_VERSION) || !valid(data)) return;
|
||||
|
||||
// Parse versions to ensure we're working with valid SemVer objects
|
||||
const currentVersion = parse(CURRENT_VERSION);
|
||||
const latestVersion = parse(data);
|
||||
|
||||
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 latestMajorMinor = `${latestVersion.major}.${latestVersion.minor}.0`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user