@lobehub/chat 1.79.2 → 1.79.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.desktop +7 -0
- package/.github/scripts/pr-release-body.js +3 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +15 -7
- package/packages/electron-client-ipc/src/events/file.ts +5 -0
- package/packages/electron-client-ipc/src/events/index.ts +19 -1
- package/packages/electron-client-ipc/src/events/shortcut.ts +4 -0
- package/packages/electron-client-ipc/src/events/system.ts +5 -0
- package/packages/electron-client-ipc/src/events/update.ts +20 -0
- package/packages/electron-client-ipc/src/events/windows.ts +9 -0
- package/packages/electron-client-ipc/src/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/file.ts +14 -0
- package/packages/electron-client-ipc/src/types/index.ts +4 -0
- package/packages/electron-client-ipc/src/types/route.ts +46 -0
- package/packages/electron-client-ipc/src/types/shortcut.ts +11 -0
- package/packages/electron-client-ipc/src/types/update.ts +23 -0
- package/packages/electron-client-ipc/src/useWatchBroadcast.ts +37 -0
- package/packages/electron-server-ipc/src/events/database.ts +4 -0
- package/packages/electron-server-ipc/src/events/file.ts +6 -0
- package/packages/electron-server-ipc/src/events/index.ts +29 -0
- package/packages/electron-server-ipc/src/events/storagePath.ts +4 -0
- package/packages/electron-server-ipc/src/index.ts +1 -0
- package/packages/electron-server-ipc/src/ipcClient.test.ts +3 -4
- package/packages/electron-server-ipc/src/ipcClient.ts +52 -24
- package/packages/electron-server-ipc/src/ipcServer.ts +6 -14
- package/packages/electron-server-ipc/src/types/file.ts +4 -0
- package/packages/electron-server-ipc/src/types/index.ts +14 -1
- package/scripts/electronWorkflow/buildElectron.ts +52 -0
- package/scripts/electronWorkflow/moveNextStandalone.ts +69 -0
- package/scripts/electronWorkflow/setDesktopVersion.ts +96 -0
- package/scripts/prebuild.mts +77 -0
- package/src/libs/agent-runtime/azureai/index.ts +3 -1
- package/src/prompts/files/file.ts +6 -4
- package/src/prompts/files/image.ts +6 -3
- package/src/prompts/files/index.test.ts +50 -0
- package/src/prompts/files/index.ts +4 -2
- package/src/services/chat.ts +4 -2
- package/src/store/global/actions/general.ts +13 -6
- package/packages/electron-server-ipc/src/types/event.ts +0 -18
package/.env.desktop
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# copy this file to .env when you want to develop the desktop app or you will fail
|
2
|
+
APP_URL=http://localhost:3015
|
3
|
+
FEATURE_FLAGS=+pin_list
|
4
|
+
KEY_VAULTS_SECRET=oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=
|
5
|
+
DATABASE_URL=postgresql://postgres@localhost:5432/postgres
|
6
|
+
DEFAULT_AGENT_CONFIG="model=qwen2.5;provider=ollama;chatConfig.searchFCModel.provider=ollama;chatConfig.searchFCModel.model=qwen2.5"
|
7
|
+
SYSTEM_AGENT="default=ollama/qwen2.5"
|
@@ -9,8 +9,10 @@ module.exports = ({ version, prNumber, branch }) => {
|
|
9
9
|
## PR Build Information
|
10
10
|
|
11
11
|
**Version**: \`${version}\`
|
12
|
+
**Release Time**: \`${new Date().toISOString()}\`
|
12
13
|
**PR**: [#${prNumber}](${prLink})
|
13
14
|
|
15
|
+
|
14
16
|
## ⚠️ Important Notice
|
15
17
|
|
16
18
|
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.
|
|
35
37
|
## PR 构建信息
|
36
38
|
|
37
39
|
**版本**: \`${version}\`
|
40
|
+
**发布时间**: \`${new Date().toISOString()}\`
|
38
41
|
**PR**: [#${prNumber}](${prLink})
|
39
42
|
|
40
43
|
## ⚠️ 重要提示
|
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.79.3](https://github.com/lobehub/lobe-chat/compare/v1.79.2...v1.79.3)
|
6
|
+
|
7
|
+
<sup>Released on **2025-04-10**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Remove Azure AI o3-mini unsupported parameters.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's fixed
|
19
|
+
|
20
|
+
- **misc**: Remove Azure AI o3-mini unsupported parameters, closes [#7355](https://github.com/lobehub/lobe-chat/issues/7355) ([fe0711f](https://github.com/lobehub/lobe-chat/commit/fe0711f))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.79.2](https://github.com/lobehub/lobe-chat/compare/v1.79.1...v1.79.2)
|
6
31
|
|
7
32
|
<sup>Released on **2025-04-09**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.79.
|
3
|
+
"version": "1.79.3",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -35,7 +35,8 @@
|
|
35
35
|
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
36
36
|
"build:analyze": "ANALYZE=true next build",
|
37
37
|
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
38
|
-
"
|
38
|
+
"prebuild:electron": "cross-env NEXT_PUBLIC_IS_DESKTOP_APP=1 tsx scripts/prebuild.mts",
|
39
|
+
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=6144 NEXT_PUBLIC_IS_DESKTOP_APP=1 NEXT_PUBLIC_SERVICE_MODE=server next build",
|
39
40
|
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
|
40
41
|
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
41
42
|
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
@@ -44,7 +45,12 @@
|
|
44
45
|
"db:studio": "drizzle-kit studio",
|
45
46
|
"db:visualize": "dbdocs build docs/development/database-schema.dbml --project lobe-chat",
|
46
47
|
"db:z-pull": "drizzle-kit introspect",
|
48
|
+
"desktop:build": "npm run desktop:build-next && npm run desktop:prepare-dist && npm run desktop:build-electron",
|
49
|
+
"desktop:build-electron": "tsx scripts/electronWorkflow/buildElectron.ts",
|
50
|
+
"desktop:build-next": "npm run build:electron",
|
51
|
+
"desktop:prepare-dist": "tsx scripts/electronWorkflow/moveNextStandalone.ts",
|
47
52
|
"dev": "next dev --turbopack -p 3010",
|
53
|
+
"dev:desktop": "next dev --turbopack -p 3015",
|
48
54
|
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx",
|
49
55
|
"docs:seo": "lobe-seo && npm run lint:mdx",
|
50
56
|
"i18n": "npm run workflow:i18n && lobe-i18n",
|
@@ -78,7 +84,8 @@
|
|
78
84
|
"workflow:docs": "tsx ./scripts/docsWorkflow/index.ts",
|
79
85
|
"workflow:i18n": "tsx ./scripts/i18nWorkflow/index.ts",
|
80
86
|
"workflow:mdx": "tsx ./scripts/mdxWorkflow/index.ts",
|
81
|
-
"workflow:readme": "tsx ./scripts/readmeWorkflow/index.ts"
|
87
|
+
"workflow:readme": "tsx ./scripts/readmeWorkflow/index.ts",
|
88
|
+
"workflow:set-desktop-version": "tsx ./scripts/electronWorkflow/setDesktopVersion.ts"
|
82
89
|
},
|
83
90
|
"lint-staged": {
|
84
91
|
"*.md": [
|
@@ -140,7 +147,7 @@
|
|
140
147
|
"@lobehub/tts": "^1.28.3",
|
141
148
|
"@lobehub/ui": "^1.170.8",
|
142
149
|
"@neondatabase/serverless": "^1.0.0",
|
143
|
-
"@next/third-parties": "15.
|
150
|
+
"@next/third-parties": "^15.3.0",
|
144
151
|
"@react-spring/web": "^9.7.5",
|
145
152
|
"@sentry/nextjs": "^7.120.3",
|
146
153
|
"@serwist/next": "^9.0.12",
|
@@ -190,7 +197,7 @@
|
|
190
197
|
"mdast-util-to-markdown": "^2.1.2",
|
191
198
|
"modern-screenshot": "^4.6.0",
|
192
199
|
"nanoid": "^5.1.5",
|
193
|
-
"next": "15.
|
200
|
+
"next": "^15.3.0",
|
194
201
|
"next-auth": "beta",
|
195
202
|
"next-mdx-remote": "^5.0.0",
|
196
203
|
"nextjs-toploader": "^3.8.16",
|
@@ -261,8 +268,8 @@
|
|
261
268
|
"@lobehub/i18n-cli": "^1.20.3",
|
262
269
|
"@lobehub/lint": "^1.26.1",
|
263
270
|
"@lobehub/seo-cli": "^1.4.3",
|
264
|
-
"@next/bundle-analyzer": "15.
|
265
|
-
"@next/eslint-plugin-next": "15.
|
271
|
+
"@next/bundle-analyzer": "^15.3.0",
|
272
|
+
"@next/eslint-plugin-next": "^15.3.0",
|
266
273
|
"@peculiar/webcrypto": "^1.5.0",
|
267
274
|
"@semantic-release/exec": "^6.0.3",
|
268
275
|
"@testing-library/jest-dom": "^6.6.3",
|
@@ -293,6 +300,7 @@
|
|
293
300
|
"ajv-keywords": "^5.1.0",
|
294
301
|
"commitlint": "^19.8.0",
|
295
302
|
"consola": "^3.4.2",
|
303
|
+
"cross-env": "^7.0.3",
|
296
304
|
"crypto-js": "^4.2.0",
|
297
305
|
"dbdocs": "^0.14.3",
|
298
306
|
"dotenv": "^16.4.7",
|
@@ -1,6 +1,9 @@
|
|
1
|
+
import { FilesDispatchEvents } from './file';
|
1
2
|
import { MenuDispatchEvents } from './menu';
|
2
3
|
import { FilesSearchDispatchEvents } from './search';
|
4
|
+
import { ShortcutDispatchEvents } from './shortcut';
|
3
5
|
import { SystemDispatchEvents } from './system';
|
6
|
+
import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
|
4
7
|
import { WindowsDispatchEvents } from './windows';
|
5
8
|
|
6
9
|
/**
|
@@ -11,10 +14,25 @@ export interface ClientDispatchEvents
|
|
11
14
|
extends WindowsDispatchEvents,
|
12
15
|
FilesSearchDispatchEvents,
|
13
16
|
SystemDispatchEvents,
|
14
|
-
MenuDispatchEvents
|
17
|
+
MenuDispatchEvents,
|
18
|
+
FilesDispatchEvents,
|
19
|
+
AutoUpdateDispatchEvents,
|
20
|
+
ShortcutDispatchEvents {}
|
15
21
|
|
16
22
|
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
17
23
|
|
18
24
|
export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType<
|
19
25
|
ClientDispatchEvents[T]
|
20
26
|
>;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* main -> render broadcast events
|
30
|
+
*/
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
32
|
+
export interface MainBroadcastEvents extends AutoUpdateBroadcastEvents {}
|
33
|
+
|
34
|
+
export type MainBroadcastEventKey = keyof MainBroadcastEvents;
|
35
|
+
|
36
|
+
export type MainBroadcastParams<T extends MainBroadcastEventKey> = Parameters<
|
37
|
+
MainBroadcastEvents[T]
|
38
|
+
>[0];
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { ProgressInfo, UpdateInfo } from '../types';
|
2
|
+
|
3
|
+
export interface AutoUpdateDispatchEvents {
|
4
|
+
checkUpdate: () => void;
|
5
|
+
downloadUpdate: () => void;
|
6
|
+
installLater: () => void;
|
7
|
+
installNow: () => void;
|
8
|
+
installUpdate: () => void;
|
9
|
+
}
|
10
|
+
|
11
|
+
export interface AutoUpdateBroadcastEvents {
|
12
|
+
updateAvailable: (info: UpdateInfo) => void;
|
13
|
+
updateCheckStart: () => void;
|
14
|
+
updateDownloadProgress: (progress: ProgressInfo) => void;
|
15
|
+
updateDownloadStart: () => void;
|
16
|
+
updateDownloaded: (info: UpdateInfo) => void;
|
17
|
+
updateError: (message: string) => void;
|
18
|
+
updateNotAvailable: (info: UpdateInfo) => void;
|
19
|
+
updateWillInstallLater: () => void;
|
20
|
+
}
|
@@ -1,4 +1,13 @@
|
|
1
|
+
import { InterceptRouteParams, InterceptRouteResponse } from '../types/route';
|
2
|
+
|
1
3
|
export interface WindowsDispatchEvents {
|
4
|
+
/**
|
5
|
+
* 拦截客户端路由导航请求
|
6
|
+
* @param params 包含路径和来源信息的参数对象
|
7
|
+
* @returns 路由拦截结果
|
8
|
+
*/
|
9
|
+
interceptRoute: (params: InterceptRouteParams) => InterceptRouteResponse;
|
10
|
+
|
2
11
|
/**
|
3
12
|
* open the LobeHub Devtools
|
4
13
|
*/
|
@@ -0,0 +1,46 @@
|
|
1
|
+
export interface InterceptRouteParams {
|
2
|
+
/**
|
3
|
+
* 请求路径
|
4
|
+
*/
|
5
|
+
path: string;
|
6
|
+
/**
|
7
|
+
* 来源类型:'link-click', 'push-state', 'replace-state'
|
8
|
+
*/
|
9
|
+
source: 'link-click' | 'push-state' | 'replace-state';
|
10
|
+
/**
|
11
|
+
* 完整URL
|
12
|
+
*/
|
13
|
+
url: string;
|
14
|
+
}
|
15
|
+
|
16
|
+
export interface InterceptRouteResponse {
|
17
|
+
/**
|
18
|
+
* 错误信息 (如果有)
|
19
|
+
*/
|
20
|
+
error?: string;
|
21
|
+
|
22
|
+
/**
|
23
|
+
* 是否已拦截
|
24
|
+
*/
|
25
|
+
intercepted: boolean;
|
26
|
+
|
27
|
+
/**
|
28
|
+
* 原始路径
|
29
|
+
*/
|
30
|
+
path: string;
|
31
|
+
|
32
|
+
/**
|
33
|
+
* 原始来源
|
34
|
+
*/
|
35
|
+
source: string;
|
36
|
+
|
37
|
+
/**
|
38
|
+
* 子路径 (如果有)
|
39
|
+
*/
|
40
|
+
subPath?: string;
|
41
|
+
|
42
|
+
/**
|
43
|
+
* 目标窗口标识符
|
44
|
+
*/
|
45
|
+
targetWindow?: string;
|
46
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
export interface ReleaseNoteInfo {
|
2
|
+
/**
|
3
|
+
* The note.
|
4
|
+
*/
|
5
|
+
note: string | null;
|
6
|
+
/**
|
7
|
+
* The version.
|
8
|
+
*/
|
9
|
+
version: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
export interface ProgressInfo {
|
13
|
+
bytesPerSecond: number;
|
14
|
+
percent: number;
|
15
|
+
total: number;
|
16
|
+
transferred: number;
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface UpdateInfo {
|
20
|
+
releaseDate: string;
|
21
|
+
releaseNotes?: string | ReleaseNoteInfo[];
|
22
|
+
version: string;
|
23
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { useEffect } from 'react';
|
4
|
+
|
5
|
+
import { MainBroadcastEventKey, MainBroadcastParams } from './events';
|
6
|
+
|
7
|
+
interface ElectronAPI {
|
8
|
+
ipcRenderer: {
|
9
|
+
on: (event: MainBroadcastEventKey, listener: (e: any, data: any) => void) => void;
|
10
|
+
removeListener: (event: MainBroadcastEventKey, listener: (e: any, data: any) => void) => void;
|
11
|
+
};
|
12
|
+
}
|
13
|
+
|
14
|
+
declare global {
|
15
|
+
interface Window {
|
16
|
+
electron: ElectronAPI;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
export const useWatchBroadcast = <T extends MainBroadcastEventKey>(
|
21
|
+
event: T,
|
22
|
+
handler: (data: MainBroadcastParams<T>) => void,
|
23
|
+
) => {
|
24
|
+
useEffect(() => {
|
25
|
+
if (!window.electron) return;
|
26
|
+
|
27
|
+
const listener = (e: any, data: MainBroadcastParams<T>) => {
|
28
|
+
handler(data);
|
29
|
+
};
|
30
|
+
|
31
|
+
window.electron.ipcRenderer.on(event, listener);
|
32
|
+
|
33
|
+
return () => {
|
34
|
+
window.electron.ipcRenderer.removeListener(event, listener);
|
35
|
+
};
|
36
|
+
}, []);
|
37
|
+
};
|
@@ -0,0 +1,29 @@
|
|
1
|
+
/* eslint-disable typescript-sort-keys/interface, sort-keys-fix/sort-keys-fix */
|
2
|
+
import { DatabaseDispatchEvents } from './database';
|
3
|
+
import { FileDispatchEvents } from './file';
|
4
|
+
import { StoragePathDispatchEvents } from './storagePath';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* next server -> main dispatch events
|
8
|
+
*/
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
10
|
+
export interface ServerDispatchEvents
|
11
|
+
extends StoragePathDispatchEvents,
|
12
|
+
DatabaseDispatchEvents,
|
13
|
+
FileDispatchEvents {}
|
14
|
+
|
15
|
+
export type ServerDispatchEventKey = keyof ServerDispatchEvents;
|
16
|
+
|
17
|
+
export type ServerEventReturnType<T extends ServerDispatchEventKey> = ReturnType<
|
18
|
+
ServerDispatchEvents[T]
|
19
|
+
>;
|
20
|
+
|
21
|
+
export type ServerEventParams<T extends ServerDispatchEventKey> = Parameters<
|
22
|
+
ServerDispatchEvents[T]
|
23
|
+
>[0];
|
24
|
+
|
25
|
+
export type IPCServerEventHandler = {
|
26
|
+
[key in ServerDispatchEventKey]: (
|
27
|
+
params: ServerEventParams<key>,
|
28
|
+
) => Promise<ServerEventReturnType<key>>;
|
29
|
+
};
|
@@ -5,7 +5,6 @@ import path from 'node:path';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
6
6
|
|
7
7
|
import { ElectronIpcClient } from './ipcClient';
|
8
|
-
import { ElectronIPCMethods } from './types';
|
9
8
|
|
10
9
|
// Mock node modules
|
11
10
|
vi.mock('node:fs');
|
@@ -124,7 +123,7 @@ describe('ElectronIpcClient', () => {
|
|
124
123
|
|
125
124
|
it('should handle connection errors', async () => {
|
126
125
|
// Start request - but don't await it yet
|
127
|
-
const requestPromise = client.sendRequest(
|
126
|
+
const requestPromise = client.sendRequest('getDatabasePath');
|
128
127
|
|
129
128
|
// Find the error event handler
|
130
129
|
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
|
@@ -154,7 +153,7 @@ describe('ElectronIpcClient', () => {
|
|
154
153
|
});
|
155
154
|
|
156
155
|
// Start request
|
157
|
-
const requestPromise = client.sendRequest(
|
156
|
+
const requestPromise = client.sendRequest('getDatabasePath');
|
158
157
|
|
159
158
|
// Simulate connection established
|
160
159
|
if (connectionCallback) connectionCallback();
|
@@ -188,7 +187,7 @@ describe('ElectronIpcClient', () => {
|
|
188
187
|
});
|
189
188
|
|
190
189
|
// Start a request to establish connection (but don't wait for it)
|
191
|
-
const requestPromise = client.sendRequest(
|
190
|
+
const requestPromise = client.sendRequest('getDatabasePath').catch(() => {}); // Ignore any errors
|
192
191
|
|
193
192
|
// Simulate connection
|
194
193
|
if (connectionCallback) connectionCallback();
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
5
5
|
|
6
6
|
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
7
|
-
import {
|
7
|
+
import { ServerDispatchEventKey } from './events';
|
8
8
|
|
9
9
|
export class ElectronIpcClient {
|
10
10
|
private socketPath: string | null = null;
|
@@ -16,6 +16,7 @@ export class ElectronIpcClient {
|
|
16
16
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
17
17
|
private connectionAttempts: number = 0;
|
18
18
|
private maxConnectionAttempts: number = 5;
|
19
|
+
private dataBuffer: string = '';
|
19
20
|
|
20
21
|
constructor() {
|
21
22
|
this.initialize();
|
@@ -53,43 +54,64 @@ export class ElectronIpcClient {
|
|
53
54
|
this.socket = net.createConnection(this.socketPath!, () => {
|
54
55
|
this.connected = true;
|
55
56
|
this.connectionAttempts = 0;
|
56
|
-
console.log('Connected to Electron IPC server');
|
57
|
+
console.log('[ElectronIpcClient] Connected to Electron IPC server');
|
57
58
|
resolve();
|
58
59
|
});
|
59
60
|
|
60
61
|
this.socket.on('data', (data) => {
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
62
|
+
const dataStr = data.toString();
|
63
|
+
console.log('output:', dataStr);
|
64
|
+
|
65
|
+
// 将新数据添加到缓冲区
|
66
|
+
this.dataBuffer += dataStr;
|
67
|
+
|
68
|
+
// 按换行符分割消息
|
69
|
+
const messages = this.dataBuffer.split('\n');
|
70
|
+
|
71
|
+
// 最后一个元素可能是不完整的消息,保留在缓冲区
|
72
|
+
this.dataBuffer = messages.pop() || '';
|
73
|
+
|
74
|
+
for (const message of messages) {
|
75
|
+
if (!message.trim()) continue; // 跳过空消息
|
76
|
+
|
77
|
+
try {
|
78
|
+
const response = JSON.parse(message);
|
79
|
+
const { id, result, error } = response;
|
80
|
+
|
81
|
+
const pending = this.requestQueue.get(id);
|
82
|
+
if (pending) {
|
83
|
+
this.requestQueue.delete(id);
|
84
|
+
if (error) {
|
85
|
+
pending.reject(new Error(error));
|
86
|
+
} else {
|
87
|
+
pending.resolve(result);
|
88
|
+
}
|
72
89
|
}
|
90
|
+
} catch (err) {
|
91
|
+
console.error(
|
92
|
+
'[ElectronIpcClient] Failed to parse response:',
|
93
|
+
err,
|
94
|
+
'message:',
|
95
|
+
message,
|
96
|
+
);
|
73
97
|
}
|
74
|
-
} catch (err) {
|
75
|
-
console.error('Failed to parse response:', err);
|
76
98
|
}
|
77
99
|
});
|
78
100
|
|
79
101
|
this.socket.on('error', (err) => {
|
80
|
-
console.error('Socket error:', err);
|
102
|
+
console.error('[ElectronIpcClient] Socket error:', err);
|
81
103
|
this.connected = false;
|
82
104
|
this.handleDisconnect();
|
83
105
|
reject(err);
|
84
106
|
});
|
85
107
|
|
86
108
|
this.socket.on('close', () => {
|
87
|
-
console.log('Socket closed');
|
109
|
+
console.log('[ElectronIpcClient] Socket closed');
|
88
110
|
this.connected = false;
|
89
111
|
this.handleDisconnect();
|
90
112
|
});
|
91
113
|
} catch (err) {
|
92
|
-
console.error('Failed to connect to IPC server:', err);
|
114
|
+
console.error('[ElectronIpcClient] Failed to connect to IPC server:', err);
|
93
115
|
this.handleDisconnect();
|
94
116
|
reject(err);
|
95
117
|
}
|
@@ -104,9 +126,12 @@ export class ElectronIpcClient {
|
|
104
126
|
this.reconnectTimeout = null;
|
105
127
|
}
|
106
128
|
|
129
|
+
// 清空数据缓冲区
|
130
|
+
this.dataBuffer = '';
|
131
|
+
|
107
132
|
// 拒绝所有待处理的请求
|
108
133
|
for (const [, { reject }] of this.requestQueue) {
|
109
|
-
reject(new Error('Connection to Electron IPC server lost'));
|
134
|
+
reject(new Error('[ElectronIpcClient] Connection to Electron IPC server lost'));
|
110
135
|
}
|
111
136
|
this.requestQueue.clear();
|
112
137
|
|
@@ -117,16 +142,19 @@ export class ElectronIpcClient {
|
|
117
142
|
|
118
143
|
this.reconnectTimeout = setTimeout(() => {
|
119
144
|
this.connect().catch((err) => {
|
120
|
-
console.error(
|
145
|
+
console.error(
|
146
|
+
`[ElectronIpcClient] Reconnection attempt ${this.connectionAttempts} failed:`,
|
147
|
+
err,
|
148
|
+
);
|
121
149
|
});
|
122
150
|
}, delay);
|
123
151
|
}
|
124
152
|
}
|
125
153
|
|
126
154
|
// 发送请求到 Electron IPC 服务器
|
127
|
-
public async sendRequest<T>(method:
|
155
|
+
public async sendRequest<T>(method: ServerDispatchEventKey, params: any = {}): Promise<T> {
|
128
156
|
if (!this.socketPath) {
|
129
|
-
throw new Error('Electron IPC connection not available');
|
157
|
+
throw new Error('[ElectronIpcClient] Electron IPC connection not available');
|
130
158
|
}
|
131
159
|
|
132
160
|
// 如果未连接,先连接
|
@@ -145,8 +173,8 @@ export class ElectronIpcClient {
|
|
145
173
|
// 设置超时
|
146
174
|
const timeout = setTimeout(() => {
|
147
175
|
this.requestQueue.delete(id);
|
148
|
-
reject(new Error(`Request ${method}
|
149
|
-
},
|
176
|
+
reject(new Error(`[ElectronIpcClient] Request timed out, method: ${method}`));
|
177
|
+
}, 5000);
|
150
178
|
|
151
179
|
// 发送请求
|
152
180
|
this.socket!.write(JSON.stringify(request), (err) => {
|
@@ -4,16 +4,8 @@ import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
5
5
|
|
6
6
|
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
7
|
-
import {
|
8
|
-
|
9
|
-
export type IPCEventMethod = (
|
10
|
-
params: any,
|
11
|
-
context: { id: string; method: string; socket: net.Socket },
|
12
|
-
) => Promise<any>;
|
13
|
-
|
14
|
-
export type ElectronIPCEventHandler = {
|
15
|
-
[key in IElectronIPCMethods]: IPCEventMethod;
|
16
|
-
};
|
7
|
+
import { ServerDispatchEventKey } from './events';
|
8
|
+
import { ElectronIPCEventHandler } from './types';
|
17
9
|
|
18
10
|
export class ElectronIPCServer {
|
19
11
|
private server: net.Server;
|
@@ -45,7 +37,7 @@ export class ElectronIPCServer {
|
|
45
37
|
});
|
46
38
|
|
47
39
|
this.server.listen(this.socketPath, () => {
|
48
|
-
console.log(`Electron IPC server listening on ${this.socketPath}`);
|
40
|
+
console.log(`[ElectronIPCServer] Electron IPC server listening on ${this.socketPath}`);
|
49
41
|
|
50
42
|
// 将套接字路径写入临时文件,供 Next.js 服务端读取
|
51
43
|
const tempDir = os.tmpdir();
|
@@ -86,7 +78,7 @@ export class ElectronIPCServer {
|
|
86
78
|
const { id, method, params } = request;
|
87
79
|
|
88
80
|
// 根据请求方法执行相应的操作
|
89
|
-
const eventHandler = this.eventHandler[method as
|
81
|
+
const eventHandler = this.eventHandler[method as ServerDispatchEventKey];
|
90
82
|
if (!eventHandler) return;
|
91
83
|
|
92
84
|
try {
|
@@ -100,12 +92,12 @@ export class ElectronIPCServer {
|
|
100
92
|
|
101
93
|
// 发送结果
|
102
94
|
private sendResult(socket: net.Socket, id: string, result: any): void {
|
103
|
-
socket.write(JSON.stringify({ id, result }));
|
95
|
+
socket.write(JSON.stringify({ id, result }) + '\n');
|
104
96
|
}
|
105
97
|
|
106
98
|
// 发送错误
|
107
99
|
private sendError(socket: net.Socket, id: string, error: string): void {
|
108
|
-
socket.write(JSON.stringify({ error, id }));
|
100
|
+
socket.write(JSON.stringify({ error, id }) + '\n');
|
109
101
|
}
|
110
102
|
|
111
103
|
// 关闭服务器
|
@@ -1 +1,14 @@
|
|
1
|
-
|
1
|
+
import net from 'node:net';
|
2
|
+
|
3
|
+
import { ServerDispatchEventKey } from '../events';
|
4
|
+
|
5
|
+
export type IPCEventMethod = (
|
6
|
+
params: any,
|
7
|
+
context: { id: string; method: string; socket: net.Socket },
|
8
|
+
) => Promise<any>;
|
9
|
+
|
10
|
+
export type ElectronIPCEventHandler = {
|
11
|
+
[key in ServerDispatchEventKey]: IPCEventMethod;
|
12
|
+
};
|
13
|
+
|
14
|
+
export * from './file';
|
@@ -0,0 +1,52 @@
|
|
1
|
+
/* eslint-disable unicorn/no-process-exit */
|
2
|
+
import { execSync } from 'node:child_process';
|
3
|
+
import os from 'node:os';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Build desktop application based on current operating system platform
|
7
|
+
*/
|
8
|
+
const buildElectron = () => {
|
9
|
+
const platform = os.platform();
|
10
|
+
const startTime = Date.now();
|
11
|
+
|
12
|
+
console.log(`🔨 Starting to build desktop app for ${platform} platform...`);
|
13
|
+
|
14
|
+
try {
|
15
|
+
let buildCommand = '';
|
16
|
+
|
17
|
+
// Determine build command based on platform
|
18
|
+
switch (platform) {
|
19
|
+
case 'darwin': {
|
20
|
+
buildCommand = 'npm run build:mac --prefix=./apps/desktop';
|
21
|
+
console.log('📦 Building macOS desktop application...');
|
22
|
+
break;
|
23
|
+
}
|
24
|
+
case 'win32': {
|
25
|
+
buildCommand = 'npm run build:win --prefix=./apps/desktop';
|
26
|
+
console.log('📦 Building Windows desktop application...');
|
27
|
+
break;
|
28
|
+
}
|
29
|
+
case 'linux': {
|
30
|
+
buildCommand = 'npm run build:linux --prefix=./apps/desktop';
|
31
|
+
console.log('📦 Building Linux desktop application...');
|
32
|
+
break;
|
33
|
+
}
|
34
|
+
default: {
|
35
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
// Execute build command
|
40
|
+
execSync(buildCommand, { stdio: 'inherit' });
|
41
|
+
|
42
|
+
const endTime = Date.now();
|
43
|
+
const buildTime = ((endTime - startTime) / 1000).toFixed(2);
|
44
|
+
console.log(`✅ Desktop application build completed! (${buildTime}s)`);
|
45
|
+
} catch (error) {
|
46
|
+
console.error('❌ Build failed:', error);
|
47
|
+
process.exit(1);
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
// Execute build
|
52
|
+
buildElectron();
|
@@ -0,0 +1,69 @@
|
|
1
|
+
/* eslint-disable unicorn/no-process-exit */
|
2
|
+
import fs from 'fs-extra';
|
3
|
+
import { execSync } from 'node:child_process';
|
4
|
+
import os from 'node:os';
|
5
|
+
import path from 'node:path';
|
6
|
+
|
7
|
+
const rootDir = path.resolve(__dirname, '../..');
|
8
|
+
|
9
|
+
// 定义源目录和目标目录
|
10
|
+
const sourceDir: string = path.join(rootDir, '.next/standalone');
|
11
|
+
const targetDir: string = path.join(rootDir, 'apps/desktop/dist/next');
|
12
|
+
|
13
|
+
// 向 sourceDir 写入 .env 文件
|
14
|
+
const env = fs.readFileSync(path.join(rootDir, '.env.desktop'), 'utf8');
|
15
|
+
|
16
|
+
fs.writeFileSync(path.join(sourceDir, '.env'), env, 'utf8');
|
17
|
+
console.log(`⚓️ Inject .env successful`);
|
18
|
+
|
19
|
+
// 确保目标目录的父目录存在
|
20
|
+
fs.ensureDirSync(path.dirname(targetDir));
|
21
|
+
|
22
|
+
// 如果目标目录已存在,先删除它
|
23
|
+
if (fs.existsSync(targetDir)) {
|
24
|
+
console.log(`🗑️ Target directory ${targetDir} already exists, deleting...`);
|
25
|
+
try {
|
26
|
+
fs.removeSync(targetDir);
|
27
|
+
console.log(`✅ Old target directory removed successfully`);
|
28
|
+
} catch (error) {
|
29
|
+
console.warn(`⚠️ Failed to delete target directory: ${error}`);
|
30
|
+
console.log('🔄 Trying to delete using system command...');
|
31
|
+
try {
|
32
|
+
if (os.platform() === 'win32') {
|
33
|
+
execSync(`rmdir /S /Q "${targetDir}"`, { stdio: 'inherit' });
|
34
|
+
} else {
|
35
|
+
execSync(`rm -rf "${targetDir}"`, { stdio: 'inherit' });
|
36
|
+
}
|
37
|
+
console.log('✅ Successfully deleted old target directory');
|
38
|
+
} catch (cmdError) {
|
39
|
+
console.error(`❌ Unable to delete target directory, might need manual cleanup: ${cmdError}`);
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
console.log(`🚚 Moving ${sourceDir} to ${targetDir}...`);
|
45
|
+
|
46
|
+
try {
|
47
|
+
// 使用 fs-extra 的 move 方法
|
48
|
+
fs.moveSync(sourceDir, targetDir, { overwrite: true });
|
49
|
+
console.log(`✅ Directory moved successfully!`);
|
50
|
+
} catch (error) {
|
51
|
+
console.error('❌ fs-extra move failed:', error);
|
52
|
+
console.log('🔄 Trying to move using system command...');
|
53
|
+
|
54
|
+
try {
|
55
|
+
// 使用系统命令进行移动
|
56
|
+
if (os.platform() === 'win32') {
|
57
|
+
execSync(`move "${sourceDir}" "${targetDir}"`, { stdio: 'inherit' });
|
58
|
+
} else {
|
59
|
+
execSync(`mv "${sourceDir}" "${targetDir}"`, { stdio: 'inherit' });
|
60
|
+
}
|
61
|
+
console.log('✅ System command move completed successfully!');
|
62
|
+
} catch (mvError) {
|
63
|
+
console.error('❌ Failed to move directory:', mvError);
|
64
|
+
console.log('💡 Try running manually: sudo mv ' + sourceDir + ' ' + targetDir);
|
65
|
+
process.exit(1);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
console.log(`🎉 Move completed!`);
|
@@ -0,0 +1,96 @@
|
|
1
|
+
/* eslint-disable unicorn/no-process-exit */
|
2
|
+
import fs from 'fs-extra';
|
3
|
+
import path from 'node:path';
|
4
|
+
|
5
|
+
// 获取脚本的命令行参数
|
6
|
+
const version = process.argv[2];
|
7
|
+
const isPr = process.argv[3] === 'true';
|
8
|
+
|
9
|
+
if (!version) {
|
10
|
+
console.error('Missing version parameter, usage: bun run setDesktopVersion.ts <version> [isPr]');
|
11
|
+
process.exit(1);
|
12
|
+
}
|
13
|
+
|
14
|
+
// 获取根目录
|
15
|
+
const rootDir = path.resolve(__dirname, '../..');
|
16
|
+
|
17
|
+
// 桌面应用 package.json 的路径
|
18
|
+
const desktopPackageJsonPath = path.join(rootDir, 'apps/desktop/package.json');
|
19
|
+
|
20
|
+
// 更新应用图标
|
21
|
+
function updateAppIcon() {
|
22
|
+
try {
|
23
|
+
const buildDir = path.join(rootDir, 'apps/desktop/build');
|
24
|
+
|
25
|
+
// 定义需要处理的图标映射,考虑到大小写敏感性
|
26
|
+
const iconMappings = [
|
27
|
+
// { ext: '.ico', nightly: 'icon-nightly.ico', normal: 'icon.ico' },
|
28
|
+
{ ext: '.png', nightly: 'icon-nightly.png', normal: 'icon.png' },
|
29
|
+
{ ext: '.icns', nightly: 'Icon-nightly.icns', normal: 'Icon.icns' },
|
30
|
+
];
|
31
|
+
|
32
|
+
// 处理每种图标格式
|
33
|
+
for (const mapping of iconMappings) {
|
34
|
+
const sourceFile = path.join(buildDir, mapping.nightly);
|
35
|
+
const targetFile = path.join(buildDir, mapping.normal);
|
36
|
+
|
37
|
+
// 检查源文件是否存在
|
38
|
+
if (fs.existsSync(sourceFile)) {
|
39
|
+
// 只有当源文件和目标文件不同,才进行复制
|
40
|
+
if (sourceFile !== targetFile) {
|
41
|
+
fs.copyFileSync(sourceFile, targetFile);
|
42
|
+
console.log(`Updated app icon: ${targetFile}`);
|
43
|
+
}
|
44
|
+
} else {
|
45
|
+
console.warn(`Warning: Source icon not found: ${sourceFile}`);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
} catch (error) {
|
49
|
+
console.error('Error updating icons:', error);
|
50
|
+
// 继续处理,不终止程序
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
function updateVersion() {
|
55
|
+
try {
|
56
|
+
// 确保文件存在
|
57
|
+
if (!fs.existsSync(desktopPackageJsonPath)) {
|
58
|
+
console.error(`Error: File not found ${desktopPackageJsonPath}`);
|
59
|
+
process.exit(1);
|
60
|
+
}
|
61
|
+
|
62
|
+
// 读取 package.json 文件
|
63
|
+
const packageJson = fs.readJSONSync(desktopPackageJsonPath);
|
64
|
+
|
65
|
+
// 更新版本号
|
66
|
+
packageJson.version = version;
|
67
|
+
packageJson.productName = 'LobeHub';
|
68
|
+
packageJson.name = 'lobehub-desktop';
|
69
|
+
|
70
|
+
// 如果是 PR 构建,设置为 Nightly 版本
|
71
|
+
if (isPr) {
|
72
|
+
// 修改包名,添加 -nightly 后缀
|
73
|
+
if (!packageJson.name.endsWith('-nightly')) {
|
74
|
+
packageJson.name = `${packageJson.name}-nightly`;
|
75
|
+
}
|
76
|
+
|
77
|
+
// 修改产品名称为 LobeHub Nightly
|
78
|
+
packageJson.productName = 'LobeHub-Nightly';
|
79
|
+
|
80
|
+
console.log('🌙 Setting as Nightly version with modified package name and productName');
|
81
|
+
|
82
|
+
// 使用 nightly 图标替换常规图标
|
83
|
+
updateAppIcon();
|
84
|
+
}
|
85
|
+
|
86
|
+
// 写回文件
|
87
|
+
fs.writeJsonSync(desktopPackageJsonPath, packageJson, { spaces: 2 });
|
88
|
+
|
89
|
+
console.log(`Desktop app version updated to: ${version}, isPr: ${isPr}`);
|
90
|
+
} catch (error) {
|
91
|
+
console.error('Error updating version:', error);
|
92
|
+
process.exit(1);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
updateVersion();
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import * as dotenv from 'dotenv';
|
2
|
+
import { existsSync } from 'node:fs';
|
3
|
+
import { rm } from 'node:fs/promises';
|
4
|
+
import path from 'node:path';
|
5
|
+
|
6
|
+
const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
|
7
|
+
|
8
|
+
dotenv.config();
|
9
|
+
// 创建需要排除的特性映射
|
10
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
11
|
+
const partialBuildPages = [
|
12
|
+
// no need for desktop
|
13
|
+
{
|
14
|
+
name: 'changelog',
|
15
|
+
disabled: isDesktop,
|
16
|
+
paths: ['src/app/[variants]/@modal/(.)changelog', 'src/app/[variants]/(main)/changelog'],
|
17
|
+
},
|
18
|
+
{
|
19
|
+
name: 'auth',
|
20
|
+
disabled: isDesktop,
|
21
|
+
paths: ['src/app/[variants]/(auth)'],
|
22
|
+
},
|
23
|
+
{
|
24
|
+
name: 'mobile',
|
25
|
+
disabled: isDesktop,
|
26
|
+
paths: ['src/app/[variants]/(main)/(mobile)'],
|
27
|
+
},
|
28
|
+
{
|
29
|
+
name: 'api-webhooks',
|
30
|
+
disabled: isDesktop,
|
31
|
+
paths: ['src/app/(backend)/api/webhooks'],
|
32
|
+
},
|
33
|
+
|
34
|
+
// no need for web
|
35
|
+
{
|
36
|
+
name: 'desktop-devtools',
|
37
|
+
disabled: !isDesktop,
|
38
|
+
paths: ['src/app/desktop'],
|
39
|
+
},
|
40
|
+
{
|
41
|
+
name: 'desktop-trpc',
|
42
|
+
disabled: !isDesktop,
|
43
|
+
paths: ['src/app/(backend)/trpc/desktop'],
|
44
|
+
},
|
45
|
+
];
|
46
|
+
/* eslint-enable */
|
47
|
+
|
48
|
+
/**
|
49
|
+
* 删除指定的目录
|
50
|
+
*/
|
51
|
+
const removeDirectories = async () => {
|
52
|
+
// 遍历 partialBuildPages 数组
|
53
|
+
for (const page of partialBuildPages) {
|
54
|
+
// 检查是否需要禁用该功能
|
55
|
+
if (page.disabled) {
|
56
|
+
for (const dirPath of page.paths) {
|
57
|
+
const fullPath = path.resolve(process.cwd(), dirPath);
|
58
|
+
|
59
|
+
// 检查目录是否存在
|
60
|
+
if (existsSync(fullPath)) {
|
61
|
+
try {
|
62
|
+
// 递归删除目录
|
63
|
+
await rm(fullPath, { force: true, recursive: true });
|
64
|
+
console.log(`♻️ Removed ${dirPath} successfully`);
|
65
|
+
} catch (error) {
|
66
|
+
console.error(`Failed to remove directory ${dirPath}:`, error);
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
};
|
73
|
+
|
74
|
+
// 执行删除操作
|
75
|
+
console.log('Starting prebuild cleanup...');
|
76
|
+
await removeDirectories();
|
77
|
+
console.log('Prebuild cleanup completed.');
|
@@ -34,7 +34,7 @@ export class LobeAzureAI implements LobeRuntimeAI {
|
|
34
34
|
baseURL: string;
|
35
35
|
|
36
36
|
async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
|
37
|
-
const { messages, model, ...params } = payload;
|
37
|
+
const { messages, model, temperature, top_p, ...params } = payload;
|
38
38
|
// o1 series models on Azure OpenAI does not support streaming currently
|
39
39
|
const enableStreaming = model.includes('o1') ? false : (params.stream ?? true);
|
40
40
|
|
@@ -56,7 +56,9 @@ export class LobeAzureAI implements LobeRuntimeAI {
|
|
56
56
|
model,
|
57
57
|
...params,
|
58
58
|
stream: enableStreaming,
|
59
|
+
temperature: model.includes('o3') ? undefined : temperature,
|
59
60
|
tool_choice: params.tools ? 'auto' : undefined,
|
61
|
+
top_p: model.includes('o3') ? undefined : top_p,
|
60
62
|
},
|
61
63
|
});
|
62
64
|
|
@@ -1,14 +1,16 @@
|
|
1
1
|
import { ChatFileItem } from '@/types/message';
|
2
2
|
|
3
|
-
const filePrompt = (item: ChatFileItem) =>
|
4
|
-
|
3
|
+
const filePrompt = (item: ChatFileItem, addUrl: boolean) =>
|
4
|
+
addUrl
|
5
|
+
? `<file id="${item.id}" name="${item.name}" type="${item.fileType}" size="${item.size}" url="${item.url}"></file>`
|
6
|
+
: `<file id="${item.id}" name="${item.name}" type="${item.fileType}" size="${item.size}"></file>`;
|
5
7
|
|
6
|
-
export const filePrompts = (fileList: ChatFileItem[]) => {
|
8
|
+
export const filePrompts = (fileList: ChatFileItem[], addUrl: boolean) => {
|
7
9
|
if (fileList.length === 0) return '';
|
8
10
|
|
9
11
|
const prompt = `<files>
|
10
12
|
<files_docstring>here are user upload files you can refer to</files_docstring>
|
11
|
-
${fileList.map((item) => filePrompt(item)).join('\n')}
|
13
|
+
${fileList.map((item) => filePrompt(item, addUrl)).join('\n')}
|
12
14
|
</files>`;
|
13
15
|
|
14
16
|
return prompt.trim();
|
@@ -1,13 +1,16 @@
|
|
1
1
|
import { ChatImageItem } from '@/types/message';
|
2
2
|
|
3
|
-
const imagePrompt = (item: ChatImageItem) =>
|
3
|
+
const imagePrompt = (item: ChatImageItem, attachUrl: boolean) =>
|
4
|
+
attachUrl
|
5
|
+
? `<image name="${item.alt}" url="${item.url}"></image>`
|
6
|
+
: `<image name="${item.alt}"></image>`;
|
4
7
|
|
5
|
-
export const imagesPrompts = (imageList: ChatImageItem[]) => {
|
8
|
+
export const imagesPrompts = (imageList: ChatImageItem[], attachUrl: boolean) => {
|
6
9
|
if (imageList.length === 0) return '';
|
7
10
|
|
8
11
|
const prompt = `<images>
|
9
12
|
<images_docstring>here are user upload images you can refer to</images_docstring>
|
10
|
-
${imageList.map((item) => imagePrompt(item)).join('\n')}
|
13
|
+
${imageList.map((item) => imagePrompt(item, attachUrl)).join('\n')}
|
11
14
|
</images>`;
|
12
15
|
|
13
16
|
return prompt.trim();
|
@@ -135,4 +135,54 @@ describe('filesPrompts', () => {
|
|
135
135
|
expect(result).toMatch(/<image.*?>.*<image.*?>/s); // Check for multiple image tags
|
136
136
|
expect(result).toMatch(/<file.*?>.*<file.*?>/s); // Check for multiple file tags
|
137
137
|
});
|
138
|
+
|
139
|
+
it('should handle without url', () => {
|
140
|
+
const images: ChatImageItem[] = [
|
141
|
+
mockImage,
|
142
|
+
{
|
143
|
+
id: 'img-2',
|
144
|
+
alt: 'second image',
|
145
|
+
url: 'https://example.com/image2.jpg',
|
146
|
+
},
|
147
|
+
];
|
148
|
+
|
149
|
+
const files: ChatFileItem[] = [
|
150
|
+
mockFile,
|
151
|
+
{
|
152
|
+
id: 'file-2',
|
153
|
+
name: 'document.docx',
|
154
|
+
fileType: 'application/docx',
|
155
|
+
size: 2048,
|
156
|
+
url: 'https://example.com/document.docx',
|
157
|
+
},
|
158
|
+
];
|
159
|
+
|
160
|
+
const result = filesPrompts({
|
161
|
+
imageList: images,
|
162
|
+
fileList: files,
|
163
|
+
addUrl: false,
|
164
|
+
});
|
165
|
+
|
166
|
+
expect(result).toMatchInlineSnapshot(`
|
167
|
+
"<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
168
|
+
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
169
|
+
|
170
|
+
1. Always prioritize handling user-visible content.
|
171
|
+
2. the context is only required when user's queries rely on it.
|
172
|
+
</context.instruction>
|
173
|
+
<files_info>
|
174
|
+
<images>
|
175
|
+
<images_docstring>here are user upload images you can refer to</images_docstring>
|
176
|
+
<image name="test image"></image>
|
177
|
+
<image name="second image"></image>
|
178
|
+
</images>
|
179
|
+
<files>
|
180
|
+
<files_docstring>here are user upload files you can refer to</files_docstring>
|
181
|
+
<file id="file-1" name="test.pdf" type="application/pdf" size="1024"></file>
|
182
|
+
<file id="file-2" name="document.docx" type="application/docx" size="2048"></file>
|
183
|
+
</files>
|
184
|
+
</files_info>
|
185
|
+
<!-- END SYSTEM CONTEXT -->"
|
186
|
+
`);
|
187
|
+
});
|
138
188
|
});
|
@@ -6,7 +6,9 @@ import { imagesPrompts } from './image';
|
|
6
6
|
export const filesPrompts = ({
|
7
7
|
imageList,
|
8
8
|
fileList,
|
9
|
+
addUrl = true,
|
9
10
|
}: {
|
11
|
+
addUrl?: boolean;
|
10
12
|
fileList?: ChatFileItem[];
|
11
13
|
imageList: ChatImageItem[];
|
12
14
|
}) => {
|
@@ -19,8 +21,8 @@ export const filesPrompts = ({
|
|
19
21
|
2. the context is only required when user's queries rely on it.
|
20
22
|
</context.instruction>
|
21
23
|
<files_info>
|
22
|
-
${imagesPrompts(imageList)}
|
23
|
-
${fileList ? filePrompts(fileList) : ''}
|
24
|
+
${imagesPrompts(imageList, addUrl)}
|
25
|
+
${fileList ? filePrompts(fileList, addUrl) : ''}
|
24
26
|
</files_info>
|
25
27
|
<!-- END SYSTEM CONTEXT -->`;
|
26
28
|
|
package/src/services/chat.ts
CHANGED
@@ -8,7 +8,7 @@ import { INBOX_GUIDE_SYSTEMROLE } from '@/const/guide';
|
|
8
8
|
import { INBOX_SESSION_ID } from '@/const/session';
|
9
9
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
10
10
|
import { TracePayload, TraceTagMap } from '@/const/trace';
|
11
|
-
import { isDeprecatedEdition, isServerMode } from '@/const/version';
|
11
|
+
import { isDeprecatedEdition, isDesktop, isServerMode } from '@/const/version';
|
12
12
|
import {
|
13
13
|
AgentRuntime,
|
14
14
|
AgentRuntimeError,
|
@@ -480,7 +480,9 @@ class ChatService {
|
|
480
480
|
|
481
481
|
const imageList = m.imageList || [];
|
482
482
|
|
483
|
-
const filesContext = isServerMode
|
483
|
+
const filesContext = isServerMode
|
484
|
+
? filesPrompts({ addUrl: !isDesktop, fileList: m.fileList, imageList })
|
485
|
+
: '';
|
484
486
|
return [
|
485
487
|
{ text: (m.content + '\n\n' + filesContext).trim(), type: 'text' },
|
486
488
|
...imageList.map(
|
@@ -5,7 +5,7 @@ import { SWRResponse } from 'swr';
|
|
5
5
|
import type { StateCreator } from 'zustand/vanilla';
|
6
6
|
|
7
7
|
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
8
|
-
import { CURRENT_VERSION } from '@/const/version';
|
8
|
+
import { CURRENT_VERSION, isDesktop } from '@/const/version';
|
9
9
|
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
10
10
|
import { globalService } from '@/services/global';
|
11
11
|
import type { SystemStatus } from '@/store/global/initialState';
|
@@ -37,6 +37,18 @@ export const generalActionSlice: StateCreator<
|
|
37
37
|
get().updateSystemStatus({ language: locale });
|
38
38
|
|
39
39
|
switchLang(locale);
|
40
|
+
|
41
|
+
if (isDesktop) {
|
42
|
+
(async () => {
|
43
|
+
try {
|
44
|
+
const { dispatch } = await import('@lobechat/electron-client-ipc');
|
45
|
+
|
46
|
+
await dispatch('updateLocale', locale);
|
47
|
+
} catch (error) {
|
48
|
+
console.error('Failed to update locale in main process:', error);
|
49
|
+
}
|
50
|
+
})();
|
51
|
+
}
|
40
52
|
},
|
41
53
|
switchThemeMode: (themeMode) => {
|
42
54
|
get().updateSystemStatus({ themeMode });
|
@@ -44,7 +56,6 @@ export const generalActionSlice: StateCreator<
|
|
44
56
|
setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
|
45
57
|
},
|
46
58
|
updateSystemStatus: (status, action) => {
|
47
|
-
// Status cannot be modified when it is not initialized
|
48
59
|
if (!get().isStatusInit) return;
|
49
60
|
|
50
61
|
const nextStatus = merge(get().status, status);
|
@@ -60,19 +71,15 @@ export const generalActionSlice: StateCreator<
|
|
60
71
|
enabledCheck ? 'checkLatestVersion' : null,
|
61
72
|
async () => globalService.getLatestVersion(),
|
62
73
|
{
|
63
|
-
// check latest version every 30 minutes
|
64
74
|
focusThrottleInterval: 1000 * 60 * 30,
|
65
75
|
onSuccess: (data: string) => {
|
66
76
|
if (!valid(CURRENT_VERSION) || !valid(data)) return;
|
67
77
|
|
68
|
-
// Parse versions to ensure we're working with valid SemVer objects
|
69
78
|
const currentVersion = parse(CURRENT_VERSION);
|
70
79
|
const latestVersion = parse(data);
|
71
80
|
|
72
81
|
if (!currentVersion || !latestVersion) return;
|
73
82
|
|
74
|
-
// only compare major and minor versions
|
75
|
-
// solve the problem of frequent patch updates
|
76
83
|
const currentMajorMinor = `${currentVersion.major}.${currentVersion.minor}.0`;
|
77
84
|
const latestMajorMinor = `${latestVersion.major}.${latestVersion.minor}.0`;
|
78
85
|
|
@@ -1,18 +0,0 @@
|
|
1
|
-
/* eslint-disable typescript-sort-keys/interface, sort-keys-fix/sort-keys-fix */
|
2
|
-
export const ElectronIPCMethods = {
|
3
|
-
getDatabasePath: 'getDatabasePath',
|
4
|
-
getUserDataPath: 'getUserDataPath',
|
5
|
-
|
6
|
-
getDatabaseSchemaHash: 'getDatabaseSchemaHash',
|
7
|
-
setDatabaseSchemaHash: 'setDatabaseSchemaHash',
|
8
|
-
} as const;
|
9
|
-
|
10
|
-
export type IElectronIPCMethods = keyof typeof ElectronIPCMethods;
|
11
|
-
|
12
|
-
export interface IpcDispatchEvent {
|
13
|
-
getDatabasePath: () => Promise<string>;
|
14
|
-
getUserDataPath: () => Promise<string>;
|
15
|
-
|
16
|
-
getDatabaseSchemaHash: () => Promise<string | undefined>;
|
17
|
-
setDatabaseSchemaHash: (hash: string) => Promise<void>;
|
18
|
-
}
|